+
{sorted.map((l) => (
- )}
- {!empty && (
-
)}
>
diff --git a/apps/app/src/styles.css b/apps/app/src/styles.css
index 719ac0f..b129e68 100644
--- a/apps/app/src/styles.css
+++ b/apps/app/src/styles.css
@@ -109,6 +109,7 @@ form .muted.small { margin-top: 14px; font-size: 13px; }
.license-name { font-weight: 600; font-size: 18px; font-family: var(--font-body); }
.key { font-size: 13px; overflow-wrap: anywhere; }
.badge { font-size: 12px; font-weight: 500; padding: 2px 8px; border-radius: var(--radius-chip); margin-left: 6px; vertical-align: middle; border: 1px solid transparent; }
+.badge-ok { color: var(--success); background: var(--success-bg); }
.badge-neutral { color: var(--text); background: var(--bg); border-color: var(--line); }
.badge-warn { color: var(--degraded); background: var(--degraded-bg); }
.badge-bad { color: var(--failing); background: var(--failing-bg); }
@@ -224,3 +225,5 @@ form .muted.small { margin-top: 14px; font-size: 13px; }
.icon-btn:disabled { cursor: default; }
.key-row { display: flex; align-items: center; gap: var(--space-1); min-width: 0; }
.key-row .key { margin: 0; overflow-x: auto; }
+.license-main p { margin: 0; }
+.license-name { margin-bottom: 4px; }
diff --git a/apps/app/src/test/licenses.test.tsx b/apps/app/src/test/licenses.test.tsx
index 9ea6d62..b9dd93b 100644
--- a/apps/app/src/test/licenses.test.tsx
+++ b/apps/app/src/test/licenses.test.tsx
@@ -59,7 +59,7 @@ describe('/licenses', () => {
'GET /account/clusters': {body: {clusters: [cl({})]}},
});
renderAt('/licenses');
- expect(await screen.findByText(/free Pro licenses used · acme.com/)).toHaveTextContent('2 of 2 free Pro licenses used · acme.com');
+ expect(await screen.findByText(/free Pro licenses used/)).toHaveTextContent('2 of 2 free Pro licenses used');
const first = await screen.findByRole('region', {name: 'License lic_1'});
expect(within(first).getByText(/KW1-••••••f3a9Qk/)).toBeInTheDocument();
expect(within(first).getByText('active')).toBeInTheDocument();
@@ -227,7 +227,6 @@ describe('/licenses', () => {
expect(screen.getByText('3 nodes')).toBeInTheDocument();
expect(screen.getAllByText(/^Last seen /).length).toBe(2);
expect(screen.queryByText(/Inactive|Stopped|Waiting|check-in/i)).toBeNull();
- expect(screen.getByRole('link', {name: /dead man.s switch/})).toHaveAttribute('href', 'https://docs.kwatch.dev/heartbeat-monitor-configuration');
expect(screen.queryByText(/reach kwatch.dev/)).toBeNull();
});
@@ -302,52 +301,6 @@ describe('/licenses', () => {
expect(container.querySelector('.btn-signal')).toBeNull();
});
- it('refresh reloads clusters', async () => {
- const {calls} = withClusters([]);
- const user = userEvent.setup();
- renderAt('/licenses');
- await user.click(await screen.findByRole('button', {name: 'Refresh'}));
- await screen.findByRole('region', {name: 'License lic_1'});
- expect(calls.filter((c) => c.key === 'GET /account/clusters').length).toBeGreaterThanOrEqual(2);
- });
-
- it('refresh shows a busy state, then Last refreshed', async () => {
- withClusters([]);
- const user = userEvent.setup();
- renderAt('/licenses');
- const status = await screen.findByText('Last refreshed just now');
- expect(status).not.toHaveAttribute('aria-live');
- const sr = document.querySelector('.sr-only[aria-live="polite"]') as HTMLElement;
- expect(sr).toHaveAttribute('aria-live', 'polite');
- expect(sr).toBeEmptyDOMElement();
- expect(status).toHaveAttribute('title', expect.stringMatching(/UTC$/));
- await user.click(screen.getByRole('button', {name: 'Refresh'}));
- const busy = screen.getByRole('button', {name: /Refreshing/});
- expect(busy).toBeDisabled();
- expect(busy).toHaveAttribute('aria-busy', 'true');
- expect(document.querySelector('div[aria-busy="true"] [aria-label="License lic_1"]')).not.toBeNull();
- expect(await screen.findByRole('button', {name: 'Refresh'})).toBeEnabled();
- expect(document.querySelector('[aria-busy="true"]')).toBeNull();
- expect(document.querySelector('.sr-only[aria-live]')).toHaveTextContent('Refreshed');
- });
-
- it('refresh error shows an alert and keeps the old timestamp', async () => {
- let n = 0;
- mockApi({
- 'GET /account': () => (++n === 1 ? {body: account({licenses: [license()]})} : {status: 500, body: {}}),
- 'GET /account/clusters': {body: {clusters: []}},
- });
- const user = userEvent.setup();
- renderAt('/licenses');
- await screen.findByText('Last refreshed just now');
- await user.click(screen.getByRole('button', {name: 'Refresh'}));
- expect(await screen.findAllByText('Could not refresh. Check your connection and try again.')).toHaveLength(2);
- expect(document.querySelector('.sr-only[aria-live]')).toHaveTextContent('Could not refresh');
- expect(screen.getAllByText(/Could not refresh/)).toHaveLength(2);
- expect(screen.getByText('Last refreshed just now')).toBeInTheDocument();
- expect(screen.getByRole('region', {name: 'License lic_1'})).toBeInTheDocument();
- });
-
it('/clusters redirects to /licenses and the nav has one item', async () => {
withClusters([cl()]);
renderAt('/clusters');