ui(deploy): port missing agent-config fields, var-view switcher, env pill, tab seam

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-22 23:45:19 +02:00
parent d33c039a17
commit b7b6bd2a96
6 changed files with 122 additions and 51 deletions

View File

@@ -6,6 +6,25 @@
min-height: 100%; min-height: 100%;
} }
/* Tabs + content grouped together with no internal gap */
.tabGroup {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
}
/* The tab-content card sits flush against the Tabs strip — no gap */
.tabContent {
border: 1px solid var(--border);
border-top: none;
border-radius: 0 0 6px 6px;
padding: 16px;
background: var(--bg-surface);
flex: 1 1 auto;
min-height: 0;
}
.section { .section {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
@@ -246,6 +265,17 @@
/* HistoryDisclosure */ /* HistoryDisclosure */
.historyRow { margin-top: 16px; } .historyRow { margin-top: 16px; }
/* Environment pill (Identity section) */
.envPill {
display: inline-block;
padding: 2px 10px;
border-radius: 10px;
font-size: 12px;
font-weight: 500;
color: var(--text-inverse, white);
width: max-content;
}
/* Env vars list */ /* Env vars list */
.envVarsList { .envVarsList {
display: flex; display: flex;

View File

@@ -42,6 +42,27 @@ export function MonitoringTab({ value, onChange, disabled }: Props) {
]} ]}
/> />
<span className={styles.configLabel}>Max Payload Size</span>
<div className={styles.configInline}>
<Input
disabled={disabled}
value={value.payloadSize}
onChange={(e) => update('payloadSize', e.target.value)}
className={styles.inputMd}
placeholder="e.g. 4"
/>
<Select
disabled={disabled}
value={value.payloadUnit}
onChange={(e) => update('payloadUnit', e.target.value)}
options={[
{ value: 'B', label: 'bytes' },
{ value: 'KB', label: 'KB' },
{ value: 'MB', label: 'MB' },
]}
/>
</div>
<span className={styles.configLabel}>App Log Level</span> <span className={styles.configLabel}>App Log Level</span>
<Select <Select
disabled={disabled} disabled={disabled}
@@ -68,6 +89,15 @@ export function MonitoringTab({ value, onChange, disabled }: Props) {
<span className={value.metricsEnabled ? styles.toggleEnabled : styles.toggleDisabled}> <span className={value.metricsEnabled ? styles.toggleEnabled : styles.toggleDisabled}>
{value.metricsEnabled ? 'Enabled' : 'Disabled'} {value.metricsEnabled ? 'Enabled' : 'Disabled'}
</span> </span>
<span className={styles.cellMeta} style={{ marginLeft: 8 }}>Interval</span>
<Input
disabled={disabled}
value={value.metricsInterval}
onChange={(e) => update('metricsInterval', e.target.value)}
className={styles.inputXs}
placeholder="60"
/>
<span className={styles.cellMeta}>s</span>
</div> </div>
<span className={styles.configLabel}>Sampling Rate</span> <span className={styles.configLabel}>Sampling Rate</span>
@@ -90,6 +120,30 @@ export function MonitoringTab({ value, onChange, disabled }: Props) {
{value.compressSuccess ? 'Enabled' : 'Disabled'} {value.compressSuccess ? 'Enabled' : 'Disabled'}
</span> </span>
</div> </div>
<span className={styles.configLabel}>Replay</span>
<div className={styles.configInline}>
<Toggle
checked={value.replayEnabled}
onChange={() => !disabled && update('replayEnabled', !value.replayEnabled)}
disabled={disabled}
/>
<span className={value.replayEnabled ? styles.toggleEnabled : styles.toggleDisabled}>
{value.replayEnabled ? 'Enabled' : 'Disabled'}
</span>
</div>
<span className={styles.configLabel}>Route Control</span>
<div className={styles.configInline}>
<Toggle
checked={value.routeControlEnabled}
onChange={() => !disabled && update('routeControlEnabled', !value.routeControlEnabled)}
disabled={disabled}
/>
<span className={value.routeControlEnabled ? styles.toggleEnabled : styles.toggleDisabled}>
{value.routeControlEnabled ? 'Enabled' : 'Disabled'}
</span>
</div>
</div> </div>
); );
} }

View File

