feat(ui): show deployment status + rich pending-deploy tooltip on app header
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m24s
CI / docker (push) Successful in 1m12s
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / cleanup-branch (pull_request) Has been skipped
CI / build (pull_request) Successful in 2m6s
CI / docker (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / deploy-feature (pull_request) Has been skipped
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m24s
CI / docker (push) Successful in 1m12s
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / cleanup-branch (pull_request) Has been skipped
CI / build (pull_request) Successful in 2m6s
CI / docker (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / deploy-feature (pull_request) Has been skipped
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useParams, useLocation, useNavigate } from 'react-router';
|
import { useParams, useLocation, useNavigate } from 'react-router';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
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 { useEnvironmentStore } from '../../../api/environment-store';
|
||||||
import { useEnvironments } from '../../../api/queries/admin/environments';
|
import { useEnvironments } from '../../../api/queries/admin/environments';
|
||||||
import {
|
import {
|
||||||
@@ -39,6 +39,16 @@ import styles from './AppDeploymentPage.module.css';
|
|||||||
|
|
||||||
type TabKey = 'monitoring' | 'resources' | 'variables' | 'sensitive-keys' | 'deployment' | 'traces' | 'recording';
|
type TabKey = 'monitoring' | 'resources' | 'variables' | 'sensitive-keys' | 'deployment' | 'traces' | 'recording';
|
||||||
|
|
||||||
|
const STATUS_COLORS: Record<string, 'success' | 'warning' | 'error' | 'auto' | 'running'> = {
|
||||||
|
RUNNING: 'running', STARTING: 'warning', FAILED: 'error', STOPPED: 'auto',
|
||||||
|
DEGRADED: 'warning', STOPPING: 'auto',
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEPLOY_STATUS_DOT: Record<string, 'live' | 'stale' | 'dead' | 'success' | 'warning' | 'error' | 'running'> = {
|
||||||
|
RUNNING: 'live', STARTING: 'running', DEGRADED: 'stale',
|
||||||
|
STOPPING: 'stale', STOPPED: 'dead', FAILED: 'error',
|
||||||
|
};
|
||||||
|
|
||||||
function slugify(name: string): string {
|
function slugify(name: string): string {
|
||||||
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 100);
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 100);
|
||||||
}
|
}
|
||||||
@@ -393,9 +403,35 @@ export default function AppDeploymentPage() {
|
|||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
<h2 style={{ margin: 0 }}>{app ? app.displayName : 'Create Application'}</h2>
|
<h2 style={{ margin: 0 }}>{app ? app.displayName : 'Create Application'}</h2>
|
||||||
{app && !deploymentInProgress && (dirty.anyLocalEdit || serverDirtyAgainstDeploy) && (
|
{app && latestDeployment && (
|
||||||
<Badge label="Pending deploy" color="warning" />
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||||||
|
<StatusDot variant={DEPLOY_STATUS_DOT[latestDeployment.status] ?? 'dead'} />
|
||||||
|
<Badge
|
||||||
|
label={latestDeployment.status}
|
||||||
|
color={STATUS_COLORS[latestDeployment.status] ?? 'auto'}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{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 (
|
||||||
|
<span title={tooltip} style={{ display: 'inline-flex' }}>
|
||||||
|
<Badge
|
||||||
|
label={dirty.anyLocalEdit ? 'Pending deploy' : `Pending deploy (${diffs.length})`}
|
||||||
|
color="warning"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
{dirty.anyLocalEdit && (
|
{dirty.anyLocalEdit && (
|
||||||
|
|||||||
Reference in New Issue
Block a user