Loading portal-gui/src/app/(app)/my-applications/DeploymentCard.tsx +30 −32 Original line number Diff line number Diff line "use client"; import React from "react"; import styles from "./myApplications.module.scss"; import { IDeployment } from "../../utils/interfaces"; import { IAppInstance } from "../../utils/interfaces"; import { StatusChip } from "../../components/Chip/StatusChip"; export const DeploymentCard = ({ deployment }: { deployment: IDeployment }) => { const getStatusClass = (status: IDeployment["status"]) => { switch (status) { case "running": return styles.statusRunning; case "deploying": return styles.statusDeploying; case "error": return styles.statusError; case "stopped": return styles.statusStopped; default: return ""; } }; export const DeploymentCard = ({ instance }: { instance: IAppInstance }) => { return ( <div className={styles.deployCard}> <div className={styles.header}> <h3 className={styles.title}>{deployment.name}</h3> <StatusChip status={deployment.status} /> <h3 className={styles.title}>{instance.name}</h3> <StatusChip status={instance.status} /> </div> <p className={styles.region}> <strong>Region:</strong> {deployment.region} </p> <p className={styles.version}> <strong>Version:</strong> {deployment.version} </p> <p className={styles.created}> <strong>Created:</strong> {new Date(deployment.createdAt).toLocaleString()} </p> <dl className={styles.fields}> <div className={styles.field}> <dt>Provider</dt> <dd>{instance.appProvider || "—"}</dd> </div> <div className={styles.field}> <dt>App ID</dt> <dd className={styles.mono}>{instance.appId}</dd> </div> <div className={styles.field}> <dt>Instance ID</dt> <dd className={styles.mono}>{instance.appInstanceId}</dd> </div> {instance.edgeCloudZoneId && ( <div className={styles.field}> <dt>Edge Zone</dt> <dd className={styles.mono}>{instance.edgeCloudZoneId}</dd> </div> )} {instance.kubernetesClusterRef && ( <div className={styles.field}> <dt>K8s Cluster</dt> <dd className={styles.mono}>{instance.kubernetesClusterRef}</dd> </div> )} </dl> </div> ); }; portal-gui/src/app/(app)/my-applications/myApplications.module.scss +49 −26 Original line number Diff line number Diff line Loading @@ -250,40 +250,63 @@ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); transition: transform 0.15s ease, box-shadow 0.15s ease; cursor: pointer; display: flex; flex-direction: column; gap: 0.75rem; &:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); } .header { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; } .title { font-size: 1.15rem; font-weight: 600; color: var(--text-heading); color: var(--blue-color); margin: 0; } .statusChip { padding: 4px 10px; border-radius: 10px; font-size: 0.75rem; font-weight: 600; .fields { display: flex; flex-direction: column; gap: 0.4rem; margin: 0; padding: 0; .field { display: grid; grid-template-columns: 100px 1fr; gap: 0.5rem; align-items: baseline; dt { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; color: white; letter-spacing: 0.06em; color: #999; white-space: nowrap; } .region, .version, .created { font-size: 0.9rem; color: var(--text-secondary); margin: 6px 0; dd { font-size: 0.85rem; color: #333; margin: 0; } } } .mono { font-family: monospace; font-size: 0.78rem; color: #555; word-break: break-all; } } No newline at end of file portal-gui/src/app/(app)/my-applications/page.tsx +45 −16 Original line number Diff line number Diff line Loading @@ -4,28 +4,49 @@ 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 { IApplicationProfile, IAppInstance, MyApplicationsTabKey } from "../../utils/interfaces"; import { tabs } from "../../utils/constants"; import { deployments } from "../../utils/tableHelpers"; import { DeploymentCard } from "./DeploymentCard"; import { CreateProfileModal } from "../../components/CreateProfileModal/CreateProfileModal"; 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); const [appsLoading, setAppsLoading] = useState(true); const [appsError, setAppsError] = useState<string | null>(null); const [instances, setInstances] = useState<IAppInstance[]>([]); const [instancesLoading, setInstancesLoading] = useState(false); const [instancesError, setInstancesError] = useState<string | null>(null); const [createOpen, setCreateOpen] = useState(false); useEffect(() => { if (activeTab !== "profile") return; setLoading(true); setError(null); setAppsLoading(true); setAppsError(null); fetch("/api/apps") .then((r) => r.json()) .then((data) => setApps(Array.isArray(data) ? data : [])) .catch(() => setError("Failed to load applications.")) .finally(() => setLoading(false)); .catch(() => setAppsError("Failed to load applications.")) .finally(() => setAppsLoading(false)); }, [activeTab]); useEffect(() => { if (activeTab !== "deployments") return; setInstancesLoading(true); setInstancesError(null); fetch("/api/appinstances") .then((r) => r.json()) .then((data) => { const list: IAppInstance[] = Array.isArray(data?.appInstanceInfo) ? data.appInstanceInfo : []; setInstances(list); }) .catch(() => setInstancesError("Failed to load deployments.")) .finally(() => setInstancesLoading(false)); }, [activeTab]); return ( Loading @@ -47,9 +68,9 @@ 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 && ( {appsLoading && <p className={styles.status}>Loading applications…</p>} {appsError && <p className={styles.statusError}>{appsError}</p>} {!appsLoading && !appsError && apps.length === 0 && ( <p className={styles.status}>No applications registered yet.</p> )} <div className={styles.cards}> Loading @@ -66,13 +87,21 @@ const page = () => { {/* ---------- DEPLOYMENTS TAB ---------- */} {activeTab === "deployments" && ( <> {instancesLoading && <p className={styles.status}>Loading deployments…</p>} {instancesError && <p className={styles.statusError}>{instancesError}</p>} {!instancesLoading && !instancesError && instances.length === 0 && ( <p className={styles.status}>No running deployments found.</p> )} <div className={styles.cards}> {deployments.map((deployment) => ( <DeploymentCard key={deployment.id} deployment={deployment} /> {instances.map((instance) => ( <DeploymentCard key={instance.appInstanceId} instance={instance} /> ))} </div> </> )} </div> <CreateProfileModal open={createOpen} onClose={() => setCreateOpen(false)} Loading portal-gui/src/app/api/appinstances/route.ts +23 −0 Original line number Diff line number Diff line import { NextResponse } from "next/server"; import { oeg } from "@/app/utils/constants"; export async function GET() { try { const res = await fetch(`${oeg.baseUrl}/appinstances`, { cache: "no-store", }); if (!res.ok) { return NextResponse.json( { error: `OEG returned ${res.status}` }, { status: res.status } ); } const data = await res.json(); return NextResponse.json(data); } catch { return NextResponse.json( { error: "Could not reach OEG" }, { status: 502 } ); } } export async function POST(req: Request) { try { const body = await req.json(); Loading portal-gui/src/app/components/Chip/StatusChip.tsx +3 −0 Original line number Diff line number Diff line Loading @@ -20,10 +20,13 @@ export const StatusChip = ({ status }: { status: string }) => { // Deployment statuses case "running": case "ready": return styles.running; case "deploying": case "pending": return styles.deploying; case "error": case "failed": return styles.error; case "stopped": return styles.stopped; Loading Loading
portal-gui/src/app/(app)/my-applications/DeploymentCard.tsx +30 −32 Original line number Diff line number Diff line "use client"; import React from "react"; import styles from "./myApplications.module.scss"; import { IDeployment } from "../../utils/interfaces"; import { IAppInstance } from "../../utils/interfaces"; import { StatusChip } from "../../components/Chip/StatusChip"; export const DeploymentCard = ({ deployment }: { deployment: IDeployment }) => { const getStatusClass = (status: IDeployment["status"]) => { switch (status) { case "running": return styles.statusRunning; case "deploying": return styles.statusDeploying; case "error": return styles.statusError; case "stopped": return styles.statusStopped; default: return ""; } }; export const DeploymentCard = ({ instance }: { instance: IAppInstance }) => { return ( <div className={styles.deployCard}> <div className={styles.header}> <h3 className={styles.title}>{deployment.name}</h3> <StatusChip status={deployment.status} /> <h3 className={styles.title}>{instance.name}</h3> <StatusChip status={instance.status} /> </div> <p className={styles.region}> <strong>Region:</strong> {deployment.region} </p> <p className={styles.version}> <strong>Version:</strong> {deployment.version} </p> <p className={styles.created}> <strong>Created:</strong> {new Date(deployment.createdAt).toLocaleString()} </p> <dl className={styles.fields}> <div className={styles.field}> <dt>Provider</dt> <dd>{instance.appProvider || "—"}</dd> </div> <div className={styles.field}> <dt>App ID</dt> <dd className={styles.mono}>{instance.appId}</dd> </div> <div className={styles.field}> <dt>Instance ID</dt> <dd className={styles.mono}>{instance.appInstanceId}</dd> </div> {instance.edgeCloudZoneId && ( <div className={styles.field}> <dt>Edge Zone</dt> <dd className={styles.mono}>{instance.edgeCloudZoneId}</dd> </div> )} {instance.kubernetesClusterRef && ( <div className={styles.field}> <dt>K8s Cluster</dt> <dd className={styles.mono}>{instance.kubernetesClusterRef}</dd> </div> )} </dl> </div> ); };
portal-gui/src/app/(app)/my-applications/myApplications.module.scss +49 −26 Original line number Diff line number Diff line Loading @@ -250,40 +250,63 @@ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); transition: transform 0.15s ease, box-shadow 0.15s ease; cursor: pointer; display: flex; flex-direction: column; gap: 0.75rem; &:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); } .header { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; } .title { font-size: 1.15rem; font-weight: 600; color: var(--text-heading); color: var(--blue-color); margin: 0; } .statusChip { padding: 4px 10px; border-radius: 10px; font-size: 0.75rem; font-weight: 600; .fields { display: flex; flex-direction: column; gap: 0.4rem; margin: 0; padding: 0; .field { display: grid; grid-template-columns: 100px 1fr; gap: 0.5rem; align-items: baseline; dt { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; color: white; letter-spacing: 0.06em; color: #999; white-space: nowrap; } .region, .version, .created { font-size: 0.9rem; color: var(--text-secondary); margin: 6px 0; dd { font-size: 0.85rem; color: #333; margin: 0; } } } .mono { font-family: monospace; font-size: 0.78rem; color: #555; word-break: break-all; } } No newline at end of file
portal-gui/src/app/(app)/my-applications/page.tsx +45 −16 Original line number Diff line number Diff line Loading @@ -4,28 +4,49 @@ 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 { IApplicationProfile, IAppInstance, MyApplicationsTabKey } from "../../utils/interfaces"; import { tabs } from "../../utils/constants"; import { deployments } from "../../utils/tableHelpers"; import { DeploymentCard } from "./DeploymentCard"; import { CreateProfileModal } from "../../components/CreateProfileModal/CreateProfileModal"; 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); const [appsLoading, setAppsLoading] = useState(true); const [appsError, setAppsError] = useState<string | null>(null); const [instances, setInstances] = useState<IAppInstance[]>([]); const [instancesLoading, setInstancesLoading] = useState(false); const [instancesError, setInstancesError] = useState<string | null>(null); const [createOpen, setCreateOpen] = useState(false); useEffect(() => { if (activeTab !== "profile") return; setLoading(true); setError(null); setAppsLoading(true); setAppsError(null); fetch("/api/apps") .then((r) => r.json()) .then((data) => setApps(Array.isArray(data) ? data : [])) .catch(() => setError("Failed to load applications.")) .finally(() => setLoading(false)); .catch(() => setAppsError("Failed to load applications.")) .finally(() => setAppsLoading(false)); }, [activeTab]); useEffect(() => { if (activeTab !== "deployments") return; setInstancesLoading(true); setInstancesError(null); fetch("/api/appinstances") .then((r) => r.json()) .then((data) => { const list: IAppInstance[] = Array.isArray(data?.appInstanceInfo) ? data.appInstanceInfo : []; setInstances(list); }) .catch(() => setInstancesError("Failed to load deployments.")) .finally(() => setInstancesLoading(false)); }, [activeTab]); return ( Loading @@ -47,9 +68,9 @@ 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 && ( {appsLoading && <p className={styles.status}>Loading applications…</p>} {appsError && <p className={styles.statusError}>{appsError}</p>} {!appsLoading && !appsError && apps.length === 0 && ( <p className={styles.status}>No applications registered yet.</p> )} <div className={styles.cards}> Loading @@ -66,13 +87,21 @@ const page = () => { {/* ---------- DEPLOYMENTS TAB ---------- */} {activeTab === "deployments" && ( <> {instancesLoading && <p className={styles.status}>Loading deployments…</p>} {instancesError && <p className={styles.statusError}>{instancesError}</p>} {!instancesLoading && !instancesError && instances.length === 0 && ( <p className={styles.status}>No running deployments found.</p> )} <div className={styles.cards}> {deployments.map((deployment) => ( <DeploymentCard key={deployment.id} deployment={deployment} /> {instances.map((instance) => ( <DeploymentCard key={instance.appInstanceId} instance={instance} /> ))} </div> </> )} </div> <CreateProfileModal open={createOpen} onClose={() => setCreateOpen(false)} Loading
portal-gui/src/app/api/appinstances/route.ts +23 −0 Original line number Diff line number Diff line import { NextResponse } from "next/server"; import { oeg } from "@/app/utils/constants"; export async function GET() { try { const res = await fetch(`${oeg.baseUrl}/appinstances`, { cache: "no-store", }); if (!res.ok) { return NextResponse.json( { error: `OEG returned ${res.status}` }, { status: res.status } ); } const data = await res.json(); return NextResponse.json(data); } catch { return NextResponse.json( { error: "Could not reach OEG" }, { status: 502 } ); } } export async function POST(req: Request) { try { const body = await req.json(); Loading
portal-gui/src/app/components/Chip/StatusChip.tsx +3 −0 Original line number Diff line number Diff line Loading @@ -20,10 +20,13 @@ export const StatusChip = ({ status }: { status: string }) => { // Deployment statuses case "running": case "ready": return styles.running; case "deploying": case "pending": return styles.deploying; case "error": case "failed": return styles.error; case "stopped": return styles.stopped; Loading