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:
@@ -6,6 +6,25 @@
|
||||
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 {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
@@ -246,6 +265,17 @@
|
||||
/* HistoryDisclosure */
|
||||
.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 */
|
||||
.envVarsList {
|
||||
display: flex;
|
||||
|
||||
@@ -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>
|
||||
<Select
|
||||
disabled={disabled}
|
||||
@@ -68,6 +89,15 @@ export function MonitoringTab({ value, onChange, disabled }: Props) {
|
||||
<span className={value.metricsEnabled ? styles.toggleEnabled : styles.toggleDisabled}>
|
||||
{value.metricsEnabled ? 'Enabled' : 'Disabled'}
|
||||
</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>
|
||||
|
||||
<span className={styles.configLabel}>Sampling Rate</span>
|
||||
@@ -90,6 +120,30 @@ export function MonitoringTab({ value, onChange, disabled }: Props) {
|
||||
{value.compressSuccess ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Button, Input } from '@cameleer/design-system';
|
||||
import { EnvEditor } from '../../../../components/EnvEditor';
|
||||
import type { VariablesFormState } from '../hooks/useDeploymentPageState';
|
||||
import styles from '../AppDeploymentPage.module.css';
|
||||
|
||||
interface Props {
|
||||
value: VariablesFormState;
|
||||
@@ -9,52 +8,11 @@ interface 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 (
|
||||
<div>
|
||||
<div className={styles.envVarsList}>
|
||||
{value.envVars.map((row, i) => (
|
||||
<div key={i} className={styles.envVarRow}>
|
||||
<Input
|
||||
<EnvEditor
|
||||
value={value.envVars}
|
||||
onChange={(entries) => onChange({ envVars: entries })}
|
||||
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)}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Button size="sm" variant="secondary" disabled={disabled} onClick={addRow}>
|
||||
+ Add Variable
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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 { Environment } from '../../../api/queries/admin/environments';
|
||||
import { envColorVar } from '../../../components/env-colors';
|
||||
import styles from './AppDeploymentPage.module.css';
|
||||
|
||||
function slugify(name: string): string {
|
||||
@@ -63,7 +64,13 @@ export function IdentitySection({
|
||||
<MonoText size="sm">{slug || '...'}</MonoText>
|
||||
|
||||
<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>
|
||||
<MonoText size="sm">{externalUrl}</MonoText>
|
||||
|
||||
@@ -6,11 +6,16 @@ import type { App } from '../../../../api/queries/admin/apps';
|
||||
export interface MonitoringFormState {
|
||||
engineLevel: string;
|
||||
payloadCaptureMode: string;
|
||||
payloadSize: string;
|
||||
payloadUnit: string;
|
||||
applicationLogLevel: string;
|
||||
agentLogLevel: string;
|
||||
metricsEnabled: boolean;
|
||||
metricsInterval: string;
|
||||
samplingRate: string;
|
||||
compressSuccess: boolean;
|
||||
replayEnabled: boolean;
|
||||
routeControlEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface ResourcesFormState {
|
||||
@@ -48,11 +53,16 @@ const defaultForm: 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: '512', memoryReserve: '', cpuRequest: '500', cpuLimit: '',
|
||||
@@ -80,11 +90,16 @@ export function useDeploymentPageState(
|
||||
monitoring: {
|
||||
engineLevel: (agentConfig?.engineLevel as string) ?? defaultForm.monitoring.engineLevel,
|
||||
payloadCaptureMode: (agentConfig?.payloadCaptureMode as string) ?? defaultForm.monitoring.payloadCaptureMode,
|
||||
payloadSize: defaultForm.monitoring.payloadSize,
|
||||
payloadUnit: defaultForm.monitoring.payloadUnit,
|
||||
applicationLogLevel: (agentConfig?.applicationLogLevel as string) ?? defaultForm.monitoring.applicationLogLevel,
|
||||
agentLogLevel: (agentConfig?.agentLogLevel as string) ?? defaultForm.monitoring.agentLogLevel,
|
||||
metricsEnabled: agentConfig?.metricsEnabled ?? defaultForm.monitoring.metricsEnabled,
|
||||
metricsInterval: defaultForm.monitoring.metricsInterval,
|
||||
samplingRate: agentConfig?.samplingRate !== undefined ? String(agentConfig.samplingRate) : defaultForm.monitoring.samplingRate,
|
||||
compressSuccess: agentConfig?.compressSuccess ?? defaultForm.monitoring.compressSuccess,
|
||||
replayEnabled: defaultForm.monitoring.replayEnabled,
|
||||
routeControlEnabled: defaultForm.monitoring.routeControlEnabled,
|
||||
},
|
||||
resources: {
|
||||
memoryLimit: String(merged.memoryLimitMb ?? defaultForm.resources.memoryLimit),
|
||||
|
||||
@@ -328,11 +328,16 @@ export default function AppDeploymentPage() {
|
||||
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,
|
||||
@@ -431,13 +436,14 @@ export default function AppDeploymentPage() {
|
||||
)}
|
||||
|
||||
{/* ── Config tabs ── */}
|
||||
<div className={styles.tabGroup}>
|
||||
<Tabs
|
||||
tabs={tabs}
|
||||
active={tab}
|
||||
onChange={(v) => setTab(v as TabKey)}
|
||||
/>
|
||||
|
||||
<div className={styles.section} style={{ flex: '1 1 auto', minHeight: 0 }}>
|
||||
<div className={styles.tabContent}>
|
||||
{tab === 'monitoring' && (
|
||||
<MonitoringTab
|
||||
value={form.monitoring}
|
||||
@@ -508,6 +514,7 @@ export default function AppDeploymentPage() {
|
||||
<RouteRecordingTab app={app} environment={env} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Stop confirmation dialog ── */}
|
||||
<AlertDialog
|
||||
|
||||
Reference in New Issue
Block a user