From 9ecc9ee72a5ca549d796dc975260858eba4009a1 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 23 Apr 2026 00:51:11 +0200 Subject: [PATCH] ui(deploy): pending-deploy badge + Start/Stop in page header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Add a 'Pending deploy' Badge next to the app name when there are local edits or the saved state differs from the last deploy. Makes the undeployed-changes state visible even when the user isn't looking at the tab asterisks. 2. Move Start/Stop buttons from StatusCard into the page header, next to Delete. Runs off the latest deployment's status — Stop when RUNNING/STARTING/DEGRADED, Start (triggers a redeploy of the last version) when STOPPED. DeploymentTab and StatusCard shed their onStop/onStart props. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../DeploymentTab/DeploymentTab.tsx | 6 +- .../DeploymentTab/StatusCard.tsx | 18 +---- .../pages/AppsTab/AppDeploymentPage/index.tsx | 66 ++++++++++++------- 3 files changed, 47 insertions(+), 43 deletions(-) diff --git a/ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/DeploymentTab.tsx b/ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/DeploymentTab.tsx index 8a3bcc0e..4322e5d1 100644 --- a/ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/DeploymentTab.tsx +++ b/ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/DeploymentTab.tsx @@ -12,11 +12,9 @@ interface Props { appSlug: string; envSlug: string; externalUrl: string; - onStop: (deploymentId: string) => void; - onStart: (deploymentId: string) => void; } -export function DeploymentTab({ deployments, versions, appSlug, envSlug, externalUrl, onStop, onStart }: Props) { +export function DeploymentTab({ deployments, versions, appSlug, envSlug, externalUrl }: Props) { const latest = deployments .slice() .sort((a, b) => (b.createdAt ?? '').localeCompare(a.createdAt ?? ''))[0] ?? null; @@ -33,8 +31,6 @@ export function DeploymentTab({ deployments, versions, appSlug, envSlug, externa deployment={latest} version={version} externalUrl={externalUrl} - onStop={() => onStop(latest.id)} - onStart={() => onStart(latest.id)} /> {latest.status === 'STARTING' && ( diff --git a/ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/StatusCard.tsx b/ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/StatusCard.tsx index 617c14e2..a8a6294c 100644 --- a/ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/StatusCard.tsx +++ b/ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/StatusCard.tsx @@ -1,4 +1,4 @@ -import { Badge, StatusDot, MonoText, Button } from '@cameleer/design-system'; +import { Badge, StatusDot, MonoText } from '@cameleer/design-system'; import type { Deployment, AppVersion } from '../../../../api/queries/admin/apps'; import { timeAgo } from '../../../../utils/format-utils'; import styles from '../AppDeploymentPage.module.css'; @@ -13,21 +13,13 @@ const DEPLOY_STATUS_DOT = { STOPPING: 'stale', STOPPED: 'dead', FAILED: 'error', } as const; -function formatBytes(bytes: number): string { - if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MB`; - if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`; - return `${bytes} B`; -} - interface Props { deployment: Deployment; version: AppVersion | null; externalUrl: string; - onStop: () => void; - onStart: () => void; } -export function StatusCard({ deployment, version, externalUrl, onStop, onStart }: Props) { +export function StatusCard({ deployment, version, externalUrl }: Props) { const running = deployment.replicaStates?.filter((r) => r.status === 'RUNNING').length ?? 0; const total = deployment.replicaStates?.length ?? 0; @@ -56,12 +48,6 @@ export function StatusCard({ deployment, version, externalUrl, onStop, onStart } {deployment.errorMessage} )} - -
- {(deployment.status === 'RUNNING' || deployment.status === 'STARTING' || deployment.status === 'DEGRADED') - && } - {deployment.status === 'STOPPED' && } -
); } diff --git a/ui/src/pages/AppsTab/AppDeploymentPage/index.tsx b/ui/src/pages/AppsTab/AppDeploymentPage/index.tsx index e12db860..503ade45 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, Button, Tabs, useToast } from '@cameleer/design-system'; +import { AlertDialog, Badge, Button, Tabs, useToast } from '@cameleer/design-system'; import { useEnvironmentStore } from '../../../api/environment-store'; import { useEnvironments } from '../../../api/queries/admin/environments'; import { @@ -60,6 +60,9 @@ export default function AppDeploymentPage() { const { data: deployments = [] } = useDeployments(selectedEnv, app?.slug); const currentDeployment = deployments.find((d) => d.status === 'RUNNING') ?? null; const activeDeployment = deployments.find((d) => d.status === 'STARTING') ?? null; + const latestDeployment = deployments + .slice() + .sort((a, b) => (b.createdAt ?? '').localeCompare(a.createdAt ?? ''))[0] ?? null; const { data: agentConfig = null } = useApplicationConfig(app?.slug, selectedEnv); const { data: dirtyState, isLoading: dirtyLoading } = useDirtyState(selectedEnv, app?.slug); @@ -398,7 +401,12 @@ export default function AppDeploymentPage() {
{/* ── Page header ── */}
-

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

+
+

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

+ {app && !deploymentInProgress && (dirty.anyLocalEdit || serverDirtyAgainstDeploy) && ( + + )} +
{dirty.anyLocalEdit && ( + )} + {app && latestDeployment && latestDeployment.status === 'STOPPED' && ( + + )} {app && (