mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Improve mobile settings table priorities
This commit is contained in:
parent
b3d9984f7e
commit
af03903a73
17 changed files with 207 additions and 41 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export const OrganizationOverviewMembersSection: Component<
|
|||
{
|
||||
key: 'addedAt',
|
||||
label: 'Added',
|
||||
hiddenOnMobile: true,
|
||||
render: (member) => <span class="text-muted">{formatOrgDate(member.addedAt)}</span>,
|
||||
},
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue