Loading portal-gui/src/app/(app)/app-profiles/[appId]/page.tsx +19 −5 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ 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 { DeployModal } from "@/app/components/DeployModal/DeployModal"; import buttons from "@/app/styles/buttons.module.scss"; import styles from "./profilePage.module.scss"; Loading @@ -16,6 +17,7 @@ const ProfilePage = () => { const [app, setApp] = useState<IApplicationProfile | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [deployOpen, setDeployOpen] = useState(false); useEffect(() => { fetch(`/api/apps/${appId}`) Loading @@ -41,15 +43,20 @@ const ProfilePage = () => { <Button className={buttons.iconSubtle} onClick={() => router.back()}> {backArrowIcon} </Button> <Button className={buttons.primary} onClick={() => setDeployOpen(true)}> Deploy </Button> </div> <div className={styles.header}> <div className={styles.headerTop}> <div className={styles.badges}> <span className={styles.badge}>{app.packageType}</span> {app.appProvider && ( <span className={styles.badgeSecondary}>{app.appProvider}</span> )} </div> </div> <h1 className={styles.title}>{app.name}</h1> <p className={styles.appId}>appID: {app.appId}</p> </div> Loading Loading @@ -125,6 +132,13 @@ const ProfilePage = () => { </pre> </> )} <DeployModal open={deployOpen} appId={app.appId} appName={app.name} onClose={() => setDeployOpen(false)} onDeployed={() => setDeployOpen(false)} /> </div> ); }; Loading portal-gui/src/app/(app)/app-profiles/[appId]/profilePage.module.scss +3 −1 Original line number Diff line number Diff line Loading @@ -9,7 +9,8 @@ .btnBox { display: flex; justify-content: flex-end; justify-content: space-between; align-items: center; } .header { Loading @@ -23,6 +24,7 @@ display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; } .badge { Loading portal-gui/src/app/(app)/my-applications/ProfileCard.tsx +87 −54 Original line number Diff line number Diff line Loading @@ -5,6 +5,8 @@ import React, { useState } from "react"; import styles from "./myApplications.module.scss"; import { truncate } from "../../utils/helpers"; import { IApplicationProfile } from "../../utils/interfaces"; import { DeployModal } from "../../components/DeployModal/DeployModal"; import { ConfirmModal } from "../../components/ConfirmModal/ConfirmModal"; type Props = { app: IApplicationProfile; Loading @@ -12,22 +14,24 @@ type Props = { }; export const ProfileCard = ({ app, onDelete }: Props) => { const [deployOpen, setDeployOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const [deleting, setDeleting] = useState(false); const [deleteError, setDeleteError] = useState<string | null>(null); const handleDelete = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!confirm(`Delete "${app.name}"?`)) return; const handleDelete = async () => { setDeleting(true); setDeleteError(null); try { const res = await fetch(`/api/apps/${app.appId}`, { method: "DELETE" }); if (res.ok) { onDelete(app.appId); setDeleteOpen(false); } else { alert("Failed to delete application."); setDeleteError("Failed to delete application."); } } catch { alert("Could not reach the server."); setDeleteError("Could not reach the server."); } finally { setDeleting(false); } Loading @@ -38,6 +42,7 @@ export const ProfileCard = ({ app, onDelete }: Props) => { ); return ( <> <Link href={`/app-profiles/${app.appId}`} className={styles.card}> <div className={styles.cardHeader}> Loading @@ -47,15 +52,23 @@ export const ProfileCard = ({ app, onDelete }: Props) => { <span className={styles.badgeSecondary}>{app.appProvider}</span> )} </div> <div className={styles.cardActions}> <button className={styles.deployBtn} onClick={(e) => { e.preventDefault(); e.stopPropagation(); setDeployOpen(true); }} aria-label="Deploy application" > Deploy </button> <button className={styles.deleteBtn} onClick={handleDelete} disabled={deleting} onClick={(e) => { e.preventDefault(); e.stopPropagation(); setDeleteOpen(true); }} aria-label="Delete application" > {deleting ? "Deleting…" : "Delete profile"} Delete </button> </div> </div> <h2 className={styles.title}>{app.name}</h2> Loading Loading @@ -94,5 +107,25 @@ export const ProfileCard = ({ app, onDelete }: Props) => { )} </dl> </Link> <DeployModal open={deployOpen} appId={app.appId} appName={app.name} onClose={() => setDeployOpen(false)} onDeployed={() => setDeployOpen(false)} /> <ConfirmModal open={deleteOpen} title="Delete application profile" message={`Are you sure you want to delete "${app.name}"?`} confirmLabel="Delete" loading={deleting} error={deleteError} onConfirm={handleDelete} onClose={() => { setDeleteOpen(false); setDeleteError(null); }} /> </> ); }; portal-gui/src/app/(app)/my-applications/myApplications.module.scss +23 −0 Original line number Diff line number Diff line Loading @@ -105,6 +105,29 @@ align-items: center; } .cardActions { display: flex; gap: 0.4rem; } .deployBtn { background: var(--main-gradient); border: none; border-radius: 4px; color: #fff; font-size: 0.72rem; font-weight: 700; padding: 3px 10px; cursor: pointer; white-space: nowrap; line-height: 1.4; transition: opacity 0.15s ease; &:hover { opacity: 0.85; } } .deleteBtn { background: none; border: 1px solid #e0e0e0; Loading portal-gui/src/app/api/appinstances/route.ts 0 → 100644 +39 −0 Original line number Diff line number Diff line import { NextResponse } from "next/server"; import { oeg } from "@/app/utils/constants"; export async function POST(req: Request) { try { const body = await req.json(); const res = await fetch(`${oeg.baseUrl}/appinstances`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), cache: "no-store", }); if (!res.ok) { const err = await res.json().catch(() => ({})); return NextResponse.json( { error: err.message ?? `OEG returned ${res.status}` }, { status: res.status } ); } const data = await res.json(); // OEG returns 202 even on conflict — surface the warning as an error if (data.warning || data.details?.error) { return NextResponse.json( { error: data.details?.error ?? data.warning }, { status: 409 } ); } return NextResponse.json(data, { status: 202 }); } catch { return NextResponse.json( { error: "Could not reach OEG" }, { status: 502 } ); } } Loading
portal-gui/src/app/(app)/app-profiles/[appId]/page.tsx +19 −5 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ 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 { DeployModal } from "@/app/components/DeployModal/DeployModal"; import buttons from "@/app/styles/buttons.module.scss"; import styles from "./profilePage.module.scss"; Loading @@ -16,6 +17,7 @@ const ProfilePage = () => { const [app, setApp] = useState<IApplicationProfile | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [deployOpen, setDeployOpen] = useState(false); useEffect(() => { fetch(`/api/apps/${appId}`) Loading @@ -41,15 +43,20 @@ const ProfilePage = () => { <Button className={buttons.iconSubtle} onClick={() => router.back()}> {backArrowIcon} </Button> <Button className={buttons.primary} onClick={() => setDeployOpen(true)}> Deploy </Button> </div> <div className={styles.header}> <div className={styles.headerTop}> <div className={styles.badges}> <span className={styles.badge}>{app.packageType}</span> {app.appProvider && ( <span className={styles.badgeSecondary}>{app.appProvider}</span> )} </div> </div> <h1 className={styles.title}>{app.name}</h1> <p className={styles.appId}>appID: {app.appId}</p> </div> Loading Loading @@ -125,6 +132,13 @@ const ProfilePage = () => { </pre> </> )} <DeployModal open={deployOpen} appId={app.appId} appName={app.name} onClose={() => setDeployOpen(false)} onDeployed={() => setDeployOpen(false)} /> </div> ); }; Loading
portal-gui/src/app/(app)/app-profiles/[appId]/profilePage.module.scss +3 −1 Original line number Diff line number Diff line Loading @@ -9,7 +9,8 @@ .btnBox { display: flex; justify-content: flex-end; justify-content: space-between; align-items: center; } .header { Loading @@ -23,6 +24,7 @@ display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; } .badge { Loading
portal-gui/src/app/(app)/my-applications/ProfileCard.tsx +87 −54 Original line number Diff line number Diff line Loading @@ -5,6 +5,8 @@ import React, { useState } from "react"; import styles from "./myApplications.module.scss"; import { truncate } from "../../utils/helpers"; import { IApplicationProfile } from "../../utils/interfaces"; import { DeployModal } from "../../components/DeployModal/DeployModal"; import { ConfirmModal } from "../../components/ConfirmModal/ConfirmModal"; type Props = { app: IApplicationProfile; Loading @@ -12,22 +14,24 @@ type Props = { }; export const ProfileCard = ({ app, onDelete }: Props) => { const [deployOpen, setDeployOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const [deleting, setDeleting] = useState(false); const [deleteError, setDeleteError] = useState<string | null>(null); const handleDelete = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!confirm(`Delete "${app.name}"?`)) return; const handleDelete = async () => { setDeleting(true); setDeleteError(null); try { const res = await fetch(`/api/apps/${app.appId}`, { method: "DELETE" }); if (res.ok) { onDelete(app.appId); setDeleteOpen(false); } else { alert("Failed to delete application."); setDeleteError("Failed to delete application."); } } catch { alert("Could not reach the server."); setDeleteError("Could not reach the server."); } finally { setDeleting(false); } Loading @@ -38,6 +42,7 @@ export const ProfileCard = ({ app, onDelete }: Props) => { ); return ( <> <Link href={`/app-profiles/${app.appId}`} className={styles.card}> <div className={styles.cardHeader}> Loading @@ -47,15 +52,23 @@ export const ProfileCard = ({ app, onDelete }: Props) => { <span className={styles.badgeSecondary}>{app.appProvider}</span> )} </div> <div className={styles.cardActions}> <button className={styles.deployBtn} onClick={(e) => { e.preventDefault(); e.stopPropagation(); setDeployOpen(true); }} aria-label="Deploy application" > Deploy </button> <button className={styles.deleteBtn} onClick={handleDelete} disabled={deleting} onClick={(e) => { e.preventDefault(); e.stopPropagation(); setDeleteOpen(true); }} aria-label="Delete application" > {deleting ? "Deleting…" : "Delete profile"} Delete </button> </div> </div> <h2 className={styles.title}>{app.name}</h2> Loading Loading @@ -94,5 +107,25 @@ export const ProfileCard = ({ app, onDelete }: Props) => { )} </dl> </Link> <DeployModal open={deployOpen} appId={app.appId} appName={app.name} onClose={() => setDeployOpen(false)} onDeployed={() => setDeployOpen(false)} /> <ConfirmModal open={deleteOpen} title="Delete application profile" message={`Are you sure you want to delete "${app.name}"?`} confirmLabel="Delete" loading={deleting} error={deleteError} onConfirm={handleDelete} onClose={() => { setDeleteOpen(false); setDeleteError(null); }} /> </> ); };
portal-gui/src/app/(app)/my-applications/myApplications.module.scss +23 −0 Original line number Diff line number Diff line Loading @@ -105,6 +105,29 @@ align-items: center; } .cardActions { display: flex; gap: 0.4rem; } .deployBtn { background: var(--main-gradient); border: none; border-radius: 4px; color: #fff; font-size: 0.72rem; font-weight: 700; padding: 3px 10px; cursor: pointer; white-space: nowrap; line-height: 1.4; transition: opacity 0.15s ease; &:hover { opacity: 0.85; } } .deleteBtn { background: none; border: 1px solid #e0e0e0; Loading
portal-gui/src/app/api/appinstances/route.ts 0 → 100644 +39 −0 Original line number Diff line number Diff line import { NextResponse } from "next/server"; import { oeg } from "@/app/utils/constants"; export async function POST(req: Request) { try { const body = await req.json(); const res = await fetch(`${oeg.baseUrl}/appinstances`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), cache: "no-store", }); if (!res.ok) { const err = await res.json().catch(() => ({})); return NextResponse.json( { error: err.message ?? `OEG returned ${res.status}` }, { status: res.status } ); } const data = await res.json(); // OEG returns 202 even on conflict — surface the warning as an error if (data.warning || data.details?.error) { return NextResponse.json( { error: data.details?.error ?? data.warning }, { status: 409 } ); } return NextResponse.json(data, { status: 202 }); } catch { return NextResponse.json( { error: "Could not reach OEG" }, { status: 502 } ); } }