diff --git a/web-client/src/main.jsx b/web-client/src/main.jsx
index 37f161d..fa20e0a 100644
--- a/web-client/src/main.jsx
+++ b/web-client/src/main.jsx
@@ -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 (
@@ -814,6 +831,78 @@ function App() {
{toast && }
+ {endpointDialogOpen && (
+
+ )}
+
+ );
+}
+
+function EndpointDialog({ draftSettings, onApply, onCancel, onChange }) {
+ return (
+
);
}
diff --git a/web-client/src/styles.css b/web-client/src/styles.css
index b6b36b5..dbfa414 100644
--- a/web-client/src/styles.css
+++ b/web-client/src/styles.css
@@ -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%;
+ }
+
}