Loading portal-gui/index.html +1 −0 Changes for portal-gui/index.html: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="theme-color" content="#ffffff" /> <title>OOP Portal</title> </head> <body> Loading portal-gui/src/app/theme-provider.tsx +24 −2 Changes for portal-gui/src/app/theme-provider.tsx: 24 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -31,6 +31,14 @@ function isTheme(value: string | null): value is Theme { return THEME_VALUES.includes(value as Theme); } function saveTheme(storageKey: string, theme: Theme) { try { localStorage.setItem(storageKey, theme); } catch { // Keep the in-memory theme when browser storage is unavailable. } } function getSystemTheme(): ResolvedTheme { if (window.matchMedia(COLOR_SCHEME_QUERY).matches) { return 'dark'; Loading Loading @@ -85,17 +93,21 @@ export function ThemeProvider({ ...props }: ThemeProviderProps) { const [theme, setThemeState] = React.useState<Theme>(() => { try { const storedTheme = localStorage.getItem(storageKey); if (isTheme(storedTheme)) { return storedTheme; } } catch { // Use the default theme when browser storage is unavailable. } return defaultTheme; }); const setTheme = React.useCallback( (nextTheme: Theme) => { localStorage.setItem(storageKey, nextTheme); saveTheme(storageKey, nextTheme); setThemeState(nextTheme); }, [storageKey], Loading @@ -112,6 +124,12 @@ export function ThemeProvider({ root.classList.remove('light', 'dark'); root.classList.add(resolvedTheme); document .querySelector('meta[name="theme-color"]') ?.setAttribute( 'content', resolvedTheme === 'dark' ? '#090b0c' : '#ffffff', ); if (restoreTransitions) { restoreTransitions(); Loading Loading @@ -167,7 +185,7 @@ export function ThemeProvider({ ? 'light' : 'dark'; localStorage.setItem(storageKey, nextTheme); saveTheme(storageKey, nextTheme); return nextTheme; }); }; Loading @@ -181,9 +199,13 @@ export function ThemeProvider({ React.useEffect(() => { const handleStorageChange = (event: StorageEvent) => { try { if (event.storageArea !== localStorage) { return; } } catch { return; } if (event.key !== storageKey) { return; Loading portal-gui/src/components/applications/create-app-dialog.tsx +44 −15 Changes for portal-gui/src/components/applications/create-app-dialog.tsx: 44 added lines, 15 removed lines. Original line number Diff line number Diff line Loading @@ -295,14 +295,14 @@ export const CreateApplicationDialog = ({ </div> <div className="space-y-2"> <Label>Package type *</Label> <Label htmlFor="app-package-type">Package type *</Label> <Select value={form.packageType} onValueChange={(v) => set('packageType', v as AppFormState['packageType']) } > <SelectTrigger> <SelectTrigger id="app-package-type"> <SelectValue /> </SelectTrigger> <SelectContent> Loading @@ -329,14 +329,14 @@ export const CreateApplicationDialog = ({ <div className="grid gap-4 md:grid-cols-[150px_1fr]"> <div className="space-y-2"> <Label>Type *</Label> <Label htmlFor="app-repo-type">Type *</Label> <Select value={form.repoType} onValueChange={(v) => set('repoType', v as AppFormState['repoType']) } > <SelectTrigger> <SelectTrigger id="app-repo-type"> <SelectValue /> </SelectTrigger> <SelectContent> Loading @@ -361,29 +361,33 @@ export const CreateApplicationDialog = ({ <div className="rounded-lg border bg-muted/30 p-4"> <div className="grid gap-4 md:grid-cols-3"> <div className="space-y-2"> <Label>Username</Label> <Label htmlFor="app-repo-username">Username</Label> <Input id="app-repo-username" value={form.userName} onChange={(v) => set('userName', v.target.value)} /> </div> <div className="space-y-2"> <Label>Credentials / token</Label> <Label htmlFor="app-repo-credentials"> Credentials / token </Label> <Input id="app-repo-credentials" value={form.credentials} onChange={(v) => set('credentials', v.target.value)} type="password" /> </div> <div className="space-y-2"> <Label>Auth type</Label> <Label htmlFor="app-auth-type">Auth type</Label> <Select value={form.authType} onValueChange={(v) => set('authType', v as AppFormState['authType']) } > <SelectTrigger> <SelectTrigger id="app-auth-type"> <SelectValue /> </SelectTrigger> <SelectContent> Loading Loading @@ -421,8 +425,11 @@ export const CreateApplicationDialog = ({ > <div className="flex items-end gap-3"> <div className="flex-1 space-y-2"> <Label>Component name *</Label> <Label htmlFor={`app-component-${ci}`}> Component name * </Label> <Input id={`app-component-${ci}`} value={comp.componentName} onChange={(v) => setComponent(ci, 'componentName', v.target.value) Loading @@ -435,9 +442,10 @@ export const CreateApplicationDialog = ({ type="button" variant="ghost" size="icon" aria-label={`Remove component ${ci + 1}`} onClick={() => removeComponent(ci)} > <Trash2 className="h-4 w-4" /> <Trash2 aria-hidden className="h-4 w-4" /> </Button> )} </div> Loading @@ -450,6 +458,7 @@ export const CreateApplicationDialog = ({ className="grid gap-2 md:grid-cols-[1fr_100px_80px_150px_auto]" > <Input aria-label={`Component ${ci + 1}, interface ${ii + 1} ID`} value={iface.interfaceId} onChange={(v) => setInterface(ci, ii, 'interfaceId', v.target.value) Loading @@ -462,7 +471,9 @@ export const CreateApplicationDialog = ({ setInterface(ci, ii, 'protocol', v) } > <SelectTrigger> <SelectTrigger aria-label={`Component ${ci + 1}, interface ${ii + 1} protocol`} > <SelectValue /> </SelectTrigger> <SelectContent> Loading @@ -472,6 +483,7 @@ export const CreateApplicationDialog = ({ </SelectContent> </Select> <Input aria-label={`Component ${ci + 1}, interface ${ii + 1} port`} type="number" min={1} max={65535} Loading @@ -488,7 +500,9 @@ export const CreateApplicationDialog = ({ setInterface(ci, ii, 'visibilityType', value) } > <SelectTrigger> <SelectTrigger aria-label={`Component ${ci + 1}, interface ${ii + 1} visibility`} > <SelectValue /> </SelectTrigger> <SelectContent> Loading @@ -506,9 +520,10 @@ export const CreateApplicationDialog = ({ type="button" variant="ghost" size="icon" aria-label={`Remove interface ${ii + 1} from component ${ci + 1}`} onClick={() => removeInterface(ci, ii)} > <Trash2 className="h-4 w-4" /> <Trash2 aria-hidden className="h-4 w-4" /> </Button> ) : ( <div /> Loading @@ -535,7 +550,9 @@ export const CreateApplicationDialog = ({ <section className="space-y-3"> <div className="flex items-center justify-between"> <div> <h3 className="text-sm font-semibold">Required Resources *</h3> <h3 id="app-resources-label" className="text-sm font-semibold"> Required Resources * </h3> <p className="text-xs text-muted-foreground"> Kubernetes resources are required. CPU values are cores; memory is MB. Loading @@ -546,6 +563,8 @@ export const CreateApplicationDialog = ({ type="button" variant="ghost" size="sm" aria-expanded={resourcesExpanded} aria-controls="app-resources" onClick={() => setResourcesExpanded((v) => !v)} > {resourcesExpanded ? '▲ Collapse' : '▼ Expand'} Loading @@ -553,13 +572,23 @@ export const CreateApplicationDialog = ({ </div> {resourcesExpanded && ( <Textarea id="app-resources" aria-labelledby="app-resources-label" aria-invalid={Boolean(jsonError) || undefined} aria-describedby={jsonError ? 'app-resources-error' : undefined} value={form.requiredResourcesJson} onChange={(e) => handleResourcesChange(e.target.value)} className="min-h-[300px] font-mono text-xs" /> )} {jsonError && ( <p className="text-sm text-destructive">{jsonError}</p> <p id="app-resources-error" role="alert" className="text-sm text-destructive" > {jsonError} </p> )} </section> Loading portal-gui/src/features/apis/api-card.tsx +1 −1 Changes for portal-gui/src/features/apis/api-card.tsx: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -7,7 +7,7 @@ import { Link } from 'react-router-dom'; export function ApiCard({ api }: ApiCardProps) { return ( <Link to={`${ROUTES.APIS}/${api.id}`}> <Card className="group h-full cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg"> <Card className="group h-full cursor-pointer transition-[transform,box-shadow] hover:-translate-y-1 hover:shadow-lg motion-reduce:hover:translate-y-0"> <CardHeader> <CardTitle className="text-2xl">{api.title}</CardTitle> <div className="flex flex-wrap items-center gap-2"> Loading portal-gui/src/features/apis/apis.tsx +5 −1 Changes for portal-gui/src/features/apis/apis.tsx: 5 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -32,8 +32,12 @@ export function ApisPage() { <PageHero title="Browse and discover OpenOP APIs"> <div className="mt-5 max-w-2xl"> <div className="relative"> <Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search aria-hidden className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Input aria-label="Search APIs by title or category" placeholder="Search APIs by title or category..." className="h-11 pl-10" value={search} Loading Loading
portal-gui/index.html +1 −0 Changes for portal-gui/index.html: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="theme-color" content="#ffffff" /> <title>OOP Portal</title> </head> <body> Loading
portal-gui/src/app/theme-provider.tsx +24 −2 Changes for portal-gui/src/app/theme-provider.tsx: 24 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -31,6 +31,14 @@ function isTheme(value: string | null): value is Theme { return THEME_VALUES.includes(value as Theme); } function saveTheme(storageKey: string, theme: Theme) { try { localStorage.setItem(storageKey, theme); } catch { // Keep the in-memory theme when browser storage is unavailable. } } function getSystemTheme(): ResolvedTheme { if (window.matchMedia(COLOR_SCHEME_QUERY).matches) { return 'dark'; Loading Loading @@ -85,17 +93,21 @@ export function ThemeProvider({ ...props }: ThemeProviderProps) { const [theme, setThemeState] = React.useState<Theme>(() => { try { const storedTheme = localStorage.getItem(storageKey); if (isTheme(storedTheme)) { return storedTheme; } } catch { // Use the default theme when browser storage is unavailable. } return defaultTheme; }); const setTheme = React.useCallback( (nextTheme: Theme) => { localStorage.setItem(storageKey, nextTheme); saveTheme(storageKey, nextTheme); setThemeState(nextTheme); }, [storageKey], Loading @@ -112,6 +124,12 @@ export function ThemeProvider({ root.classList.remove('light', 'dark'); root.classList.add(resolvedTheme); document .querySelector('meta[name="theme-color"]') ?.setAttribute( 'content', resolvedTheme === 'dark' ? '#090b0c' : '#ffffff', ); if (restoreTransitions) { restoreTransitions(); Loading Loading @@ -167,7 +185,7 @@ export function ThemeProvider({ ? 'light' : 'dark'; localStorage.setItem(storageKey, nextTheme); saveTheme(storageKey, nextTheme); return nextTheme; }); }; Loading @@ -181,9 +199,13 @@ export function ThemeProvider({ React.useEffect(() => { const handleStorageChange = (event: StorageEvent) => { try { if (event.storageArea !== localStorage) { return; } } catch { return; } if (event.key !== storageKey) { return; Loading
portal-gui/src/components/applications/create-app-dialog.tsx +44 −15 Changes for portal-gui/src/components/applications/create-app-dialog.tsx: 44 added lines, 15 removed lines. Original line number Diff line number Diff line Loading @@ -295,14 +295,14 @@ export const CreateApplicationDialog = ({ </div> <div className="space-y-2"> <Label>Package type *</Label> <Label htmlFor="app-package-type">Package type *</Label> <Select value={form.packageType} onValueChange={(v) => set('packageType', v as AppFormState['packageType']) } > <SelectTrigger> <SelectTrigger id="app-package-type"> <SelectValue /> </SelectTrigger> <SelectContent> Loading @@ -329,14 +329,14 @@ export const CreateApplicationDialog = ({ <div className="grid gap-4 md:grid-cols-[150px_1fr]"> <div className="space-y-2"> <Label>Type *</Label> <Label htmlFor="app-repo-type">Type *</Label> <Select value={form.repoType} onValueChange={(v) => set('repoType', v as AppFormState['repoType']) } > <SelectTrigger> <SelectTrigger id="app-repo-type"> <SelectValue /> </SelectTrigger> <SelectContent> Loading @@ -361,29 +361,33 @@ export const CreateApplicationDialog = ({ <div className="rounded-lg border bg-muted/30 p-4"> <div className="grid gap-4 md:grid-cols-3"> <div className="space-y-2"> <Label>Username</Label> <Label htmlFor="app-repo-username">Username</Label> <Input id="app-repo-username" value={form.userName} onChange={(v) => set('userName', v.target.value)} /> </div> <div className="space-y-2"> <Label>Credentials / token</Label> <Label htmlFor="app-repo-credentials"> Credentials / token </Label> <Input id="app-repo-credentials" value={form.credentials} onChange={(v) => set('credentials', v.target.value)} type="password" /> </div> <div className="space-y-2"> <Label>Auth type</Label> <Label htmlFor="app-auth-type">Auth type</Label> <Select value={form.authType} onValueChange={(v) => set('authType', v as AppFormState['authType']) } > <SelectTrigger> <SelectTrigger id="app-auth-type"> <SelectValue /> </SelectTrigger> <SelectContent> Loading Loading @@ -421,8 +425,11 @@ export const CreateApplicationDialog = ({ > <div className="flex items-end gap-3"> <div className="flex-1 space-y-2"> <Label>Component name *</Label> <Label htmlFor={`app-component-${ci}`}> Component name * </Label> <Input id={`app-component-${ci}`} value={comp.componentName} onChange={(v) => setComponent(ci, 'componentName', v.target.value) Loading @@ -435,9 +442,10 @@ export const CreateApplicationDialog = ({ type="button" variant="ghost" size="icon" aria-label={`Remove component ${ci + 1}`} onClick={() => removeComponent(ci)} > <Trash2 className="h-4 w-4" /> <Trash2 aria-hidden className="h-4 w-4" /> </Button> )} </div> Loading @@ -450,6 +458,7 @@ export const CreateApplicationDialog = ({ className="grid gap-2 md:grid-cols-[1fr_100px_80px_150px_auto]" > <Input aria-label={`Component ${ci + 1}, interface ${ii + 1} ID`} value={iface.interfaceId} onChange={(v) => setInterface(ci, ii, 'interfaceId', v.target.value) Loading @@ -462,7 +471,9 @@ export const CreateApplicationDialog = ({ setInterface(ci, ii, 'protocol', v) } > <SelectTrigger> <SelectTrigger aria-label={`Component ${ci + 1}, interface ${ii + 1} protocol`} > <SelectValue /> </SelectTrigger> <SelectContent> Loading @@ -472,6 +483,7 @@ export const CreateApplicationDialog = ({ </SelectContent> </Select> <Input aria-label={`Component ${ci + 1}, interface ${ii + 1} port`} type="number" min={1} max={65535} Loading @@ -488,7 +500,9 @@ export const CreateApplicationDialog = ({ setInterface(ci, ii, 'visibilityType', value) } > <SelectTrigger> <SelectTrigger aria-label={`Component ${ci + 1}, interface ${ii + 1} visibility`} > <SelectValue /> </SelectTrigger> <SelectContent> Loading @@ -506,9 +520,10 @@ export const CreateApplicationDialog = ({ type="button" variant="ghost" size="icon" aria-label={`Remove interface ${ii + 1} from component ${ci + 1}`} onClick={() => removeInterface(ci, ii)} > <Trash2 className="h-4 w-4" /> <Trash2 aria-hidden className="h-4 w-4" /> </Button> ) : ( <div /> Loading @@ -535,7 +550,9 @@ export const CreateApplicationDialog = ({ <section className="space-y-3"> <div className="flex items-center justify-between"> <div> <h3 className="text-sm font-semibold">Required Resources *</h3> <h3 id="app-resources-label" className="text-sm font-semibold"> Required Resources * </h3> <p className="text-xs text-muted-foreground"> Kubernetes resources are required. CPU values are cores; memory is MB. Loading @@ -546,6 +563,8 @@ export const CreateApplicationDialog = ({ type="button" variant="ghost" size="sm" aria-expanded={resourcesExpanded} aria-controls="app-resources" onClick={() => setResourcesExpanded((v) => !v)} > {resourcesExpanded ? '▲ Collapse' : '▼ Expand'} Loading @@ -553,13 +572,23 @@ export const CreateApplicationDialog = ({ </div> {resourcesExpanded && ( <Textarea id="app-resources" aria-labelledby="app-resources-label" aria-invalid={Boolean(jsonError) || undefined} aria-describedby={jsonError ? 'app-resources-error' : undefined} value={form.requiredResourcesJson} onChange={(e) => handleResourcesChange(e.target.value)} className="min-h-[300px] font-mono text-xs" /> )} {jsonError && ( <p className="text-sm text-destructive">{jsonError}</p> <p id="app-resources-error" role="alert" className="text-sm text-destructive" > {jsonError} </p> )} </section> Loading
portal-gui/src/features/apis/api-card.tsx +1 −1 Changes for portal-gui/src/features/apis/api-card.tsx: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -7,7 +7,7 @@ import { Link } from 'react-router-dom'; export function ApiCard({ api }: ApiCardProps) { return ( <Link to={`${ROUTES.APIS}/${api.id}`}> <Card className="group h-full cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg"> <Card className="group h-full cursor-pointer transition-[transform,box-shadow] hover:-translate-y-1 hover:shadow-lg motion-reduce:hover:translate-y-0"> <CardHeader> <CardTitle className="text-2xl">{api.title}</CardTitle> <div className="flex flex-wrap items-center gap-2"> Loading
portal-gui/src/features/apis/apis.tsx +5 −1 Changes for portal-gui/src/features/apis/apis.tsx: 5 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -32,8 +32,12 @@ export function ApisPage() { <PageHero title="Browse and discover OpenOP APIs"> <div className="mt-5 max-w-2xl"> <div className="relative"> <Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search aria-hidden className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Input aria-label="Search APIs by title or category" placeholder="Search APIs by title or category..." className="h-11 pl-10" value={search} Loading