fix: resolve UI glitches and improve consistency
- Sidebar: make +App button more subtle (lower opacity, brightens on hover) - Sidebar: add filter chips to hide empty routes and offline/stale apps - Sidebar: hide filter chips and +App button when sidebar is collapsed - Exchange table: reorder columns to Status, Attributes, App, Route, Started, Duration; remove ExchangeId and Agent columns - Exchange detail log tab: query by exchangeId only (no applicationId required), filter by processorId when processor selected - KPI tooltips: styled tooltips with current/previous values, time period labels, percentage change, themed with DS variables - KPI tooltips: fix overflow by left-aligning first two and right-aligning last two - Exchange detail: show full datetime (YYYY-MM-DD HH:mm:ss.SSS) for start/end times - Status labels: unify to title-case (Completed, Failed, Running) across all views - Status filter buttons: match title-case labels (Completed, Warning, Failed, Running) - Create app: show full external URL using routingDomain from env config or window.location.origin fallback - Create app: add Runtime Type selector and Custom Arguments to Resources tab - Create app: add Sensitive Keys tab with agent defaults, global keys, and app-specific keys (matching admin page design) - Create app: add placeholder text to all Input fields for consistency - Update design-system to 0.1.52 (sidebar collapse toggle fix) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,10 +13,12 @@ import {
|
||||
Select,
|
||||
StatusDot,
|
||||
Tabs,
|
||||
Tag,
|
||||
Toggle,
|
||||
useToast,
|
||||
} from '@cameleer/design-system';
|
||||
import type { Column } from '@cameleer/design-system';
|
||||
import { Shield, Info } from 'lucide-react';
|
||||
import { EnvEditor } from '../../components/EnvEditor';
|
||||
import { useEnvironmentStore } from '../../api/environment-store';
|
||||
import { useEnvironments } from '../../api/queries/admin/environments';
|
||||
@@ -37,6 +39,7 @@ import type { Environment } from '../../api/queries/admin/environments';
|
||||
import { useApplicationConfig, useUpdateApplicationConfig, useProcessorRouteMapping } from '../../api/queries/commands';
|
||||
import type { ApplicationConfig, TapDefinition } from '../../api/queries/commands';
|
||||
import { useCatalog } from '../../api/queries/catalog';
|
||||
import { useSensitiveKeys } from '../../api/queries/admin/sensitive-keys';
|
||||
import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog';
|
||||
import { DeploymentProgress } from '../../components/DeploymentProgress';
|
||||
import { StartupLogPanel } from '../../components/StartupLogPanel';
|
||||
@@ -46,6 +49,7 @@ import { PageLoader } from '../../components/PageLoader';
|
||||
import styles from './AppsTab.module.css';
|
||||
import sectionStyles from '../../styles/section-card.module.css';
|
||||
import tableStyles from '../../styles/table-section.module.css';
|
||||
import skStyles from '../Admin/SensitiveKeysPage.module.css';
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MB`;
|
||||
@@ -166,6 +170,8 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
const createApp = useCreateApp();
|
||||
const uploadJar = useUploadJar();
|
||||
const createDeployment = useCreateDeployment();
|
||||
const { data: globalKeysConfig } = useSensitiveKeys();
|
||||
const globalKeys = globalKeysConfig?.keys ?? [];
|
||||
const updateAgentConfig = useUpdateApplicationConfig();
|
||||
const updateContainerConfig = useUpdateContainerConfig();
|
||||
|
||||
@@ -215,7 +221,11 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
const [extraNetworks, setExtraNetworks] = useState<string[]>(Array.isArray(defaults.extraNetworks) ? defaults.extraNetworks as string[] : []);
|
||||
const [newNetwork, setNewNetwork] = useState('');
|
||||
|
||||
const [configTab, setConfigTab] = useState<'monitoring' | 'resources' | 'variables'>('monitoring');
|
||||
// Sensitive keys
|
||||
const [sensitiveKeys, setSensitiveKeys] = useState<string[]>([]);
|
||||
const [newSensitiveKey, setNewSensitiveKey] = useState('');
|
||||
|
||||
const [configTab, setConfigTab] = useState<'monitoring' | 'resources' | 'variables' | 'sensitive-keys'>('monitoring');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [step, setStep] = useState('');
|
||||
|
||||
@@ -292,6 +302,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
taps: [],
|
||||
tapVersion: 0,
|
||||
routeRecording: {},
|
||||
sensitiveKeys: sensitiveKeys.length > 0 ? sensitiveKeys : undefined,
|
||||
},
|
||||
environment: selectedEnv,
|
||||
});
|
||||
@@ -337,7 +348,16 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Payment Gateway" disabled={busy} />
|
||||
|
||||
<span className={styles.configLabel}>External URL</span>
|
||||
<MonoText size="sm">/{env?.slug ?? '...'}/{slug || '...'}/</MonoText>
|
||||
<MonoText size="sm">{(() => {
|
||||
const domain = String(defaults.routingDomain ?? '');
|
||||
const envSlug = env?.slug ?? '...';
|
||||
const appSlug = slug || '...';
|
||||
if (defaults.routingMode === 'subdomain' && domain) {
|
||||
return `https://${appSlug}-${envSlug}.${domain}/`;
|
||||
}
|
||||
const base = domain ? `https://${domain}` : window.location.origin;
|
||||
return `${base}/${envSlug}/${appSlug}/`;
|
||||
})()}</MonoText>
|
||||
|
||||
<span className={styles.configLabel}>Environment</span>
|
||||
<Select value={envId} onChange={(e) => setEnvId(e.target.value)} disabled={busy}
|
||||
@@ -370,6 +390,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
{ label: 'Monitoring', value: 'monitoring' },
|
||||
{ label: 'Resources', value: 'resources' },
|
||||
{ label: 'Variables', value: 'variables' },
|
||||
{ label: 'Sensitive Keys', value: 'sensitive-keys' },
|
||||
]}
|
||||
active={configTab}
|
||||
onChange={(v) => setConfigTab(v as typeof configTab)}
|
||||
@@ -396,7 +417,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
|
||||
<span className={styles.configLabel}>Max Payload Size</span>
|
||||
<div className={styles.configInline}>
|
||||
<Input disabled={busy} value={payloadSize} onChange={(e) => setPayloadSize(e.target.value)} className={styles.inputMd} />
|
||||
<Input disabled={busy} value={payloadSize} onChange={(e) => setPayloadSize(e.target.value)} className={styles.inputMd} placeholder="e.g. 4" />
|
||||
<Select disabled={busy} value={payloadUnit} onChange={(e) => setPayloadUnit(e.target.value)} className={styles.inputXl}
|
||||
options={[{ value: 'bytes', label: 'bytes' }, { value: 'KB', label: 'KB' }, { value: 'MB', label: 'MB' }]} />
|
||||
</div>
|
||||
@@ -414,12 +435,12 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
<Toggle checked={metricsEnabled} onChange={() => !busy && setMetricsEnabled(!metricsEnabled)} disabled={busy} />
|
||||
<span className={metricsEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{metricsEnabled ? 'Enabled' : 'Disabled'}</span>
|
||||
<span className={styles.cellMeta} style={{ marginLeft: 8 }}>Interval</span>
|
||||
<Input disabled={busy} value={metricsInterval} onChange={(e) => setMetricsInterval(e.target.value)} className={styles.inputXs} />
|
||||
<Input disabled={busy} value={metricsInterval} onChange={(e) => setMetricsInterval(e.target.value)} className={styles.inputXs} placeholder="60" />
|
||||
<span className={styles.cellMeta}>s</span>
|
||||
</div>
|
||||
|
||||
<span className={styles.configLabel}>Sampling Rate</span>
|
||||
<Input disabled={busy} value={samplingRate} onChange={(e) => setSamplingRate(e.target.value)} className={styles.inputLg} />
|
||||
<Input disabled={busy} value={samplingRate} onChange={(e) => setSamplingRate(e.target.value)} className={styles.inputLg} placeholder="1.0" />
|
||||
|
||||
<span className={styles.configLabel}>Compress Success</span>
|
||||
<div className={styles.configInline}>
|
||||
@@ -438,6 +459,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
<Toggle checked={routeControlEnabled} onChange={() => !busy && setRouteControlEnabled(!routeControlEnabled)} disabled={busy} />
|
||||
<span className={routeControlEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{routeControlEnabled ? 'Enabled' : 'Disabled'}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -446,23 +468,42 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
<div className={sectionStyles.section}>
|
||||
<SectionHeader>Container Resources</SectionHeader>
|
||||
<div className={styles.configGrid}>
|
||||
<span className={styles.configLabel}>Runtime Type</span>
|
||||
<Select disabled={busy} value={runtimeType} onChange={(e) => setRuntimeType(e.target.value)}
|
||||
options={[
|
||||
{ value: 'auto', label: 'Auto (detect from JAR)' },
|
||||
{ value: 'spring-boot', label: 'Spring Boot' },
|
||||
{ value: 'quarkus', label: 'Quarkus' },
|
||||
{ value: 'plain-java', label: 'Plain Java' },
|
||||
{ value: 'native', label: 'Native' },
|
||||
]} />
|
||||
|
||||
<span className={styles.configLabel}>Custom Arguments</span>
|
||||
<div>
|
||||
<Input disabled={busy} value={customArgs} onChange={(e) => setCustomArgs(e.target.value)}
|
||||
placeholder="-Xmx256m -Dfoo=bar" className={styles.inputLg} />
|
||||
<span className={styles.configHint}>
|
||||
{runtimeType === 'native' ? 'Arguments passed to the native binary' : 'Additional JVM arguments appended to the start command'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span className={styles.configLabel}>Memory Limit</span>
|
||||
<div className={styles.configInline}>
|
||||
<Input disabled={busy} value={memoryLimit} onChange={(e) => setMemoryLimit(e.target.value)} className={styles.inputLg} />
|
||||
<Input disabled={busy} value={memoryLimit} onChange={(e) => setMemoryLimit(e.target.value)} className={styles.inputLg} placeholder="e.g. 512" />
|
||||
<span className={styles.cellMeta}>MB</span>
|
||||
</div>
|
||||
|
||||
<span className={styles.configLabel}>Memory Reserve</span>
|
||||
<div>
|
||||
<div className={styles.configInline}>
|
||||
<Input disabled={!isProd || busy} value={memoryReserve} onChange={(e) => setMemoryReserve(e.target.value)} placeholder="---" className={styles.inputLg} />
|
||||
<Input disabled={!isProd || busy} value={memoryReserve} onChange={(e) => setMemoryReserve(e.target.value)} placeholder="e.g. 256" className={styles.inputLg} />
|
||||
<span className={styles.cellMeta}>MB</span>
|
||||
</div>
|
||||
{!isProd && <span className={styles.configHint}>Available in production environments only</span>}
|
||||
</div>
|
||||
|
||||
<span className={styles.configLabel}>CPU Request</span>
|
||||
<Input disabled={busy} value={cpuRequest} onChange={(e) => setCpuRequest(e.target.value)} className={styles.inputLg} />
|
||||
<Input disabled={busy} value={cpuRequest} onChange={(e) => setCpuRequest(e.target.value)} className={styles.inputLg} placeholder="e.g. 500 millicores" />
|
||||
|
||||
<span className={styles.configLabel}>CPU Limit</span>
|
||||
<div className={styles.configInline}>
|
||||
@@ -485,10 +526,10 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
</div>
|
||||
|
||||
<span className={styles.configLabel}>App Port</span>
|
||||
<Input disabled={busy} value={appPort} onChange={(e) => setAppPort(e.target.value)} className={styles.inputLg} />
|
||||
<Input disabled={busy} value={appPort} onChange={(e) => setAppPort(e.target.value)} className={styles.inputLg} placeholder="e.g. 8080" />
|
||||
|
||||
<span className={styles.configLabel}>Replicas</span>
|
||||
<Input disabled={busy} value={replicas} onChange={(e) => setReplicas(e.target.value)} className={styles.inputSm} type="number" />
|
||||
<Input disabled={busy} value={replicas} onChange={(e) => setReplicas(e.target.value)} className={styles.inputSm} type="number" placeholder="1" />
|
||||
|
||||
<span className={styles.configLabel}>Deploy Strategy</span>
|
||||
<Select disabled={busy} value={deployStrategy} onChange={(e) => setDeployStrategy(e.target.value)}
|
||||
@@ -525,6 +566,87 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{configTab === 'sensitive-keys' && (
|
||||
<div className={sectionStyles.section}>
|
||||
<div className={skStyles.sectionTitle}>
|
||||
<Shield size={14} />
|
||||
<span>Agent built-in defaults</span>
|
||||
</div>
|
||||
<div className={skStyles.defaultsList}>
|
||||
{['Authorization', 'Cookie', 'Set-Cookie', 'X-API-Key', 'X-Auth-Token', 'Proxy-Authorization'].map((key) => (
|
||||
<Badge key={key} label={key} variant="outlined" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{globalKeys.length > 0 && (
|
||||
<>
|
||||
<hr style={{ border: 'none', borderTop: '1px solid var(--border-subtle)', margin: '10px 0' }} />
|
||||
<div className={skStyles.sectionTitle}>
|
||||
<span>Global keys (enforced)</span>
|
||||
<span className={skStyles.keyCount}>{globalKeys.length}</span>
|
||||
</div>
|
||||
<div className={skStyles.defaultsList}>
|
||||
{globalKeys.map((key) => (
|
||||
<Badge key={key} label={key} color="auto" variant="filled" />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<hr style={{ border: 'none', borderTop: '1px solid var(--border-subtle)', margin: '10px 0' }} />
|
||||
<div className={skStyles.sectionTitle}>
|
||||
<span>Application-specific keys</span>
|
||||
{sensitiveKeys.length > 0 && <span className={skStyles.keyCount}>{sensitiveKeys.length}</span>}
|
||||
</div>
|
||||
|
||||
<div className={skStyles.pillList}>
|
||||
{sensitiveKeys.map((k, i) => (
|
||||
<Tag key={`${k}-${i}`} label={k} onRemove={() => !busy && setSensitiveKeys(sensitiveKeys.filter((_, idx) => idx !== i))} />
|
||||
))}
|
||||
{sensitiveKeys.length === 0 && (
|
||||
<span className={skStyles.emptyState}>No app-specific keys — agents use built-in defaults{globalKeys.length > 0 ? ' and global keys' : ''}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={skStyles.inputRow}>
|
||||
<Input
|
||||
value={newSensitiveKey}
|
||||
onChange={(e) => setNewSensitiveKey(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const v = newSensitiveKey.trim();
|
||||
if (v && !sensitiveKeys.some((k) => k.toLowerCase() === v.toLowerCase())) {
|
||||
setSensitiveKeys([...sensitiveKeys, v]);
|
||||
setNewSensitiveKey('');
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder="Add key or glob pattern (e.g. *password*)"
|
||||
disabled={busy}
|
||||
/>
|
||||
<Button variant="secondary" size="sm" disabled={busy || !newSensitiveKey.trim()}
|
||||
onClick={() => {
|
||||
const v = newSensitiveKey.trim();
|
||||
if (v && !sensitiveKeys.some((k) => k.toLowerCase() === v.toLowerCase())) {
|
||||
setSensitiveKeys([...sensitiveKeys, v]);
|
||||
setNewSensitiveKey('');
|
||||
}
|
||||
}}>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={skStyles.hint}>
|
||||
<Info size={12} />
|
||||
<span>
|
||||
The final masking configuration is: agent defaults + global keys + app-specific keys.
|
||||
Supports exact header names and glob patterns.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,12 +54,6 @@ function durationClass(ms: number, status: string): string {
|
||||
return styles.durBreach
|
||||
}
|
||||
|
||||
function shortAgentName(name: string): string {
|
||||
const parts = name.split('-')
|
||||
if (parts.length >= 3) return parts.slice(-2).join('-')
|
||||
return name
|
||||
}
|
||||
|
||||
// ─── Table columns ────────────────────────────────────────────────────────────
|
||||
|
||||
function buildColumns(hasAttributes: boolean): Column<Row>[] {
|
||||
@@ -77,22 +71,6 @@ function buildColumns(hasAttributes: boolean): Column<Row>[] {
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'routeId',
|
||||
header: 'Route',
|
||||
sortable: true,
|
||||
render: (_: unknown, row: Row) => (
|
||||
<span className={styles.routeName}>{row.routeId}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'applicationId',
|
||||
header: 'Application',
|
||||
sortable: true,
|
||||
render: (_: unknown, row: Row) => (
|
||||
<span className={styles.appName}>{row.applicationId ?? ''}</span>
|
||||
),
|
||||
},
|
||||
...(hasAttributes ? [{
|
||||
key: 'attributes' as const,
|
||||
header: 'Attributes',
|
||||
@@ -115,20 +93,19 @@ function buildColumns(hasAttributes: boolean): Column<Row>[] {
|
||||
},
|
||||
}] : []),
|
||||
{
|
||||
key: 'executionId',
|
||||
header: 'Exchange ID',
|
||||
key: 'applicationId',
|
||||
header: 'App',
|
||||
sortable: true,
|
||||
render: (_: unknown, row: Row) => (
|
||||
<span
|
||||
title={row.executionId}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(row.executionId);
|
||||
}}
|
||||
>
|
||||
<MonoText size="xs">...{row.executionId.slice(-8)}</MonoText>
|
||||
</span>
|
||||
<span className={styles.appName}>{row.applicationId ?? ''}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'routeId',
|
||||
header: 'Route',
|
||||
sortable: true,
|
||||
render: (_: unknown, row: Row) => (
|
||||
<span className={styles.routeName}>{row.routeId}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -149,16 +126,6 @@ function buildColumns(hasAttributes: boolean): Column<Row>[] {
|
||||
</MonoText>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'instanceId',
|
||||
header: 'Agent',
|
||||
render: (_: unknown, row: Row) => (
|
||||
<span className={styles.agentBadge}>
|
||||
<span className={styles.agentDot} />
|
||||
<span title={row.instanceId}>{shortAgentName(row.instanceId)}</span>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import type { TapDefinition } from '../../api/queries/commands';
|
||||
import type { ExecutionSummary } from '../../api/types';
|
||||
import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog';
|
||||
import { buildFlowSegments } from '../../utils/diagram-mapping';
|
||||
import { statusLabel } from '../../utils/format-utils';
|
||||
import styles from './RouteDetail.module.css';
|
||||
import tableStyles from '../../styles/table-section.module.css';
|
||||
import rateStyles from '../../styles/rate-colors.module.css';
|
||||
@@ -658,7 +659,7 @@ export default function RouteDetail() {
|
||||
const recentExchangeOptions = useMemo(() =>
|
||||
exchangeRows.slice(0, 20).map(e => ({
|
||||
value: e.executionId,
|
||||
label: `${e.executionId.slice(0, 12)} — ${e.status}`,
|
||||
label: `${e.executionId.slice(0, 12)} — ${statusLabel(e.status)}`,
|
||||
})),
|
||||
[exchangeRows],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user