From 888f5899345ad594500ad5b56237dd8ba4d1d5d9 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:47:04 +0200 Subject: [PATCH] feat(ui): show deployment status + rich pending-deploy tooltip on app header Add a StatusDot + colored Badge next to the app name in the deployment page header, showing the latest deployment's status (RUNNING / STARTING / FAILED / STOPPED / DEGRADED / STOPPING). The existing "Pending deploy" badge now carries a tooltip explaining *why*: either a list of local unsaved edits, or a per-field diff against the last successful deploy's snapshot (field, staged vs deployed values). When server-side differences exist, the badge shows the count. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../pages/AppsTab/AppDeploymentPage/index.tsx | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/ui/src/pages/AppsTab/AppDeploymentPage/index.tsx b/ui/src/pages/AppsTab/AppDeploymentPage/index.tsx index 89ac7498..9be0545f 100644 --- a/ui/src/pages/AppsTab/AppDeploymentPage/index.tsx +++ b/ui/src/pages/AppsTab/AppDeploymentPage/index.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef } from 'react'; import { useParams, useLocation, useNavigate } from 'react-router'; import { useQueryClient } from '@tanstack/react-query'; -import { AlertDialog, Badge, Button, Tabs, useToast } from '@cameleer/design-system'; +import { AlertDialog, Badge, Button, StatusDot, Tabs, useToast } from '@cameleer/design-system'; import { useEnvironmentStore } from '../../../api/environment-store'; import { useEnvironments } from '../../../api/queries/admin/environments'; import { @@ -39,6 +39,16 @@ import styles from './AppDeploymentPage.module.css'; type TabKey = 'monitoring' | 'resources' | 'variables' | 'sensitive-keys' | 'deployment' | 'traces' | 'recording'; +const STATUS_COLORS: Record = { + RUNNING: 'running', STARTING: 'warning', FAILED: 'error', STOPPED: 'auto', + DEGRADED: 'warning', STOPPING: 'auto', +}; + +const DEPLOY_STATUS_DOT: Record = { + RUNNING: 'live', STARTING: 'running', DEGRADED: 'stale', + STOPPING: 'stale', STOPPED: 'dead', FAILED: 'error', +}; + function slugify(name: string): string { return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 100); } @@ -393,9 +403,35 @@ export default function AppDeploymentPage() {

{app ? app.displayName : 'Create Application'}

- {app && !deploymentInProgress && (dirty.anyLocalEdit || serverDirtyAgainstDeploy) && ( - + {app && latestDeployment && ( + + + + )} + {app && !deploymentInProgress && (dirty.anyLocalEdit || serverDirtyAgainstDeploy) && (() => { + const diffs = dirtyState?.differences ?? []; + const noSnapshot = diffs.length === 1 && diffs[0].field === 'snapshot'; + const tooltip = dirty.anyLocalEdit + ? 'Local edits not yet saved — see tabs marked with *.' + : noSnapshot + ? 'No successful deployment recorded for this app yet.' + : diffs.length > 0 + ? `Differs from last successful deploy:\n` + + diffs.map((d) => `• ${d.field}\n staged: ${d.staged}\n deployed: ${d.deployed}`).join('\n') + : 'Server reports config differs from last successful deploy.'; + return ( + + + + ); + })()}
{dirty.anyLocalEdit && (