diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index a9efe71fe..06116f738 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -582,6 +582,10 @@ or other self-hosted uncapped continuity plans. table state stay cloud-paid owned, while the refresh button chrome must compose the frontend-primitives `Button` family instead of carrying a hosted-billing-local compact action shell. + Hosted billing admin organization row actions follow the same ownership + 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. 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` @@ -1626,6 +1630,10 @@ subscription update runtime, and owns the tenant grid plus expanded JSON presentation. Future hosted billing admin changes must extend that split instead of pulling hosted state and mutation flow back into the panel render shell. +Hosted billing admin organization row actions follow the shared frontend +primitive contract as well: cloud-paid owns Suspend, Activate, Reload, tenant +billing state, and mutation semantics, while frontend-primitives owns the +`Button` chrome for those row actions. The organization billing settings surface now follows the same rule. Changes to `frontend-modern/src/components/Settings/OrganizationBillingPanel.tsx` must carry this contract and the dedicated organization-billing proof file instead diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 1c95f60f4..50a02598a 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -568,6 +568,10 @@ not a replacement status card, CTA band, or page-local nested card. entitlement state, and click handlers, while `Button`, `ButtonLink`, and `UpgradeButtonLink` own the primary, outline, warning, and upgrade/link chrome. + Hosted billing admin organization row actions follow the same boundary: + cloud-paid surfaces own Suspend, Activate, Reload, tenant state, and mutation + semantics, while `Button` owns the row-action chrome through the secondary + `sm` and `xs` sizes. If a new surface needs a variant that the shared primitive does not expose, extend the primitive and registry guard rather than adding a page-local class string. diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json index 180e414ef..d3dac3aba 100644 --- a/frontend-modern/scripts/shared-template-registry.json +++ b/frontend-modern/scripts/shared-template-registry.json @@ -945,6 +945,7 @@ { "path": "src/components/Settings/APIAccessPanel.tsx" }, { "path": "src/components/Settings/AvailabilitySettingsPanel.tsx" }, { "path": "src/components/Settings/BillingAdminPanel.tsx" }, + { "path": "src/components/Settings/BillingAdminOrganizationsTable.tsx" }, { "path": "src/components/Settings/ConnectionEditor/AddressProbeStep.tsx" }, { "path": "src/components/Settings/ConnectionEditor/ConnectionEditor.tsx" }, { @@ -989,6 +990,13 @@ "w-full sm:w-auto px-3 py-1.5 text-xs font-medium rounded-md border border-border bg-surface hover:bg-surface-hover disabled:opacity-50" ] }, + { + "path": "src/components/Settings/BillingAdminOrganizationsTable.tsx", + "patterns": [ + "px-2.5 py-1.5 text-xs font-medium rounded-md border border-border bg-surface hover:bg-surface-hover disabled:opacity-50", + "px-2 py-1 text-xs rounded-md border border-border bg-surface hover:bg-surface-hover" + ] + }, { "path": "src/components/Settings/APIAccessPanel.tsx", "patterns": [ @@ -5525,6 +5533,50 @@ "scripts/shared-template-audit.mjs" ] }, + { + "id": "billing-admin-organization-row-action-local-shell", + "category": "action-button", + "summary": "Billing admin organization Suspend and Activate row actions must not recreate the compact hosted-billing button shell; use Button with the secondary variant and sm size.", + "canonical": { + "path": "src/components/shared/buttonModel.ts", + "export": "getButtonClass" + }, + "scopes": ["src/components/Settings/BillingAdminOrganizationsTable.tsx"], + "extensions": [".tsx"], + "allPatterns": [ + "px-2.5 py-1.5 text-xs font-medium rounded-md border border-border bg-surface hover:bg-surface-hover disabled:opacity-50" + ], + "legacyReason": "Retired migration debt. Hosted billing organization row actions belong to the shared Button primitive family.", + "allowedPaths": [], + "ignoredPaths": ["src/components/shared/Button.test.tsx"], + "proof": [ + "src/components/shared/SharedPrimitives.guardrails.test.ts", + "src/components/shared/Button.test.tsx", + "scripts/shared-template-audit.mjs" + ] + }, + { + "id": "billing-admin-organization-reload-action-local-shell", + "category": "action-button", + "summary": "Billing admin organization expanded-row Reload actions must not recreate the compact hosted-billing button shell; use Button with the secondary variant and xs size.", + "canonical": { + "path": "src/components/shared/buttonModel.ts", + "export": "getButtonClass" + }, + "scopes": ["src/components/Settings/BillingAdminOrganizationsTable.tsx"], + "extensions": [".tsx"], + "allPatterns": [ + "px-2 py-1 text-xs rounded-md border border-border bg-surface hover:bg-surface-hover" + ], + "legacyReason": "Retired migration debt. Hosted billing expanded-row reload actions belong to the shared Button primitive family.", + "allowedPaths": [], + "ignoredPaths": ["src/components/shared/Button.test.tsx"], + "proof": [ + "src/components/shared/SharedPrimitives.guardrails.test.ts", + "src/components/shared/Button.test.tsx", + "scripts/shared-template-audit.mjs" + ] + }, { "id": "sso-provider-primary-action-local-shell", "category": "action-button", diff --git a/frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx b/frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx index c1e9af9f4..f2115d3ba 100644 --- a/frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx +++ b/frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx @@ -1,4 +1,5 @@ import type { Component } from 'solid-js'; +import { Button } from '@/components/shared/Button'; import { PulseDataGrid } from '@/components/shared/PulseDataGrid'; import { BILLING_ADMIN_EMPTY_STATE, @@ -54,7 +55,9 @@ export const BillingAdminOrganizationsTable: Component {org.org_id} {getBillingAdminOrganizationBadges(org).map((badge) => ( - {badge.label} + + {badge.label} + ))} @@ -85,7 +88,9 @@ export const BillingAdminOrganizationsTable: Component { const billing = props.billingByOrgID[(org.org_id || '').trim()]; - return {getBillingAdminTrialStatus(billing)}; + return ( + {getBillingAdminTrialStatus(billing)} + ); }, }, { @@ -111,8 +116,9 @@ export const BillingAdminOrganizationsTable: Component - - + ); }, @@ -153,15 +158,15 @@ export const BillingAdminOrganizationsTable: Component
Billing state JSON
- +
               {JSON.stringify(props.billingByOrgID[orgID] ?? { loading: true }, null, 2)}
diff --git a/frontend-modern/src/components/Settings/__tests__/BillingAdminPanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/BillingAdminPanel.test.tsx
index 385cefaa5..0e96c627a 100644
--- a/frontend-modern/src/components/Settings/__tests__/BillingAdminPanel.test.tsx
+++ b/frontend-modern/src/components/Settings/__tests__/BillingAdminPanel.test.tsx
@@ -147,6 +147,16 @@ describe('BillingAdminPanel', () => {
     expect(billingAdminPanelStateSource).toContain('promisePool');
     expect(billingAdminOrganizationsTableSource).toContain('PulseDataGrid');
     expect(billingAdminOrganizationsTableSource).toContain('Billing state JSON');
+    expect(billingAdminOrganizationsTableSource).toContain('@/components/shared/Button');
+    expect(billingAdminOrganizationsTableSource).toContain('variant="secondary"');
+    expect(billingAdminOrganizationsTableSource).toContain('size="sm"');
+    expect(billingAdminOrganizationsTableSource).toContain('size="xs"');
+    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',
+    );
+    expect(billingAdminOrganizationsTableSource).not.toContain(
+      'px-2 py-1 text-xs rounded-md border border-border bg-surface hover:bg-surface-hover',
+    );
   });
 
   it('stays unavailable in demo mode without loading hosted billing admin data', () => {
diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts
index d70e4786d..749f753e1 100644
--- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts
+++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts
@@ -130,6 +130,7 @@ import thresholdsTableDockerIgnoredPrefixesSectionSource from '@/components/Aler
 import webhookConfigFormSource from '@/components/Alerts/WebhookConfigForm.tsx?raw';
 import reportMergeModalSource from '@/components/Infrastructure/ReportMergeModal.tsx?raw';
 import availabilitySettingsPanelSource from '@/components/Settings/AvailabilitySettingsPanel.tsx?raw';
+import billingAdminOrganizationsTableSource from '@/components/Settings/BillingAdminOrganizationsTable.tsx?raw';
 import addressProbeStepSource from '@/components/Settings/ConnectionEditor/AddressProbeStep.tsx?raw';
 import connectionEditorSource from '@/components/Settings/ConnectionEditor/ConnectionEditor.tsx?raw';
 import availabilityTargetSlotSource from '@/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx?raw';
@@ -2906,6 +2907,12 @@ describe('shared primitive guardrails', () => {
     const settingsDialogCloseGuard = registry.patternGuards?.find(
       (guard) => guard.id === 'button-outline-settings-dialog-close-local-shell',
     );
+    const billingAdminOrganizationRowActionGuard = registry.patternGuards?.find(
+      (guard) => guard.id === 'billing-admin-organization-row-action-local-shell',
+    );
+    const billingAdminOrganizationReloadActionGuard = registry.patternGuards?.find(
+      (guard) => guard.id === 'billing-admin-organization-reload-action-local-shell',
+    );
     const ssoProviderPrimaryActionGuard = registry.patternGuards?.find(
       (guard) => guard.id === 'sso-provider-primary-action-local-shell',
     );
@@ -2958,6 +2965,7 @@ describe('shared primitive guardrails', () => {
       'src/components/Settings/APIAccessPanel.tsx',
       'src/components/Settings/AvailabilitySettingsPanel.tsx',
       'src/components/Settings/BillingAdminPanel.tsx',
+      'src/components/Settings/BillingAdminOrganizationsTable.tsx',
       'src/components/Settings/ConnectionEditor/AddressProbeStep.tsx',
       'src/components/Settings/ConnectionEditor/ConnectionEditor.tsx',
       'src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx',
@@ -2997,6 +3005,13 @@ describe('shared primitive guardrails', () => {
             'w-full sm:w-auto px-3 py-1.5 text-xs font-medium rounded-md border border-border bg-surface hover:bg-surface-hover disabled:opacity-50',
           ]),
         }),
+        expect.objectContaining({
+          path: 'src/components/Settings/BillingAdminOrganizationsTable.tsx',
+          patterns: expect.arrayContaining([
+            'px-2.5 py-1.5 text-xs font-medium rounded-md border border-border bg-surface hover:bg-surface-hover disabled:opacity-50',
+            'px-2 py-1 text-xs rounded-md border border-border bg-surface hover:bg-surface-hover',
+          ]),
+        }),
         expect.objectContaining({
           path: 'src/components/Settings/APIAccessPanel.tsx',
           patterns: expect.arrayContaining([
@@ -3107,6 +3122,17 @@ describe('shared primitive guardrails', () => {
     expect(updateProgressModalSource).not.toContain(
       'px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors',
     );
+    expect(billingAdminOrganizationsTableSource).toContain('@/components/shared/Button');
+    expect(billingAdminOrganizationsTableSource).toContain(' {
     expect(settingsDialogCloseGuard?.ignoredPaths).toEqual([
       'src/components/shared/Button.test.tsx',
     ]);
+    expect(billingAdminOrganizationRowActionGuard?.canonical?.path).toBe(
+      'src/components/shared/buttonModel.ts',
+    );
+    expect(billingAdminOrganizationRowActionGuard?.canonical?.export).toBe('getButtonClass');
+    expect(billingAdminOrganizationRowActionGuard?.allPatterns).toEqual([
+      'px-2.5 py-1.5 text-xs font-medium rounded-md border border-border bg-surface hover:bg-surface-hover disabled:opacity-50',
+    ]);
+    expect(billingAdminOrganizationRowActionGuard?.scopes).toEqual([
+      'src/components/Settings/BillingAdminOrganizationsTable.tsx',
+    ]);
+    expect(billingAdminOrganizationRowActionGuard?.allowedPaths ?? []).toHaveLength(0);
+    expect(billingAdminOrganizationRowActionGuard?.ignoredPaths).toEqual([
+      'src/components/shared/Button.test.tsx',
+    ]);
+    expect(billingAdminOrganizationReloadActionGuard?.canonical?.path).toBe(
+      'src/components/shared/buttonModel.ts',
+    );
+    expect(billingAdminOrganizationReloadActionGuard?.canonical?.export).toBe('getButtonClass');
+    expect(billingAdminOrganizationReloadActionGuard?.allPatterns).toEqual([
+      'px-2 py-1 text-xs rounded-md border border-border bg-surface hover:bg-surface-hover',
+    ]);
+    expect(billingAdminOrganizationReloadActionGuard?.scopes).toEqual([
+      'src/components/Settings/BillingAdminOrganizationsTable.tsx',
+    ]);
+    expect(billingAdminOrganizationReloadActionGuard?.allowedPaths ?? []).toHaveLength(0);
+    expect(billingAdminOrganizationReloadActionGuard?.ignoredPaths).toEqual([
+      'src/components/shared/Button.test.tsx',
+    ]);
     expect(ssoProviderPrimaryActionGuard?.canonical?.path).toBe(
       'src/components/shared/buttonModel.ts',
     );