update check active
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import {
|
||||
Activity,
|
||||
@@ -121,6 +121,36 @@ function statusBadgeClass(status) {
|
||||
return `badge badge-${tone}`;
|
||||
}
|
||||
|
||||
function serviceCheckTone(check) {
|
||||
if (check?.healthy) return 'success';
|
||||
if (check?.activeState === 'activating' || check?.status === 'checking') return 'warning';
|
||||
if (['failed', 'inactive', 'not-found'].includes(check?.status || check?.activeState)) return 'danger';
|
||||
return 'muted';
|
||||
}
|
||||
|
||||
function serviceCheckLabel(check) {
|
||||
if (check?.readinessStatus === 'failed') return 'Not ready';
|
||||
if (check?.healthy) return check.readinessStatus === 'ready' ? 'Active · Ready' : 'Active';
|
||||
if (check?.loadState === 'not-found' || check?.status === 'not-found') return 'Not found';
|
||||
if (check?.activeState === 'failed') return 'Failed';
|
||||
if (check?.activeState === 'inactive') return 'Inactive';
|
||||
if (check?.activeState === 'activating') return 'Activating';
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
function serviceCheckSummary(status, checks = []) {
|
||||
if (status === 'checking') return 'Checking services';
|
||||
if (status === 'not-applicable') return 'No systemd service';
|
||||
if (status === 'unhealthy') {
|
||||
const count = checks.filter((check) => !check.healthy).length || checks.length;
|
||||
return `${count} service${count === 1 ? '' : 's'} need attention`;
|
||||
}
|
||||
if (status === 'healthy') {
|
||||
return `${checks.length} service${checks.length === 1 ? '' : 's'} active`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function formatTaskTime(value) {
|
||||
if (!value) return '--';
|
||||
const parsed = new Date(value);
|
||||
@@ -218,6 +248,7 @@ function App() {
|
||||
const [draftSettings, setDraftSettings] = useState(settings);
|
||||
const [apps, setApps] = useState([]);
|
||||
const [installedApps, setInstalledApps] = useState([]);
|
||||
const [installedAppsReady, setInstalledAppsReady] = useState(false);
|
||||
const [latestAgentPackage, setLatestAgentPackage] = useState(null);
|
||||
const [agentHealth, setAgentHealth] = useState(null);
|
||||
const [systemInfo, setSystemInfo] = useState(null);
|
||||
@@ -234,9 +265,13 @@ function App() {
|
||||
const [activeTask, setActiveTask] = useState(null);
|
||||
const [endpointDialogOpen, setEndpointDialogOpen] = useState(false);
|
||||
const [agentDialogOpen, setAgentDialogOpen] = useState(false);
|
||||
const agentEndpointRef = useRef(settings.agentBaseUrl);
|
||||
const agentEndpointGenerationRef = useRef(0);
|
||||
const agentRefreshGenerationRef = useRef(0);
|
||||
|
||||
const packageBaseUrl = settings.packageBaseUrl;
|
||||
const agentBaseUrl = settings.agentBaseUrl;
|
||||
agentEndpointRef.current = agentBaseUrl;
|
||||
const installCommand = `curl -fsSL ${joinUrl(packageBaseUrl, '/install-agent.sh')} | sudo bash`;
|
||||
const agentCommand = latestAgentPackage?.installCommand || installCommand;
|
||||
const clientOs = useMemo(() => detectClientOs(), []);
|
||||
@@ -260,12 +295,14 @@ function App() {
|
||||
const installed = installedByAppId.get(app.appId) || installedByAppId.get(app.appCode);
|
||||
const isInstalled = Boolean(installed);
|
||||
const canUpdate = Boolean(isInstalled && installed.version && installed.version !== app.version);
|
||||
const needsAttention = Boolean(isInstalled && installed.status === 'attention');
|
||||
|
||||
return {
|
||||
...app,
|
||||
installed,
|
||||
localStatus: canUpdate ? 'update' : (isInstalled ? 'installed' : 'available'),
|
||||
canUpdate
|
||||
canUpdate,
|
||||
needsAttention
|
||||
};
|
||||
});
|
||||
}, [apps, installedByAppId]);
|
||||
@@ -326,10 +363,21 @@ function App() {
|
||||
}, [packageBaseUrl]);
|
||||
|
||||
const refreshAgent = useCallback(async () => {
|
||||
const requestEndpoint = agentBaseUrl;
|
||||
if (agentEndpointRef.current !== requestEndpoint) return false;
|
||||
|
||||
const requestGeneration = ++agentRefreshGenerationRef.current;
|
||||
const isCurrentRequest = () => (
|
||||
agentEndpointRef.current === requestEndpoint
|
||||
&& agentRefreshGenerationRef.current === requestGeneration
|
||||
);
|
||||
|
||||
if (!canUseAgentEndpoint) {
|
||||
if (!isCurrentRequest()) return false;
|
||||
setAgentHealth(null);
|
||||
setSystemInfo(null);
|
||||
setInstalledApps([]);
|
||||
setInstalledAppsReady(false);
|
||||
setAgentStatus({
|
||||
state: 'warning',
|
||||
message: isClientWindows
|
||||
@@ -340,22 +388,37 @@ function App() {
|
||||
}
|
||||
|
||||
setAgentStatus({ state: 'loading', message: `Checking ${agentTargetLabel}` });
|
||||
setInstalledAppsReady(false);
|
||||
try {
|
||||
const health = await fetchAgentHealth(agentBaseUrl);
|
||||
const health = await fetchAgentHealth(requestEndpoint);
|
||||
if (!isCurrentRequest()) return false;
|
||||
setAgentHealth(health);
|
||||
setAgentStatus({ state: 'success', message: `${health.hostname || agentTargetLabel} online` });
|
||||
|
||||
const [info, installed] = await Promise.all([
|
||||
fetchAgentSystemInfo(agentBaseUrl).catch(() => null),
|
||||
fetchInstalledApps(agentBaseUrl)
|
||||
const [infoResult, installedResult] = await Promise.allSettled([
|
||||
fetchAgentSystemInfo(requestEndpoint),
|
||||
fetchInstalledApps(requestEndpoint)
|
||||
]);
|
||||
setSystemInfo(info);
|
||||
setInstalledApps(installed);
|
||||
if (!isCurrentRequest()) return false;
|
||||
setSystemInfo(infoResult.status === 'fulfilled' ? infoResult.value : null);
|
||||
if (installedResult.status === 'fulfilled') {
|
||||
setInstalledApps(installedResult.value);
|
||||
setInstalledAppsReady(true);
|
||||
} else {
|
||||
setInstalledApps([]);
|
||||
setInstalledAppsReady(false);
|
||||
setAgentStatus({
|
||||
state: 'warning',
|
||||
message: `${health.hostname || agentTargetLabel} online · installed app status unavailable: ${getErrorMessage(installedResult.reason)}`
|
||||
});
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (!isCurrentRequest()) return false;
|
||||
setAgentHealth(null);
|
||||
setSystemInfo(null);
|
||||
setInstalledApps([]);
|
||||
setInstalledAppsReady(false);
|
||||
setAgentStatus({ state: 'danger', message: getErrorMessage(error) });
|
||||
return false;
|
||||
}
|
||||
@@ -392,13 +455,13 @@ function App() {
|
||||
try {
|
||||
const [nextTask, logs, components] = await Promise.all([
|
||||
fetchTaskStatus(agentBaseUrl, taskId),
|
||||
fetchTaskLogs(agentBaseUrl, taskId).catch(() => []),
|
||||
fetchTaskComponents(agentBaseUrl, taskId).catch(() => [])
|
||||
fetchTaskLogs(agentBaseUrl, taskId).catch(() => null),
|
||||
fetchTaskComponents(agentBaseUrl, taskId).catch(() => null)
|
||||
]);
|
||||
const snapshot = {
|
||||
...nextTask,
|
||||
logs,
|
||||
components,
|
||||
...(logs ? { logs } : {}),
|
||||
...(components ? { components } : {}),
|
||||
pollError: ''
|
||||
};
|
||||
|
||||
@@ -483,31 +546,54 @@ function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!installedAppsReady) {
|
||||
notify('warning', 'Installed app status is not ready. Refresh the Agent before running an action.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'remove' && !window.confirm(`Remove and clean ${app.appName} from ${agentTargetMachine}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actionEndpoint = agentBaseUrl;
|
||||
const endpointGeneration = agentEndpointGenerationRef.current;
|
||||
const isCurrentEndpoint = () => (
|
||||
agentEndpointRef.current === actionEndpoint
|
||||
&& agentEndpointGenerationRef.current === endpointGeneration
|
||||
);
|
||||
if (!isCurrentEndpoint()) {
|
||||
notify('warning', 'Agent endpoint changed. Retry the action on the current Agent.');
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `${action}:${app.appId}`;
|
||||
setBusyAction(key);
|
||||
try {
|
||||
let queuedTask;
|
||||
if (action === 'install') {
|
||||
const manifest = await fetchApplicationManifest(packageBaseUrl, app.appId, app.version);
|
||||
if (!isCurrentEndpoint()) return;
|
||||
setSelectedManifest(manifest);
|
||||
setDetailStatus({ state: 'success', message: 'Manifest ready' });
|
||||
queuedTask = await queueInstall(agentBaseUrl, app);
|
||||
queuedTask = await queueInstall(actionEndpoint, app);
|
||||
} else if (action === 'update') {
|
||||
const manifest = await fetchApplicationManifest(packageBaseUrl, app.appId, app.version);
|
||||
if (!isCurrentEndpoint()) return;
|
||||
setSelectedManifest(manifest);
|
||||
setDetailStatus({ state: 'success', message: 'Manifest ready' });
|
||||
queuedTask = await queueUpdate(agentBaseUrl, app, app.installed);
|
||||
queuedTask = await queueUpdate(actionEndpoint, app, app.installed);
|
||||
} else {
|
||||
queuedTask = await queueRemove(agentBaseUrl, app);
|
||||
queuedTask = await queueRemove(actionEndpoint, app);
|
||||
}
|
||||
|
||||
if (!isCurrentEndpoint()) {
|
||||
notify('warning', 'Agent endpoint changed while the request was running. Switch back to monitor the previous Agent.');
|
||||
return;
|
||||
}
|
||||
startTask(queuedTask, action, app);
|
||||
notify('success', `Đã queue task ${queuedTask.taskId}`);
|
||||
} catch (error) {
|
||||
if (!isCurrentEndpoint()) return;
|
||||
const message = getErrorMessage(error);
|
||||
if (action === 'install' || action === 'update') {
|
||||
setSelectedManifest(null);
|
||||
@@ -516,9 +602,9 @@ function App() {
|
||||
}
|
||||
notify('failure', message);
|
||||
} finally {
|
||||
setBusyAction('');
|
||||
setBusyAction((current) => current === key ? '' : current);
|
||||
}
|
||||
}, [agentBaseUrl, agentHealth, agentTargetLabel, agentTargetMachine, canManageApps, notify, packageBaseUrl, startPreflightFailedTask, startTask]);
|
||||
}, [agentBaseUrl, agentHealth, agentTargetLabel, agentTargetMachine, canManageApps, installedAppsReady, notify, packageBaseUrl, startPreflightFailedTask, startTask]);
|
||||
|
||||
const openEndpointDialog = useCallback(() => {
|
||||
setDraftSettings(settings);
|
||||
@@ -535,12 +621,30 @@ function App() {
|
||||
packageBaseUrl: normalizeUrl(draftSettings.packageBaseUrl || DEFAULT_PACKAGE_BASE_URL),
|
||||
agentBaseUrl: normalizeUrl(draftSettings.agentBaseUrl || DEFAULT_AGENT_BASE_URL)
|
||||
};
|
||||
const endpointsChanged = (
|
||||
nextSettings.packageBaseUrl !== settings.packageBaseUrl
|
||||
|| nextSettings.agentBaseUrl !== settings.agentBaseUrl
|
||||
);
|
||||
if (busyAction && endpointsChanged) {
|
||||
notify('warning', 'Wait for the current Agent request to finish before changing endpoints.');
|
||||
return;
|
||||
}
|
||||
if (nextSettings.agentBaseUrl !== settings.agentBaseUrl) {
|
||||
agentEndpointRef.current = nextSettings.agentBaseUrl;
|
||||
agentEndpointGenerationRef.current += 1;
|
||||
agentRefreshGenerationRef.current += 1;
|
||||
setAgentHealth(null);
|
||||
setSystemInfo(null);
|
||||
setInstalledApps([]);
|
||||
setInstalledAppsReady(false);
|
||||
setActiveTask(null);
|
||||
}
|
||||
setSettings(nextSettings);
|
||||
setDraftSettings(nextSettings);
|
||||
saveSettings(nextSettings);
|
||||
setEndpointDialogOpen(false);
|
||||
notify('info', 'Đã cập nhật endpoint test');
|
||||
}, [draftSettings, notify]);
|
||||
}, [busyAction, draftSettings, notify, settings.agentBaseUrl, settings.packageBaseUrl]);
|
||||
|
||||
const copyInstallCommand = useCallback(async () => {
|
||||
if (!canShowAgentCommand) {
|
||||
@@ -842,7 +946,7 @@ function App() {
|
||||
<button
|
||||
className="btn btn-primary compact"
|
||||
type="button"
|
||||
disabled={!agentHealth || rowTaskBusy || installBusy}
|
||||
disabled={!agentHealth || !installedAppsReady || rowTaskBusy || installBusy}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
runAppAction('install', app);
|
||||
@@ -852,11 +956,25 @@ function App() {
|
||||
Install
|
||||
</button>
|
||||
)}
|
||||
{canManageApps && app.installed && app.canUpdate && (
|
||||
{canManageApps && app.installed && app.needsAttention && (
|
||||
<button
|
||||
className="btn btn-warning compact"
|
||||
type="button"
|
||||
disabled={!agentHealth || rowTaskBusy || updateBusy}
|
||||
disabled={!agentHealth || !installedAppsReady || rowTaskBusy || updateBusy}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
runAppAction('update', app);
|
||||
}}
|
||||
>
|
||||
{updateBusy ? <Loader2 className="spin" size={14} aria-hidden="true" /> : <RotateCcw size={14} aria-hidden="true" />}
|
||||
Retry
|
||||
</button>
|
||||
)}
|
||||
{canManageApps && app.installed && app.canUpdate && !app.needsAttention && (
|
||||
<button
|
||||
className="btn btn-warning compact"
|
||||
type="button"
|
||||
disabled={!agentHealth || !installedAppsReady || rowTaskBusy || updateBusy}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
runAppAction('update', app);
|
||||
@@ -884,7 +1002,7 @@ function App() {
|
||||
className="icon-button danger"
|
||||
type="button"
|
||||
title="Remove"
|
||||
disabled={!agentHealth || rowTaskBusy || removeBusy}
|
||||
disabled={!agentHealth || !installedAppsReady || rowTaskBusy || removeBusy}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
runAppAction('remove', app);
|
||||
@@ -1102,20 +1220,28 @@ function LocalStatus({ app, task }) {
|
||||
);
|
||||
}
|
||||
|
||||
if (app.localStatus === 'update') {
|
||||
return (
|
||||
<span className="status-inline">
|
||||
<span className="badge badge-warning">Update</span>
|
||||
<small>{app.installed.version}</small>
|
||||
</span>
|
||||
if (app.installed) {
|
||||
const serviceSummary = serviceCheckSummary(
|
||||
app.installed.serviceCheckStatus,
|
||||
app.installed.serviceChecks
|
||||
);
|
||||
}
|
||||
|
||||
if (app.localStatus === 'installed') {
|
||||
const statusLabel = app.needsAttention
|
||||
? 'Attention'
|
||||
: app.localStatus === 'update'
|
||||
? 'Update'
|
||||
: 'Installed';
|
||||
const statusTone = app.needsAttention || app.localStatus === 'update' ? 'warning' : 'success';
|
||||
return (
|
||||
<span className="status-inline">
|
||||
<span className="badge badge-success">Installed</span>
|
||||
<small>{app.installed.version}</small>
|
||||
<span className="status-stack">
|
||||
<span className="status-inline">
|
||||
<span className={`badge badge-${statusTone}`}>{statusLabel}</span>
|
||||
<small>{app.installed.version}</small>
|
||||
</span>
|
||||
{serviceSummary && (
|
||||
<small className={app.installed.serviceCheckStatus === 'unhealthy' ? 'danger-text' : 'success-text'}>
|
||||
{serviceSummary}
|
||||
</small>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1189,6 +1315,12 @@ function TaskPanel({ task, onClear, onRefresh }) {
|
||||
<span>{component.currentStep || component.type || '-'}</span>
|
||||
</div>
|
||||
<span className={statusBadgeClass(component.status)}>{component.progress}%</span>
|
||||
{component.type === 'apt' && (
|
||||
<ServiceChecks
|
||||
checks={component.serviceChecks}
|
||||
status={component.serviceCheckStatus}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!components.length && (
|
||||
@@ -1212,6 +1344,46 @@ function TaskPanel({ task, onClear, onRefresh }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ServiceChecks({ checks = [], status = 'not-checked', showSource = false }) {
|
||||
if (!checks.length) {
|
||||
const message = serviceCheckSummary(status, checks);
|
||||
if (!message && status === 'not-checked') return null;
|
||||
return (
|
||||
<div className="service-check-list">
|
||||
<div className="service-check-empty">
|
||||
<Activity size={14} aria-hidden="true" />
|
||||
<span>{message || 'Service status unavailable'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="service-check-list" aria-label="Automatic service checks">
|
||||
{checks.map((check) => (
|
||||
<div className="service-check-row" key={`${check.componentId || ''}:${check.serviceName}`}>
|
||||
<div className="service-check-name">
|
||||
{check.healthy
|
||||
? <CheckCircle2 className="service-check-icon healthy" size={14} aria-hidden="true" />
|
||||
: <AlertCircle className="service-check-icon unhealthy" size={14} aria-hidden="true" />}
|
||||
<span>
|
||||
<strong>{check.serviceName}</strong>
|
||||
<small>
|
||||
{showSource && (check.packageName || check.componentId)
|
||||
? `${check.packageName || check.componentId} · `
|
||||
: ''}
|
||||
{check.subState || check.activeState || 'unknown'} · {check.unitFileState || 'unknown'}
|
||||
{check.checkedAt ? ` · ${check.stale ? 'last checked' : 'checked'} ${formatTaskTime(check.checkedAt)}` : ''}
|
||||
</small>
|
||||
</span>
|
||||
</div>
|
||||
<span className={`badge badge-${serviceCheckTone(check)}`}>{serviceCheckLabel(check)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentPanel({ health, systemInfo, status, title, endpoint, latestAgentPackage, needsUpdate, onClose, onCopyUpdate, showAgentActions }) {
|
||||
const statusTone = needsUpdate
|
||||
? 'warning'
|
||||
@@ -1306,6 +1478,8 @@ function AgentPanel({ health, systemInfo, status, title, endpoint, latestAgentPa
|
||||
function AppDetailPanel({ app, detail, manifest, status, packageBaseUrl }) {
|
||||
const packages = detail?.packages || [];
|
||||
const components = manifest?.components || [];
|
||||
const installedServiceChecks = app?.installed?.serviceChecks || [];
|
||||
const installedServiceStatus = app?.installed?.serviceCheckStatus || 'not-checked';
|
||||
|
||||
return (
|
||||
<section className="panel app-detail-panel">
|
||||
@@ -1355,6 +1529,23 @@ function AppDetailPanel({ app, detail, manifest, status, packageBaseUrl }) {
|
||||
<div className="table-empty compact-empty">{status.message || 'Chưa có component.'}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{app.installed && installedServiceStatus !== 'not-checked' && (
|
||||
<div className="service-health-panel">
|
||||
<div className="component-list-title">
|
||||
<Activity size={15} aria-hidden="true" />
|
||||
Service health
|
||||
<span className={`badge badge-${installedServiceStatus === 'healthy' ? 'success' : installedServiceStatus === 'unhealthy' ? 'danger' : 'muted'}`}>
|
||||
{serviceCheckSummary(installedServiceStatus, installedServiceChecks)}
|
||||
</span>
|
||||
</div>
|
||||
<ServiceChecks
|
||||
checks={installedServiceChecks}
|
||||
status={installedServiceStatus}
|
||||
showSource
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="table-empty compact-empty">Chọn app để xem manifest.</div>
|
||||
|
||||
@@ -181,7 +181,7 @@ export async function fetchAgentSystemInfo(agentBaseUrl) {
|
||||
}
|
||||
|
||||
export async function fetchInstalledApps(agentBaseUrl) {
|
||||
const payload = await requestJson(agentBaseUrl, '/apps/installed', { timeoutMs: 7000 });
|
||||
const payload = await requestJson(agentBaseUrl, '/apps/installed', { timeoutMs: 20000 });
|
||||
return Array.isArray(payload) ? payload.map(normalizeInstalledApp) : [];
|
||||
}
|
||||
|
||||
@@ -256,6 +256,7 @@ function normalizePackageApp(app) {
|
||||
}
|
||||
|
||||
function normalizeInstalledApp(app) {
|
||||
const serviceChecks = normalizeServiceChecks(app.serviceChecks || app.service_checks);
|
||||
return {
|
||||
appId: String(app.appId || app.app_id || '').trim(),
|
||||
appName: String(app.appName || app.app_name || '').trim(),
|
||||
@@ -263,6 +264,12 @@ function normalizeInstalledApp(app) {
|
||||
status: String(app.status || 'installed').trim(),
|
||||
installedAt: app.installedAt || app.installed_at || '',
|
||||
updatedAt: app.updatedAt || app.updated_at || '',
|
||||
serviceCheckStatus: String(
|
||||
app.serviceCheckStatus
|
||||
|| app.service_check_status
|
||||
|| deriveServiceCheckStatus(serviceChecks)
|
||||
).trim(),
|
||||
serviceChecks,
|
||||
openUrl: normalizeOpenUrl(
|
||||
app.openUrl
|
||||
|| app.open_url
|
||||
@@ -312,6 +319,7 @@ function normalizeLog(log) {
|
||||
}
|
||||
|
||||
function normalizeComponent(component) {
|
||||
const serviceChecks = normalizeServiceChecks(component.serviceChecks || component.service_checks);
|
||||
return {
|
||||
componentId: component.componentId || component.component_id,
|
||||
type: component.type,
|
||||
@@ -319,7 +327,50 @@ function normalizeComponent(component) {
|
||||
progress: Number(component.progress || 0),
|
||||
currentStep: component.currentStep || component.current_step,
|
||||
errorMessage: component.errorMessage || component.error_message,
|
||||
serviceCheckStatus: String(
|
||||
component.serviceCheckStatus
|
||||
|| component.service_check_status
|
||||
|| deriveServiceCheckStatus(serviceChecks)
|
||||
).trim(),
|
||||
serviceChecks,
|
||||
startedAt: component.startedAt || component.started_at,
|
||||
finishedAt: component.finishedAt || component.finished_at
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeServiceChecks(value) {
|
||||
return Array.isArray(value) ? value.map(normalizeServiceCheck).filter((item) => item.serviceName) : [];
|
||||
}
|
||||
|
||||
function normalizeServiceCheck(check) {
|
||||
const activeState = String(check?.activeState || check?.active_state || check?.status || 'unknown').trim();
|
||||
const active = typeof check?.active === 'boolean' ? check.active : activeState === 'active';
|
||||
const readinessStatus = String(check?.readinessStatus || check?.readiness_status || '').trim();
|
||||
const healthy = typeof check?.healthy === 'boolean'
|
||||
? check.healthy
|
||||
: active && (!readinessStatus || readinessStatus === 'ready');
|
||||
|
||||
return {
|
||||
serviceName: String(check?.serviceName || check?.service_name || '').trim(),
|
||||
componentId: String(check?.componentId || check?.component_id || '').trim(),
|
||||
packageName: String(check?.packageName || check?.package_name || '').trim(),
|
||||
loadState: String(check?.loadState || check?.load_state || 'unknown').trim(),
|
||||
activeState,
|
||||
subState: String(check?.subState || check?.sub_state || 'unknown').trim(),
|
||||
unitFileState: String(check?.unitFileState || check?.unit_file_state || 'unknown').trim(),
|
||||
readinessType: String(check?.readinessType || check?.readiness_type || '').trim(),
|
||||
readinessStatus,
|
||||
status: String(check?.status || activeState || 'unknown').trim(),
|
||||
active,
|
||||
enabled: Boolean(check?.enabled),
|
||||
healthy,
|
||||
stale: Boolean(check?.stale),
|
||||
checkedAt: check?.checkedAt || check?.checked_at || '',
|
||||
errorMessage: check?.errorMessage || check?.error_message || ''
|
||||
};
|
||||
}
|
||||
|
||||
function deriveServiceCheckStatus(serviceChecks) {
|
||||
if (!serviceChecks.length) return 'not-checked';
|
||||
return serviceChecks.every((check) => check.healthy) ? 'healthy' : 'unhealthy';
|
||||
}
|
||||
|
||||
@@ -782,6 +782,10 @@ tbody tr.selected-row {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.success-text {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.action-col {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
@@ -847,6 +851,18 @@ tbody tr.selected-row td.action-col {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-stack {
|
||||
align-items: flex-start;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.status-stack > small {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.page-pager {
|
||||
align-items: center;
|
||||
background: #f8fafc;
|
||||
@@ -1053,6 +1069,97 @@ tbody tr.selected-row td.action-col {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.service-check-list {
|
||||
border-top: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
grid-column: 1 / -1;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.service-check-row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.service-check-name {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.service-check-icon.healthy {
|
||||
color: var(--success);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.service-check-icon.unhealthy {
|
||||
color: var(--danger);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.component-item .service-check-name > span,
|
||||
.service-check-name > span {
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
margin-top: 0;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.component-item .service-check-name strong,
|
||||
.service-check-name strong {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.component-item .service-check-name small,
|
||||
.service-check-name small {
|
||||
color: #64748b;
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
margin-top: 2px;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.service-check-row > .badge {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.service-check-empty {
|
||||
align-items: center;
|
||||
color: #64748b;
|
||||
display: flex;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.service-health-panel {
|
||||
border-top: 1px solid #eef2f7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px 16px 14px;
|
||||
}
|
||||
|
||||
.service-health-panel > .component-list-title {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.service-health-panel .service-check-list {
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.task-log-list {
|
||||
border-top: 1px solid #eef2f7;
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user