feat(ui): ConfigPanel snapshot+diff modes; extract snapshotToForm helper
- Extract inline handleRestore mapping into snapshotToForm(snapshot, defaults) helper - Export defaultForm from useDeploymentPageState for use in ConfigPanel - Replace ConfigPanel stub with real read-only snapshot renderer + Snapshot/Diff toggle - Add fieldDiff deep-equal field-walk helper with nested object + array support - Forward optional currentForm prop through CheckpointDetailDrawer to ConfigPanel - 13 new tests across diff.test.ts, snapshotToForm.test.ts, ConfigPanel.test.tsx (all pass) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ThemeProvider } from '@cameleer/design-system';
|
||||
import { ConfigPanel } from './ConfigPanel';
|
||||
import type { DeploymentPageFormState } from '../hooks/useDeploymentPageState';
|
||||
|
||||
const baseDep: any = {
|
||||
id: 'd1', appId: 'a', appVersionId: 'v', environmentId: 'e',
|
||||
status: 'STOPPED', targetState: 'STOPPED', deploymentStrategy: 'BLUE_GREEN',
|
||||
replicaStates: [], deployStage: null, containerId: null, containerName: null,
|
||||
errorMessage: null, deployedAt: '2026-04-23T10:35:00Z', stoppedAt: null,
|
||||
createdAt: '2026-04-23T10:35:00Z', createdBy: 'alice',
|
||||
deployedConfigSnapshot: {
|
||||
jarVersionId: 'v', agentConfig: { engineLevel: 'COMPLETE' },
|
||||
containerConfig: { memoryLimitMb: 512, replicas: 3 }, sensitiveKeys: ['SECRET'],
|
||||
},
|
||||
};
|
||||
|
||||
function wrap(ui: React.ReactNode) {
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
return render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<ThemeProvider>{ui}</ThemeProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('ConfigPanel', () => {
|
||||
it('renders sub-tabs in Snapshot mode', () => {
|
||||
wrap(<ConfigPanel deployment={baseDep} appSlug="a" envSlug="e" archived={false} />);
|
||||
// Use role=tab to avoid matching text inside rendered tab content (e.g. hint text)
|
||||
const tabs = screen.getAllByRole('tab');
|
||||
const tabLabels = tabs.map((t) => t.textContent ?? '');
|
||||
expect(tabLabels.some((l) => /resources/i.test(l))).toBe(true);
|
||||
expect(tabLabels.some((l) => /monitoring/i.test(l))).toBe(true);
|
||||
expect(tabLabels.some((l) => /variables/i.test(l))).toBe(true);
|
||||
expect(tabLabels.some((l) => /sensitive keys/i.test(l))).toBe(true);
|
||||
});
|
||||
|
||||
it('hides the Snapshot/Diff toggle when archived', () => {
|
||||
wrap(<ConfigPanel deployment={baseDep} appSlug="a" envSlug="e" archived />);
|
||||
expect(screen.queryByText(/diff vs current/i)).toBeNull();
|
||||
});
|
||||
|
||||
it('hides the toggle when no currentForm provided', () => {
|
||||
wrap(<ConfigPanel deployment={baseDep} appSlug="a" envSlug="e" archived={false} />);
|
||||
expect(screen.queryByText(/diff vs current/i)).toBeNull();
|
||||
});
|
||||
|
||||
it('shows the toggle when currentForm is provided and not archived', () => {
|
||||
const currentForm: DeploymentPageFormState = {
|
||||
monitoring: {
|
||||
engineLevel: 'REGULAR',
|
||||
payloadCaptureMode: 'BOTH',
|
||||
payloadSize: '4',
|
||||
payloadUnit: 'KB',
|
||||
applicationLogLevel: 'INFO',
|
||||
agentLogLevel: 'INFO',
|
||||
metricsEnabled: true,
|
||||
metricsInterval: '60',
|
||||
samplingRate: '1.0',
|
||||
compressSuccess: false,
|
||||
replayEnabled: true,
|
||||
routeControlEnabled: true,
|
||||
},
|
||||
resources: {
|
||||
memoryLimit: '256', memoryReserve: '', cpuRequest: '500', cpuLimit: '',
|
||||
ports: [], appPort: '8080', replicas: '1', deployStrategy: 'blue-green',
|
||||
stripPrefix: true, sslOffloading: true, runtimeType: 'auto', customArgs: '',
|
||||
extraNetworks: [],
|
||||
},
|
||||
variables: { envVars: [] },
|
||||
sensitiveKeys: { sensitiveKeys: [] },
|
||||
};
|
||||
wrap(<ConfigPanel deployment={baseDep} appSlug="a" envSlug="e" archived={false} currentForm={currentForm} />);
|
||||
expect(screen.getByText(/snapshot/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/diff vs current/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders empty-state when no snapshot', () => {
|
||||
const noSnap = { ...baseDep, deployedConfigSnapshot: null };
|
||||
wrap(<ConfigPanel deployment={noSnap} appSlug="a" envSlug="e" archived={false} />);
|
||||
expect(screen.getByText(/no config snapshot/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,144 @@
|
||||
// ConfigPanel.tsx — stub for Task 10; Task 11 implements snapshot/diff modes
|
||||
import { useMemo, useState } from 'react';
|
||||
import { SegmentedTabs, Tabs } from '@cameleer/design-system';
|
||||
import type { Deployment } from '../../../../api/queries/admin/apps';
|
||||
import { MonitoringTab } from '../ConfigTabs/MonitoringTab';
|
||||
import { ResourcesTab } from '../ConfigTabs/ResourcesTab';
|
||||
import { VariablesTab } from '../ConfigTabs/VariablesTab';
|
||||
import { SensitiveKeysTab } from '../ConfigTabs/SensitiveKeysTab';
|
||||
import { defaultForm, type DeploymentPageFormState } from '../hooks/useDeploymentPageState';
|
||||
import { snapshotToForm } from './snapshotToForm';
|
||||
import { fieldDiff, type FieldDiff } from './diff';
|
||||
import styles from './CheckpointDetailDrawer.module.css';
|
||||
|
||||
interface Props {
|
||||
deployment: Deployment;
|
||||
appSlug: string;
|
||||
envSlug: string;
|
||||
archived: boolean;
|
||||
currentForm?: DeploymentPageFormState;
|
||||
}
|
||||
|
||||
export function ConfigPanel(_: Props) {
|
||||
return <div style={{ padding: 16, color: 'var(--text-muted)' }}>Config panel — implemented in Task 11</div>;
|
||||
type Mode = 'snapshot' | 'diff';
|
||||
type SubTab = 'monitoring' | 'resources' | 'variables' | 'sensitive';
|
||||
|
||||
export function ConfigPanel({ deployment, archived, currentForm }: Props) {
|
||||
const [mode, setMode] = useState<Mode>('snapshot');
|
||||
const [subTab, setSubTab] = useState<SubTab>('resources');
|
||||
|
||||
const snapshot = deployment.deployedConfigSnapshot;
|
||||
const snapshotForm = useMemo(
|
||||
() => (snapshot ? snapshotToForm(snapshot, defaultForm) : defaultForm),
|
||||
[snapshot],
|
||||
);
|
||||
|
||||
const diff: FieldDiff[] = useMemo(
|
||||
() => (mode === 'diff' && currentForm ? fieldDiff(snapshotForm, currentForm) : []),
|
||||
[mode, snapshotForm, currentForm],
|
||||
);
|
||||
|
||||
const counts = useMemo(() => {
|
||||
const c: Record<SubTab, number> = { monitoring: 0, resources: 0, variables: 0, sensitive: 0 };
|
||||
for (const d of diff) {
|
||||
const root = d.path.split('.')[0].split('[')[0];
|
||||
if (root === 'monitoring') c.monitoring++;
|
||||
else if (root === 'resources') c.resources++;
|
||||
else if (root === 'variables') c.variables++;
|
||||
else if (root === 'sensitiveKeys') c.sensitive++;
|
||||
}
|
||||
return c;
|
||||
}, [diff]);
|
||||
|
||||
if (!snapshot) {
|
||||
return <div className={styles.emptyState}>This deployment has no config snapshot.</div>;
|
||||
}
|
||||
|
||||
const showToggle = !archived && !!currentForm;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showToggle && (
|
||||
<SegmentedTabs
|
||||
tabs={[
|
||||
{ value: 'snapshot', label: 'Snapshot' },
|
||||
{ value: 'diff', label: `Diff vs current${diff.length ? ` (${diff.length})` : ''}` },
|
||||
]}
|
||||
active={mode}
|
||||
onChange={(m) => setMode(m as Mode)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tabs
|
||||
active={subTab}
|
||||
onChange={(t) => setSubTab(t as SubTab)}
|
||||
tabs={[
|
||||
{ value: 'resources', label: `Resources${counts.resources ? ` (${counts.resources})` : ''}` },
|
||||
{ value: 'monitoring', label: `Monitoring${counts.monitoring ? ` (${counts.monitoring})` : ''}` },
|
||||
{ value: 'variables', label: `Variables${counts.variables ? ` (${counts.variables})` : ''}` },
|
||||
{ value: 'sensitive', label: `Sensitive Keys${counts.sensitive ? ` (${counts.sensitive})` : ''}` },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div style={{ marginTop: 12 }}>
|
||||
{mode === 'snapshot' && (
|
||||
<>
|
||||
{subTab === 'resources' && (
|
||||
<ResourcesTab value={snapshotForm.resources} onChange={() => {}} disabled isProd={false} />
|
||||
)}
|
||||
{subTab === 'monitoring' && (
|
||||
<MonitoringTab value={snapshotForm.monitoring} onChange={() => {}} disabled />
|
||||
)}
|
||||
{subTab === 'variables' && (
|
||||
<VariablesTab value={snapshotForm.variables} onChange={() => {}} disabled />
|
||||
)}
|
||||
{subTab === 'sensitive' && (
|
||||
<SensitiveKeysTab value={snapshotForm.sensitiveKeys} onChange={() => {}} disabled />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{mode === 'diff' && (
|
||||
<DiffView diffs={diff.filter((d) => filterTab(d.path, subTab))} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function filterTab(path: string, tab: SubTab): boolean {
|
||||
const root = path.split('.')[0].split('[')[0];
|
||||
if (tab === 'sensitive') return root === 'sensitiveKeys';
|
||||
return root === tab;
|
||||
}
|
||||
|
||||
function DiffView({ diffs }: { diffs: FieldDiff[] }) {
|
||||
if (diffs.length === 0) {
|
||||
return <div className={styles.emptyState}>No differences in this section.</div>;
|
||||
}
|
||||
return (
|
||||
<div style={{ fontFamily: 'monospace', fontSize: 12 }}>
|
||||
{diffs.map((d) => (
|
||||
<div key={d.path} style={{ marginBottom: 8 }}>
|
||||
<div style={{ color: 'var(--text-muted)' }}>{d.path}</div>
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--red-bg, rgba(239,68,68,0.15))',
|
||||
borderLeft: '2px solid var(--red, #ef4444)',
|
||||
padding: '2px 6px',
|
||||
}}
|
||||
>
|
||||
- {JSON.stringify(d.oldValue)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--green-bg, rgba(34,197,94,0.15))',
|
||||
borderLeft: '2px solid var(--green, #22c55e)',
|
||||
padding: '2px 6px',
|
||||
}}
|
||||
>
|
||||
+ {JSON.stringify(d.newValue)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { fieldDiff } from './diff';
|
||||
|
||||
describe('fieldDiff', () => {
|
||||
it('returns empty list for equal objects', () => {
|
||||
expect(fieldDiff({ a: 1, b: 'x' }, { a: 1, b: 'x' })).toEqual([]);
|
||||
});
|
||||
|
||||
it('detects changed values', () => {
|
||||
expect(fieldDiff({ a: 1 }, { a: 2 })).toEqual([{ path: 'a', oldValue: 1, newValue: 2 }]);
|
||||
});
|
||||
|
||||
it('detects added keys', () => {
|
||||
expect(fieldDiff({}, { a: 1 })).toEqual([{ path: 'a', oldValue: undefined, newValue: 1 }]);
|
||||
});
|
||||
|
||||
it('detects removed keys', () => {
|
||||
expect(fieldDiff({ a: 1 }, {})).toEqual([{ path: 'a', oldValue: 1, newValue: undefined }]);
|
||||
});
|
||||
|
||||
it('walks nested objects', () => {
|
||||
const diff = fieldDiff({ resources: { mem: 512 } }, { resources: { mem: 1024 } });
|
||||
expect(diff).toEqual([{ path: 'resources.mem', oldValue: 512, newValue: 1024 }]);
|
||||
});
|
||||
|
||||
it('compares arrays by position', () => {
|
||||
const diff = fieldDiff({ keys: ['a', 'b'] }, { keys: ['a', 'c'] });
|
||||
expect(diff).toEqual([{ path: 'keys[1]', oldValue: 'b', newValue: 'c' }]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
export interface FieldDiff {
|
||||
path: string;
|
||||
oldValue: unknown;
|
||||
newValue: unknown;
|
||||
}
|
||||
|
||||
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
||||
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
||||
}
|
||||
|
||||
export function fieldDiff(a: unknown, b: unknown, path = ''): FieldDiff[] {
|
||||
if (Object.is(a, b)) return [];
|
||||
|
||||
if (isPlainObject(a) && isPlainObject(b)) {
|
||||
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
||||
const out: FieldDiff[] = [];
|
||||
for (const k of keys) {
|
||||
const sub = path ? `${path}.${k}` : k;
|
||||
out.push(...fieldDiff(a[k], b[k], sub));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
const len = Math.max(a.length, b.length);
|
||||
const out: FieldDiff[] = [];
|
||||
for (let i = 0; i < len; i++) {
|
||||
out.push(...fieldDiff(a[i], b[i], `${path}[${i}]`));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
return [{ path: path || '(root)', oldValue: a, newValue: b }];
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { SideDrawer } from '../../../../components/SideDrawer';
|
||||
import { Tabs, Button, Badge } from '@cameleer/design-system';
|
||||
import type { Deployment, AppVersion } from '../../../../api/queries/admin/apps';
|
||||
import type { DeploymentPageFormState } from '../hooks/useDeploymentPageState';
|
||||
import { LogsPanel } from './LogsPanel';
|
||||
import { ConfigPanel } from './ConfigPanel';
|
||||
import { timeAgo } from '../../../../utils/format-utils';
|
||||
@@ -15,12 +16,13 @@ interface Props {
|
||||
appSlug: string;
|
||||
envSlug: string;
|
||||
onRestore: (deploymentId: string) => void;
|
||||
currentForm?: DeploymentPageFormState;
|
||||
}
|
||||
|
||||
type TabId = 'logs' | 'config';
|
||||
|
||||
export function CheckpointDetailDrawer({
|
||||
open, onClose, deployment, version, appSlug, envSlug, onRestore,
|
||||
open, onClose, deployment, version, appSlug, envSlug, onRestore, currentForm,
|
||||
}: Props) {
|
||||
const [tab, setTab] = useState<TabId>('logs');
|
||||
const archived = !version;
|
||||
@@ -74,7 +76,7 @@ export function CheckpointDetailDrawer({
|
||||
<LogsPanel deployment={deployment} appSlug={appSlug} envSlug={envSlug} />
|
||||
)}
|
||||
{tab === 'config' && (
|
||||
<ConfigPanel deployment={deployment} appSlug={appSlug} envSlug={envSlug} archived={archived} />
|
||||
<ConfigPanel deployment={deployment} appSlug={appSlug} envSlug={envSlug} archived={archived} currentForm={currentForm} />
|
||||
)}
|
||||
</div>
|
||||
</SideDrawer>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { snapshotToForm } from './snapshotToForm';
|
||||
import { defaultForm } from '../hooks/useDeploymentPageState';
|
||||
|
||||
describe('snapshotToForm', () => {
|
||||
it('overrides monitoring fields from agentConfig', () => {
|
||||
const snapshot = {
|
||||
jarVersionId: 'v1',
|
||||
agentConfig: { engineLevel: 'COMPLETE', samplingRate: 0.5, compressSuccess: true },
|
||||
containerConfig: {},
|
||||
sensitiveKeys: null,
|
||||
};
|
||||
const result = snapshotToForm(snapshot, defaultForm);
|
||||
expect(result.monitoring.engineLevel).toBe('COMPLETE');
|
||||
expect(result.monitoring.samplingRate).toBe('0.5');
|
||||
expect(result.monitoring.compressSuccess).toBe(true);
|
||||
// fields not in snapshot fall back to defaults
|
||||
expect(result.monitoring.payloadSize).toBe(defaultForm.monitoring.payloadSize);
|
||||
expect(result.monitoring.replayEnabled).toBe(defaultForm.monitoring.replayEnabled);
|
||||
});
|
||||
|
||||
it('overrides resources fields from containerConfig', () => {
|
||||
const snapshot = {
|
||||
jarVersionId: 'v1',
|
||||
agentConfig: null,
|
||||
containerConfig: {
|
||||
memoryLimitMb: 1024,
|
||||
replicas: 3,
|
||||
deploymentStrategy: 'rolling',
|
||||
customEnvVars: { FOO: 'bar', BAZ: 'qux' },
|
||||
exposedPorts: [8080, 9090],
|
||||
},
|
||||
sensitiveKeys: ['SECRET_KEY'],
|
||||
};
|
||||
const result = snapshotToForm(snapshot, defaultForm);
|
||||
expect(result.resources.memoryLimit).toBe('1024');
|
||||
expect(result.resources.replicas).toBe('3');
|
||||
expect(result.resources.deployStrategy).toBe('rolling');
|
||||
expect(result.resources.ports).toEqual([8080, 9090]);
|
||||
expect(result.variables.envVars).toEqual([
|
||||
{ key: 'FOO', value: 'bar' },
|
||||
{ key: 'BAZ', value: 'qux' },
|
||||
]);
|
||||
expect(result.sensitiveKeys.sensitiveKeys).toEqual(['SECRET_KEY']);
|
||||
});
|
||||
|
||||
it('falls back to defaults for missing fields', () => {
|
||||
const snapshot = {
|
||||
jarVersionId: 'v1',
|
||||
agentConfig: null,
|
||||
containerConfig: {},
|
||||
sensitiveKeys: null,
|
||||
};
|
||||
const result = snapshotToForm(snapshot, defaultForm);
|
||||
expect(result.resources.memoryLimit).toBe(defaultForm.resources.memoryLimit);
|
||||
expect(result.resources.cpuRequest).toBe(defaultForm.resources.cpuRequest);
|
||||
expect(result.variables.envVars).toEqual(defaultForm.variables.envVars);
|
||||
expect(result.sensitiveKeys.sensitiveKeys).toEqual(defaultForm.sensitiveKeys.sensitiveKeys);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { DeploymentPageFormState } from '../hooks/useDeploymentPageState';
|
||||
|
||||
interface DeployedConfigSnapshot {
|
||||
jarVersionId: string;
|
||||
agentConfig: Record<string, unknown> | null;
|
||||
containerConfig: Record<string, unknown>;
|
||||
sensitiveKeys: string[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a deployment snapshot to the page's form-state shape.
|
||||
* Used by both the live page's "restore" action and the read-only ConfigPanel
|
||||
* in the checkpoint detail drawer.
|
||||
*
|
||||
* Fields not represented in the snapshot fall back to `defaults`.
|
||||
*/
|
||||
export function snapshotToForm(
|
||||
snapshot: DeployedConfigSnapshot,
|
||||
defaults: DeploymentPageFormState,
|
||||
): DeploymentPageFormState {
|
||||
const a = snapshot.agentConfig ?? {};
|
||||
const c = snapshot.containerConfig ?? {};
|
||||
return {
|
||||
monitoring: {
|
||||
engineLevel: (a.engineLevel as string) ?? defaults.monitoring.engineLevel,
|
||||
payloadCaptureMode: (a.payloadCaptureMode as string) ?? defaults.monitoring.payloadCaptureMode,
|
||||
payloadSize: defaults.monitoring.payloadSize,
|
||||
payloadUnit: defaults.monitoring.payloadUnit,
|
||||
applicationLogLevel: (a.applicationLogLevel as string) ?? defaults.monitoring.applicationLogLevel,
|
||||
agentLogLevel: (a.agentLogLevel as string) ?? defaults.monitoring.agentLogLevel,
|
||||
metricsEnabled: (a.metricsEnabled as boolean) ?? defaults.monitoring.metricsEnabled,
|
||||
metricsInterval: defaults.monitoring.metricsInterval,
|
||||
samplingRate: a.samplingRate !== undefined ? String(a.samplingRate) : defaults.monitoring.samplingRate,
|
||||
compressSuccess: (a.compressSuccess as boolean) ?? defaults.monitoring.compressSuccess,
|
||||
replayEnabled: defaults.monitoring.replayEnabled,
|
||||
routeControlEnabled: defaults.monitoring.routeControlEnabled,
|
||||
},
|
||||
resources: {
|
||||
memoryLimit: c.memoryLimitMb !== undefined ? String(c.memoryLimitMb) : defaults.resources.memoryLimit,
|
||||
memoryReserve: c.memoryReserveMb != null ? String(c.memoryReserveMb) : defaults.resources.memoryReserve,
|
||||
cpuRequest: c.cpuRequest !== undefined ? String(c.cpuRequest) : defaults.resources.cpuRequest,
|
||||
cpuLimit: c.cpuLimit != null ? String(c.cpuLimit) : defaults.resources.cpuLimit,
|
||||
ports: Array.isArray(c.exposedPorts) ? (c.exposedPorts as number[]) : defaults.resources.ports,
|
||||
appPort: c.appPort !== undefined ? String(c.appPort) : defaults.resources.appPort,
|
||||
replicas: c.replicas !== undefined ? String(c.replicas) : defaults.resources.replicas,
|
||||
deployStrategy: (c.deploymentStrategy as string) ?? defaults.resources.deployStrategy,
|
||||
stripPrefix: c.stripPathPrefix !== undefined ? (c.stripPathPrefix as boolean) : defaults.resources.stripPrefix,
|
||||
sslOffloading: c.sslOffloading !== undefined ? (c.sslOffloading as boolean) : defaults.resources.sslOffloading,
|
||||
runtimeType: (c.runtimeType as string) ?? defaults.resources.runtimeType,
|
||||
customArgs: c.customArgs !== undefined ? String(c.customArgs ?? '') : defaults.resources.customArgs,
|
||||
extraNetworks: Array.isArray(c.extraNetworks) ? (c.extraNetworks as string[]) : defaults.resources.extraNetworks,
|
||||
},
|
||||
variables: {
|
||||
envVars: c.customEnvVars
|
||||
? Object.entries(c.customEnvVars as Record<string, string>).map(([key, value]) => ({ key, value }))
|
||||
: defaults.variables.envVars,
|
||||
},
|
||||
sensitiveKeys: {
|
||||
sensitiveKeys: Array.isArray(snapshot.sensitiveKeys)
|
||||
? snapshot.sensitiveKeys
|
||||
: Array.isArray(a.sensitiveKeys)
|
||||
? (a.sensitiveKeys as string[])
|
||||
: defaults.sensitiveKeys.sensitiveKeys,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export interface DeploymentPageFormState {
|
||||
sensitiveKeys: SensitiveKeysFormState;
|
||||
}
|
||||
|
||||
const defaultForm: DeploymentPageFormState = {
|
||||
export const defaultForm: DeploymentPageFormState = {
|
||||
monitoring: {
|
||||
engineLevel: 'REGULAR',
|
||||
payloadCaptureMode: 'BOTH',
|
||||
|
||||
@@ -31,6 +31,7 @@ import { DeploymentTab } from './DeploymentTab/DeploymentTab';
|
||||
import { PrimaryActionButton, computeMode } from './PrimaryActionButton';
|
||||
import { useDeploymentPageState } from './hooks/useDeploymentPageState';
|
||||
import { useFormDirty } from './hooks/useFormDirty';
|
||||
import { snapshotToForm } from './CheckpointDetailDrawer/snapshotToForm';
|
||||
import { useUnsavedChangesBlocker } from './hooks/useUnsavedChangesBlocker';
|
||||
import { deriveAppName } from './utils/deriveAppName';
|
||||
import styles from './AppDeploymentPage.module.css';
|
||||
@@ -337,53 +338,7 @@ export default function AppDeploymentPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
setForm((prev) => {
|
||||
const a = snap.agentConfig ?? {};
|
||||
const c = snap.containerConfig ?? {};
|
||||
return {
|
||||
monitoring: {
|
||||
engineLevel: (a.engineLevel as string) ?? prev.monitoring.engineLevel,
|
||||
payloadCaptureMode: (a.payloadCaptureMode as string) ?? prev.monitoring.payloadCaptureMode,
|
||||
payloadSize: prev.monitoring.payloadSize,
|
||||
payloadUnit: prev.monitoring.payloadUnit,
|
||||
applicationLogLevel: (a.applicationLogLevel as string) ?? prev.monitoring.applicationLogLevel,
|
||||
agentLogLevel: (a.agentLogLevel as string) ?? prev.monitoring.agentLogLevel,
|
||||
metricsEnabled: (a.metricsEnabled as boolean) ?? prev.monitoring.metricsEnabled,
|
||||
metricsInterval: prev.monitoring.metricsInterval,
|
||||
samplingRate: a.samplingRate !== undefined ? String(a.samplingRate) : prev.monitoring.samplingRate,
|
||||
compressSuccess: (a.compressSuccess as boolean) ?? prev.monitoring.compressSuccess,
|
||||
replayEnabled: prev.monitoring.replayEnabled,
|
||||
routeControlEnabled: prev.monitoring.routeControlEnabled,
|
||||
},
|
||||
resources: {
|
||||
memoryLimit: c.memoryLimitMb !== undefined ? String(c.memoryLimitMb) : prev.resources.memoryLimit,
|
||||
memoryReserve: c.memoryReserveMb != null ? String(c.memoryReserveMb) : prev.resources.memoryReserve,
|
||||
cpuRequest: c.cpuRequest !== undefined ? String(c.cpuRequest) : prev.resources.cpuRequest,
|
||||
cpuLimit: c.cpuLimit != null ? String(c.cpuLimit) : prev.resources.cpuLimit,
|
||||
ports: Array.isArray(c.exposedPorts) ? (c.exposedPorts as number[]) : prev.resources.ports,
|
||||
appPort: c.appPort !== undefined ? String(c.appPort) : prev.resources.appPort,
|
||||
replicas: c.replicas !== undefined ? String(c.replicas) : prev.resources.replicas,
|
||||
deployStrategy: (c.deploymentStrategy as string) ?? prev.resources.deployStrategy,
|
||||
stripPrefix: c.stripPathPrefix !== undefined ? (c.stripPathPrefix as boolean) : prev.resources.stripPrefix,
|
||||
sslOffloading: c.sslOffloading !== undefined ? (c.sslOffloading as boolean) : prev.resources.sslOffloading,
|
||||
runtimeType: (c.runtimeType as string) ?? prev.resources.runtimeType,
|
||||
customArgs: c.customArgs !== undefined ? String(c.customArgs ?? '') : prev.resources.customArgs,
|
||||
extraNetworks: Array.isArray(c.extraNetworks) ? (c.extraNetworks as string[]) : prev.resources.extraNetworks,
|
||||
},
|
||||
variables: {
|
||||
envVars: c.customEnvVars
|
||||
? Object.entries(c.customEnvVars as Record<string, string>).map(([key, value]) => ({ key, value }))
|
||||
: prev.variables.envVars,
|
||||
},
|
||||
sensitiveKeys: {
|
||||
sensitiveKeys: Array.isArray(snap.sensitiveKeys)
|
||||
? snap.sensitiveKeys
|
||||
: Array.isArray(a.sensitiveKeys)
|
||||
? (a.sensitiveKeys as string[])
|
||||
: prev.sensitiveKeys.sensitiveKeys,
|
||||
},
|
||||
};
|
||||
});
|
||||
setForm((prev) => snapshotToForm(snap, prev));
|
||||
}
|
||||
|
||||
// ── Primary button enabled logic ───────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user