Converge standalone machine actions on ActionIconButton

This commit is contained in:
rcourtman
2026-06-13 23:35:23 +01:00
parent 52f094f11f
commit dbb936e623
5 changed files with 74 additions and 7 deletions
@@ -557,6 +557,9 @@ not a replacement status card, CTA band, or page-local nested card.
disabled treatment, title fallback, and accessible name wiring must come
from that shared primitive rather than page-local `<button>` plus inline SVG
shells.
Standalone machine row action triggers follow the same rule: the Machines
table owns remove-agent semantics and menu placement, while
`ActionIconButton` owns the compact muted trigger chrome.
AI Chat follows that same boundary for drawer header controls, session row
actions, transcript fallback close/download actions, activity-dock queued
follow-up controls, composer send, footer help/route actions, and compact
@@ -913,6 +913,11 @@ AI-only summary payloads, or page-local heuristics.
provider-owned nodes must not become machine-page members through
hostname, `agent` platform scope, or agent telemetry alone; those facts
surface as facets on the owning provider page.
`AgentsMachinesTable.tsx` may own row membership, resource-derived menu
eligibility, and remove-agent semantics for these projected rows, but the
compact row action trigger chrome stays under the frontend-primitives
`ActionIconButton` boundary rather than becoming a unified-resource-local
button shell.
The default tab for each platform path must point at a sub-tab whose
canonical unified-resource projection actually populates, and visible
workflow subtabs must stay evidence-gated by the same canonical row or
@@ -1259,7 +1259,8 @@
{ "path": "src/components/Alerts/ResourceTable.tsx" },
{ "path": "src/components/AI/Chat/index.tsx" },
{ "path": "src/components/Settings/RolesPanel.tsx" },
{ "path": "src/components/Settings/SSOProvidersPanel.tsx" }
{ "path": "src/components/Settings/SSOProvidersPanel.tsx" },
{ "path": "src/features/standalone/AgentsMachinesTable.tsx" }
],
"forbiddenPatterns": [
{
@@ -1306,6 +1307,12 @@
"flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content focus:outline-none focus:ring-2 focus:ring-blue-500/30"
]
},
{
"path": "src/features/standalone/AgentsMachinesTable.tsx",
"patterns": [
"inline-flex h-7 w-7 items-center justify-center rounded-md text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60"
]
},
{
"path": "src/components/Settings/SSOProvidersPanel.tsx",
"patterns": [
@@ -3186,6 +3193,29 @@
"scripts/shared-template-audit.mjs"
]
},
{
"id": "standalone-agent-machine-action-icon-local-shell",
"category": "action-button",
"summary": "Standalone machine row action triggers must not recreate local muted h-7 icon-button shells; compose ActionIconButton.",
"canonical": {
"path": "src/components/shared/Button.tsx",
"export": "ActionIconButton"
},
"scopes": ["src/features/standalone/AgentsMachinesTable.tsx"],
"extensions": [".tsx"],
"allPatterns": [
"inline-flex h-7 w-7 items-center justify-center rounded-md text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60"
],
"legacyReason": "Retired migration debt. Standalone machine action triggers compose ActionIconButton for shared icon-action size, tone, focus, title, and accessible-label behavior.",
"allowedPaths": [],
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"src/components/shared/Button.test.tsx",
"src/features/standalone/__tests__/AgentsMachinesTable.test.tsx",
"scripts/shared-template-audit.mjs"
]
},
{
"id": "ai-chat-action-icon-header-local-shell",
"category": "action-button",
@@ -2982,6 +2982,9 @@ describe('shared primitive guardrails', () => {
const alertResourceActionGuard = registry.patternGuards?.find(
(guard) => guard.id === 'alert-resource-action-local-svg-button-shell',
);
const standaloneAgentMachineActionGuard = registry.patternGuards?.find(
(guard) => guard.id === 'standalone-agent-machine-action-icon-local-shell',
);
const aiChatActionIconHeaderGuard = registry.patternGuards?.find(
(guard) => guard.id === 'ai-chat-action-icon-header-local-shell',
);
@@ -3835,6 +3838,7 @@ describe('shared primitive guardrails', () => {
'src/components/AI/Chat/index.tsx',
'src/components/Settings/RolesPanel.tsx',
'src/components/Settings/SSOProvidersPanel.tsx',
'src/features/standalone/AgentsMachinesTable.tsx',
]);
expect(actionIconRule?.forbiddenPatterns).toEqual(
expect.arrayContaining([
@@ -3886,6 +3890,12 @@ describe('shared primitive guardrails', () => {
'flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content focus:outline-none focus:ring-2 focus:ring-blue-500/30',
]),
}),
expect.objectContaining({
path: 'src/features/standalone/AgentsMachinesTable.tsx',
patterns: expect.arrayContaining([
'inline-flex h-7 w-7 items-center justify-center rounded-md text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60',
]),
}),
expect.objectContaining({
path: 'src/components/Settings/SSOProvidersPanel.tsx',
patterns: expect.arrayContaining([
@@ -3913,6 +3923,20 @@ describe('shared primitive guardrails', () => {
'src/components/shared/Button.test.tsx',
'src/components/shared/SharedPrimitives.guardrails.test.ts',
]);
expect(standaloneAgentMachineActionGuard?.canonical?.path).toBe(
'src/components/shared/Button.tsx',
);
expect(standaloneAgentMachineActionGuard?.canonical?.export).toBe('ActionIconButton');
expect(standaloneAgentMachineActionGuard?.allPatterns).toEqual([
'inline-flex h-7 w-7 items-center justify-center rounded-md text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60',
]);
expect(standaloneAgentMachineActionGuard?.scopes).toEqual([
'src/features/standalone/AgentsMachinesTable.tsx',
]);
expect(standaloneAgentMachineActionGuard?.allowedPaths ?? []).toHaveLength(0);
expect(standaloneAgentMachineActionGuard?.ignoredPaths).toEqual([
'src/components/shared/Button.test.tsx',
]);
for (const guard of [
aiChatActionIconHeaderGuard,
aiChatActionIconAccentGuard,
@@ -4079,6 +4103,11 @@ describe('shared primitive guardrails', () => {
]) {
expect(aiChatSource).not.toContain(retiredActionIconShell);
}
expect(agentsMachinesTableSource).toContain('@/components/shared/Button');
expect(agentsMachinesTableSource).toContain('ActionIconButton');
expect(agentsMachinesTableSource).not.toContain(
'inline-flex h-7 w-7 items-center justify-center rounded-md text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60',
);
expect(resourceDetailDrawerDebugTabSource).toContain('@/components/shared/Button');
expect(resourceDetailDrawerDebugTabSource).not.toContain(
'rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-base-content',
@@ -17,6 +17,7 @@ import { MonitoringAPI } from '@/api/monitoring';
import { EnhancedCPUBar } from '@/components/Workloads/EnhancedCPUBar';
import { StackedDiskBar } from '@/components/Workloads/StackedDiskBar';
import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
import { ActionIconButton } from '@/components/shared/Button';
import { ColumnPicker } from '@/components/shared/ColumnPicker';
import { StatusDot } from '@/components/shared/StatusDot';
import { TemperatureGauge } from '@/components/shared/TemperatureGauge';
@@ -720,12 +721,11 @@ const AgentMachineActionsCell: Component<{
if (event.key === 'Escape' && props.menuOpen) props.onToggleMenu();
}}
>
<button
<ActionIconButton
ref={triggerRef}
type="button"
class="inline-flex h-7 w-7 items-center justify-center rounded-md text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60"
title="Machine actions"
aria-label={`Machine actions for ${props.name}`}
label={`Machine actions for ${props.name}`}
tone="muted"
size="sm"
aria-haspopup="menu"
aria-expanded={props.menuOpen ? 'true' : 'false'}
onClick={(event) => {
@@ -735,7 +735,7 @@ const AgentMachineActionsCell: Component<{
}}
>
<MoreHorizontalIcon class="h-3.5 w-3.5" />
</button>
</ActionIconButton>
<Show when={props.menuOpen}>
<Portal mount={document.body}>