fix: add unsaved changes banners to edit mode forms
Adds amber edit-mode banners to AppConfigDetailPage and both DefaultResourcesSection/JarRetentionSection in EnvironmentsPage, matching the existing ConfigSubTab pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -90,3 +90,13 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editBanner {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: color-mix(in srgb, var(--amber) 8%, transparent);
|
||||||
|
border: 1px solid var(--amber);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -317,6 +317,12 @@ export default function AppConfigDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{editing && (
|
||||||
|
<div className={styles.editBanner}>
|
||||||
|
Editing configuration. Changes are not saved until you click Save.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<h2 className={styles.title}><MonoText size="md">{appId}</MonoText></h2>
|
<h2 className={styles.title}><MonoText size="md">{appId}</MonoText></h2>
|
||||||
<div className={styles.meta}>
|
<div className={styles.meta}>
|
||||||
|
|||||||
@@ -392,6 +392,11 @@ function DefaultResourcesSection({ environment, onSave, saving }: {
|
|||||||
return (
|
return (
|
||||||
<div className={sectionStyles.section}>
|
<div className={sectionStyles.section}>
|
||||||
<SectionHeader>Default Resource Limits</SectionHeader>
|
<SectionHeader>Default Resource Limits</SectionHeader>
|
||||||
|
{editing && (
|
||||||
|
<div className={styles.editBanner}>
|
||||||
|
Editing resource defaults. Changes are not saved until you click Save.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<p className={styles.inheritedNote}>
|
<p className={styles.inheritedNote}>
|
||||||
These defaults apply to new apps in this environment unless overridden per-app.
|
These defaults apply to new apps in this environment unless overridden per-app.
|
||||||
</p>
|
</p>
|
||||||
@@ -485,6 +490,11 @@ function JarRetentionSection({ environment, onSave, saving }: {
|
|||||||
return (
|
return (
|
||||||
<div className={sectionStyles.section}>
|
<div className={sectionStyles.section}>
|
||||||
<SectionHeader>JAR Retention</SectionHeader>
|
<SectionHeader>JAR Retention</SectionHeader>
|
||||||
|
{editing && (
|
||||||
|
<div className={styles.editBanner}>
|
||||||
|
Editing resource defaults. Changes are not saved until you click Save.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<p className={styles.inheritedNote}>
|
<p className={styles.inheritedNote}>
|
||||||
Old JAR versions are cleaned up nightly. Currently deployed versions are never deleted.
|
Old JAR versions are cleaned up nightly. Currently deployed versions are never deleted.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Eye, EyeOff } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Button, Input, Toggle, FormField, SectionHeader, Tag, ConfirmDialog,
|
Button, Input, Toggle, FormField, SectionHeader, Tag, ConfirmDialog,
|
||||||
} from '@cameleer/design-system';
|
} from '@cameleer/design-system';
|
||||||
@@ -41,6 +42,7 @@ export default function OidcConfigPage() {
|
|||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [formDraft, setFormDraft] = useState<OidcFormData | null>(null);
|
const [formDraft, setFormDraft] = useState<OidcFormData | null>(null);
|
||||||
const [newRole, setNewRole] = useState('');
|
const [newRole, setNewRole] = useState('');
|
||||||
|
const [showSecret, setShowSecret] = useState(false);
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [testing, setTesting] = useState(false);
|
const [testing, setTesting] = useState(false);
|
||||||
@@ -200,13 +202,25 @@ export default function OidcConfigPage() {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Client Secret" htmlFor="client-secret">
|
<FormField label="Client Secret" htmlFor="client-secret">
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
<Input
|
<Input
|
||||||
id="client-secret"
|
id="client-secret"
|
||||||
type="password"
|
type={showSecret ? 'text' : 'password'}
|
||||||
value={current?.clientSecret ?? ''}
|
value={current?.clientSecret ?? ''}
|
||||||
onChange={(e) => updateDraft('clientSecret', e.target.value)}
|
onChange={(e) => updateDraft('clientSecret', e.target.value)}
|
||||||
disabled={!editing}
|
disabled={!editing}
|
||||||
/>
|
/>
|
||||||
|
{editing && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
style={{ position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)' }}
|
||||||
|
onClick={() => setShowSecret(!showSecret)}
|
||||||
|
>
|
||||||
|
{showSecret ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Audience / API Resource" htmlFor="audience" hint="RFC 8707 resource indicator sent in the authorization request">
|
<FormField label="Audience / API Resource" htmlFor="audience" hint="RFC 8707 resource indicator sent in the authorization request">
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -161,3 +161,13 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editBanner {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: color-mix(in srgb, var(--amber) 8%, transparent);
|
||||||
|
border: 1px solid var(--amber);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
DataTable,
|
DataTable,
|
||||||
|
EmptyState,
|
||||||
Input,
|
Input,
|
||||||
MonoText,
|
MonoText,
|
||||||
SectionHeader,
|
SectionHeader,
|
||||||
@@ -714,7 +715,7 @@ function OverviewSubTab({ app, deployments, versions, environments, envMap, sele
|
|||||||
<span className={tableStyles.tableTitle}>Deployments</span>
|
<span className={tableStyles.tableTitle}>Deployments</span>
|
||||||
</div>
|
</div>
|
||||||
{deploymentRows.length === 0
|
{deploymentRows.length === 0
|
||||||
? <p className={styles.emptyNote} style={{ padding: '12px 16px', margin: 0 }}>No deployments yet.</p>
|
? <EmptyState title="No deployments" description="Deploy this application to see deployment history." />
|
||||||
: <DataTable<DeploymentRow> columns={deploymentColumns} data={deploymentRows} flush />
|
: <DataTable<DeploymentRow> columns={deploymentColumns} data={deploymentRows} flush />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -726,7 +727,7 @@ function OverviewSubTab({ app, deployments, versions, environments, envMap, sele
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<SectionHeader>Versions ({versions.length})</SectionHeader>
|
<SectionHeader>Versions ({versions.length})</SectionHeader>
|
||||||
{versions.length === 0 && <p className={styles.emptyNote}>No versions uploaded yet.</p>}
|
{versions.length === 0 && <EmptyState title="No versions" description="Upload a JAR to create the first version." />}
|
||||||
{versions.map((v) => (
|
{versions.map((v) => (
|
||||||
<VersionRow key={v.id} version={v} environments={environments} onDeploy={(envId) => onDeploy(v.id, envId)} />
|
<VersionRow key={v.id} version={v} environments={environments} onDeploy={(envId) => onDeploy(v.id, envId)} />
|
||||||
))}
|
))}
|
||||||
@@ -1005,7 +1006,7 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
|
|||||||
{editing && (
|
{editing && (
|
||||||
<Button size="sm" variant="secondary" onClick={() => setEnvVars([...envVars, { key: '', value: '' }])}>+ Add Variable</Button>
|
<Button size="sm" variant="secondary" onClick={() => setEnvVars([...envVars, { key: '', value: '' }])}>+ Add Variable</Button>
|
||||||
)}
|
)}
|
||||||
{envVars.length === 0 && !editing && <p className={styles.emptyNote}>No environment variables configured.</p>}
|
{envVars.length === 0 && !editing && <EmptyState title="No variables" description="No environment variables configured." />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1075,7 +1076,7 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
|
|||||||
<span className={styles.sectionSummary}>{tracedCount} traced · {tapCount} taps</span>
|
<span className={styles.sectionSummary}>{tracedCount} traced · {tapCount} taps</span>
|
||||||
{tracedTapRows.length > 0
|
{tracedTapRows.length > 0
|
||||||
? <DataTable<TracedTapRow> columns={tracedTapColumns} data={tracedTapRows} pageSize={20} flush />
|
? <DataTable<TracedTapRow> columns={tracedTapColumns} data={tracedTapRows} pageSize={20} flush />
|
||||||
: <p className={styles.emptyNote}>No processor traces or taps configured.</p>}
|
: <EmptyState title="No traces or taps" description="No processor traces or taps configured." />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1085,7 +1086,7 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
|
|||||||
<span className={styles.sectionSummary}>{recordingCount} of {routeRecordingRows.length} routes recording</span>
|
<span className={styles.sectionSummary}>{recordingCount} of {routeRecordingRows.length} routes recording</span>
|
||||||
{routeRecordingRows.length > 0
|
{routeRecordingRows.length > 0
|
||||||
? <DataTable<RouteRecordingRow> columns={routeRecordingColumns} data={routeRecordingRows} pageSize={20} flush />
|
? <DataTable<RouteRecordingRow> columns={routeRecordingColumns} data={routeRecordingRows} pageSize={20} flush />
|
||||||
: <p className={styles.emptyNote}>No routes found for this application.</p>}
|
: <EmptyState title="No routes" description="No routes found for this application. Routes appear once agents report data." />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
StatusDot,
|
StatusDot,
|
||||||
DataTable,
|
DataTable,
|
||||||
|
EmptyState,
|
||||||
Tabs,
|
Tabs,
|
||||||
AreaChart,
|
AreaChart,
|
||||||
LineChart,
|
LineChart,
|
||||||
@@ -702,9 +703,7 @@ export default function RouteDetail() {
|
|||||||
{diagramFlows.length > 0 ? (
|
{diagramFlows.length > 0 ? (
|
||||||
<RouteFlow flows={diagramFlows} />
|
<RouteFlow flows={diagramFlows} />
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.emptyText}>
|
<EmptyState title="No diagram" description="No diagram available for this route." />
|
||||||
No diagram available for this route.
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.statsPane}>
|
<div className={styles.statsPane}>
|
||||||
@@ -714,9 +713,7 @@ export default function RouteDetail() {
|
|||||||
) : processorRows.length > 0 ? (
|
) : processorRows.length > 0 ? (
|
||||||
<DataTable columns={processorColumns} data={processorRows} sortable pageSize={10} />
|
<DataTable columns={processorColumns} data={processorRows} sortable pageSize={10} />
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.emptyText}>
|
<EmptyState title="No processor data" description="No processor data available." />
|
||||||
No processor data available.
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -819,9 +816,7 @@ export default function RouteDetail() {
|
|||||||
{activeTab === 'errors' && (
|
{activeTab === 'errors' && (
|
||||||
<div className={styles.errorPatterns} style={{ marginTop: 16 }}>
|
<div className={styles.errorPatterns} style={{ marginTop: 16 }}>
|
||||||
{errorPatterns.length === 0 ? (
|
{errorPatterns.length === 0 ? (
|
||||||
<div className={styles.emptyText}>
|
<EmptyState title="No error patterns" description="No error patterns found in the selected time range." />
|
||||||
No error patterns found in the selected time range.
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
errorPatterns.map((ep, i) => (
|
errorPatterns.map((ep, i) => (
|
||||||
<div key={i} className={styles.errorRow}>
|
<div key={i} className={styles.errorRow}>
|
||||||
@@ -841,9 +836,7 @@ export default function RouteDetail() {
|
|||||||
<Button variant="primary" size="sm" onClick={() => openTapModal(null)}>+ Add Tap</Button>
|
<Button variant="primary" size="sm" onClick={() => openTapModal(null)}>+ Add Tap</Button>
|
||||||
</div>
|
</div>
|
||||||
{routeTaps.length === 0 ? (
|
{routeTaps.length === 0 ? (
|
||||||
<div className={styles.emptyState}>
|
<EmptyState title="No taps" description="No taps configured for this route. Add a tap to extract business attributes from exchange data." />
|
||||||
No taps configured for this route. Add a tap to extract business attributes from exchange data.
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={tapColumns}
|
columns={tapColumns}
|
||||||
|
|||||||
Reference in New Issue
Block a user