fix client
This commit is contained in:
@@ -20,7 +20,6 @@ import {
|
|||||||
Server,
|
Server,
|
||||||
Settings,
|
Settings,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
TerminalSquare,
|
|
||||||
Trash2,
|
Trash2,
|
||||||
WifiOff,
|
WifiOff,
|
||||||
XCircle
|
XCircle
|
||||||
@@ -34,8 +33,6 @@ import {
|
|||||||
fetchApplicationManifest,
|
fetchApplicationManifest,
|
||||||
fetchInstalledApps,
|
fetchInstalledApps,
|
||||||
fetchPackageApps,
|
fetchPackageApps,
|
||||||
fetchTaskComponents,
|
|
||||||
fetchTaskLogs,
|
|
||||||
fetchTaskStatus,
|
fetchTaskStatus,
|
||||||
joinUrl,
|
joinUrl,
|
||||||
normalizeUrl,
|
normalizeUrl,
|
||||||
@@ -67,24 +64,6 @@ function saveSettings(settings) {
|
|||||||
window.localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
window.localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusTone(status) {
|
|
||||||
if (status === 'success' || status === 'installed' || status === 'online') return 'success';
|
|
||||||
if (status === 'running' || status === 'queued') return 'info';
|
|
||||||
if (status === 'failed' || status === 'offline') return 'danger';
|
|
||||||
if (status === 'update') return 'warning';
|
|
||||||
return 'muted';
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(value) {
|
|
||||||
if (!value) return '-';
|
|
||||||
const date = new Date(value);
|
|
||||||
if (Number.isNaN(date.getTime())) return value;
|
|
||||||
return new Intl.DateTimeFormat('vi-VN', {
|
|
||||||
dateStyle: 'short',
|
|
||||||
timeStyle: 'short'
|
|
||||||
}).format(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getErrorMessage(error) {
|
function getErrorMessage(error) {
|
||||||
return error instanceof Error ? error.message : String(error || 'Có lỗi xảy ra');
|
return error instanceof Error ? error.message : String(error || 'Có lỗi xảy ra');
|
||||||
}
|
}
|
||||||
@@ -107,10 +86,6 @@ function App() {
|
|||||||
const [toast, setToast] = useState(null);
|
const [toast, setToast] = useState(null);
|
||||||
const [busyAction, setBusyAction] = useState('');
|
const [busyAction, setBusyAction] = useState('');
|
||||||
const [activeTask, setActiveTask] = useState(null);
|
const [activeTask, setActiveTask] = useState(null);
|
||||||
const [task, setTask] = useState(null);
|
|
||||||
const [taskLogs, setTaskLogs] = useState([]);
|
|
||||||
const [taskComponents, setTaskComponents] = useState([]);
|
|
||||||
const [taskStatus, setTaskStatus] = useState({ state: 'idle', message: '' });
|
|
||||||
|
|
||||||
const packageBaseUrl = settings.packageBaseUrl;
|
const packageBaseUrl = settings.packageBaseUrl;
|
||||||
const agentBaseUrl = settings.agentBaseUrl;
|
const agentBaseUrl = settings.agentBaseUrl;
|
||||||
@@ -235,24 +210,14 @@ function App() {
|
|||||||
}, [packageBaseUrl]);
|
}, [packageBaseUrl]);
|
||||||
|
|
||||||
const loadTaskSnapshot = useCallback(async (taskId) => {
|
const loadTaskSnapshot = useCallback(async (taskId) => {
|
||||||
setTaskStatus({ state: 'loading', message: 'Đang cập nhật task' });
|
|
||||||
try {
|
try {
|
||||||
const [nextTask, nextLogs, nextComponents] = await Promise.all([
|
const nextTask = await fetchTaskStatus(agentBaseUrl, taskId);
|
||||||
fetchTaskStatus(agentBaseUrl, taskId),
|
|
||||||
fetchTaskLogs(agentBaseUrl, taskId),
|
|
||||||
fetchTaskComponents(agentBaseUrl, taskId).catch(() => [])
|
|
||||||
]);
|
|
||||||
setTask(nextTask);
|
|
||||||
setTaskLogs(nextLogs);
|
|
||||||
setTaskComponents(nextComponents);
|
|
||||||
setTaskStatus({ state: statusTone(nextTask.status), message: nextTask.status });
|
|
||||||
|
|
||||||
if (TERMINAL_TASK_STATUSES.has(nextTask.status)) {
|
if (TERMINAL_TASK_STATUSES.has(nextTask.status)) {
|
||||||
await refreshAgent();
|
await refreshAgent();
|
||||||
}
|
}
|
||||||
return nextTask;
|
return nextTask;
|
||||||
} catch (error) {
|
} catch {
|
||||||
setTaskStatus({ state: 'danger', message: getErrorMessage(error) });
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, [agentBaseUrl, refreshAgent]);
|
}, [agentBaseUrl, refreshAgent]);
|
||||||
@@ -265,17 +230,6 @@ function App() {
|
|||||||
appName: app.appName,
|
appName: app.appName,
|
||||||
queuedAt: new Date().toISOString()
|
queuedAt: new Date().toISOString()
|
||||||
});
|
});
|
||||||
setTask({
|
|
||||||
taskId: queuedTask.taskId,
|
|
||||||
type: action,
|
|
||||||
appId: app.appId,
|
|
||||||
appName: app.appName,
|
|
||||||
status: queuedTask.status || 'queued',
|
|
||||||
progress: 0,
|
|
||||||
currentStep: 'queued'
|
|
||||||
});
|
|
||||||
setTaskLogs([]);
|
|
||||||
setTaskComponents([]);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const runAppAction = useCallback(async (action, app) => {
|
const runAppAction = useCallback(async (action, app) => {
|
||||||
@@ -618,14 +572,6 @@ function App() {
|
|||||||
status={detailStatus}
|
status={detailStatus}
|
||||||
packageBaseUrl={packageBaseUrl}
|
packageBaseUrl={packageBaseUrl}
|
||||||
/>
|
/>
|
||||||
<TaskPanel
|
|
||||||
activeTask={activeTask}
|
|
||||||
task={task}
|
|
||||||
logs={taskLogs}
|
|
||||||
components={taskComponents}
|
|
||||||
status={taskStatus}
|
|
||||||
onRefresh={() => activeTask?.taskId && loadTaskSnapshot(activeTask.taskId)}
|
|
||||||
/>
|
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -723,7 +669,7 @@ function AppDetailPanel({ app, detail, manifest, status, packageBaseUrl }) {
|
|||||||
const components = manifest?.components || [];
|
const components = manifest?.components || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel">
|
<section className="panel app-detail-panel">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>{app?.appName || 'App detail'}</h2>
|
<h2>{app?.appName || 'App detail'}</h2>
|
||||||
@@ -775,78 +721,6 @@ function AppDetailPanel({ app, detail, manifest, status, packageBaseUrl }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TaskPanel({ activeTask, task, logs, components, status, onRefresh }) {
|
|
||||||
const progress = Math.max(0, Math.min(100, Number(task?.progress || 0)));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="panel task-panel">
|
|
||||||
<div className="panel-header">
|
|
||||||
<div>
|
|
||||||
<h2>Task monitor</h2>
|
|
||||||
<p>{activeTask?.taskId || 'Chưa có task'}</p>
|
|
||||||
</div>
|
|
||||||
<button className="icon-button subtle" type="button" onClick={onRefresh} disabled={!activeTask?.taskId} title="Refresh task">
|
|
||||||
<RefreshCcw size={16} aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{task ? (
|
|
||||||
<>
|
|
||||||
<div className="task-summary">
|
|
||||||
<div>
|
|
||||||
<span className={`badge badge-${statusTone(task.status)}`}>{task.status}</span>
|
|
||||||
<strong>{task.appName || task.appId}</strong>
|
|
||||||
<small>{task.currentStep || '-'}</small>
|
|
||||||
</div>
|
|
||||||
<span>{progress}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="progress-track">
|
|
||||||
<div style={{ width: `${progress}%` }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{components.length > 0 && (
|
|
||||||
<div className="component-list task-components">
|
|
||||||
<div className="component-list-title">
|
|
||||||
<Activity size={15} aria-hidden="true" />
|
|
||||||
Component progress
|
|
||||||
</div>
|
|
||||||
{components.map((component) => (
|
|
||||||
<div className="component-item" key={component.componentId}>
|
|
||||||
<div>
|
|
||||||
<strong>{component.componentId}</strong>
|
|
||||||
<span>{component.currentStep || component.type}</span>
|
|
||||||
</div>
|
|
||||||
<span className={`badge badge-${statusTone(component.status)}`}>{component.progress || 0}%</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="logs-box">
|
|
||||||
<div className="component-list-title">
|
|
||||||
<TerminalSquare size={15} aria-hidden="true" />
|
|
||||||
Logs
|
|
||||||
</div>
|
|
||||||
<div className="log-lines">
|
|
||||||
{logs.slice(-8).map((log, index) => (
|
|
||||||
<div className={`log-line level-${log.level || 'info'}`} key={`${log.time}-${index}`}>
|
|
||||||
<time>{formatDate(log.time)}</time>
|
|
||||||
<span>{log.message}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{logs.length === 0 && (
|
|
||||||
<div className="table-empty compact-empty">{status.message || 'Đang chờ log.'}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="table-empty compact-empty">Install, update hoặc remove để bắt đầu theo dõi.</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Toast({ toast }) {
|
function Toast({ toast }) {
|
||||||
const tone = toast.type === 'failure' ? 'danger' : toast.type;
|
const tone = toast.type === 'failure' ? 'danger' : toast.type;
|
||||||
const Icon = tone === 'danger' ? AlertCircle : tone === 'success' ? CheckCircle2 : Activity;
|
const Icon = tone === 'danger' ? AlertCircle : tone === 'success' ? CheckCircle2 : Activity;
|
||||||
|
|||||||
@@ -541,6 +541,20 @@ code {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-stack > .panel:not(.app-detail-panel) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-detail-panel {
|
||||||
|
flex: 1 1 360px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-detail-panel .component-list {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.page-filters {
|
.page-filters {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -879,104 +893,6 @@ tbody tr.selected-row td.action-col {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-summary {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 14px 16px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-summary > div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-summary strong {
|
|
||||||
color: #111827;
|
|
||||||
font-size: 13px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-summary small {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-summary > span {
|
|
||||||
color: #111827;
|
|
||||||
font-family: "Manrope", Arial, sans-serif;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-track {
|
|
||||||
background: #e2e8f0;
|
|
||||||
border-radius: 999px;
|
|
||||||
height: 8px;
|
|
||||||
margin: 0 16px 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-track div {
|
|
||||||
background: var(--primary);
|
|
||||||
border-radius: inherit;
|
|
||||||
height: 100%;
|
|
||||||
transition: width 0.18s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-components {
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs-box {
|
|
||||||
border-top: 1px solid #eef2f7;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0;
|
|
||||||
padding: 12px 16px 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-lines {
|
|
||||||
background: #111827;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
color: #f8fafc;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0;
|
|
||||||
margin-top: 8px;
|
|
||||||
max-height: 240px;
|
|
||||||
overflow: auto;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-line {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
grid-template-columns: 118px minmax(0, 1fr);
|
|
||||||
padding: 4px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-line time {
|
|
||||||
color: #94a3b8;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-line span {
|
|
||||||
font-family: Consolas, "Liberation Mono", monospace;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.45;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-line.level-error span {
|
|
||||||
color: #fecaca;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast {
|
.toast {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #111827;
|
background: #111827;
|
||||||
@@ -1028,10 +944,6 @@ tbody tr.selected-row td.action-col {
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-panel {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
@@ -1126,7 +1038,4 @@ tbody tr.selected-row td.action-col {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-line {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user