Improve mobile settings table priorities

This commit is contained in:
rcourtman 2026-08-25 21:02:58 +01:00
parent b3d9984f7e
commit af03903a73
17 changed files with 207 additions and 41 deletions

View file

@ -925,6 +925,10 @@ responsive shell appear to ignore an operator.
split: cloud-paid owns Suspend, Activate, Reload, tenant billing state, and
mutation semantics, while frontend-primitives owns the shared `Button`
chrome used by those row actions.
On phones, the tenant grid must retain organization identity, subscription
state, and mutation actions. Owner IDs, trial detail, and Stripe customer
IDs remain desktop review fields or expanded billing-state detail rather
than forcing the primary mobile decision surface into horizontal scanning.
14. Add or change shared commercial plan/usage presentation through `frontend-modern/src/components/Settings/CommercialBillingSections.tsx` and `frontend-modern/src/utils/commercialBillingModel.ts`
15. Add or change organization billing and usage presentation through `frontend-modern/src/components/Settings/OrganizationBillingPanel.tsx`, `frontend-modern/src/components/Settings/OrganizationBillingLoadingState.tsx`, and `frontend-modern/src/components/Settings/useOrganizationBillingPanelState.ts`
16. Add or change self-hosted Pro plan, recovery, and entitlement actions through `frontend-modern/src/components/Settings/ProLicensePanel.tsx`, `frontend-modern/src/components/Settings/ProLicensePlanSection.tsx`, `frontend-modern/src/components/Settings/SelfHostedCommercialRecoverySection.tsx`, and `frontend-modern/src/components/Settings/useProLicensePanelState.ts`

View file

@ -1072,9 +1072,14 @@ not a replacement status card, CTA band, or page-local nested card.
focus, and embedded-row propagation behavior.
Global app-shell prompts are part of the same action boundary.
`frontend-modern/src/components/GitHubStarBanner.tsx` may own its display
timing, product copy, and GitHub destination, but its primary, defer, and
dismiss controls must compose `Button` and `ActionIconButton` instead of
carrying local floating-prompt button shells.
timing, product copy, and GitHub destination, but its primary and dismiss
controls must compose `Button` and `ActionIconButton` instead of carrying
local floating-prompt button shells. The prompt must wait for fourteen
distinct active days with connected infrastructure, consume at most one
lifetime appearance, treat close as permanent dismissal, and never restore
recurring snooze behavior. It must yield the browser session to an existing
release, disclosure, or other low-priority app-shell notice and suppress
itself for the remainder of a session that presents a blocking dialog.
Settings selection helpers such as `ResourcePicker` must use the same
`Button` primitive for select-all, clear, and chip remove actions instead of
restoring footer-local action shells.
@ -4987,6 +4992,9 @@ update-surface wording inline. `CopyCommandBlock` must use the shared
`copyToClipboard` helper so install/update/agent snippets keep the same
Clipboard API fallback path and only report copied state after the shared copy
path succeeds.
The running published version must keep a direct `Current release notes` link
in this settings shell so suppressing or dismissing the one-time post-update
notice never makes the changelog undiscoverable.
The update verdict is honest about its age. The frontend serves the update
verdict from a 24-hour localStorage cache, so the panel's "Up to date" state
@ -6091,6 +6099,15 @@ disclosure, navigation, copy, and row-action controls must keep a 40-pixel
mobile touch floor while retaining their compact desktop density. Settings
navigation must own a viewport-bounded vertical scroll region so its route list
does not push the active panel below the page.
Settings data grids must also define a phone information hierarchy instead of
depending on horizontal scrolling as their first responsive behavior. Identity,
current state, and immediately available actions stay visible; lower-priority
timestamps, external IDs, and verbose detail columns may be hidden through the
shared symmetric column boundary. When a hidden field is still needed for the
phone decision, its concise value belongs as mobile-only secondary context in
the surviving identity cell. Multi-action cells may collapse visible labels on
phones only when every action keeps an accessible name and the shared touch
floor.
Patrol finding handoffs must derive approval posture from the canonical typed
action state and approval policy, not only from a legacy approval id. A
@ -6478,7 +6495,8 @@ element ref, because a ref runs before the node is in the document, where the
measured height is zero.
Every surface that must sit on top of the bar reads that property: the
Assistant overlay panel and its backdrop, the global GitHub star banner, and
Assistant overlay panel and its backdrop, the compact post-update notice, the
global GitHub star banner, and
the `.filter-bottom-nav-aware-panel` rule in
`frontend-modern/src/index.css`. Consumers must not add
`env(safe-area-inset-bottom)` on top of the published value, and must not

