mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Keep Actions target names whole on narrow cards
Each Actions card's second line shows the target name, its type and the time. Name and type were sibling flex items that shrank together, so on a phone the line read "analytics-ba... · Virtual ma..." and hid which resource the action touches, which is what the operator needs before opening the review. From 360 to 430px every target name was cut. The name and the type now sit in their own row where the name keeps its width (capped at the line) and the type, with its separator, takes whatever is left. Names read in full at every width from 360px (the box now equals the text width at 1x, 2x and 3x pixel ratio), a name longer than the line still truncates with the time visible, and nothing changes from 480px up.
This commit is contained in:
@@ -3568,8 +3568,12 @@ not part of `ActionRequest`, durable plan identity, or `planHash`; a resource
|
||||
rename therefore cannot change action authority. Opaque canonical resource IDs
|
||||
remain in the row's accessible name and title, while the visual row prefers the
|
||||
API-supplied name and type and falls back to the bounded ID-derived type plus
|
||||
short suffix only when the resource projection is unavailable. Read-only demo
|
||||
posture is quiet supporting context rather than a page-level callout. The
|
||||
short suffix only when the resource projection is unavailable. When the row is
|
||||
too narrow for both, the resource name keeps its width and only the type, with
|
||||
its separator, gives way: on a phone they used to shrink together, so the row
|
||||
read "analytics-ba… · Virtual ma…" and hid which resource the action touches.
|
||||
Read-only demo posture is quiet supporting context rather than a page-level
|
||||
callout. The
|
||||
empty Open queue must explain producer state rather than promising content:
|
||||
when the effective Patrol mode is Watch only (read via the canonical
|
||||
`/api/ai/patrol/autonomy` settings read, failing closed to the generic calm
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "1dcf9418aae87f9427dbfc6bcae67dbde114bf8a",
|
||||
"verified_at": "2026-10-05T21:08:04Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/pages/Actions.tsx"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/pages/Actions.tsx": "17ded57aa6d0476a01c4d0cfec5dee8c9fed2be0fbf50c3d3f5cd2e40c278de6"
|
||||
},
|
||||
"routes": [
|
||||
"/actions",
|
||||
"/actions/history"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 360,
|
||||
"height": 844
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844
|
||||
},
|
||||
{
|
||||
"width": 430,
|
||||
"height": 844
|
||||
},
|
||||
{
|
||||
"width": 480,
|
||||
"height": 844
|
||||
},
|
||||
{
|
||||
"width": 640,
|
||||
"height": 844
|
||||
},
|
||||
{
|
||||
"width": 768,
|
||||
"height": 844
|
||||
},
|
||||
{
|
||||
"width": 1024,
|
||||
"height": 844
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Private mock stack (PULSE_MOCK_MODE) serving this tree on main 1dcf9418aa; before = main, after = main plus this change, measured in the same session on the three open demo actions",
|
||||
"Main: the target name and its type shrank together, so from 360 to 430 every name was cut (analytics-batch-01 61-98 of 107px, backup-coordinator 67-107 of 112px) beside a cut type (Virtual ma\u2026, App conta\u2026); from 480 everything fit",
|
||||
"Patched: the name keeps its full width at every width (box equals text width, 106.61px for analytics-batch-01, at 1x, 2x and 3x device pixel ratio), and the type with its separator takes the rest (Virtual machine 21px at 360, 51px at 390, 91px at 430, whole from 480); nothing changes from 480 up",
|
||||
"A name longer than the line (very-long-resource-name-on-a-production-cluster-node-07, injected) truncates inside the line at 360 and 390 with the relative time still fully visible, and fits whole at 768",
|
||||
"Before and after element screenshots of the three cards at 360, 390 and 1024 at 3x density"
|
||||
],
|
||||
"interactions": [
|
||||
"Loaded /actions fresh at 390 (mobile emulation) and resized through each viewport before measuring",
|
||||
"Opened the second card's review at 390 and 1024 (Governed action review, analytics-batch-01 \u00b7 Virtual machine) and closed it with Escape",
|
||||
"Switched to the History tab (/actions/history, 3 rows); no console errors"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { cleanup, render, screen } from '@solidjs/testing-library';
|
||||
import { Route, Router } from '@solidjs/router';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ResourceActionsAPI } from '@/api/resourceActions';
|
||||
import { SecurityAPI } from '@/api/security';
|
||||
import type { ActionAuditRecord } from '@/types/actionAudit';
|
||||
import { getActionResourcePresentation } from '../actionPresentation';
|
||||
import Actions from '../../../pages/Actions';
|
||||
|
||||
vi.mock('@/api/resourceActions', () => ({
|
||||
ResourceActionsAPI: { listActions: vi.fn(), getAction: vi.fn() },
|
||||
}));
|
||||
vi.mock('@/api/security', () => ({ SecurityAPI: { getStatus: vi.fn() } }));
|
||||
vi.mock('@/api/patrol', () => ({ getPatrolAutonomySettings: vi.fn().mockResolvedValue({}) }));
|
||||
|
||||
const resourceId = 'docker:container:analytics-batch-01';
|
||||
|
||||
const audit: ActionAuditRecord = {
|
||||
id: 'restart-analytics',
|
||||
createdAt: '2026-09-29T18:00:00Z',
|
||||
updatedAt: '2026-09-29T18:01:00Z',
|
||||
state: 'executing',
|
||||
decisionRevision: 1,
|
||||
request: {
|
||||
requestId: 'request-restart-analytics',
|
||||
resourceId,
|
||||
capabilityName: 'restart',
|
||||
reason: 'Recover checkout after three failed health checks',
|
||||
requestedBy: 'operator',
|
||||
},
|
||||
plan: {
|
||||
actionId: 'restart-analytics',
|
||||
requestId: 'request-restart-analytics',
|
||||
allowed: true,
|
||||
requiresApproval: false,
|
||||
approvalPolicy: 'none',
|
||||
rollbackAvailable: false,
|
||||
expiresAt: '2026-09-29T21:00:00Z',
|
||||
planHash: 'sha256:plan-restart-analytics',
|
||||
},
|
||||
verificationOutcome: { status: 'unknown' },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.history.replaceState(null, '', '/');
|
||||
vi.mocked(ResourceActionsAPI.listActions).mockResolvedValue({
|
||||
actions: [audit],
|
||||
count: 1,
|
||||
view: 'pending',
|
||||
readOnly: false,
|
||||
});
|
||||
vi.mocked(SecurityAPI.getStatus).mockResolvedValue({
|
||||
hasAuthentication: true,
|
||||
requiresAuth: true,
|
||||
settingsCapabilities: { authenticationWrite: false },
|
||||
} as Awaited<ReturnType<typeof SecurityAPI.getStatus>>);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
window.history.replaceState(null, '', '/');
|
||||
});
|
||||
|
||||
describe('Actions card resource line', () => {
|
||||
it('keeps the target name whole and lets its type give way on narrow cards', async () => {
|
||||
const { label, detail } = getActionResourcePresentation(resourceId);
|
||||
expect(label).toBe('analytics-batch-01');
|
||||
expect(detail).not.toBe('');
|
||||
|
||||
render(() => (
|
||||
<Router>
|
||||
<Route path="*" component={Actions} />
|
||||
</Router>
|
||||
));
|
||||
|
||||
// On a phone the name and its type used to shrink together, so the line
|
||||
// read "analytics-ba… · Virtual ma…" and hid which resource the action
|
||||
// touches. The name never shrinks below its own width; the type and its
|
||||
// separator take whatever room is left.
|
||||
const name = await screen.findByText(label);
|
||||
expect(name).toHaveClass('shrink-0', 'max-w-full', 'truncate');
|
||||
const type = screen.getByText(detail).parentElement!;
|
||||
expect(type).toHaveClass('min-w-0', 'flex-1', 'truncate');
|
||||
expect(type.querySelector('[aria-hidden="true"]')).toHaveTextContent('·');
|
||||
expect(type.parentElement).toBe(name.parentElement);
|
||||
expect(name.parentElement?.closest(`[title="${resourceId}"]`)).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -334,13 +334,19 @@ export function Actions() {
|
||||
class="mt-1 flex min-w-0 items-center gap-1.5 text-xs text-muted"
|
||||
title={action.request.resourceId}
|
||||
>
|
||||
<span class="truncate">{resource().label}</span>
|
||||
<Show when={resource().detail}>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span class="truncate font-mono text-[11px]">
|
||||
{resource().detail}
|
||||
</span>
|
||||
</Show>
|
||||
{/* The name says what the action touches, so it keeps its
|
||||
width and only the type, with its separator, gives way. */}
|
||||
<span class="flex min-w-0 items-center gap-1.5">
|
||||
<span class="max-w-full shrink-0 truncate">{resource().label}</span>
|
||||
<Show when={resource().detail}>
|
||||
<span class="min-w-0 flex-1 truncate">
|
||||
<span class="mr-1.5" aria-hidden="true">
|
||||
·
|
||||
</span>
|
||||
<span class="font-mono text-[11px]">{resource().detail}</span>
|
||||
</span>
|
||||
</Show>
|
||||
</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span class="shrink-0">{formatRelativeTime(action.updatedAt)}</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user