feat: replicas column, deploy progress, and new config fields in Deployments UI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ import { useApplicationConfig, useUpdateApplicationConfig, useProcessorRouteMapp
|
|||||||
import type { ApplicationConfig, TapDefinition } from '../../api/queries/commands';
|
import type { ApplicationConfig, TapDefinition } from '../../api/queries/commands';
|
||||||
import { useRouteCatalog } from '../../api/queries/catalog';
|
import { useRouteCatalog } from '../../api/queries/catalog';
|
||||||
import type { AppCatalogEntry, RouteSummary } from '../../api/types';
|
import type { AppCatalogEntry, RouteSummary } from '../../api/types';
|
||||||
|
import { DeploymentProgress } from '../../components/DeploymentProgress';
|
||||||
import styles from './AppsTab.module.css';
|
import styles from './AppsTab.module.css';
|
||||||
|
|
||||||
function formatBytes(bytes: number): string {
|
function formatBytes(bytes: number): string {
|
||||||
@@ -54,6 +55,7 @@ function timeAgo(date: string): string {
|
|||||||
|
|
||||||
const STATUS_COLORS: Record<string, 'success' | 'warning' | 'error' | 'auto' | 'running'> = {
|
const STATUS_COLORS: Record<string, 'success' | 'warning' | 'error' | 'auto' | 'running'> = {
|
||||||
RUNNING: 'running', STARTING: 'warning', FAILED: 'error', STOPPED: 'auto',
|
RUNNING: 'running', STARTING: 'warning', FAILED: 'error', STOPPED: 'auto',
|
||||||
|
DEGRADED: 'warning', STOPPING: 'auto',
|
||||||
};
|
};
|
||||||
|
|
||||||
function slugify(name: string): string {
|
function slugify(name: string): string {
|
||||||
@@ -174,6 +176,11 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
|||||||
const [ports, setPorts] = useState<number[]>(Array.isArray(defaults.exposedPorts) ? defaults.exposedPorts as number[] : []);
|
const [ports, setPorts] = useState<number[]>(Array.isArray(defaults.exposedPorts) ? defaults.exposedPorts as number[] : []);
|
||||||
const [newPort, setNewPort] = useState('');
|
const [newPort, setNewPort] = useState('');
|
||||||
const [envVars, setEnvVars] = useState<{ key: string; value: string }[]>([]);
|
const [envVars, setEnvVars] = useState<{ key: string; value: string }[]>([]);
|
||||||
|
const [appPort, setAppPort] = useState('8080');
|
||||||
|
const [replicas, setReplicas] = useState('1');
|
||||||
|
const [deployStrategy, setDeployStrategy] = useState('blue-green');
|
||||||
|
const [stripPrefix, setStripPrefix] = useState(true);
|
||||||
|
const [sslOffloading, setSslOffloading] = useState(true);
|
||||||
|
|
||||||
const [configTab, setConfigTab] = useState<'variables' | 'monitoring' | 'resources'>('variables');
|
const [configTab, setConfigTab] = useState<'variables' | 'monitoring' | 'resources'>('variables');
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
@@ -221,6 +228,11 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
|||||||
cpuLimit: cpuLimit ? parseFloat(cpuLimit) : null,
|
cpuLimit: cpuLimit ? parseFloat(cpuLimit) : null,
|
||||||
exposedPorts: ports,
|
exposedPorts: ports,
|
||||||
customEnvVars: Object.fromEntries(envVars.filter((v) => v.key.trim()).map((v) => [v.key, v.value])),
|
customEnvVars: Object.fromEntries(envVars.filter((v) => v.key.trim()).map((v) => [v.key, v.value])),
|
||||||
|
appPort: appPort ? parseInt(appPort) : 8080,
|
||||||
|
replicas: replicas ? parseInt(replicas) : 1,
|
||||||
|
deploymentStrategy: deployStrategy,
|
||||||
|
stripPathPrefix: stripPrefix,
|
||||||
|
sslOffloading: sslOffloading,
|
||||||
};
|
};
|
||||||
await updateContainerConfig.mutateAsync({ appId: app.id, config: containerConfig });
|
await updateContainerConfig.mutateAsync({ appId: app.id, config: containerConfig });
|
||||||
|
|
||||||
@@ -430,6 +442,28 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
|||||||
onChange={(e) => setNewPort(e.target.value)}
|
onChange={(e) => setNewPort(e.target.value)}
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addPort(); } }} />
|
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addPort(); } }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span className={styles.configLabel}>App Port</span>
|
||||||
|
<Input disabled={busy} value={appPort} onChange={(e) => setAppPort(e.target.value)} style={{ width: 80 }} />
|
||||||
|
|
||||||
|
<span className={styles.configLabel}>Replicas</span>
|
||||||
|
<Input disabled={busy} value={replicas} onChange={(e) => setReplicas(e.target.value)} style={{ width: 60 }} type="number" />
|
||||||
|
|
||||||
|
<span className={styles.configLabel}>Deploy Strategy</span>
|
||||||
|
<Select disabled={busy} value={deployStrategy} onChange={(e) => setDeployStrategy(e.target.value)}
|
||||||
|
options={[{ value: 'blue-green', label: 'Blue/Green' }, { value: 'rolling', label: 'Rolling' }]} />
|
||||||
|
|
||||||
|
<span className={styles.configLabel}>Strip Path Prefix</span>
|
||||||
|
<div className={styles.configInline}>
|
||||||
|
<Toggle checked={stripPrefix} onChange={() => !busy && setStripPrefix(!stripPrefix)} disabled={busy} />
|
||||||
|
<span className={stripPrefix ? styles.toggleEnabled : styles.toggleDisabled}>{stripPrefix ? 'Enabled' : 'Disabled'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className={styles.configLabel}>SSL Offloading</span>
|
||||||
|
<div className={styles.configInline}>
|
||||||
|
<Toggle checked={sslOffloading} onChange={() => !busy && setSslOffloading(!sslOffloading)} disabled={busy} />
|
||||||
|
<span className={sslOffloading ? styles.toggleEnabled : styles.toggleDisabled}>{sslOffloading ? 'Enabled' : 'Disabled'}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -567,6 +601,7 @@ function OverviewSubTab({ app, deployments, versions, environments, envMap, sele
|
|||||||
<th>Environment</th>
|
<th>Environment</th>
|
||||||
<th>Version</th>
|
<th>Version</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>Replicas</th>
|
||||||
<th>URL</th>
|
<th>URL</th>
|
||||||
<th>Deployed</th>
|
<th>Deployed</th>
|
||||||
<th style={{ textAlign: 'right' }}>Actions</th>
|
<th style={{ textAlign: 'right' }}>Actions</th>
|
||||||
@@ -588,6 +623,13 @@ function OverviewSubTab({ app, deployments, versions, environments, envMap, sele
|
|||||||
</td>
|
</td>
|
||||||
<td><Badge label={version ? `v${version.version}` : '?'} color="auto" /></td>
|
<td><Badge label={version ? `v${version.version}` : '?'} color="auto" /></td>
|
||||||
<td><Badge label={d.status} color={STATUS_COLORS[d.status] ?? 'auto'} /></td>
|
<td><Badge label={d.status} color={STATUS_COLORS[d.status] ?? 'auto'} /></td>
|
||||||
|
<td>
|
||||||
|
{d.replicaStates && d.replicaStates.length > 0 ? (
|
||||||
|
<span className={styles.cellMeta}>
|
||||||
|
{d.replicaStates.filter((r) => r.status === 'RUNNING').length}/{d.replicaStates.length}
|
||||||
|
</span>
|
||||||
|
) : '—'}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{d.status === 'RUNNING' ? (
|
{d.status === 'RUNNING' ? (
|
||||||
<MonoText size="xs">{url}</MonoText>
|
<MonoText size="xs">{url}</MonoText>
|
||||||
@@ -607,6 +649,12 @@ function OverviewSubTab({ app, deployments, versions, environments, envMap, sele
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
)}
|
)}
|
||||||
|
{deployments.filter((d) => d.deployStage).map((d) => (
|
||||||
|
<div key={`progress-${d.id}`} style={{ marginBottom: 8 }}>
|
||||||
|
<span className={styles.cellMeta}>{d.containerName}</span>
|
||||||
|
<DeploymentProgress currentStage={d.deployStage} failed={d.status === 'FAILED'} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
<SectionHeader>Versions ({versions.length})</SectionHeader>
|
<SectionHeader>Versions ({versions.length})</SectionHeader>
|
||||||
{versions.length === 0 && <p className={styles.emptyNote}>No versions uploaded yet.</p>}
|
{versions.length === 0 && <p className={styles.emptyNote}>No versions uploaded yet.</p>}
|
||||||
@@ -685,6 +733,11 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
|
|||||||
const [ports, setPorts] = useState<number[]>([]);
|
const [ports, setPorts] = useState<number[]>([]);
|
||||||
const [newPort, setNewPort] = useState('');
|
const [newPort, setNewPort] = useState('');
|
||||||
const [envVars, setEnvVars] = useState<{ key: string; value: string }[]>([]);
|
const [envVars, setEnvVars] = useState<{ key: string; value: string }[]>([]);
|
||||||
|
const [appPort, setAppPort] = useState('8080');
|
||||||
|
const [replicas, setReplicas] = useState('1');
|
||||||
|
const [deployStrategy, setDeployStrategy] = useState('blue-green');
|
||||||
|
const [stripPrefix, setStripPrefix] = useState(true);
|
||||||
|
const [sslOffloading, setSslOffloading] = useState(true);
|
||||||
|
|
||||||
// Sync from server data
|
// Sync from server data
|
||||||
const syncFromServer = useCallback(() => {
|
const syncFromServer = useCallback(() => {
|
||||||
@@ -707,6 +760,11 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
|
|||||||
setPorts(Array.isArray(merged.exposedPorts) ? merged.exposedPorts as number[] : []);
|
setPorts(Array.isArray(merged.exposedPorts) ? merged.exposedPorts as number[] : []);
|
||||||
const vars = merged.customEnvVars as Record<string, string> | undefined;
|
const vars = merged.customEnvVars as Record<string, string> | undefined;
|
||||||
setEnvVars(vars ? Object.entries(vars).map(([key, value]) => ({ key, value })) : []);
|
setEnvVars(vars ? Object.entries(vars).map(([key, value]) => ({ key, value })) : []);
|
||||||
|
setAppPort(String(merged.appPort ?? 8080));
|
||||||
|
setReplicas(String(merged.replicas ?? 1));
|
||||||
|
setDeployStrategy(String(merged.deploymentStrategy ?? 'blue-green'));
|
||||||
|
setStripPrefix(merged.stripPathPrefix !== false);
|
||||||
|
setSslOffloading(merged.sslOffloading !== false);
|
||||||
}, [agentConfig, merged]);
|
}, [agentConfig, merged]);
|
||||||
|
|
||||||
useEffect(() => { syncFromServer(); }, [syncFromServer]);
|
useEffect(() => { syncFromServer(); }, [syncFromServer]);
|
||||||
@@ -751,6 +809,11 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
|
|||||||
cpuLimit: cpuLimit ? parseFloat(cpuLimit) : null,
|
cpuLimit: cpuLimit ? parseFloat(cpuLimit) : null,
|
||||||
exposedPorts: ports,
|
exposedPorts: ports,
|
||||||
customEnvVars: Object.fromEntries(envVars.filter((v) => v.key.trim()).map((v) => [v.key, v.value])),
|
customEnvVars: Object.fromEntries(envVars.filter((v) => v.key.trim()).map((v) => [v.key, v.value])),
|
||||||
|
appPort: appPort ? parseInt(appPort) : 8080,
|
||||||
|
replicas: replicas ? parseInt(replicas) : 1,
|
||||||
|
deploymentStrategy: deployStrategy,
|
||||||
|
stripPathPrefix: stripPrefix,
|
||||||
|
sslOffloading: sslOffloading,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await updateContainerConfig.mutateAsync({ appId: app.id, config: containerConfig });
|
await updateContainerConfig.mutateAsync({ appId: app.id, config: containerConfig });
|
||||||
@@ -989,6 +1052,28 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
|
|||||||
onChange={(e) => setNewPort(e.target.value)}
|
onChange={(e) => setNewPort(e.target.value)}
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addPort(); } }} />
|
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addPort(); } }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span className={styles.configLabel}>App Port</span>
|
||||||
|
<Input disabled={!editing} value={appPort} onChange={(e) => setAppPort(e.target.value)} style={{ width: 80 }} />
|
||||||
|
|
||||||
|
<span className={styles.configLabel}>Replicas</span>
|
||||||
|
<Input disabled={!editing} value={replicas} onChange={(e) => setReplicas(e.target.value)} style={{ width: 60 }} type="number" />
|
||||||
|
|
||||||
|
<span className={styles.configLabel}>Deploy Strategy</span>
|
||||||
|
<Select disabled={!editing} value={deployStrategy} onChange={(e) => setDeployStrategy(e.target.value)}
|
||||||
|
options={[{ value: 'blue-green', label: 'Blue/Green' }, { value: 'rolling', label: 'Rolling' }]} />
|
||||||
|
|
||||||
|
<span className={styles.configLabel}>Strip Path Prefix</span>
|
||||||
|
<div className={styles.configInline}>
|
||||||
|
<Toggle checked={stripPrefix} onChange={() => editing && setStripPrefix(!stripPrefix)} disabled={!editing} />
|
||||||
|
<span className={stripPrefix ? styles.toggleEnabled : styles.toggleDisabled}>{stripPrefix ? 'Enabled' : 'Disabled'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className={styles.configLabel}>SSL Offloading</span>
|
||||||
|
<div className={styles.configInline}>
|
||||||
|
<Toggle checked={sslOffloading} onChange={() => editing && setSslOffloading(!sslOffloading)} disabled={!editing} />
|
||||||
|
<span className={sslOffloading ? styles.toggleEnabled : styles.toggleDisabled}>{sslOffloading ? 'Enabled' : 'Disabled'}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user