Loading portal-gui/Dockerfile +15 −7 Original line number Diff line number Diff line FROM node:20-slim # Stage 1 – install dependencies FROM node:20-alpine AS deps WORKDIR /app COPY package.json package-lock.json* ./ RUN npm install --legacy-peer-deps FROM node:20-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . ENV NEXT_TELEMETRY_DISABLED=1 RUN npm run build FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" RUN npm run build COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public EXPOSE 3000 CMD ["npm", "run", "start"] CMD ["node", "server.js"] portal-gui/next.config.ts +1 −0 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { reactCompiler: true, output: "standalone", }; export default nextConfig; portal-gui/package-lock.json +24 −18 Original line number Diff line number Diff line Loading @@ -59,6 +59,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", Loading Loading @@ -164,7 +165,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" Loading @@ -174,7 +175,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" Loading Loading @@ -279,7 +280,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", Loading Loading @@ -2178,18 +2179,6 @@ "node": "^18 || ^20 || >= 21" } }, "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/tree-sitter": { "version": "0.22.4", "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz", "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==", "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "node_modules/@swagger-api/apidom-reference": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-1.6.0.tgz", Loading Loading @@ -2379,6 +2368,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", "peer": true, "dependencies": { "csstype": "^3.2.2" } Loading Loading @@ -2466,6 +2456,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", Loading Loading @@ -2991,6 +2982,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "peer": true, "bin": { "acorn": "bin/acorn" }, Loading Loading @@ -3305,8 +3297,9 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "dev": true, "devOptional": true, "license": "MIT", "peer": true, "dependencies": { "@babel/types": "^7.26.0" } Loading Loading @@ -3395,6 +3388,7 @@ } ], "license": "MIT", "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", Loading Loading @@ -4138,6 +4132,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", Loading Loading @@ -4323,6 +4318,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", Loading Loading @@ -5134,7 +5130,8 @@ "version": "5.1.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", "license": "MIT" "license": "MIT", "peer": true }, "node_modules/import-fresh": { "version": "3.3.1", Loading Loading @@ -6599,6 +6596,7 @@ "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", "integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==", "license": "MIT", "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/ramda" Loading Loading @@ -6647,6 +6645,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" } Loading @@ -6656,6 +6655,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", "peer": true, "dependencies": { "scheduler": "^0.27.0" }, Loading Loading @@ -6795,7 +6795,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "license": "MIT" "license": "MIT", "peer": true }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", Loading Loading @@ -7111,6 +7112,7 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", "license": "MIT", "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", Loading Loading @@ -7719,6 +7721,7 @@ "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" } Loading Loading @@ -7808,6 +7811,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "peer": true, "engines": { "node": ">=12" }, Loading Loading @@ -8074,6 +8078,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" Loading Loading @@ -8455,6 +8460,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } Loading portal-gui/src/app/(app)/my-applications/DeploymentCard.tsx +53 −3 Original line number Diff line number Diff line "use client"; import React from "react"; import React, { useState } from "react"; import styles from "./myApplications.module.scss"; import { IAppInstance } from "../../utils/interfaces"; import { StatusChip } from "../../components/Chip/StatusChip"; import { ConfirmModal } from "../../components/ConfirmModal/ConfirmModal"; type Props = { instance: IAppInstance; onDelete: (appInstanceId: string) => void; }; export const DeploymentCard = ({ instance, onDelete }: Props) => { const [deleteOpen, setDeleteOpen] = useState(false); const [deleting, setDeleting] = useState(false); const [deleteError, setDeleteError] = useState<string | null>(null); const handleDelete = async () => { setDeleting(true); setDeleteError(null); try { const res = await fetch(`/api/appinstances/${instance.appInstanceId}`, { method: "DELETE" }); if (res.ok) { onDelete(instance.appInstanceId); setDeleteOpen(false); } else { setDeleteError("Failed to remove deployment."); } } catch { setDeleteError("Could not reach the server."); } finally { setDeleting(false); } }; export const DeploymentCard = ({ instance }: { instance: IAppInstance }) => { return ( <> <div className={styles.deployCard}> <div className={styles.header}> <h3 className={styles.title}>{instance.name}</h3> <div className={styles.headerRight}> <StatusChip status={instance.status} /> <button className={styles.deleteBtn} onClick={() => setDeleteOpen(true)} aria-label="Remove deployment" > Remove </button> </div> </div> <dl className={styles.fields}> Loading Loading @@ -39,5 +77,17 @@ export const DeploymentCard = ({ instance }: { instance: IAppInstance }) => { )} </dl> </div> <ConfirmModal open={deleteOpen} title="Remove deployment" message={`Are you sure you want to remove "${instance.name}"?`} confirmLabel="Remove" loading={deleting} error={deleteError} onConfirm={handleDelete} onClose={() => { setDeleteOpen(false); setDeleteError(null); }} /> </> ); }; portal-gui/src/app/(app)/my-applications/myApplications.module.scss +31 −0 Original line number Diff line number Diff line Loading @@ -266,6 +266,37 @@ gap: 0.5rem; } .headerRight { display: flex; align-items: center; gap: 0.5rem; } .deleteBtn { background: none; border: 1px solid #e0e0e0; border-radius: 4px; color: #aaa; font-size: 0.72rem; font-weight: 600; 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; } } .title { font-size: 1.15rem; font-weight: 600; Loading Loading
portal-gui/Dockerfile +15 −7 Original line number Diff line number Diff line FROM node:20-slim # Stage 1 – install dependencies FROM node:20-alpine AS deps WORKDIR /app COPY package.json package-lock.json* ./ RUN npm install --legacy-peer-deps FROM node:20-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . ENV NEXT_TELEMETRY_DISABLED=1 RUN npm run build FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" RUN npm run build COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public EXPOSE 3000 CMD ["npm", "run", "start"] CMD ["node", "server.js"]
portal-gui/next.config.ts +1 −0 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { reactCompiler: true, output: "standalone", }; export default nextConfig;
portal-gui/package-lock.json +24 −18 Original line number Diff line number Diff line Loading @@ -59,6 +59,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", Loading Loading @@ -164,7 +165,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" Loading @@ -174,7 +175,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" Loading Loading @@ -279,7 +280,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", Loading Loading @@ -2178,18 +2179,6 @@ "node": "^18 || ^20 || >= 21" } }, "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/tree-sitter": { "version": "0.22.4", "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz", "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==", "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "node_modules/@swagger-api/apidom-reference": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-1.6.0.tgz", Loading Loading @@ -2379,6 +2368,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", "peer": true, "dependencies": { "csstype": "^3.2.2" } Loading Loading @@ -2466,6 +2456,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", Loading Loading @@ -2991,6 +2982,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "peer": true, "bin": { "acorn": "bin/acorn" }, Loading Loading @@ -3305,8 +3297,9 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "dev": true, "devOptional": true, "license": "MIT", "peer": true, "dependencies": { "@babel/types": "^7.26.0" } Loading Loading @@ -3395,6 +3388,7 @@ } ], "license": "MIT", "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", Loading Loading @@ -4138,6 +4132,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", Loading Loading @@ -4323,6 +4318,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", Loading Loading @@ -5134,7 +5130,8 @@ "version": "5.1.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", "license": "MIT" "license": "MIT", "peer": true }, "node_modules/import-fresh": { "version": "3.3.1", Loading Loading @@ -6599,6 +6596,7 @@ "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", "integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==", "license": "MIT", "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/ramda" Loading Loading @@ -6647,6 +6645,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" } Loading @@ -6656,6 +6655,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", "peer": true, "dependencies": { "scheduler": "^0.27.0" }, Loading Loading @@ -6795,7 +6795,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "license": "MIT" "license": "MIT", "peer": true }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", Loading Loading @@ -7111,6 +7112,7 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", "license": "MIT", "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", Loading Loading @@ -7719,6 +7721,7 @@ "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" } Loading Loading @@ -7808,6 +7811,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "peer": true, "engines": { "node": ">=12" }, Loading Loading @@ -8074,6 +8078,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" Loading Loading @@ -8455,6 +8460,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } Loading
portal-gui/src/app/(app)/my-applications/DeploymentCard.tsx +53 −3 Original line number Diff line number Diff line "use client"; import React from "react"; import React, { useState } from "react"; import styles from "./myApplications.module.scss"; import { IAppInstance } from "../../utils/interfaces"; import { StatusChip } from "../../components/Chip/StatusChip"; import { ConfirmModal } from "../../components/ConfirmModal/ConfirmModal"; type Props = { instance: IAppInstance; onDelete: (appInstanceId: string) => void; }; export const DeploymentCard = ({ instance, onDelete }: Props) => { const [deleteOpen, setDeleteOpen] = useState(false); const [deleting, setDeleting] = useState(false); const [deleteError, setDeleteError] = useState<string | null>(null); const handleDelete = async () => { setDeleting(true); setDeleteError(null); try { const res = await fetch(`/api/appinstances/${instance.appInstanceId}`, { method: "DELETE" }); if (res.ok) { onDelete(instance.appInstanceId); setDeleteOpen(false); } else { setDeleteError("Failed to remove deployment."); } } catch { setDeleteError("Could not reach the server."); } finally { setDeleting(false); } }; export const DeploymentCard = ({ instance }: { instance: IAppInstance }) => { return ( <> <div className={styles.deployCard}> <div className={styles.header}> <h3 className={styles.title}>{instance.name}</h3> <div className={styles.headerRight}> <StatusChip status={instance.status} /> <button className={styles.deleteBtn} onClick={() => setDeleteOpen(true)} aria-label="Remove deployment" > Remove </button> </div> </div> <dl className={styles.fields}> Loading Loading @@ -39,5 +77,17 @@ export const DeploymentCard = ({ instance }: { instance: IAppInstance }) => { )} </dl> </div> <ConfirmModal open={deleteOpen} title="Remove deployment" message={`Are you sure you want to remove "${instance.name}"?`} confirmLabel="Remove" loading={deleting} error={deleteError} onConfirm={handleDelete} onClose={() => { setDeleteOpen(false); setDeleteError(null); }} /> </> ); };
portal-gui/src/app/(app)/my-applications/myApplications.module.scss +31 −0 Original line number Diff line number Diff line Loading @@ -266,6 +266,37 @@ gap: 0.5rem; } .headerRight { display: flex; align-items: center; gap: 0.5rem; } .deleteBtn { background: none; border: 1px solid #e0e0e0; border-radius: 4px; color: #aaa; font-size: 0.72rem; font-weight: 600; 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; } } .title { font-size: 1.15rem; font-weight: 600; Loading