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%;
|
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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</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 { 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>
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user