View file

@ -80,6 +80,11 @@ create, review, and approve cross-organization shares.
come from `frontend-primitives` through `PulseDataGrid`/`Table`; do not add
organization-local `overflow-x-auto`, negative-margin, or side-border
compensation around grids.
Their phone projection must keep the identity, current access/state, and
available row action needed for an immediate decision. Added/requested
timestamps, repeated permission detail, and other review metadata are
desktop detail columns; they must not squeeze those decision fields into an
unreadable six-column phone grid.
Organization role and share-status badges follow the same primitive
boundary: role/status semantics remain organization-owned, but visible badge
shell, tone vocabulary, and label rendering must compose

View file

@ -1,35 +1,54 @@
{
"version": 1,
"base_sha": "3678d583bf43840d0086e4ba70c81aec77dadd01",
"verified_at": "2026-08-25T19:06:05Z",
"base_sha": "b3d9984f7e3a92b9b8f13ecd71efcd8a7babc923",
"verified_at": "2026-08-25T19:34:45Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Workloads/guestDrawerModel.ts"
"frontend-modern/src/components/Settings/AuditLogPanel.tsx",
"frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx",
"frontend-modern/src/components/Settings/OrganizationAccessMembersSection.tsx",
"frontend-modern/src/components/Settings/OrganizationIncomingSharesSection.tsx",
"frontend-modern/src/components/Settings/OrganizationOutgoingSharesSection.tsx",
"frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx",
"frontend-modern/src/components/Settings/ReportingPanel.tsx",
"frontend-modern/src/components/Settings/RolesPanel.tsx",
"frontend-modern/src/components/Settings/UserAssignmentsPanel.tsx"
],
"content_sha256": {
"frontend-modern/src/components/Workloads/guestDrawerModel.ts": "5aaccadfaa746f391431c5138e7a1c051b643e264e57fb8e2f58f121cffb0de9"
"frontend-modern/src/components/Settings/AuditLogPanel.tsx": "8dc3a831609e02c69bc497be2c91daea5ced4c59f79cbf554fa3fc41b05b1f65",
"frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx": "47f94e29a76f9f545029c7ae8f4ee7e7b66e5c5b32fb4458608d24e57bd70b50",
"frontend-modern/src/components/Settings/OrganizationAccessMembersSection.tsx": "d2dbc0bbd350b7871a6591c146079972be520769987d91dd101c2aa840a552aa",
"frontend-modern/src/components/Settings/OrganizationIncomingSharesSection.tsx": "f22e4a8c7fc2898561d1039d1c9885e84c2246a788ef086e4868a74e93502b67",
"frontend-modern/src/components/Settings/OrganizationOutgoingSharesSection.tsx": "a4764da645e44ade198a438091b092c89d2a90021dbea85004ed0e911997f42f",
"frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx": "8f0a16bdd5523e54520f66fab68fde9571d682ba088cc75e111b6949d4a5c471",
"frontend-modern/src/components/Settings/ReportingPanel.tsx": "004e78cc0b32263fa05555d43b6fbc3a61b8b0d6aa5dda4b3a16dbee01a42547",
"frontend-modern/src/components/Settings/RolesPanel.tsx": "293d21bf65340202581b314ae6c402b19a9f29dbcc4dbebc841c20989e205b53",
"frontend-modern/src/components/Settings/UserAssignmentsPanel.tsx": "4651822c6467732b182eb5a888beeee5b976f1819fab12cc357d69095659ce20"
},
"routes": [
"/proxmox/overview",
"/docker/overview",
"/kubernetes/overview",
"/truenas/apps"
"/settings/security-audit",
"/settings/support/reporting",
"/settings/security-roles",
"/settings/security-users",
"/settings/organization/access",
"/settings/organization/sharing",
"/settings/organization/billing-admin"
],
"viewports": [
{ "width": 1280, "height": 800 },
{ "width": 1440, "height": 900 },
{ "width": 390, "height": 844 }
],
"states": [
"stopped Proxmox guest History drawer using the negative unknown-disk sentinel",
"running Docker container History drawer with populated utilization data",
"degraded Kubernetes pod History drawer with populated utilization and network data",
"TrueNAS app Overview drawer with long container and identity values"
"audit log, reporting, roles, and user access paid-feature fallbacks in the local self-hosted session",
"organization access and billing-admin capability redirects",
"organization sharing unavailable-capability route state",
"populated mobile column priorities and row-action names exercised by the targeted component tests"
],
"interactions": [
"opened the stopped dev-portal Proxmox guest and switched from Overview to History at 390px",
"verified the unavailable disk fallback renders as a dash instead of a negative percentage at 390px and 1280px",
"opened Docker and Kubernetes History tabs to confirm valid current metrics and chart geometry remain intact",
"opened a TrueNAS app drawer and inspected long-value truncation, stacking, scrolling, and tab placement",
"navigated every affected settings route at 390px and 1440px",
"confirmed zero document-level horizontal overflow on every rendered route state",
"confirmed the phone projections retain primary identity, current state, and actions while desktop detail remains available",
"confirmed report schedule phone actions collapse behind one 44px accessible actions trigger",
"restored the browser viewport after responsive verification"
]
}

