Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 26 additions & 16 deletions apps/app/src/pages/Licenses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function LastSeen({unix, skewMs}: {unix: number; skewMs: number}) {
const id = setInterval(() => setNow(Date.now()), 60_000);
return () => clearInterval(id);
}, []);
return <span title={fmtUtcFull(unix)}>Last seen {fmtLastSeenAgo(unix, now + skewMs)}</span>;
return <span title={fmtUtcFull(unix)}>{fmtLastSeenAgo(unix, now + skewMs)}</span>;
}

function statusOf(l: License): {label: string; tone: 'ok' | 'warn' | 'bad'} {
Expand Down Expand Up @@ -49,23 +49,33 @@ function ClusterHalf({license, cs}: {license: License; cs: ClusterState}) {
body = <div className="skeleton" aria-busy="true" aria-label="Loading cluster" />;
} else if (c) {
body = (
<div className="cluster-grid">
<div className="cluster-name">
<span className="mono cluster-id">{c.id_short}</span>
<dl className="cluster-grid">
<div>
<dt className="mono cluster-label">Cluster</dt>
<dd className="mono cluster-id">{c.id_short}</dd>
</div>
<div className="mono cluster-sub cluster-last">
<LastSeen unix={c.last_seen} skewMs={cs.skewMs} />
<div>
<dt className="mono cluster-label">Last update</dt>
<dd className="cluster-last">
<LastSeen unix={c.last_seen} skewMs={cs.skewMs} />
</dd>
</div>
<div className="cluster-version-col">
<div className="mono cluster-version">{c.version ? `v${c.version.replace(/^v/, '')}` : ''}</div>
{cs.latest && isOutdated(c.version, cs.latest) && (
<a className="small update-available" href={`${DOCS_URL}/update`}>
Update available: v{cs.latest.replace(/^v/, '')}
</a>
)}
<div>
<dt className="mono cluster-label">Version</dt>
<dd className="cluster-version-col">
<span className="mono cluster-version">{c.version ? `v${c.version.replace(/^v/, '')}` : ''}</span>
{cs.latest && isOutdated(c.version, cs.latest) && (
<a className="small update-available" href={`${DOCS_URL}/update`}>
Update available: v{cs.latest.replace(/^v/, '')}
</a>
)}
</dd>
</div>
<div className="cluster-nodes">{c.nodes != null ? plural(c.nodes, 'node', 'nodes') : ''}</div>
</div>
<div>
<dt className="mono cluster-label">Nodes</dt>
<dd className="cluster-nodes">{c.nodes != null ? plural(c.nodes, 'node', 'nodes') : ''}</dd>
</div>
</dl>
);
} else if (license.status === 'active') {
body = (
Expand All @@ -80,7 +90,7 @@ function ClusterHalf({license, cs}: {license: License; cs: ClusterState}) {
if (!body && !ended) return null;
return (
<div className="license-cluster" role="group" aria-label={label}>
<p className="mono cluster-label">Cluster</p>
{!c && <p className="mono cluster-label">Cluster</p>}
{ended}
{body}
</div>
Expand Down
4 changes: 4 additions & 0 deletions apps/app/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,7 @@ form .muted.small { margin-top: 14px; font-size: 13px; }
.key-row .key { margin: 0; overflow-x: auto; }
.license-main p { margin: 0; }
.license-name { margin-bottom: 4px; }
.cluster-grid { margin: 0; }
.cluster-grid dt { margin: 0 0 4px; }
.cluster-grid dd { margin: 0; font-family: var(--font-mono); font-size: 13px; }
.cluster-grid dd, .cluster-grid dd .cluster-version { color: var(--text-muted); font-weight: 400; font-size: 13px; }
8 changes: 4 additions & 4 deletions apps/app/src/test/licenses.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ describe('/licenses', () => {
expect(await screen.findByText('7c1e04ab')).toHaveClass('mono');
expect(screen.getByText('v1.0.0')).toBeInTheDocument();
expect(screen.getByText('3 nodes')).toBeInTheDocument();
expect(screen.getAllByText(/^Last seen /).length).toBe(2);
expect(screen.getAllByText(/ ago$|^just now$/).length).toBe(2);
expect(screen.queryByText(/Inactive|Stopped|Waiting|check-in/i)).toBeNull();
expect(screen.queryByText(/reach kwatch.dev/)).toBeNull();
});
Expand All @@ -236,10 +236,10 @@ describe('/licenses', () => {
const t = Math.floor(Date.now() / 1000);
withClusters([cl({last_seen: t - 20 * 60})]);
renderAt('/licenses');
const el = await screen.findByText('Last seen 20 min ago');
const el = await screen.findByText('20 min ago');
expect(el).toHaveAttribute('title', expect.stringMatching(/^\d{4}-\d\d-\d\d \d\d:\d\d UTC$/));
await act(async () => { await vi.advanceTimersByTimeAsync(60_000); });
expect(screen.getByText('Last seen 21 min ago')).toBeInTheDocument();
expect(screen.getByText('21 min ago')).toBeInTheDocument();
} finally {
vi.useRealTimers();
}
Expand All @@ -260,7 +260,7 @@ describe('/licenses', () => {
'GET /account/clusters': {body: {clusters: [cl({last_seen: t - 600})], srv_time: t + 3600}},
});
renderAt('/licenses');
expect(await screen.findByText('Last seen 1 h ago')).toBeInTheDocument();
expect(await screen.findByText('1 h ago')).toBeInTheDocument();
});

it('sorts non-active licenses first, then newest first', async () => {
Expand Down
Loading