mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Converge billing admin row actions on shared Button primitives
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<BillingAdminOrganizations
|
||||
<div class="text-xs text-muted">
|
||||
<span class="font-mono">{org.org_id}</span>
|
||||
{getBillingAdminOrganizationBadges(org).map((badge) => (
|
||||
<span class={`ml-2 rounded px-1.5 py-0.5 ${badge.badgeClass}`}>{badge.label}</span>
|
||||
<span class={`ml-2 rounded px-1.5 py-0.5 ${badge.badgeClass}`}>
|
||||
{badge.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
@@ -85,7 +88,9 @@ export const BillingAdminOrganizationsTable: Component<BillingAdminOrganizations
|
||||
label: 'Trial',
|
||||
render: (org) => {
|
||||
const billing = props.billingByOrgID[(org.org_id || '').trim()];
|
||||
return <span class="text-xs text-base-content">{getBillingAdminTrialStatus(billing)}</span>;
|
||||
return (
|
||||
<span class="text-xs text-base-content">{getBillingAdminTrialStatus(billing)}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -111,8 +116,9 @@ export const BillingAdminOrganizationsTable: Component<BillingAdminOrganizations
|
||||
const currentSubState = (billing?.subscription_state || '').toLowerCase() || 'unknown';
|
||||
return (
|
||||
<div class="inline-flex flex-col sm:flex-row sm:items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
props.onSuspendOrganization(orgID);
|
||||
}}
|
||||
@@ -121,12 +127,12 @@ export const BillingAdminOrganizationsTable: Component<BillingAdminOrganizations
|
||||
props.billingLoadingByOrgID[orgID] ||
|
||||
currentSubState === 'suspended'
|
||||
}
|
||||
class="px-2.5 py-1.5 text-xs font-medium rounded-md border border-border bg-surface hover:bg-surface-hover disabled:opacity-50"
|
||||
>
|
||||
Suspend Org
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
props.onActivateOrganization(orgID);
|
||||
}}
|
||||
@@ -135,10 +141,9 @@ export const BillingAdminOrganizationsTable: Component<BillingAdminOrganizations
|
||||
props.billingLoadingByOrgID[orgID] ||
|
||||
currentSubState === 'active'
|
||||
}
|
||||
class="px-2.5 py-1.5 text-xs font-medium rounded-md border border-border bg-surface hover:bg-surface-hover disabled:opacity-50"
|
||||
>
|
||||
Activate Org
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -153,15 +158,15 @@ export const BillingAdminOrganizationsTable: Component<BillingAdminOrganizations
|
||||
<div class="rounded-md border border-border bg-surface-alt p-3">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-2">
|
||||
<div class="text-xs font-semibold text-muted">Billing state JSON</div>
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
props.onReloadOrganization(orgID);
|
||||
}}
|
||||
class="px-2 py-1 text-xs rounded-md border border-border bg-surface hover:bg-surface-hover"
|
||||
>
|
||||
Reload
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<pre class="text-xs overflow-x-auto whitespace-pre-wrap font-mono text-base-content">
|
||||
{JSON.stringify(props.billingByOrgID[orgID] ?? { loading: true }, null, 2)}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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('<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',
|
||||
);
|
||||
expect(registeredGuard?.canonical?.path).toBe('src/components/shared/buttonModel.ts');
|
||||
expect(registeredGuard?.canonical?.export).toBe('getButtonClass');
|
||||
expect(registeredGuard?.allPatterns).toEqual([
|
||||
@@ -3317,6 +3343,34 @@ describe('shared primitive guardrails', () => {
|
||||
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',
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user