View file

@ -381,6 +381,7 @@ export default function AuditLogPanel() {
label: 'Status',
width: '64px',
align: 'center',
hiddenOnMobile: true,
render: (event) => {
const presentation = getAuditEventStatusPresentation(event.success);
const Icon = presentation.icon;
@ -390,6 +391,7 @@ export default function AuditLogPanel() {
{
key: 'timestamp',
label: 'Timestamp',
hiddenOnMobile: true,
render: (event) => (
<span class="text-muted">{formatTimestamp(event.timestamp)}</span>
),
@ -398,16 +400,23 @@ export default function AuditLogPanel() {
key: 'event',
label: 'Event',
render: (event) => (
<span
class={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${getAuditEventTypeBadgeClass(event.event)}`}
>
{event.event}
</span>
<div class="min-w-0">
<span
class={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${getAuditEventTypeBadgeClass(event.event)}`}
>
{event.event}
</span>
<div class="mt-1 truncate text-[11px] text-muted sm:hidden">
{event.success ? 'Success' : 'Failed'} · {event.user || 'Unknown user'} ·{' '}
{formatTimestamp(event.timestamp)}
</div>
</div>
),
},
{
key: 'user',
label: 'User',
hiddenOnMobile: true,
render: (event) => <span class="text-base-content">{event.user || '-'}</span>,
},
{

View file

@ -67,6 +67,7 @@ export const BillingAdminOrganizationsTable: Component<BillingAdminOrganizations
{
key: 'owner',
label: 'Owner',
hiddenOnMobile: true,
render: (org) => (
<span class="font-mono text-xs text-base-content">{org.owner_user_id || 'N/A'}</span>
),
@ -86,6 +87,7 @@ export const BillingAdminOrganizationsTable: Component<BillingAdminOrganizations
{
key: 'trial',
label: 'Trial',
hiddenOnMobile: true,
render: (org) => {
const billing = props.billingByOrgID[(org.org_id || '').trim()];
return (
@ -96,6 +98,7 @@ export const BillingAdminOrganizationsTable: Component<BillingAdminOrganizations
{
key: 'stripeCustomer',
label: 'Stripe Customer',
hiddenOnMobile: true,
render: (org) => {
const billing = props.billingByOrgID[(org.org_id || '').trim()];
const cellValue = stripeCustomerCell(billing);

View file

@ -74,6 +74,7 @@ export const OrganizationAccessMembersSection: Component<OrganizationAccessMembe
{
key: 'addedAt',
label: 'Added',
hiddenOnMobile: true,
render: (member) => <span class="text-muted">{formatOrgDate(member.addedAt)}</span>,
},
{

View file

@ -49,6 +49,7 @@ export const OrganizationIncomingSharesSection: Component<
{
key: 'accessRole',
label: 'Access',
hiddenOnMobile: true,
render: (share) => {
const role = normalizeOrganizationShareRole(share.accessRole);
return <OrganizationRoleBadge role={role} />;
@ -73,6 +74,7 @@ export const OrganizationIncomingSharesSection: Component<
{
key: 'createdAt',
label: 'Requested',
hiddenOnMobile: true,
render: (share) => <span class="text-muted">{formatOrgDate(share.createdAt)}</span>,
},
{

View file

@ -52,6 +52,7 @@ export const OrganizationOutgoingSharesSection: Component<
{
key: 'accessRole',
label: 'Access',
hiddenOnMobile: true,
render: (share) => {
const role = normalizeOrganizationShareRole(share.accessRole);
return <OrganizationRoleBadge role={role} />;
@ -76,6 +77,7 @@ export const OrganizationOutgoingSharesSection: Component<
{
key: 'createdAt',
label: 'Requested',
hiddenOnMobile: true,
render: (share) => <span class="text-muted">{formatOrgDate(share.createdAt)}</span>,
},
{

View file

@ -34,6 +34,7 @@ export const OrganizationOverviewMembersSection: Component<
{
key: 'addedAt',
label: 'Added',
hiddenOnMobile: true,
render: (member) => <span class="text-muted">{formatOrgDate(member.addedAt)}</span>,
},
]}

View file

@ -4,6 +4,7 @@ import Download from 'lucide-solid/icons/download';
import BarChart from 'lucide-solid/icons/bar-chart';
import TableProperties from 'lucide-solid/icons/table-properties';
import Plus from 'lucide-solid/icons/plus';
import EllipsisVertical from 'lucide-solid/icons/ellipsis-vertical';
import Pencil from 'lucide-solid/icons/pencil';
import Play from 'lucide-solid/icons/play';
import RefreshCw from 'lucide-solid/icons/refresh-cw';
@ -353,8 +354,10 @@ export function ReportingPanel() {
<table class="w-full min-w-0 table-fixed text-left text-sm">
<thead class="border-b border-base-300 bg-base-200/50 text-xs uppercase text-muted">
<tr>
<th class="w-[18%] px-3 py-2 font-semibold">Name</th>
<th class="w-[16%] px-3 py-2 font-semibold">Cadence</th>
<th class="w-[56%] px-3 py-2 font-semibold sm:w-[18%]">Name</th>
<th class="hidden w-[16%] px-3 py-2 font-semibold sm:table-cell">
Cadence
</th>
<th class="report-schedule-detail-column w-[14%] px-3 py-2 font-semibold">
Scope
</th>
@ -364,8 +367,10 @@ export function ReportingPanel() {
<th class="hidden w-[16%] px-3 py-2 font-semibold sm:table-cell">
Last run
</th>
<th class="w-[8%] px-3 py-2 font-semibold">Enabled</th>
<th class="w-[14%] px-3 py-2 text-right font-semibold">Actions</th>
<th class="w-[16%] px-3 py-2 font-semibold sm:w-[8%]">Enabled</th>
<th class="w-[28%] px-3 py-2 text-right font-semibold sm:w-[14%]">
Actions
</th>
</tr>
</thead>
<tbody class="divide-y divide-base-300">
@ -377,9 +382,12 @@ export function ReportingPanel() {
title={schedule.name}
>
{schedule.name}
<span class="mt-0.5 block truncate text-xs font-normal text-muted sm:hidden">
{reportScheduleCadenceLabel(schedule)}
</span>
</td>
<td
class="truncate px-3 py-2 text-muted"
class="hidden truncate px-3 py-2 text-muted sm:table-cell"
title={reportScheduleCadenceLabel(schedule)}
>
{reportScheduleCadenceLabel(schedule)}
@ -418,12 +426,59 @@ export function ReportingPanel() {
</label>
</td>
<td class="px-3 py-2">
<div class="flex justify-end gap-1">
<details class="group sm:hidden">
<summary
class="ml-auto flex min-h-11 min-w-11 cursor-pointer list-none items-center justify-center rounded-md text-base-content hover:bg-surface-hover"
aria-label={`Actions for ${schedule.name}`}
>
<EllipsisVertical size={18} />
</summary>
<div class="mt-1 flex flex-col gap-1">
<Button
variant="ghost"
size="sm"
class="min-h-11 w-full justify-start gap-2 px-2"
aria-label={`Run ${schedule.name} now`}
isLoading={runningScheduleID() === schedule.id}
disabled={runningScheduleID() !== ''}
onClick={() => runReportScheduleNow(schedule)}
>
<Show when={runningScheduleID() !== schedule.id}>
<Play size={15} />
</Show>
Run
</Button>
<Button
variant="ghost"
size="sm"
class="min-h-11 w-full justify-start gap-2 px-2"
aria-label={`Edit ${schedule.name}`}
onClick={() => startEditSchedule(schedule)}
>
<Pencil size={15} />
Edit
</Button>
<Button
variant="ghost"
size="sm"
class="min-h-11 w-full justify-start gap-2 px-2"
aria-label={`Delete ${schedule.name}`}
isLoading={deletingScheduleID() === schedule.id}
disabled={deletingScheduleID() !== ''}
onClick={() => deleteReportSchedule(schedule)}
>
<Trash2 size={15} />
Delete
</Button>
</div>
</details>
<div class="hidden justify-end gap-1 sm:flex">
<Button
variant="ghost"
size="sm"
class="gap-1 px-2"
title="Run now"
aria-label={`Run ${schedule.name} now`}
isLoading={runningScheduleID() === schedule.id}
disabled={runningScheduleID() !== ''}
onClick={() => runReportScheduleNow(schedule)}
@ -438,6 +493,7 @@ export function ReportingPanel() {
size="sm"
class="gap-1 px-2"
title="Edit"
aria-label={`Edit ${schedule.name}`}
onClick={() => startEditSchedule(schedule)}
>
<Pencil size={15} />
@ -448,6 +504,7 @@ export function ReportingPanel() {
size="sm"
class="gap-1 px-2"
title="Delete"
aria-label={`Delete ${schedule.name}`}
isLoading={deletingScheduleID() === schedule.id}
disabled={deletingScheduleID() !== ''}
onClick={() => deleteReportSchedule(schedule)}

View file

@ -88,6 +88,7 @@ export const RolesPanel: Component = () => {
{
key: 'permissions',
label: 'Permissions',
hiddenOnMobile: true,
render: (role) => (
<div class="flex flex-wrap gap-1">
<For each={role.permissions}>

View file

@ -160,20 +160,24 @@ export const UserAssignmentsPanel: Component = () => {
<Button
variant="ghost"
size="settingsAction"
class="min-h-11 gap-2 sm:min-h-9"
class="min-h-11 min-w-11 gap-2 px-2 sm:min-h-9 sm:min-w-0"
aria-label={`Manage access for ${getUserIdentityDisplayName(assignment)}`}
title="Manage access"
onClick={() => state.openManageAccess(assignment)}
>
<Pencil class="w-4 h-4" />
Manage Access
<span class="hidden sm:inline">Manage Access</span>
</Button>
<Button
variant="ghost"
size="settingsAction"
class="min-h-11 gap-2 text-red-600 hover:text-red-700 sm:min-h-9"
class="min-h-11 min-w-11 gap-2 px-2 text-red-600 hover:text-red-700 sm:min-h-9 sm:min-w-0"
aria-label={`Remove ${getUserIdentityDisplayName(assignment)}`}
title="Remove user access"
onClick={() => state.openDeleteUser(assignment)}
>
<Trash2 class="w-4 h-4" />
Remove
<span class="hidden sm:inline">Remove</span>
</Button>
</div>
),

View file

@ -151,6 +151,14 @@ describe('BillingAdminPanel', () => {
expect(billingAdminOrganizationsTableSource).toContain('variant="secondary"');
expect(billingAdminOrganizationsTableSource).toContain('size="sm"');
expect(billingAdminOrganizationsTableSource).toContain('size="xs"');
for (const key of ['owner', 'trial', 'stripeCustomer']) {
expect(billingAdminOrganizationsTableSource).toMatch(
new RegExp(`key: '${key}',[\\s\\S]*?hiddenOnMobile: true`),
);
}
expect(billingAdminOrganizationsTableSource).not.toMatch(
/key: 'subscription',\s+label: 'Subscription',\s+hiddenOnMobile:/,
);
expect(billingAdminOrganizationsTableSource).not.toContain(
'px-2.5 py-1.5 text-xs font-medium rounded-md border border-border bg-surface hover:bg-surface-hover disabled:opacity-50',
);

View file

@ -240,7 +240,7 @@ describe('RBAC paywall settings panels', () => {
expect(getUsersMock).toHaveBeenCalled();
expect(getRolesMock).toHaveBeenCalled();
expect(screen.getByPlaceholderText('Search users...')).not.toBeDisabled();
expect(screen.getByRole('button', { name: 'Manage Access' })).toHaveClass(
expect(screen.getByRole('button', { name: 'Manage access for alice' })).toHaveClass(
'min-h-11',
'sm:min-h-9',
);
@ -265,7 +265,7 @@ describe('RBAC paywall settings panels', () => {
expect(screen.getByText('OIDC · okta')).toBeInTheDocument();
expect(screen.getByTitle('sso:oidc:okta:opaque_subject')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Remove' }));
fireEvent.click(screen.getByRole('button', { name: 'Remove Alice Example' }));
const dialog = await screen.findByRole('dialog', { name: 'Remove user access: Alice Example' });
expect(dialog).toHaveTextContent('does not disable the account at the identity provider');
@ -331,7 +331,7 @@ describe('RBAC paywall settings panels', () => {
expect(screen.getByText('alice')).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: 'Manage Access' }));
fireEvent.click(screen.getByRole('button', { name: 'Manage access for alice' }));
await waitFor(() => {
expect(screen.getByRole('dialog', { name: 'Manage access: alice' })).toBeInTheDocument();

View file

@ -83,6 +83,7 @@ import organizationAccessMembersSectionSource from '../OrganizationAccessMembers
import organizationIncomingSharesSectionSource from '../OrganizationIncomingSharesSection.tsx?raw';
import organizationOutgoingSharesSectionSource from '../OrganizationOutgoingSharesSection.tsx?raw';
import organizationOverviewDetailsSectionSource from '../OrganizationOverviewDetailsSection.tsx?raw';
import organizationOverviewMembersSectionSource from '../OrganizationOverviewMembersSection.tsx?raw';
import organizationSharingCreateSectionSource from '../OrganizationSharingCreateSection.tsx?raw';
import rolesEditorDialogSource from '../RolesEditorDialog.tsx?raw';
import rolesPanelSource from '../RolesPanel.tsx?raw';
@ -720,6 +721,37 @@ describe('settings architecture guardrails', () => {
);
});
it('keeps dense settings tables on deliberate phone information priorities', () => {
const expectHiddenColumn = (source: string, key: string) => {
expect(source).toMatch(
new RegExp(`key: '${key}',[\\s\\S]*?label: '[^']+',[\\s\\S]*?hiddenOnMobile: true`),
);
};
expectHiddenColumn(organizationAccessMembersSectionSource, 'addedAt');
expectHiddenColumn(organizationOverviewMembersSectionSource, 'addedAt');
expectHiddenColumn(organizationIncomingSharesSectionSource, 'accessRole');
expectHiddenColumn(organizationIncomingSharesSectionSource, 'createdAt');
expectHiddenColumn(organizationOutgoingSharesSectionSource, 'accessRole');
expectHiddenColumn(organizationOutgoingSharesSectionSource, 'createdAt');
expectHiddenColumn(rolesPanelSource, 'permissions');
expectHiddenColumn(auditLogPanelSource, 'success');
expectHiddenColumn(auditLogPanelSource, 'timestamp');
expectHiddenColumn(auditLogPanelSource, 'user');
expect(auditLogPanelSource).toContain('text-[11px] text-muted sm:hidden');
expect(auditLogPanelSource).toContain("event.user || 'Unknown user'");
expect(userAssignmentsPanelSource).toContain(
'<span class="hidden sm:inline">Manage Access</span>',
);
expect(userAssignmentsPanelSource).toContain('<span class="hidden sm:inline">Remove</span>');
expect(reportingPanelSource).toContain('hidden w-[16%] px-3 py-2 font-semibold sm:table-cell');
expect(reportingPanelSource).toContain('text-xs font-normal text-muted sm:hidden');
expect(reportingPanelSource).toContain('<details class="group sm:hidden">');
expect(reportingPanelSource).toContain('aria-label={`Actions for ${schedule.name}`}');
expect(reportingPanelSource).toContain('class="hidden justify-end gap-1 sm:flex"');
});
it('keeps settings loading skeletons on the shared skeleton primitive', () => {
for (const source of [dataHandlingPanelSource, securityOverviewPanelSource]) {
expect(source).toContain('@/components/shared/SettingsLoadingSkeleton');

View file

@ -3030,7 +3030,7 @@ class SubsystemLookupTest(unittest.TestCase):
{
"heading": "## Extension Points",
"path": "frontend-modern/src/api/orgs.ts",
"line": 89,
"line": 94,
"heading_line": 74,
},
],
@ -3105,7 +3105,7 @@ class SubsystemLookupTest(unittest.TestCase):
{
"heading": "## Extension Points",
"path": "internal/api/access_control_handlers.go",
"line": 103,
"line": 108,
"heading_line": 74,
},
],