@@ -1,6 +1,5 @@
import { Button, Input } from '@cameleer/design-system'; import { EnvEditor } from '../../../../components/EnvEditor';
import type { VariablesFormState } from '../hooks/useDeploymentPageState'; import type { VariablesFormState } from '../hooks/useDeploymentPageState';
import styles from '../AppDeploymentPage.module.css';
interface Props { interface Props {
value: VariablesFormState; value: VariablesFormState;
@@ -9,52 +8,11 @@ interface Props {
} }
export function VariablesTab({ value, onChange, disabled }: Props) { export function VariablesTab({ value, onChange, disabled }: Props) {
function updateRow(index: number, field: 'key' | 'value', v: string) {
const next = value.envVars.map((row, i) => (i === index ? { ...row, [field]: v } : row));
onChange({ envVars: next });
}
function removeRow(index: number) {
onChange({ envVars: value.envVars.filter((_, i) => i !== index) });
}
function addRow() {
onChange({ envVars: [...value.envVars, { key: '', value: '' }] });
}
return ( return (
<div> <EnvEditor
<div className={styles.envVarsList}> value={value.envVars}
{value.envVars.map((row, i) => ( onChange={(entries) => onChange({ envVars: entries })}
<div key={i} className={styles.envVarRow}> disabled={disabled}
<Input />
disabled={disabled}
value={row.key}
onChange={(e) => updateRow(i, 'key', e.target.value)}
placeholder="KEY"
/>
<Input
disabled={disabled}
value={row.value}
onChange={(e) => updateRow(i, 'value', e.target.value)}
placeholder="value"
/>
<Button
size="sm"
variant="ghost"
disabled={disabled}
onClick={() => removeRow(i)}
>
&times;
</Button>
</div>
))}
</div>
<div style={{ marginTop: 10 }}>
<Button size="sm" variant="secondary" disabled={disabled} onClick={addRow}>
+ Add Variable
</Button>
</div>
</div>
); );
} }

View File

@@ -1,7 +1,8 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { SectionHeader, Input, MonoText, Button, Badge } from '@cameleer/design-system'; import { SectionHeader, Input, MonoText, Button } from '@cameleer/design-system';
import type { App, AppVersion } from '../../../api/queries/admin/apps'; import type { App, AppVersion } from '../../../api/queries/admin/apps';
import type { Environment } from '../../../api/queries/admin/environments'; import type { Environment } from '../../../api/queries/admin/environments';
import { envColorVar } from '../../../components/env-colors';
import styles from './AppDeploymentPage.module.css'; import styles from './AppDeploymentPage.module.css';
function slugify(name: string): string { function slugify(name: string): string {
@@ -63,7 +64,13 @@ export function IdentitySection({
<MonoText size="sm">{slug || '...'}</MonoText> <MonoText size="sm">{slug || '...'}</MonoText>
<span className={styles.configLabel}>Environment</span> <span className={styles.configLabel}>Environment</span>
<Badge label={environment.displayName} color="auto" /> <span
className={styles.envPill}
style={{ backgroundColor: envColorVar(environment.color) }}
title={environment.displayName}
>
{environment.displayName}
</span>
<span className={styles.configLabel}>External URL</span> <span className={styles.configLabel}>External URL</span>
<MonoText size="sm">{externalUrl}</MonoText> <MonoText size="sm">{externalUrl}</MonoText>

View File

@@ -6,11 +6,16 @@ import type { App } from '../../../../api/queries/admin/apps';
export interface MonitoringFormState { export interface MonitoringFormState {
engineLevel: string; engineLevel: string;
payloadCaptureMode: string; payloadCaptureMode: string;
payloadSize: string;
payloadUnit: string;
applicationLogLevel: string; applicationLogLevel: string;
agentLogLevel: string; agentLogLevel: string;
metricsEnabled: boolean; metricsEnabled: boolean;
metricsInterval: string;
samplingRate: string; samplingRate: string;
compressSuccess: boolean; compressSuccess: boolean;
replayEnabled: boolean;
routeControlEnabled: boolean;
} }
export interface ResourcesFormState { export interface ResourcesFormState {
@@ -48,11 +53,16 @@ const defaultForm: DeploymentPageFormState = {
monitoring: { monitoring: {
engineLevel: 'REGULAR', engineLevel: 'REGULAR',
payloadCaptureMode: 'BOTH', payloadCaptureMode: 'BOTH',
payloadSize: '4',
payloadUnit: 'KB',
applicationLogLevel: 'INFO', applicationLogLevel: 'INFO',
agentLogLevel: 'INFO', agentLogLevel: 'INFO',
metricsEnabled: true, metricsEnabled: true,
metricsInterval: '60',
samplingRate: '1.0', samplingRate: '1.0',
compressSuccess: false, compressSuccess: false,
replayEnabled: true,
routeControlEnabled: true,
}, },
resources: { resources: {
memoryLimit: '512', memoryReserve: '', cpuRequest: '500', cpuLimit: '', memoryLimit: '512', memoryReserve: '', cpuRequest: '500', cpuLimit: '',
@@ -80,11 +90,16 @@ export function useDeploymentPageState(
monitoring: { monitoring: {
engineLevel: (agentConfig?.engineLevel as string) ?? defaultForm.monitoring.engineLevel, engineLevel: (agentConfig?.engineLevel as string) ?? defaultForm.monitoring.engineLevel,
payloadCaptureMode: (agentConfig?.payloadCaptureMode as string) ?? defaultForm.monitoring.payloadCaptureMode, payloadCaptureMode: (agentConfig?.payloadCaptureMode as string) ?? defaultForm.monitoring.payloadCaptureMode,
payloadSize: defaultForm.monitoring.payloadSize,
payloadUnit: defaultForm.monitoring.payloadUnit,
applicationLogLevel: (agentConfig?.applicationLogLevel as string) ?? defaultForm.monitoring.applicationLogLevel, applicationLogLevel: (agentConfig?.applicationLogLevel as string) ?? defaultForm.monitoring.applicationLogLevel,
agentLogLevel: (agentConfig?.agentLogLevel as string) ?? defaultForm.monitoring.agentLogLevel, agentLogLevel: (agentConfig?.agentLogLevel as string) ?? defaultForm.monitoring.agentLogLevel,
metricsEnabled: agentConfig?.metricsEnabled ?? defaultForm.monitoring.metricsEnabled, metricsEnabled: agentConfig?.metricsEnabled ?? defaultForm.monitoring.metricsEnabled,
metricsInterval: defaultForm.monitoring.metricsInterval,
samplingRate: agentConfig?.samplingRate !== undefined ? String(agentConfig.samplingRate) : defaultForm.monitoring.samplingRate, samplingRate: agentConfig?.samplingRate !== undefined ? String(agentConfig.samplingRate) : defaultForm.monitoring.samplingRate,
compressSuccess: agentConfig?.compressSuccess ?? defaultForm.monitoring.compressSuccess, compressSuccess: agentConfig?.compressSuccess ?? defaultForm.monitoring.compressSuccess,
replayEnabled: defaultForm.monitoring.replayEnabled,
routeControlEnabled: defaultForm.monitoring.routeControlEnabled,
}, },
resources: { resources: {
memoryLimit: String(merged.memoryLimitMb ?? defaultForm.resources.memoryLimit), memoryLimit: String(merged.memoryLimitMb ?? defaultForm.resources.memoryLimit),

View File

@@ -328,11 +328,16 @@ export default function AppDeploymentPage() {
monitoring: { monitoring: {
engineLevel: (a.engineLevel as string) ?? prev.monitoring.engineLevel, engineLevel: (a.engineLevel as string) ?? prev.monitoring.engineLevel,
payloadCaptureMode: (a.payloadCaptureMode as string) ?? prev.monitoring.payloadCaptureMode, payloadCaptureMode: (a.payloadCaptureMode as string) ?? prev.monitoring.payloadCaptureMode,
payloadSize: prev.monitoring.payloadSize,
payloadUnit: prev.monitoring.payloadUnit,
applicationLogLevel: (a.applicationLogLevel as string) ?? prev.monitoring.applicationLogLevel, applicationLogLevel: (a.applicationLogLevel as string) ?? prev.monitoring.applicationLogLevel,
agentLogLevel: (a.agentLogLevel as string) ?? prev.monitoring.agentLogLevel, agentLogLevel: (a.agentLogLevel as string) ?? prev.monitoring.agentLogLevel,
metricsEnabled: (a.metricsEnabled as boolean) ?? prev.monitoring.metricsEnabled, metricsEnabled: (a.metricsEnabled as boolean) ?? prev.monitoring.metricsEnabled,
metricsInterval: prev.monitoring.metricsInterval,
samplingRate: a.samplingRate !== undefined ? String(a.samplingRate) : prev.monitoring.samplingRate, samplingRate: a.samplingRate !== undefined ? String(a.samplingRate) : prev.monitoring.samplingRate,
compressSuccess: (a.compressSuccess as boolean) ?? prev.monitoring.compressSuccess, compressSuccess: (a.compressSuccess as boolean) ?? prev.monitoring.compressSuccess,
replayEnabled: prev.monitoring.replayEnabled,
routeControlEnabled: prev.monitoring.routeControlEnabled,
}, },
resources: { resources: {
memoryLimit: c.memoryLimitMb !== undefined ? String(c.memoryLimitMb) : prev.resources.memoryLimit, memoryLimit: c.memoryLimitMb !== undefined ? String(c.memoryLimitMb) : prev.resources.memoryLimit,
@@ -431,13 +436,14 @@ export default function AppDeploymentPage() {
)} )}
{/* ── Config tabs ── */} {/* ── Config tabs ── */}
<div className={styles.tabGroup}>
<Tabs <Tabs
tabs={tabs} tabs={tabs}
active={tab} active={tab}
onChange={(v) => setTab(v as TabKey)} onChange={(v) => setTab(v as TabKey)}
/> />
<div className={styles.section} style={{ flex: '1 1 auto', minHeight: 0 }}> <div className={styles.tabContent}>
{tab === 'monitoring' && ( {tab === 'monitoring' && (
<MonitoringTab <MonitoringTab
value={form.monitoring} value={form.monitoring}
@@ -508,6 +514,7 @@ export default function AppDeploymentPage() {
<RouteRecordingTab app={app} environment={env} /> <RouteRecordingTab app={app} environment={env} />
)} )}
</div> </div>
</div>
{/* ── Stop confirmation dialog ── */} {/* ── Stop confirmation dialog ── */}
<AlertDialog <AlertDialog