fix: resolve UI glitches and improve consistency
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m22s
CI / docker (push) Successful in 1m36s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 38s

- 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:
hsiegeln
2026-04-15 19:41:36 +02:00
parent 091dfb34d0
commit 457650012b
12 changed files with 493 additions and 102 deletions

View File

@@ -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>
);
}