feat: display platform agent status and version
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -11,6 +11,9 @@ export type PlatformDevice = {
|
||||
os?: string | null;
|
||||
arch?: string | null;
|
||||
distro?: string | null;
|
||||
agentVersion?: string | null;
|
||||
agentUpdatedAt?: string | null;
|
||||
agentLastSeenAt?: string | null;
|
||||
installedAt: string;
|
||||
lastProvisionedAt: string;
|
||||
status: string;
|
||||
|
||||
@@ -38,7 +38,9 @@ type ImageTags = {
|
||||
|
||||
function releaseArtifacts(release?: AppRelease | null) {
|
||||
const imageTags = release?.imageTags as ImageTags | null | undefined;
|
||||
const entries = Object.entries(imageTags?.images ?? {});
|
||||
const entries = Object.entries(imageTags?.images ?? {}).filter(([, image]) => (
|
||||
!/^(postgres|mysql|mariadb|mongo)(:|@)/i.test(image.trim())
|
||||
));
|
||||
if (entries.length) return entries;
|
||||
if (release?.artifact) return [[release.app.name, release.artifact]];
|
||||
return [];
|
||||
|
||||
@@ -22,6 +22,8 @@ type HeadscaleNode = {
|
||||
type AclRule = { id: string; action: string; sources: string[]; destinations: string[] };
|
||||
type PlatformRow = HeadscaleNode & { device?: PlatformDevice; controllers: string[]; no: number };
|
||||
|
||||
type DeviceKind = 'agent' | 'launcher' | 'unknown';
|
||||
|
||||
function normalize(value?: string | null) {
|
||||
return (value ?? '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
}
|
||||
@@ -41,6 +43,17 @@ function findControllers(node: HeadscaleNode, rules: AclRule[]) {
|
||||
.flatMap((rule) => rule.sources))];
|
||||
}
|
||||
|
||||
function getDeviceKind(row: PlatformRow): DeviceKind {
|
||||
if (!row.device) return 'unknown';
|
||||
return row.device.agentVersion || row.device.agentLastSeenAt ? 'agent' : 'launcher';
|
||||
}
|
||||
|
||||
function formatDateTime(value?: string | null) {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString('ko-KR', { hour12: false });
|
||||
}
|
||||
|
||||
export function RuntimeDeviceControlPage() {
|
||||
const [rows, setRows] = useState<PlatformRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -91,6 +104,19 @@ export function RuntimeDeviceControlPage() {
|
||||
width: 180,
|
||||
cell: (row) => <span className="font-mono text-xs text-muted-foreground">{row.ip || '-'}</span>,
|
||||
},
|
||||
{
|
||||
key: 'deviceKind',
|
||||
header: '유형',
|
||||
width: 150,
|
||||
cell: (row) => {
|
||||
const kind = getDeviceKind(row);
|
||||
if (kind === 'agent') {
|
||||
return <Badge tone="green">에이전트</Badge>;
|
||||
}
|
||||
if (kind === 'launcher') return <Badge tone="neutral">런처 클라이언트</Badge>;
|
||||
return <Badge tone="neutral">미확인</Badge>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'controllers',
|
||||
header: '제어 사용자',
|
||||
@@ -155,6 +181,9 @@ export function RuntimeDeviceControlPage() {
|
||||
<dt className="text-muted-foreground">Workspace</dt><dd className="min-w-0 break-all">{selected?.device?.workspaceRoot || '-'}</dd>
|
||||
<dt className="text-muted-foreground">SSH 사용자</dt><dd>{selected?.device?.sshUser || '-'}</dd>
|
||||
<dt className="text-muted-foreground">사용 유형</dt><dd>{selected?.device?.usageType || '-'}</dd>
|
||||
<dt className="text-muted-foreground">장비 유형</dt><dd>{selected ? ({ agent: '에이전트', launcher: '런처 클라이언트', unknown: '미확인' }[getDeviceKind(selected)]) : '-'}</dd>
|
||||
<dt className="text-muted-foreground">에이전트 버전</dt><dd>{selected?.device?.agentVersion || '-'}</dd>
|
||||
<dt className="text-muted-foreground">마지막 에이전트 확인</dt><dd>{formatDateTime(selected?.device?.agentLastSeenAt)}</dd>
|
||||
</dl>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user