Commit e818f78e authored by Dimitrios Gogos's avatar Dimitrios Gogos
Browse files

feat: add app profile details page, retrieve apps and delete appIds

parent 2fd67598
Loading
Loading
Loading
Loading
+132 −0
Original line number Diff line number Diff line
"use client";

import { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { Button, Divider } from "rsuite";
import { IApplicationProfile } from "@/app/utils/interfaces";
import { backArrowIcon } from "@/app/utils/icons";
import Loader from "@/app/components/Loader/Loader";
import Error from "@/app/components/Error/Error";
import buttons from "@/app/styles/buttons.module.scss";
import styles from "./profilePage.module.scss";

const ProfilePage = () => {
  const { appId } = useParams<{ appId: string }>();
  const router = useRouter();
  const [app, setApp] = useState<IApplicationProfile | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);

  useEffect(() => {
    fetch(`/api/apps/${appId}`)
      .then((r) => r.json())
      .then((data) => setApp(data))
      .catch(() => setError(true))
      .finally(() => setLoading(false));
  }, [appId]);

  if (loading) return <Loader />;
  if (error || !app)
    return (
      <Error
        href="/my-applications"
        message="Application not found"
        linkText="Back to My Applications"
      />
    );

  return (
    <div className={styles.container}>
      <div className={styles.btnBox}>
        <Button className={buttons.iconSubtle} onClick={() => router.back()}>
          {backArrowIcon}
        </Button>
      </div>

      <div className={styles.header}>
        <div className={styles.badges}>
          <span className={styles.badge}>{app.packageType}</span>
          {app.appProvider && (
            <span className={styles.badgeSecondary}>{app.appProvider}</span>
          )}
        </div>
        <h1 className={styles.title}>{app.name}</h1>
        <p className={styles.appId}>appID: {app.appId}</p>
      </div>

      <Divider label="Repository" color="#004a8d" />

      <div className={styles.section}>
        <div className={styles.field}>
          <span className={styles.label}>Type</span>
          <span>{app.appRepo.type}</span>
        </div>
        <div className={styles.field}>
          <span className={styles.label}>Image path</span>
          <span className={styles.mono}>{app.appRepo.imagePath}</span>
        </div>
        {app.appRepo.userName && (
          <div className={styles.field}>
            <span className={styles.label}>Username</span>
            <span>{app.appRepo.userName}</span>
          </div>
        )}
        {app.appRepo.authType && (
          <div className={styles.field}>
            <span className={styles.label}>Auth type</span>
            <span>{app.appRepo.authType}</span>
          </div>
        )}
      </div>

      <Divider label="Components" color="#004a8d" />

      {app.componentSpec.map((component) => (
        <div key={component.componentName} className={styles.componentBox}>
          <h3 className={styles.componentName}>{component.componentName}</h3>
          <table className={styles.table}>
            <thead>
              <tr>
                <th>Protocol</th>
                <th>Port</th>
                <th>Visibility</th>
              </tr>
            </thead>
            <tbody>
              {component.networkInterfaces.map((iface, i) => (
                <tr key={i}>
                  <td>{iface.protocol}</td>
                  <td>{iface.port}</td>
                  <td>
                    <span
                      className={
                        iface.visibilityType === "VISIBILITY_EXTERNAL"
                          ? styles.chipExternal
                          : styles.chipInternal
                      }
                    >
                      {iface.visibilityType === "VISIBILITY_EXTERNAL"
                        ? "External"
                        : "Internal"}
                    </span>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      ))}

      {app.requiredResources && (
        <>
          <Divider label="Required Resources" color="#004a8d" />
          <pre className={styles.json}>
            {JSON.stringify(app.requiredResources, null, 2)}
          </pre>
        </>
      )}
    </div>
  );
};

export default ProfilePage;
+150 −0
Original line number Diff line number Diff line
.container {
  padding: 2rem clamp(0.5rem, 5vw, 10rem);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  width: 100%;

}

.btnBox {
  display: flex;
  justify-content: flex-end;
}

.header {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin-bottom: 0.5rem;
}

.badges {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
}

.badge {
  background: var(--main-gradient);
  color: #fff;
  font-size: 0.7rem;
  font-weight: 700;
  padding: 3px 10px;
  border-radius: 4px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.badgeSecondary {
  background: #f0f4ff;
  color: var(--blue-color);
  font-size: 0.7rem;
  font-weight: 600;
  padding: 3px 10px;
  border-radius: 4px;
  border: 1px solid var(--blue-color);
}

.title {
  font-size: 1.9rem;
  font-weight: 700;
  color: var(--blue-color);
  margin: 0;
}

.appId {
  font-size: 0.78rem;
  color: #999;
  font-family: monospace;
  margin: 0;
}

.section {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-bottom: 0.5rem;
}

.field {
  display: grid;
  grid-template-columns: 130px 1fr;
  gap: 0.5rem;
  font-size: 0.9rem;
  align-items: baseline;
}

.label {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: #999;
}

.mono {
  font-family: monospace;
  font-size: 0.85rem;
  word-break: break-all;
}

.componentBox {
  background: #f9f9f9;
  border: 1px solid #e8e8e8;
  border-radius: 8px;
  padding: 1rem 1.25rem;
  margin-bottom: 0.75rem;
}

.componentName {
  font-size: 1rem;
  font-weight: 700;
  color: var(--blue-color);
  margin: 0 0 0.75rem 0;
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;

  th {
    text-align: left;
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: #999;
    padding: 0 0.75rem 0.5rem 0;
    border-bottom: 1px solid #e8e8e8;
  }

  td {
    padding: 0.4rem 0.75rem 0.4rem 0;
    color: #333;
    border-bottom: 1px solid #f0f0f0;
    font-family: monospace;
  }
}

.chipExternal,
.chipInternal {
  background: #efefef;
  color: #555;
  font-size: 0.7rem;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 4px;
}

.json {
  background: #f5f5f5;
  border: 1px solid #e8e8e8;
  border-radius: 8px;
  padding: 1rem;
  font-size: 0.8rem;
  line-height: 1.5;
  overflow-x: auto;
  color: #333;
}
+82 −12
Original line number Diff line number Diff line
"use client";

import Link from "next/link";
import React, { useState } from "react";
import styles from "./myApplications.module.scss";
import { truncate } from "../../utils/helpers";
import { IApplicationProfile } from "../../utils/interfaces";

type Props = {
  app: IApplicationProfile;
  onDelete: (appId: string) => void;
};

export const ProfileCard = ({ app, onDelete }: Props) => {
  const [deleting, setDeleting] = useState(false);

  const handleDelete = async (e: React.MouseEvent) => {
    e.preventDefault();
    e.stopPropagation();
    if (!confirm(`Delete "${app.name}"?`)) return;
    setDeleting(true);
    try {
      const res = await fetch(`/api/apps/${app.appId}`, { method: "DELETE" });
      if (res.ok) {
        onDelete(app.appId);
      } else {
        alert("Failed to delete application.");
      }
    } catch {
      alert("Could not reach the server.");
    } finally {
      setDeleting(false);
    }
  };

  const externalPorts = app.componentSpec.flatMap((c) =>
    c.networkInterfaces.map((n) => `${n.protocol}:${n.port}`)
  );

export const ProfileCard = ({ profile }: { profile: IApplicationProfile }) => {
  return (
    <Link href={`/profiles/${profile.id}`} className={styles.card}>
      <h2 className={styles.title}>{profile.title}</h2>
    <Link href={`/app-profiles/${app.appId}`} className={styles.card}>

      <div className={styles.cardHeader}>
        <div className={styles.badges}>
          <span className={styles.badge}>{app.packageType}</span>
          {app.appProvider && (
            <span className={styles.badgeSecondary}>{app.appProvider}</span>
          )}
        </div>
        <button
          className={styles.deleteBtn}
          onClick={handleDelete}
          disabled={deleting}
          aria-label="Delete application"
        >
          {deleting ? "Deleting…" : "Delete profile"}
        </button>
      </div>

      <h2 className={styles.title}>{app.name}</h2>

      <hr className={styles.divider} />

      <dl className={styles.fields}>
        <div className={styles.field}>
          <dt>Version</dt>
          <dd>{app.version}</dd>
        </div>

        <div className={styles.field}>
          <dt>Image</dt>
          <dd className={styles.mono}>{app.appRepo.imagePath}</dd>
        </div>

      <p className={styles.meta}>
        {truncate(profile.metadata, { by: "chars", length: 140 })}
      </p>
        <div className={styles.field}>
          <dt>Repo</dt>
          <dd>{app.appRepo.type}</dd>
        </div>

      <p className={styles.api}>
        API: <strong>{profile.api}</strong>
      </p>
        <div className={styles.field}>
          <dt>Components</dt>
          <dd>{app.componentSpec.map((c) => c.componentName).join(", ")}</dd>
        </div>

      <p className={styles.jsonPreview}>
        {truncate(profile.json, { by: "chars", length: 160 })}
      </p>
        {externalPorts.length > 0 && (
          <div className={styles.field}>
            <dt>Exposed ports</dt>
            <dd className={styles.ports}>
              {externalPorts.map((p) => (
                <span key={p} className={styles.portChip}>{p}</span>
              ))}
            </dd>
          </div>
        )}
      </dl>
    </Link>
  );
};
+139 −27
Original line number Diff line number Diff line
@@ -59,6 +59,16 @@

//--------------- PROFILE CARD -------------------//

.status {
    color: #666;
    font-size: 0.9rem;
}

.statusError {
    color: #e03a3a;
    font-size: 0.9rem;
}

.card {
    background: linear-gradient(314deg, #ffffff 0%, #f4f4f4 100%);
    padding: 1.5rem;
@@ -68,43 +78,145 @@
    display: flex;
    cursor: pointer;
    flex-direction: column;
    gap: 0.6rem;
    gap: 0.75rem;
    transition: all 0.2s ease;
    align-items: flex-start;
    text-decoration: none;
    color: inherit;
    width: 100%;

    &:hover {
        box-shadow: 6px 6px 12px rgba(0, 0, 0, 0.1);
        transform: translateY(-2px);
    }

   .title {
  font-size: 1.4rem;
    .cardHeader {
        display: flex;
        width: 100%;
        align-items: center;
        justify-content: space-between;
        gap: 0.4rem;
    }

    .badges {
        display: flex;
        gap: 0.4rem;
        flex-wrap: wrap;
        align-items: center;
    }

    .deleteBtn {
        background: none;
        border: 1px solid #e0e0e0;
        border-radius: 4px;
        color: #aaa;
        font-size: 0.72rem;
        font-weight: 600;
  margin-bottom: 0.6rem;
        padding: 3px 10px;
        cursor: pointer;
        white-space: nowrap;
        line-height: 1.4;
        transition: all 0.15s ease;

        &:hover:not(:disabled) {
            background: #ffe5e5;
            border-color: #e03a3a;
            color: #e03a3a;
        }

        &:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }
    }

    .badge {
        background: var(--main-gradient);
        color: #fff;
        font-size: 0.7rem;
        font-weight: 700;
        padding: 3px 10px;
        border-radius: 4px;
        text-transform: uppercase;
        letter-spacing: 0.05em;
    }

    .badgeSecondary {
        background: #f0f4ff;
        color: var(--blue-color);
        font-size: 0.7rem;
        font-weight: 600;
        padding: 3px 10px;
        border-radius: 4px;
        border: 1px solid var(--blue-color);
    }

.meta {
  font-size: 0.75rem;
  color: #555;
  margin-bottom: 1rem;
    .title {
        font-size: 1.25rem;
        font-weight: 700;
        color: var(--blue-color);
        margin: 0;
    }

    .divider {
        width: 100%;
        border: none;
        border-top: 1px solid #e8e8e8;
        margin: 0;
    }

    .fields {
        display: flex;
        flex-direction: column;
        gap: 0.5rem;
        width: 100%;
        margin: 0;
        padding: 0;

        .field {
            display: grid;
            grid-template-columns: 110px 1fr;
            gap: 0.5rem;
            align-items: baseline;

            dt {
                font-size: 0.72rem;
                font-weight: 700;
                text-transform: uppercase;
                letter-spacing: 0.06em;
                color: #999;
                white-space: nowrap;
            }

.api {
  font-size: 0.95rem;
  margin-bottom: 0.6rem;
  strong{
    color:var(--orange-color);
            dd {
                font-size: 0.88rem;
                color: #333;
                margin: 0;
            }
        }
    }

.jsonPreview {
  background: #f5f5f5;
    .mono {
        font-family: monospace;
  padding: 0.6rem;
  border-radius: 6px;
  font-size: 0.85rem;
  color: #333;
        font-size: 0.8rem;
        color: #555;
        word-break: break-all;
    }

    .ports {
        display: flex;
        gap: 0.35rem;
        flex-wrap: wrap;
    }

    .portChip {
        font-family: monospace;
        font-size: 0.72rem;
        font-weight: 600;
        background: #e8f0fe;
        color: var(--blue-color);
        padding: 2px 7px;
        border-radius: 4px;
    }
}

+34 −8
Original line number Diff line number Diff line
"use client";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { Button, SegmentedControl } from "rsuite";
import styles from "./myApplications.module.scss";
import buttons from "@/app/styles/buttons.module.scss";
import { ProfileCard } from "./ProfileCard";
import { IApplicationProfile, MyApplicationsTabKey } from "../../utils/interfaces";
import { tabs } from "../../utils/constants";
import { deployments, profiles } from "../../utils/tableHelpers";
import { deployments } from "../../utils/tableHelpers";
import { DeploymentCard } from "./DeploymentCard";

const page = () => {
  const [activeTab, setActiveTab] = useState<MyApplicationsTabKey>("profile");
  const [apps, setApps] = useState<IApplicationProfile[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    if (activeTab !== "profile") return;
    setLoading(true);
    setError(null);
    fetch("/api/apps")
      .then((r) => r.json())
      .then((data) => setApps(Array.isArray(data) ? data : []))
      .catch(() => setError("Failed to load applications."))
      .finally(() => setLoading(false));
  }, [activeTab]);

  return (
    <div className={styles.container}>
      <SegmentedControl
@@ -29,11 +44,22 @@ const page = () => {

        {/* ---------- PROFILE TAB ---------- */}
        {activeTab === "profile" && (
          <>
            {loading && <p className={styles.status}>Loading applications…</p>}
            {error && <p className={styles.statusError}>{error}</p>}
            {!loading && !error && apps.length === 0 && (
              <p className={styles.status}>No applications registered yet.</p>
            )}
            <div className={styles.cards}>
            {profiles.map((profile) => (
              <ProfileCard key={profile.id} profile={profile} />
              {apps.map((app) => (
                <ProfileCard
                  key={app.appId}
                  app={app}
                  onDelete={(id) => setApps((prev) => prev.filter((a) => a.appId !== id))}
                />
              ))}
            </div>
          </>
        )}

        {/* ---------- DEPLOYMENTS TAB ---------- */}
@@ -41,7 +67,7 @@ const page = () => {
          <div className={styles.cards}>
            {deployments.map((deployment) => (
              <DeploymentCard key={deployment.id} deployment={deployment} />
            ))}{" "}
            ))}
          </div>
        )}
      </div>
Loading