From 2846d1ca050cb9408d48ba7db641748152d8a674 Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Wed, 30 Sep 2026 16:17:14 +0000 Subject: [PATCH 1/2] feat: refactor app navigation links to use app drawer for all app configuration settings --- src/app/monitoring/app-monitoring.tsx | 3 +- src/app/monitoring/app-volumes-monitoring.tsx | 3 +- .../[projectId]/app-components/apps-table.tsx | 9 ++- .../app-components/edit-app-dialog.tsx | 3 +- .../app-components/project-network-graph.tsx | 60 ++++++++++++++++--- .../drawer/drawer-settings.tsx | 31 ++++++---- .../node-details-drawer.tsx | 28 ++++++++- ...project-network-graph-app-context-menu.tsx | 10 +++- ...-network-graph-connection-context-menu.tsx | 22 +++++-- .../app-components/project-overview.tsx | 19 +++--- .../app-network-policy-rule-section.tsx | 3 +- .../[appId]/advanced/network-policy-graph.tsx | 11 +++- .../app/[appId]/environment/env-edit.tsx | 2 +- .../overview/deployments-grid-view.tsx | 23 ++++++- src/app/project/app/[appId]/overview/logs.tsx | 21 ++++++- src/app/sidebar-client.tsx | 11 ++-- src/shared/utils/path-builder.utils.ts | 16 +++++ 17 files changed, 218 insertions(+), 57 deletions(-) create mode 100644 src/shared/utils/path-builder.utils.ts diff --git a/src/app/monitoring/app-monitoring.tsx b/src/app/monitoring/app-monitoring.tsx index 7cb75598..a9af74a5 100644 --- a/src/app/monitoring/app-monitoring.tsx +++ b/src/app/monitoring/app-monitoring.tsx @@ -16,6 +16,7 @@ import { Button } from '@/components/ui/button'; import { AppMonitoringUsageModel } from '@/shared/model/app-monitoring-usage.model'; import PodStatusIndicator from '@/components/custom/pod-status-indicator'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { PathBuilderUtils } from '@/shared/utils/path-builder.utils'; export default function AppRessourceMonitoring({ appsRessourceUsage @@ -74,7 +75,7 @@ export default function AppRessourceMonitoring({ {KubeSizeConverter.convertBytesToReadableSize(item.ramUsageBytes)} - + diff --git a/src/app/monitoring/app-volumes-monitoring.tsx b/src/app/monitoring/app-volumes-monitoring.tsx index 8a552eaf..3466850d 100644 --- a/src/app/monitoring/app-volumes-monitoring.tsx +++ b/src/app/monitoring/app-volumes-monitoring.tsx @@ -16,6 +16,7 @@ import { ExternalLink } from 'lucide-react'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { Progress } from "@/components/ui/progress" +import { PathBuilderUtils } from '@/shared/utils/path-builder.utils'; type AppVolumeMonitoringUsageExtendedModel = AppVolumeMonitoringUsageModel & { usedPercentage: number; @@ -92,7 +93,7 @@ export default function AppVolumeMonitoring({
{KubeSizeConverter.convertBytesToReadableSize(item.usedBytes)} / {KubeSizeConverter.convertBytesToReadableSize(item.capacityBytes)}
- + diff --git a/src/app/project/[projectId]/app-components/apps-table.tsx b/src/app/project/[projectId]/app-components/apps-table.tsx index 7d073e77..b45a5bf9 100644 --- a/src/app/project/[projectId]/app-components/apps-table.tsx +++ b/src/app/project/[projectId]/app-components/apps-table.tsx @@ -15,6 +15,7 @@ import { EditAppDialog } from "./edit-app-dialog"; import { UserSession } from "@/shared/model/sim-session.model"; import { UserGroupUtils } from "@/shared/utils/role.utils"; import PodStatusIndicator from "@/components/custom/pod-status-indicator"; +import { PathBuilderUtils } from "@/shared/utils/path-builder.utils"; export default function AppTable({ @@ -28,11 +29,13 @@ export default function AppTable({ }) { const { openConfirmDialog: openDialog } = useConfirmDialog(); + const drawerHref = (appId: string) => + PathBuilderUtils.projectAppDrawer(projectId, appId, 'deployments', 'table'); return <> {item.name} ], @@ -47,7 +50,7 @@ export default function AppTable({ ['status', 'Status', true, (item) => ], ]} data={apps} - onItemClickLink={(item) => `/project/app/${item.id}`} + onItemClickLink={(item) => drawerHref(item.id)} actionCol={(item) => <>
@@ -61,7 +64,7 @@ export default function AppTable({ Actions - + Show App Details diff --git a/src/app/project/[projectId]/app-components/edit-app-dialog.tsx b/src/app/project/[projectId]/app-components/edit-app-dialog.tsx index db337dd2..eb4e9973 100644 --- a/src/app/project/[projectId]/app-components/edit-app-dialog.tsx +++ b/src/app/project/[projectId]/app-components/edit-app-dialog.tsx @@ -6,6 +6,7 @@ import { useRouter } from "next/navigation"; import { cloneElement, type MouseEvent, type ReactElement } from "react"; import type { App } from "@prisma/client"; import { useInputDialog } from "@/frontend/states/zustand.states"; +import { PathBuilderUtils } from "@/shared/utils/path-builder.utils"; export function EditAppDialog({ children, @@ -32,7 +33,7 @@ export function EditAppDialog({ if (!name) { return; } const result = await Toast.fromAction(() => createApp(name, projectId, existingItem?.id)); if (result.status === "success" && !existingItem && openAppAfterCreate) { - router.push(existingItem ? `/project/app/${result!.data!.id}` : `/project/app/${result!.data!.id}?tabName=general`); + router.push(PathBuilderUtils.projectAppDrawer(projectId, result.data!.id, 'settings')); } }; diff --git a/src/app/project/[projectId]/app-components/project-network-graph.tsx b/src/app/project/[projectId]/app-components/project-network-graph.tsx index 146ea23f..1b09f76e 100644 --- a/src/app/project/[projectId]/app-components/project-network-graph.tsx +++ b/src/app/project/[projectId]/app-components/project-network-graph.tsx @@ -44,7 +44,7 @@ import { type ProjectNetworkGraphAppContextMenuProps, } from './project-network-graph/project-network-graph-app-context-menu'; import { ProjectNetworkGraphConnectionContextMenu } from './project-network-graph/project-network-graph-connection-context-menu'; -import { connectionDeletionProvenance, NetworkGraphNode } from './project-network-graph/project-network-graph-projection'; +import { connectionDeletionProvenance, type NetworkGraphEdge, NetworkGraphNode } from './project-network-graph/project-network-graph-projection'; import { useProjectNetworkGraph } from './project-network-graph/use-project-network-graph'; import { graphEdgePresentation, graphLegendItems, NETWORK_GRAPH_COLORS } from './project-network-graph/project-network-graph-visual-semantics'; import { getFocusedNodeViewport } from './project-network-graph/project-network-graph-focus'; @@ -53,6 +53,7 @@ import { Toast } from '@/frontend/utils/toast.utils'; import { AppNetworkPolicyRuleEditModel, NetworkPolicySelectableTarget } from '@/shared/model/app-network-policy-edit.model'; import { NetworkPolicyRuleUtils } from '@/shared/utils/network-policy-rule.utils'; import { AppNetworkPolicyDraft, AppNetworkPolicyDraftUtils } from '@/shared/utils/app-network-policy-draft.utils'; +import { InternalHostnameUtils } from '@/server/utils/internal-hostname.utils'; import AppNetworkPolicyRuleDialog from '@/app/project/app/[appId]/advanced/app-network-policy-rule-dialog'; import { saveAppNetworkPolicyConfiguration } from '@/app/project/app/[appId]/advanced/actions'; import { deleteApp } from '@/app/project/[projectId]/actions'; @@ -77,7 +78,10 @@ type WorkloadNodeData = NetworkGraphNode & { connectedToSelection?: boolean; contextMenu?: Omit; }; -type ConnectionEdgeData = { onDelete: () => void }; +type ConnectionEdgeData = { + onDelete?: () => void; + internalHostnames: { hostname: string; port: number }[]; +}; type ProjectNetworkGraphProps = { apps: AppExtendedModel[]; projectId: string; @@ -147,11 +151,39 @@ function ConnectionEdge(props: EdgeProps) { const data = props.data as ConnectionEdgeData | undefined; if (!data) return ; return ( - + ); } + +function getInternalHostnames( + edge: NetworkGraphEdge, + drafts: Record, + apps: AppExtendedModel[], +) { + const hostnames = new Map(); + + for (const provenance of edge.ruleProvenance) { + const rule = drafts[provenance.ownerAppId]?.rules.find(item => item.key === provenance.ruleKey); + if (!rule) continue; + + const target = rule.type === 'INGRESS' + ? apps.find(app => app.id === provenance.ownerAppId) + : rule.targetType === 'APP' + ? { id: rule.targetId, projectId: rule.targetProjectId } + : undefined; + if (!target?.projectId) continue; + + const hostname = InternalHostnameUtils.getInternalBaseUrlForApp(target, rule.port); + hostnames.set(hostname, { hostname, port: rule.port }); + } + + return Array.from(hostnames.values()).sort((left, right) => left.port - right.port); +} const edgeTypes = { connection: ConnectionEdge }; function ProjectNetworkGraphCanvasContextMenu({ @@ -244,6 +276,7 @@ function ProjectNetworkGraphEditor({ const [saving, setSaving] = useState(false); const [connectionSourceNodeId, setConnectionSourceNodeId] = useState(); const [connectionTargetNodeId, setConnectionTargetNodeId] = useState(); + const [environmentAppId, setEnvironmentAppId] = useState(); const [graphHeight, setGraphHeight] = useState(); const graphContainerRef = useRef(null); const drawerContentRef = useRef(null); @@ -385,6 +418,10 @@ function ProjectNetworkGraphEditor({ onOpenDrawerTab: (tab: DrawerTab) => { drawerSession.openAppTab(app.id, tab); }, + onShowEnvironment: () => { + setEnvironmentAppId(app.id); + drawerSession.openAppTab(app.id, 'settings'); + }, onDelete: () => void deleteLocalApp(app.id), } : undefined, connectedToSelection: !selectedNodeId || (layout?.edges ?? []).some(edge => @@ -398,15 +435,22 @@ function ProjectNetworkGraphEditor({ useEffect(() => setNodes(projectedNodes), [projectedNodes, setNodes]); const edges = useMemo(() => (layout?.edges ?? []).map(edge => { const presentation = graphEdgePresentation(edge); + const deletionProvenance = connectionDeletionProvenance(edge, writableAppIds); + const internalHostnames = getInternalHostnames(edge, drafts, apps); return { id: edge.id, source: edge.source, target: edge.target, sourceHandle: presentation.sourceHandle, targetHandle: presentation.targetHandle, - type: connectionDeletionProvenance(edge, writableAppIds) ? 'connection' : 'smoothstep', - data: connectionDeletionProvenance(edge, writableAppIds) - ? { onDelete: () => deleteConnection(edge.id) } + type: edge.direction === 'CONNECTION' && (deletionProvenance || internalHostnames.length > 0) + ? 'connection' + : 'smoothstep', + data: edge.direction === 'CONNECTION' && (deletionProvenance || internalHostnames.length > 0) + ? { + ...(deletionProvenance ? { onDelete: () => deleteConnection(edge.id) } : {}), + internalHostnames, + } : undefined, pathOptions: { offset: 20 }, markerStart: edge.internetIngress ? { type: MarkerType.ArrowClosed, color: presentation.color, width: 16, height: 16 } : undefined, @@ -425,7 +469,7 @@ function ProjectNetworkGraphEditor({ labelBgPadding: [6, 3] as [number, number], labelBgBorderRadius: 6, }; - }), [deleteConnection, layout?.edges, selectedNodeId, writableAppIds]); + }), [apps, deleteConnection, drafts, layout?.edges, selectedNodeId, writableAppIds]); const selectedNode = nodes.find(node => node.id === selectedNodeId)?.data as NetworkGraphNode | undefined; const selectedApp = selectedNode?.kind === 'APP' ? apps.find(app => app.id === selectedNode.id.replace('APP:', '')) : undefined; const selectedAppRole = selectedApp ? UserGroupUtils.getRolePermissionForApp(session, selectedApp.id) ?? undefined : undefined; @@ -611,6 +655,8 @@ function ProjectNetworkGraphEditor({ onTabChange={tab => { if (selectedApp) drawerSession.openAppTab(selectedApp.id, tab); }} + openEnvironment={environmentAppId === selectedApp?.id} + onEnvironmentOpened={() => setEnvironmentAppId(undefined)} />}
diff --git a/src/app/project/[projectId]/app-components/project-network-graph/drawer/drawer-settings.tsx b/src/app/project/[projectId]/app-components/project-network-graph/drawer/drawer-settings.tsx index 3bbade4a..00592c31 100644 --- a/src/app/project/[projectId]/app-components/project-network-graph/drawer/drawer-settings.tsx +++ b/src/app/project/[projectId]/app-components/project-network-graph/drawer/drawer-settings.tsx @@ -8,6 +8,7 @@ import { SlidersHorizontal, Zap, } from 'lucide-react'; +import { useCallback, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import BasicAuth from '@/app/project/app/[appId]/advanced/basic-auth'; @@ -33,14 +34,31 @@ export function DrawerSettings({ role, storageClasses, gitSshPublicKey, + openEnvironment = false, + onEnvironmentOpened, }: { app: AppExtendedModel; role: RolePermissionEnum; storageClasses: string[]; gitSshPublicKey?: string; + openEnvironment?: boolean; + onEnvironmentOpened?: () => void; }) { const readonly = role !== RolePermissionEnum.READWRITE; const { openNestedDrawer } = useNestedDrawer(); + const openEnvironmentEditor = useCallback(() => { + openNestedDrawer({ + title: 'Environment variables', + description: <>Values are encrypted at rest and never shown. Names starting with QS_ are reserved by QuickStack., + content: , + }); + }, [app, openNestedDrawer, readonly]); + + useEffect(() => { + if (!openEnvironment) return; + openEnvironmentEditor(); + onEnvironmentOpened?.(); + }, [onEnvironmentOpened, openEnvironment, openEnvironmentEditor]); return (
@@ -88,18 +106,7 @@ export function DrawerSettings({ > - openNestedDrawer({ - title: 'Environment variables', - content: ( - - ), - }) - } + onEdit={openEnvironmentEditor} /> ; node: NetworkGraphNode; @@ -262,8 +271,11 @@ export function NodeDetailsDrawer({ onOpenChangeComplete: (open: boolean) => void; requestedTab?: string | null; onTabChange: (tab: DrawerTab) => void; + openEnvironment?: boolean; + onEnvironmentOpened?: () => void; }) { const isApp = node.kind === 'APP'; + const isExternalApp = isApp && node.external; const needsSourceConfiguration = app && role === RolePermissionEnum.READWRITE && @@ -394,7 +406,19 @@ export function NodeDetailsDrawer({
)} - {needsSourceConfiguration ? ( + {isExternalApp ? ( + + + + + + Settings unavailable + + This app belongs to another project. Its settings can only be edited from that project. + + + + ) : needsSourceConfiguration ? (
diff --git a/src/app/project/[projectId]/app-components/project-network-graph/project-network-graph-app-context-menu.tsx b/src/app/project/[projectId]/app-components/project-network-graph/project-network-graph-app-context-menu.tsx index 7b5d2d02..a3d73f7c 100644 --- a/src/app/project/[projectId]/app-components/project-network-graph/project-network-graph-app-context-menu.tsx +++ b/src/app/project/[projectId]/app-components/project-network-graph/project-network-graph-app-context-menu.tsx @@ -1,7 +1,7 @@ 'use client'; import type { ReactNode } from 'react'; -import { Box, Globe2, Hammer, Logs, Play, Rocket, RotateCwClock, Settings, Square, Trash2 } from 'lucide-react'; +import { Box, Globe2, Hammer, Logs, Play, Rocket, RotateCwClock, Settings, Square, Trash2, Zap } from 'lucide-react'; import { ContextMenu, ContextMenuContent, @@ -25,6 +25,7 @@ export type ProjectNetworkGraphAppContextMenuProps = { allowInternetAccess: boolean; onToggleInternetAccess: () => void; onOpenDrawerTab: (tab: 'deployments' | 'logs' | 'backups' | 'settings') => void; + onShowEnvironment: () => void; onDelete: () => void; children: ReactNode; }; @@ -36,7 +37,8 @@ export function ProjectNetworkGraphAppContextMenu({ onToggleInternetAccess, onDelete, children, - onOpenDrawerTab + onOpenDrawerTab, + onShowEnvironment, }: ProjectNetworkGraphAppContextMenuProps) { const deploymentStatus = usePodsStatus( (state) => state.podsStatus.get(app.id)?.deploymentStatus ?? 'UNKNOWN', @@ -99,6 +101,10 @@ export function ProjectNetworkGraphAppContextMenu({ View Backups )} + + + Show Env Variables + onOpenDrawerTab('settings')}> View Settings diff --git a/src/app/project/[projectId]/app-components/project-network-graph/project-network-graph-connection-context-menu.tsx b/src/app/project/[projectId]/app-components/project-network-graph/project-network-graph-connection-context-menu.tsx index c9026dc0..a4c00f96 100644 --- a/src/app/project/[projectId]/app-components/project-network-graph/project-network-graph-connection-context-menu.tsx +++ b/src/app/project/[projectId]/app-components/project-network-graph/project-network-graph-connection-context-menu.tsx @@ -1,29 +1,43 @@ 'use client'; import type { ReactNode } from 'react'; -import { Trash2 } from 'lucide-react'; +import { Copy, Trash2 } from 'lucide-react'; +import { toast } from 'sonner'; import { ContextMenu, ContextMenuContent, ContextMenuItem, + ContextMenuSeparator, ContextMenuTrigger, } from '@/components/ui/context-menu'; export function ProjectNetworkGraphConnectionContextMenu({ onDelete, + internalHostnames, children, }: { - onDelete: () => void; + onDelete?: () => void; + internalHostnames: { hostname: string; port: number }[]; children: ReactNode; }) { return ( }>{children} event.stopPropagation()}> - + {internalHostnames.map(({ hostname, port }) => ( + { + void navigator.clipboard.writeText(hostname); + toast.success(`Internal hostname for port ${port} copied`); + }}> + + Copy internal hostname ({port}) + + ))} + {onDelete && internalHostnames.length > 0 && } + {onDelete && Delete connection - + } ); diff --git a/src/app/project/[projectId]/app-components/project-overview.tsx b/src/app/project/[projectId]/app-components/project-overview.tsx index f0cda138..8bcc34ec 100644 --- a/src/app/project/[projectId]/app-components/project-overview.tsx +++ b/src/app/project/[projectId]/app-components/project-overview.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import AppTable from "./apps-table"; import ProjectNetworkGraph from "../app-components/project-network-graph"; import { UserSession } from "@/shared/model/sim-session.model"; @@ -57,7 +57,6 @@ export default function AppProjectOverview({ const searchParams = useSearchParams(); const { openDialog } = useDialog(); const requestedTab = searchParams.get('tab'); - const requestedDrawerAppId = searchParams.get('drawerAppId'); const [currentTab, setCurrentTab] = useState('graph'); useEffect(() => { @@ -72,17 +71,13 @@ export default function AppProjectOverview({ }, [openDialog, showNewNetworkPolicyExplanation, apps.length]); useEffect(() => { - if (requestedDrawerAppId) { - setCurrentTab('graph'); - return; - } if (isProjectOverviewTab(requestedTab)) { setCurrentTab(requestedTab); return; } const savedTab = window.localStorage.getItem(tabStorageKey()); setCurrentTab(isProjectOverviewTab(savedTab) ? savedTab : 'graph'); - }, [projectId, requestedDrawerAppId, requestedTab]); + }, [projectId, requestedTab]); const handleTabChange = (value: string) => { if (!isProjectOverviewTab(value)) return; @@ -140,7 +135,7 @@ export default function AppProjectOverview({
- + @@ -150,10 +145,10 @@ export default function AppProjectOverview({ {canCreate && }
- +
- - +
+
- +
); } diff --git a/src/app/project/app/[appId]/advanced/app-network-policy-rule-section.tsx b/src/app/project/app/[appId]/advanced/app-network-policy-rule-section.tsx index a64cffcc..3d2870e8 100644 --- a/src/app/project/app/[appId]/advanced/app-network-policy-rule-section.tsx +++ b/src/app/project/app/[appId]/advanced/app-network-policy-rule-section.tsx @@ -11,6 +11,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@ import { toast } from 'sonner'; import { NetworkPolicyDirection, NetworkPolicyTargetProject } from '@/shared/model/app-network-policy-edit.model'; import { InternalHostnameUtils } from '@/server/utils/internal-hostname.utils'; +import { PathBuilderUtils } from '@/shared/utils/path-builder.utils'; export type AppNetworkPolicyDirection = NetworkPolicyDirection; @@ -130,7 +131,7 @@ function RuleRow({ rule, readonly, onDelete, currentProjectId, projects }: { {rule.targetType === 'APP' - ? {rule.targetName} + ? {rule.targetName} : rule.targetName} {projectName} ยท {targetTypeLabel} diff --git a/src/app/project/app/[appId]/advanced/network-policy-graph.tsx b/src/app/project/app/[appId]/advanced/network-policy-graph.tsx index 534dac7a..47daa018 100644 --- a/src/app/project/app/[appId]/advanced/network-policy-graph.tsx +++ b/src/app/project/app/[appId]/advanced/network-policy-graph.tsx @@ -20,6 +20,7 @@ import { Bot, Boxes, Cloud, MoveRight } from 'lucide-react'; import { cn } from '@/frontend/utils/utils'; import { AppNetworkPolicyRuleWithTargetAppModel } from '@/shared/model/app-extended.model'; import { NetworkPolicyTargetProject } from '@/shared/model/app-network-policy-edit.model'; +import { PathBuilderUtils } from '@/shared/utils/path-builder.utils'; type WorkloadType = 'APP' | 'AGENT'; @@ -86,6 +87,7 @@ function collectNeighbors(rules: AppNetworkPolicyRuleWithTargetAppModel[], appId type PeerNodeData = { type: WorkloadType; + projectId: string; name: string; side: 'left' | 'right'; both: boolean; @@ -248,7 +250,7 @@ export default function NetworkPolicyGraph({ id: `${neighbor.type}:${neighbor.id}`, type: 'peer', position: { x: COLUMN_X_LEFT - PEER_WIDTH / 2, y: y - 28 }, - data: { type: neighbor.type, name: neighbor.name, side: 'left', both: false, externalProject: neighbor.projectId !== appProjectId, caption: captionFor(neighbor) }, + data: { type: neighbor.type, projectId: neighbor.projectId, name: neighbor.name, side: 'left', both: false, externalProject: neighbor.projectId !== appProjectId, caption: captionFor(neighbor) }, }); edges.push({ ...edgeLabelStyle(), @@ -270,7 +272,7 @@ export default function NetworkPolicyGraph({ id: `${neighbor.type}:${neighbor.id}`, type: 'peer', position: { x: COLUMN_X_RIGHT - PEER_WIDTH / 2, y: y - 28 }, - data: { type: neighbor.type, name: neighbor.name, side: 'right', both: hasIngress, externalProject: neighbor.projectId !== appProjectId, caption: captionFor(neighbor) }, + data: { type: neighbor.type, projectId: neighbor.projectId, name: neighbor.name, side: 'right', both: hasIngress, externalProject: neighbor.projectId !== appProjectId, caption: captionFor(neighbor) }, }); edges.push({ ...edgeLabelStyle(), @@ -366,7 +368,10 @@ export default function NetworkPolicyGraph({ onNodeClick={(_event, node) => { if (node.type !== 'peer') return; const [workloadType, workloadId] = String(node.id).split(':'); - router.push(workloadType === 'AGENT' ? `/project/agent/${workloadId}` : `/project/app/${workloadId}`); + const peer = node.data as PeerNodeData; + router.push(workloadType === 'AGENT' + ? `/project/agent/${workloadId}` + : PathBuilderUtils.projectAppDrawer(peer.projectId, workloadId)); }} > diff --git a/src/app/project/app/[appId]/environment/env-edit.tsx b/src/app/project/app/[appId]/environment/env-edit.tsx index 5150153b..65c22920 100644 --- a/src/app/project/app/[appId]/environment/env-edit.tsx +++ b/src/app/project/app/[appId]/environment/env-edit.tsx @@ -97,7 +97,7 @@ export default function EnvEdit({ app, readonly, hideCard = false }: { )} /> - {!readonly && + {!readonly && Save } diff --git a/src/app/project/app/[appId]/overview/deployments-grid-view.tsx b/src/app/project/app/[appId]/overview/deployments-grid-view.tsx index 41f38caf..200f2114 100644 --- a/src/app/project/app/[appId]/overview/deployments-grid-view.tsx +++ b/src/app/project/app/[appId]/overview/deployments-grid-view.tsx @@ -1,4 +1,4 @@ -import { Boxes, EllipsisVertical, File, GitCommit, LucideTerminal, RotateCcw } from 'lucide-react'; +import { Boxes, EllipsisVertical, File, GitCommit, LucideTerminal, Rocket, RotateCcw } from 'lucide-react'; import ShortCommitHash from '@/components/custom/short-commit-hash'; import { Button } from '@/components/ui/button'; import { @@ -12,6 +12,13 @@ import { formatDateTime } from '@/frontend/utils/format.utils'; import type { DeploymentInfoModel } from '@/shared/model/deployment-info.model'; import { appBuildMethodLabels } from '@/shared/model/app-source-info.model'; import DeploymentStatusBadge from './deployment-status-badge'; +import { + Empty, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from '@/components/ui/empty'; type DeploymentsGridViewProps = { deployments: DeploymentInfoModel[]; @@ -31,7 +38,19 @@ export function DeploymentsGridView({ onRollback, }: DeploymentsGridViewProps) { if (deployments.length === 0) { - return

No deployments yet.

; + return ( + + + + + + No deployments available + + This app has no deployments yet. + + + + ); } return ( diff --git a/src/app/project/app/[appId]/overview/logs.tsx b/src/app/project/app/[appId]/overview/logs.tsx index 96599212..d8cd0758 100644 --- a/src/app/project/app/[appId]/overview/logs.tsx +++ b/src/app/project/app/[appId]/overview/logs.tsx @@ -15,6 +15,13 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip import { RolePermissionEnum } from "@/shared/model/role-extended.model.ts"; import { useDialog, usePodsStatus } from "@/frontend/states/zustand.states"; import { cn } from "@/frontend/utils/utils"; +import { + Empty, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from "@/components/ui/empty"; export default function Logs({ app, @@ -123,7 +130,19 @@ export default function Logs({ useFullHeight && 'flex min-h-0 flex-1 flex-col', )}> {!appPods && } - {appPods && appPods.length === 0 &&
No running pods found for this app.
} + {appPods && appPods.length === 0 && ( + + + + + + No running pods found + + This app has no running pods with logs available. + + + + )} {selectedPod && appPods &&