change UI set Endpoint

This commit is contained in:
2026-06-03 09:12:51 +07:00
parent e7dce2f0e9
commit c01d9c7e40
2 changed files with 220 additions and 22 deletions

View File

@@ -22,6 +22,7 @@ import {
ShieldCheck,
Trash2,
WifiOff,
X,
XCircle
} from 'lucide-react';
import {
@@ -159,6 +160,7 @@ function App() {
const [toast, setToast] = useState(null);
const [busyAction, setBusyAction] = useState('');
const [activeTask, setActiveTask] = useState(null);
const [endpointDialogOpen, setEndpointDialogOpen] = useState(false);
const packageBaseUrl = settings.packageBaseUrl;
const agentBaseUrl = settings.agentBaseUrl;
@@ -425,13 +427,25 @@ function App() {
}
}, [agentBaseUrl, agentHealth, notify, packageBaseUrl, startPreflightFailedTask, startTask]);
const openEndpointDialog = useCallback(() => {
setDraftSettings(settings);
setEndpointDialogOpen(true);
}, [settings]);
const closeEndpointDialog = useCallback(() => {
setDraftSettings(settings);
setEndpointDialogOpen(false);
}, [settings]);
const applySettings = useCallback(() => {
const nextSettings = {
packageBaseUrl: normalizeUrl(draftSettings.packageBaseUrl || DEFAULT_PACKAGE_BASE_URL),
agentBaseUrl: normalizeUrl(draftSettings.agentBaseUrl || DEFAULT_AGENT_BASE_URL)
};
setSettings(nextSettings);
setDraftSettings(nextSettings);
saveSettings(nextSettings);
setEndpointDialogOpen(false);
notify('info', 'Đã cập nhật endpoint test');
}, [draftSettings, notify]);
@@ -507,6 +521,19 @@ function App() {
return () => window.clearTimeout(timer);
}, [toast]);
useEffect(() => {
if (!endpointDialogOpen) return undefined;
function onKeyDown(event) {
if (event.key === 'Escape') {
closeEndpointDialog();
}
}
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [closeEndpointDialog, endpointDialogOpen]);
return (
<div className="app-shell">
<aside className="sidebar">
@@ -536,29 +563,19 @@ function App() {
/>
<div className="nav-label">Endpoint</div>
<label className="settings-field">
<span>Package server</span>
<input
value={draftSettings.packageBaseUrl}
onChange={(event) => setDraftSettings((current) => ({
...current,
packageBaseUrl: event.target.value
}))}
/>
</label>
<label className="settings-field">
<span>Local Agent</span>
<input
value={draftSettings.agentBaseUrl}
onChange={(event) => setDraftSettings((current) => ({
...current,
agentBaseUrl: event.target.value
}))}
/>
</label>
<button className="btn btn-secondary full" type="button" onClick={applySettings}>
<div className="endpoint-summary" aria-label="Current endpoints">
<div className="endpoint-summary-row">
<span>Package server</span>
<strong title={packageBaseUrl}>{packageBaseUrl}</strong>
</div>
<div className="endpoint-summary-row">
<span>Local Agent</span>
<strong title={agentBaseUrl}>{agentBaseUrl}</strong>
</div>
</div>
<button className="btn btn-secondary full" type="button" onClick={openEndpointDialog}>
<Settings size={15} aria-hidden="true" />
Apply
Endpoint settings
</button>
</div>
</aside>
@@ -814,6 +831,78 @@ function App() {
</main>
{toast && <Toast toast={toast} />}
{endpointDialogOpen && (
<EndpointDialog
draftSettings={draftSettings}
onApply={applySettings}
onCancel={closeEndpointDialog}
onChange={setDraftSettings}
/>
)}
</div>
);
}
function EndpointDialog({ draftSettings, onApply, onCancel, onChange }) {
return (
<div className="dialog-backdrop">
<section
aria-labelledby="endpoint-dialog-title"
aria-modal="true"
className="dialog-panel"
role="dialog"
>
<form
onSubmit={(event) => {
event.preventDefault();
onApply();
}}
>
<div className="dialog-header">
<div>
<h2 id="endpoint-dialog-title">Endpoint settings</h2>
<p>Changes only apply after you press Apply.</p>
</div>
<button className="icon-button subtle" type="button" title="Close" onClick={onCancel}>
<X size={16} aria-hidden="true" />
</button>
</div>
<div className="dialog-body">
<label className="settings-field">
<span>Package server</span>
<input
autoFocus
value={draftSettings.packageBaseUrl}
onChange={(event) => onChange((current) => ({
...current,
packageBaseUrl: event.target.value
}))}
/>
</label>
<label className="settings-field">
<span>Local Agent</span>
<input
value={draftSettings.agentBaseUrl}
onChange={(event) => onChange((current) => ({
...current,
agentBaseUrl: event.target.value
}))}
/>
</label>
</div>
<div className="dialog-actions">
<button className="btn btn-secondary" type="button" onClick={onCancel}>
Cancel
</button>
<button className="btn btn-primary" type="submit">
<Settings size={15} aria-hidden="true" />
Apply endpoints
</button>
</div>
</form>
</section>
</div>
);
}

View File

@@ -144,6 +144,7 @@ code {
}
.brand-copy span,
.endpoint-summary-row span,
.settings-field span,
.nav-label {
color: var(--on-surface-variant);
@@ -237,6 +238,35 @@ code {
gap: 5px;
}
.endpoint-summary {
background: rgba(255, 255, 255, 0.68);
border: 1px solid rgba(169, 180, 185, 0.35);
border-radius: var(--radius);
display: flex;
flex-direction: column;
gap: 9px;
padding: 10px;
}
.endpoint-summary-row {
min-width: 0;
}
.endpoint-summary-row span,
.endpoint-summary-row strong {
display: block;
}
.endpoint-summary-row strong {
color: #172033;
font-size: 12px;
font-weight: 800;
margin-top: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.settings-field input,
.filter-field input,
.filter-field select {
@@ -998,6 +1028,71 @@ tbody tr.selected-row td.action-col {
overflow-wrap: anywhere;
}
.dialog-backdrop {
align-items: center;
background: rgba(15, 23, 42, 0.36);
display: flex;
inset: 0;
justify-content: center;
padding: 24px;
position: fixed;
z-index: 110;
}
.dialog-panel {
background: var(--surface-lowest);
border: 1px solid rgba(169, 180, 185, 0.5);
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
max-height: calc(100vh - 48px);
overflow: auto;
width: min(480px, 100%);
}
.dialog-panel form {
display: flex;
flex-direction: column;
}
.dialog-header {
align-items: flex-start;
border-bottom: 1px solid #eef2f7;
display: flex;
gap: 14px;
justify-content: space-between;
padding: 16px;
}
.dialog-header h2 {
color: #111827;
font-size: 18px;
font-weight: 800;
line-height: 1.2;
}
.dialog-header p {
color: var(--on-surface-variant);
font-size: 12px;
line-height: 1.45;
margin-top: 4px;
}
.dialog-body {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
}
.dialog-actions {
align-items: center;
border-top: 1px solid #eef2f7;
display: flex;
gap: 8px;
justify-content: flex-end;
padding: 12px 16px 16px;
}
.toast {
align-items: center;
background: #111827;
@@ -1143,4 +1238,18 @@ tbody tr.selected-row td.action-col {
gap: 6px;
}
.dialog-backdrop {
align-items: flex-start;
padding: 16px;
}
.dialog-actions {
align-items: stretch;
flex-direction: column-reverse;
}
.dialog-actions .btn {
width: 100%;
}
}