Loading portal-gui/src/app/(app)/app-profiles/[appId]/page.tsx 0 → 100644 +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; portal-gui/src/app/(app)/app-profiles/[appId]/profilePage.module.scss 0 → 100644 +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; } portal-gui/src/app/(app)/my-applications/ProfileCard.tsx +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> ); }; portal-gui/src/app/(app)/my-applications/myApplications.module.scss +139 −27 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; } } Loading portal-gui/src/app/(app)/my-applications/page.tsx +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 Loading @@ -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 ---------- */} Loading @@ -41,7 +67,7 @@ const page = () => { <div className={styles.cards}> {deployments.map((deployment) => ( <DeploymentCard key={deployment.id} deployment={deployment} /> ))}{" "} ))} </div> )} </div> Loading Loading
portal-gui/src/app/(app)/app-profiles/[appId]/page.tsx 0 → 100644 +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;
portal-gui/src/app/(app)/app-profiles/[appId]/profilePage.module.scss 0 → 100644 +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; }
portal-gui/src/app/(app)/my-applications/ProfileCard.tsx +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> ); };
portal-gui/src/app/(app)/my-applications/myApplications.module.scss +139 −27 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; } } Loading
portal-gui/src/app/(app)/my-applications/page.tsx +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 Loading @@ -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 ---------- */} Loading @@ -41,7 +67,7 @@ const page = () => { <div className={styles.cards}> {deployments.map((deployment) => ( <DeploymentCard key={deployment.id} deployment={deployment} /> ))}{" "} ))} </div> )} </div> Loading