From 7b7adb07d43363e4d2fb56f4b3ec9e99e701c458 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 21 Mar 2026 01:46:11 +0000 Subject: [PATCH] Split organization overview runtime owners --- .../subsystems/organization-settings.md | 47 ++-- .../v6/internal/subsystems/registry.json | 10 + .../OrganizationOverviewDetailsSection.tsx | 71 ++++++ .../OrganizationOverviewLoadingState.tsx | 42 +++ .../OrganizationOverviewMembersSection.tsx | 52 ++++ .../Settings/OrganizationOverviewPanel.tsx | 239 +----------------- .../OrganizationOverviewPanel.test.tsx | 127 ++++++++++ .../monitoredSystemModelGuardrails.test.ts | 40 ++- .../__tests__/settingsArchitecture.test.ts | 29 +++ .../useOrganizationOverviewPanelState.ts | 99 ++++++++ .../frontendResourceTypeBoundaries.test.ts | 23 +- .../organizationSettingsPresentation.test.ts | 6 + .../utils/organizationSettingsPresentation.ts | 8 + .../release_control/subsystem_lookup_test.py | 57 +++++ 14 files changed, 595 insertions(+), 255 deletions(-) create mode 100644 frontend-modern/src/components/Settings/OrganizationOverviewDetailsSection.tsx create mode 100644 frontend-modern/src/components/Settings/OrganizationOverviewLoadingState.tsx create mode 100644 frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx create mode 100644 frontend-modern/src/components/Settings/__tests__/OrganizationOverviewPanel.test.tsx create mode 100644 frontend-modern/src/components/Settings/useOrganizationOverviewPanelState.ts diff --git a/docs/release-control/v6/internal/subsystems/organization-settings.md b/docs/release-control/v6/internal/subsystems/organization-settings.md index 0e7a415c6a..04d6f81bbc 100644 --- a/docs/release-control/v6/internal/subsystems/organization-settings.md +++ b/docs/release-control/v6/internal/subsystems/organization-settings.md @@ -29,24 +29,28 @@ create cross-organization shares. 4. `frontend-modern/src/components/Settings/OrganizationAccessManagementSection.tsx` 5. `frontend-modern/src/components/Settings/OrganizationAccessMembersSection.tsx` 6. `frontend-modern/src/components/Settings/OrganizationOverviewPanel.tsx` -7. `frontend-modern/src/components/Settings/OrganizationSharingPanel.tsx` -8. `frontend-modern/src/components/Settings/OrganizationSharingCreateSection.tsx` -9. `frontend-modern/src/components/Settings/OrganizationSharingLoadingState.tsx` -10. `frontend-modern/src/components/Settings/OrganizationOutgoingSharesSection.tsx` -11. `frontend-modern/src/components/Settings/OrganizationIncomingSharesSection.tsx` -12. `frontend-modern/src/components/Settings/useOrganizationAccessPanelState.ts` -13. `frontend-modern/src/components/Settings/useOrganizationSharingPanelState.ts` -14. `frontend-modern/src/components/Settings/RolesPanel.tsx` -15. `frontend-modern/src/components/Settings/RBACFeatureGateSection.tsx` -16. `frontend-modern/src/components/Settings/RolesEditorDialog.tsx` -17. `frontend-modern/src/components/Settings/useRBACFeatureGateState.ts` -18. `frontend-modern/src/components/Settings/useRolesPanelState.ts` -19. `frontend-modern/src/components/Settings/UserAssignmentsPanel.tsx` -20. `frontend-modern/src/components/Settings/UserAssignmentsDialog.tsx` -21. `frontend-modern/src/components/Settings/useUserAssignmentsPanelState.ts` -22. `frontend-modern/src/utils/orgUtils.ts` -23. `frontend-modern/src/utils/organizationRolePresentation.ts` -24. `frontend-modern/src/utils/organizationSettingsPresentation.ts` +7. `frontend-modern/src/components/Settings/OrganizationOverviewLoadingState.tsx` +8. `frontend-modern/src/components/Settings/OrganizationOverviewDetailsSection.tsx` +9. `frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx` +10. `frontend-modern/src/components/Settings/OrganizationSharingPanel.tsx` +11. `frontend-modern/src/components/Settings/OrganizationSharingCreateSection.tsx` +12. `frontend-modern/src/components/Settings/OrganizationSharingLoadingState.tsx` +13. `frontend-modern/src/components/Settings/OrganizationOutgoingSharesSection.tsx` +14. `frontend-modern/src/components/Settings/OrganizationIncomingSharesSection.tsx` +15. `frontend-modern/src/components/Settings/useOrganizationAccessPanelState.ts` +16. `frontend-modern/src/components/Settings/useOrganizationOverviewPanelState.ts` +17. `frontend-modern/src/components/Settings/useOrganizationSharingPanelState.ts` +18. `frontend-modern/src/components/Settings/RolesPanel.tsx` +19. `frontend-modern/src/components/Settings/RBACFeatureGateSection.tsx` +20. `frontend-modern/src/components/Settings/RolesEditorDialog.tsx` +21. `frontend-modern/src/components/Settings/useRBACFeatureGateState.ts` +22. `frontend-modern/src/components/Settings/useRolesPanelState.ts` +23. `frontend-modern/src/components/Settings/UserAssignmentsPanel.tsx` +24. `frontend-modern/src/components/Settings/UserAssignmentsDialog.tsx` +25. `frontend-modern/src/components/Settings/useUserAssignmentsPanelState.ts` +26. `frontend-modern/src/utils/orgUtils.ts` +27. `frontend-modern/src/utils/organizationRolePresentation.ts` +28. `frontend-modern/src/utils/organizationSettingsPresentation.ts` ## Shared Boundaries @@ -55,7 +59,7 @@ create cross-organization shares. ## Extension Points 1. Add or change organization role and share semantics through `internal/models/organization.go` -2. Add or change organization access, overview, sharing, RBAC feature-gating, role-management, or user-assignment presentation through `frontend-modern/src/components/Settings/OrganizationAccessPanel.tsx`, `frontend-modern/src/components/Settings/OrganizationAccessLoadingState.tsx`, `frontend-modern/src/components/Settings/OrganizationAccessManagementSection.tsx`, `frontend-modern/src/components/Settings/OrganizationAccessMembersSection.tsx`, `frontend-modern/src/components/Settings/OrganizationOverviewPanel.tsx`, `frontend-modern/src/components/Settings/OrganizationSharingPanel.tsx`, `frontend-modern/src/components/Settings/OrganizationSharingCreateSection.tsx`, `frontend-modern/src/components/Settings/OrganizationSharingLoadingState.tsx`, `frontend-modern/src/components/Settings/OrganizationOutgoingSharesSection.tsx`, `frontend-modern/src/components/Settings/OrganizationIncomingSharesSection.tsx`, `frontend-modern/src/components/Settings/useOrganizationAccessPanelState.ts`, `frontend-modern/src/components/Settings/useOrganizationSharingPanelState.ts`, `frontend-modern/src/components/Settings/RBACFeatureGateSection.tsx`, `frontend-modern/src/components/Settings/RolesPanel.tsx`, `frontend-modern/src/components/Settings/RolesEditorDialog.tsx`, `frontend-modern/src/components/Settings/useRBACFeatureGateState.ts`, `frontend-modern/src/components/Settings/useRolesPanelState.ts`, `frontend-modern/src/components/Settings/UserAssignmentsPanel.tsx`, `frontend-modern/src/components/Settings/UserAssignmentsDialog.tsx`, and `frontend-modern/src/components/Settings/useUserAssignmentsPanelState.ts` +2. Add or change organization access, overview, sharing, RBAC feature-gating, role-management, or user-assignment presentation through `frontend-modern/src/components/Settings/OrganizationAccessPanel.tsx`, `frontend-modern/src/components/Settings/OrganizationAccessLoadingState.tsx`, `frontend-modern/src/components/Settings/OrganizationAccessManagementSection.tsx`, `frontend-modern/src/components/Settings/OrganizationAccessMembersSection.tsx`, `frontend-modern/src/components/Settings/OrganizationOverviewPanel.tsx`, `frontend-modern/src/components/Settings/OrganizationOverviewLoadingState.tsx`, `frontend-modern/src/components/Settings/OrganizationOverviewDetailsSection.tsx`, `frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx`, `frontend-modern/src/components/Settings/OrganizationSharingPanel.tsx`, `frontend-modern/src/components/Settings/OrganizationSharingCreateSection.tsx`, `frontend-modern/src/components/Settings/OrganizationSharingLoadingState.tsx`, `frontend-modern/src/components/Settings/OrganizationOutgoingSharesSection.tsx`, `frontend-modern/src/components/Settings/OrganizationIncomingSharesSection.tsx`, `frontend-modern/src/components/Settings/useOrganizationAccessPanelState.ts`, `frontend-modern/src/components/Settings/useOrganizationOverviewPanelState.ts`, `frontend-modern/src/components/Settings/useOrganizationSharingPanelState.ts`, `frontend-modern/src/components/Settings/RBACFeatureGateSection.tsx`, `frontend-modern/src/components/Settings/RolesPanel.tsx`, `frontend-modern/src/components/Settings/RolesEditorDialog.tsx`, `frontend-modern/src/components/Settings/useRBACFeatureGateState.ts`, `frontend-modern/src/components/Settings/useRolesPanelState.ts`, `frontend-modern/src/components/Settings/UserAssignmentsPanel.tsx`, `frontend-modern/src/components/Settings/UserAssignmentsDialog.tsx`, and `frontend-modern/src/components/Settings/useUserAssignmentsPanelState.ts` 3. Route organization transport changes through `frontend-modern/src/api/orgs.ts` and `frontend-modern/src/api/rbac.ts` 4. Keep backend organization and RBAC handler changes aligned through `internal/api/org_handlers.go`, `internal/api/org_lifecycle_handlers.go`, and `internal/api/enterprise_extension_rbac_admin.go` @@ -125,6 +129,11 @@ the panel is the shell, `useOrganizationAccessPanelState.ts` owns the membership runtime, and the loading, management, and members views each live in dedicated section owners instead of collapsing API lifecycle, permission gates, and table rendering into one file. +The organization overview surface now follows that same extracted-owner +pattern: the panel is the shell, `useOrganizationOverviewPanelState.ts` owns +the org/member load and display-name runtime, and the loading, details, and +membership views each live in dedicated section owners instead of mixing +summary cards, form actions, and table rendering into the shell. The sharing surface now follows the same extracted-owner pattern: the panel is the shell, `useOrganizationSharingPanelState.ts` owns the API-backed runtime, and the loading, create, outgoing, and incoming views each live in dedicated diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index e0fc9a752e..7d57e0dcff 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -2119,6 +2119,9 @@ "frontend-modern/src/components/Settings/OrganizationAccessPanel.tsx", "frontend-modern/src/components/Settings/OrganizationIncomingSharesSection.tsx", "frontend-modern/src/components/Settings/OrganizationOutgoingSharesSection.tsx", + "frontend-modern/src/components/Settings/OrganizationOverviewDetailsSection.tsx", + "frontend-modern/src/components/Settings/OrganizationOverviewLoadingState.tsx", + "frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx", "frontend-modern/src/components/Settings/OrganizationOverviewPanel.tsx", "frontend-modern/src/components/Settings/OrganizationSharingCreateSection.tsx", "frontend-modern/src/components/Settings/OrganizationSharingLoadingState.tsx", @@ -2127,6 +2130,7 @@ "frontend-modern/src/components/Settings/RolesEditorDialog.tsx", "frontend-modern/src/components/Settings/RolesPanel.tsx", "frontend-modern/src/components/Settings/useOrganizationAccessPanelState.ts", + "frontend-modern/src/components/Settings/useOrganizationOverviewPanelState.ts", "frontend-modern/src/components/Settings/useOrganizationSharingPanelState.ts", "frontend-modern/src/components/Settings/UserAssignmentsDialog.tsx", "frontend-modern/src/components/Settings/UserAssignmentsPanel.tsx", @@ -2144,6 +2148,7 @@ "exact_files": [ "frontend-modern/src/api/__tests__/orgs.test.ts", "frontend-modern/src/components/Settings/__tests__/OrganizationAccessPanel.test.tsx", + "frontend-modern/src/components/Settings/__tests__/OrganizationOverviewPanel.test.tsx", "frontend-modern/src/utils/__tests__/organizationSettingsPresentation.test.ts", "internal/models/organization_additional_test.go" ], @@ -2176,6 +2181,9 @@ "frontend-modern/src/components/Settings/OrganizationAccessPanel.tsx", "frontend-modern/src/components/Settings/OrganizationIncomingSharesSection.tsx", "frontend-modern/src/components/Settings/OrganizationOutgoingSharesSection.tsx", + "frontend-modern/src/components/Settings/OrganizationOverviewDetailsSection.tsx", + "frontend-modern/src/components/Settings/OrganizationOverviewLoadingState.tsx", + "frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx", "frontend-modern/src/components/Settings/OrganizationOverviewPanel.tsx", "frontend-modern/src/components/Settings/OrganizationSharingCreateSection.tsx", "frontend-modern/src/components/Settings/OrganizationSharingLoadingState.tsx", @@ -2184,6 +2192,7 @@ "frontend-modern/src/components/Settings/RolesEditorDialog.tsx", "frontend-modern/src/components/Settings/RolesPanel.tsx", "frontend-modern/src/components/Settings/useOrganizationAccessPanelState.ts", + "frontend-modern/src/components/Settings/useOrganizationOverviewPanelState.ts", "frontend-modern/src/components/Settings/useOrganizationSharingPanelState.ts", "frontend-modern/src/components/Settings/UserAssignmentsDialog.tsx", "frontend-modern/src/components/Settings/UserAssignmentsPanel.tsx", @@ -2200,6 +2209,7 @@ "frontend-modern/src/api/__tests__/orgs.test.ts", "frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts", "frontend-modern/src/components/Settings/__tests__/OrganizationAccessPanel.test.tsx", + "frontend-modern/src/components/Settings/__tests__/OrganizationOverviewPanel.test.tsx", "frontend-modern/src/components/Settings/__tests__/OrganizationSharingPanel.test.tsx", "frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx", "frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts", diff --git a/frontend-modern/src/components/Settings/OrganizationOverviewDetailsSection.tsx b/frontend-modern/src/components/Settings/OrganizationOverviewDetailsSection.tsx new file mode 100644 index 0000000000..ccc31f7d1b --- /dev/null +++ b/frontend-modern/src/components/Settings/OrganizationOverviewDetailsSection.tsx @@ -0,0 +1,71 @@ +import { Component, Show } from 'solid-js'; +import { formatOrgDate } from '@/utils/orgUtils'; +import { getOrganizationOverviewManageRequiredMessage } from '@/utils/organizationSettingsPresentation'; +import type { useOrganizationOverviewPanelState } from './useOrganizationOverviewPanelState'; + +interface OrganizationOverviewDetailsSectionProps { + state: ReturnType; +} + +export const OrganizationOverviewDetailsSection: Component< + OrganizationOverviewDetailsSectionProps +> = (props) => ( + + {(currentOrg) => ( +
+
+
+

Organization

+

+ {currentOrg().displayName || currentOrg().id} +

+
+
+

Org ID

+

{currentOrg().id}

+
+
+

Created

+

+ {formatOrgDate(currentOrg().createdAt)} +

+
+
+

Members

+

{props.state.members().length}

+
+
+ +
+ +
+ props.state.setDisplayNameDraft(event.currentTarget.value)} + disabled={!props.state.canManageCurrentOrg() || props.state.saving()} + class="w-full rounded-md border px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 " + /> + +
+ +

{getOrganizationOverviewManageRequiredMessage()}

+
+
+
+ )} +
+); diff --git a/frontend-modern/src/components/Settings/OrganizationOverviewLoadingState.tsx b/frontend-modern/src/components/Settings/OrganizationOverviewLoadingState.tsx new file mode 100644 index 0000000000..4531fae041 --- /dev/null +++ b/frontend-modern/src/components/Settings/OrganizationOverviewLoadingState.tsx @@ -0,0 +1,42 @@ +import { Component, For } from 'solid-js'; + +export const OrganizationOverviewLoadingState: Component = () => ( +
+
+ + {() => ( +
+
+
+
+ )} + +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + {() => ( +
+
+
+
+
+
+
+ )} + +
+
+
+); diff --git a/frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx b/frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx new file mode 100644 index 0000000000..d71b7129a6 --- /dev/null +++ b/frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx @@ -0,0 +1,52 @@ +import { Component } from 'solid-js'; +import { PulseDataGrid } from '@/components/shared/PulseDataGrid'; +import { getOrganizationOverviewMembersEmptyState } from '@/utils/organizationSettingsPresentation'; +import { formatOrgDate, normalizeRole, roleBadgeClass } from '@/utils/orgUtils'; +import type { useOrganizationOverviewPanelState } from './useOrganizationOverviewPanelState'; + +interface OrganizationOverviewMembersSectionProps { + state: ReturnType; +} + +export const OrganizationOverviewMembersSection: Component< + OrganizationOverviewMembersSectionProps +> = (props) => ( +
+

Membership

+
+ {member.userId}, + }, + { + key: 'role', + label: 'Role', + render: (member) => { + const role = normalizeRole(member.role); + return ( + + {role} + + ); + }, + }, + { + key: 'addedAt', + label: 'Added', + render: (member) => {formatOrgDate(member.addedAt)}, + }, + ]} + keyExtractor={(member) => member.userId} + emptyState={getOrganizationOverviewMembersEmptyState()} + desktopMinWidth="560px" + class="border-x-0 sm:border-x sm:border-t sm:border-b sm:rounded-md border-y border-border" + /> +
+
+); diff --git a/frontend-modern/src/components/Settings/OrganizationOverviewPanel.tsx b/frontend-modern/src/components/Settings/OrganizationOverviewPanel.tsx index ca4a06ff84..a7352b6cfb 100644 --- a/frontend-modern/src/components/Settings/OrganizationOverviewPanel.tsx +++ b/frontend-modern/src/components/Settings/OrganizationOverviewPanel.tsx @@ -1,96 +1,22 @@ -import { Component, Show, createSignal, onCleanup, onMount } from 'solid-js'; +import { Component, Show } from 'solid-js'; import SettingsPanel from '@/components/shared/SettingsPanel'; -import { OrgsAPI, type Organization, type OrganizationMember } from '@/api/orgs'; -import { getOrgID } from '@/utils/apiClient'; -import { normalizeOrgScope } from '@/utils/orgScope'; -import { canManageOrg, formatOrgDate, normalizeRole, roleBadgeClass } from '@/utils/orgUtils'; +import { isMultiTenantEnabled } from '@/stores/license'; import { - getOrganizationDisplayNameRequiredMessage, - getOrganizationDisplayNameUpdateErrorMessage, - getOrganizationOverviewMembersEmptyState, - getOrganizationSettingsLoadErrorMessage, ORGANIZATION_SETTINGS_UNAVAILABLE_CLASS, ORGANIZATION_SETTINGS_UNAVAILABLE_MESSAGE, } from '@/utils/organizationSettingsPresentation'; -import { isMultiTenantEnabled } from '@/stores/license'; -import { eventBus } from '@/stores/events'; -import { notificationStore } from '@/stores/notifications'; -import { logger } from '@/utils/logger'; import Building2 from 'lucide-solid/icons/building-2'; -import { PulseDataGrid } from '@/components/shared/PulseDataGrid'; +import { OrganizationOverviewDetailsSection } from './OrganizationOverviewDetailsSection'; +import { OrganizationOverviewLoadingState } from './OrganizationOverviewLoadingState'; +import { OrganizationOverviewMembersSection } from './OrganizationOverviewMembersSection'; +import { useOrganizationOverviewPanelState } from './useOrganizationOverviewPanelState'; -interface OrganizationOverviewPanelProps { +export interface OrganizationOverviewPanelProps { currentUser?: string; } export const OrganizationOverviewPanel: Component = (props) => { - const [loading, setLoading] = createSignal(true); - const [saving, setSaving] = createSignal(false); - const [org, setOrg] = createSignal(null); - const [members, setMembers] = createSignal([]); - const [displayNameDraft, setDisplayNameDraft] = createSignal(''); - - const activeOrgId = () => normalizeOrgScope(getOrgID()); - - const loadOrganization = async () => { - setLoading(true); - try { - const orgId = activeOrgId(); - const [orgData, memberData] = await Promise.all([ - OrgsAPI.get(orgId), - OrgsAPI.listMembers(orgId), - ]); - setOrg(orgData); - setMembers(memberData); - setDisplayNameDraft(orgData.displayName || ''); - } catch (error) { - logger.error('Failed to load organization overview', error); - const msg = error instanceof Error ? error.message : ''; - notificationStore.error(getOrganizationSettingsLoadErrorMessage(msg, 'overview')); - } finally { - setLoading(false); - } - }; - - const saveDisplayName = async () => { - const currentOrg = org(); - if (!currentOrg) return; - const nextName = displayNameDraft().trim(); - if (!nextName) { - notificationStore.error(getOrganizationDisplayNameRequiredMessage()); - return; - } - if (nextName === currentOrg.displayName) { - return; - } - - setSaving(true); - try { - const updated = await OrgsAPI.update(currentOrg.id, { displayName: nextName }); - setOrg(updated); - setDisplayNameDraft(updated.displayName || ''); - notificationStore.success('Organization name updated'); - } catch (error) { - logger.error('Failed to update organization display name', error); - notificationStore.error( - getOrganizationDisplayNameUpdateErrorMessage( - error instanceof Error ? error.message : undefined, - ), - ); - } finally { - setSaving(false); - } - }; - - onMount(() => { - if (!isMultiTenantEnabled()) return; - void loadOrganization(); - - const unsubscribe = eventBus.on('org_switched', () => { - void loadOrganization(); - }); - onCleanup(unsubscribe); - }); + const state = useOrganizationOverviewPanelState(props); return ( - -
- {Array.from({ length: 4 }).map(() => ( -
-
-
-
- ))} -
- -
-
-
-
-
-
-
- -
-
-
-
- {Array.from({ length: 3 }).map(() => ( -
-
-
-
-
-
-
- ))} -
-
-
- } - > - - {(currentOrg) => ( - <> -
-
-
-

Organization

-

- {currentOrg().displayName || currentOrg().id} -

-
-
-

Org ID

-

- {currentOrg().id} -

-
-
-

Created

-

- {formatOrgDate(currentOrg().createdAt)} -

-
-
-

Members

-

{members().length}

-
-
- -
- -
- setDisplayNameDraft(event.currentTarget.value)} - disabled={!canManageOrg(currentOrg(), props.currentUser) || saving()} - class="w-full rounded-md border px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 " - /> - -
- -

- Admin or owner role required to update organization details. -

-
-
-
- -
-

Membership

-
- ( - {member.userId} - ), - }, - { - key: 'role', - label: 'Role', - render: (member) => { - const role = normalizeRole(member.role); - return ( - - {role} - - ); - }, - }, - { - key: 'addedAt', - label: 'Added', - render: (member) => ( - {formatOrgDate(member.addedAt)} - ), - }, - ]} - keyExtractor={(member) => member.userId} - emptyState={getOrganizationOverviewMembersEmptyState()} - desktopMinWidth="560px" - class="border-x-0 sm:border-x sm:border-t sm:border-b sm:rounded-md border-y border-border" - /> -
-
- - )} + }> + + + diff --git a/frontend-modern/src/components/Settings/__tests__/OrganizationOverviewPanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/OrganizationOverviewPanel.test.tsx new file mode 100644 index 0000000000..b748e63eb4 --- /dev/null +++ b/frontend-modern/src/components/Settings/__tests__/OrganizationOverviewPanel.test.tsx @@ -0,0 +1,127 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { cleanup, render, screen, waitFor } from '@solidjs/testing-library'; +import { OrganizationOverviewPanel } from '../OrganizationOverviewPanel'; +import organizationOverviewStateSource from '../useOrganizationOverviewPanelState.ts?raw'; + +const orgGetMock = vi.fn(); +const listMembersMock = vi.fn(); +const updateOrgMock = vi.fn(); +const isMultiTenantEnabledMock = vi.fn(); +const getOrgIDMock = vi.fn(); +const notificationSuccessMock = vi.fn(); +const notificationErrorMock = vi.fn(); +const eventBusOnMock = vi.fn(); +const loggerErrorMock = vi.fn(); + +vi.mock('@/api/orgs', () => ({ + OrgsAPI: { + get: (...args: unknown[]) => orgGetMock(...args), + listMembers: (...args: unknown[]) => listMembersMock(...args), + update: (...args: unknown[]) => updateOrgMock(...args), + }, +})); + +vi.mock('@/stores/license', () => ({ + isMultiTenantEnabled: (...args: unknown[]) => isMultiTenantEnabledMock(...args), +})); + +vi.mock('@/utils/apiClient', () => ({ + getOrgID: (...args: unknown[]) => getOrgIDMock(...args), +})); + +vi.mock('@/stores/notifications', () => ({ + notificationStore: { + success: (...args: unknown[]) => notificationSuccessMock(...args), + error: (...args: unknown[]) => notificationErrorMock(...args), + }, +})); + +vi.mock('@/stores/events', () => ({ + eventBus: { + on: (...args: unknown[]) => eventBusOnMock(...args), + }, +})); + +vi.mock('@/utils/logger', () => ({ + logger: { + error: (...args: unknown[]) => loggerErrorMock(...args), + }, +})); + +const baseOrg = { + id: 'org-a', + displayName: 'Organization A', + ownerUserId: 'user-1', + createdAt: '2024-01-10T00:00:00Z', +}; + +const baseMembers = [ + { + userId: 'user-1', + role: 'owner', + addedAt: '2024-01-10T00:00:00Z', + }, + { + userId: 'user-2', + role: 'viewer', + addedAt: '2024-01-11T00:00:00Z', + }, +]; + +const renderPanel = () => render(() => ); + +const deferred = () => { + let resolve!: (value: T) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +}; + +beforeEach(() => { + orgGetMock.mockReset(); + listMembersMock.mockReset(); + updateOrgMock.mockReset(); + isMultiTenantEnabledMock.mockReset(); + getOrgIDMock.mockReset(); + notificationSuccessMock.mockReset(); + notificationErrorMock.mockReset(); + eventBusOnMock.mockReset(); + loggerErrorMock.mockReset(); + + isMultiTenantEnabledMock.mockReturnValue(true); + getOrgIDMock.mockReturnValue('org-a'); + eventBusOnMock.mockReturnValue(() => undefined); + orgGetMock.mockResolvedValue(baseOrg); + listMembersMock.mockResolvedValue(baseMembers); + updateOrgMock.mockResolvedValue({ ...baseOrg, displayName: 'Updated Org' }); +}); + +afterEach(() => { + cleanup(); +}); + +describe('OrganizationOverviewPanel', () => { + it('renders loading skeleton first, then loaded content', async () => { + const orgDeferred = deferred(); + orgGetMock.mockReturnValueOnce(orgDeferred.promise); + + const { container } = renderPanel(); + + expect(container.querySelector('.animate-pulse')).toBeTruthy(); + expect(screen.queryByLabelText('Display Name')).not.toBeInTheDocument(); + + orgDeferred.resolve(baseOrg); + + await waitFor(() => { + expect(screen.getByLabelText('Display Name')).toBeInTheDocument(); + }); + }); + + it('normalizes org scope through the shared helper', () => { + expect(organizationOverviewStateSource).toContain('normalizeOrgScope(getOrgID())'); + expect(organizationOverviewStateSource).not.toContain("getOrgID() || 'default'"); + }); +}); diff --git a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts index b1e8165b61..e8504cee30 100644 --- a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts @@ -74,12 +74,16 @@ import organizationAccessLoadingStateSource from '../OrganizationAccessLoadingSt import organizationAccessManagementSectionSource from '../OrganizationAccessManagementSection.tsx?raw'; import organizationAccessMembersSectionSource from '../OrganizationAccessMembersSection.tsx?raw'; import organizationOverviewPanelSource from '../OrganizationOverviewPanel.tsx?raw'; +import organizationOverviewLoadingStateSource from '../OrganizationOverviewLoadingState.tsx?raw'; +import organizationOverviewDetailsSectionSource from '../OrganizationOverviewDetailsSection.tsx?raw'; +import organizationOverviewMembersSectionSource from '../OrganizationOverviewMembersSection.tsx?raw'; import organizationSharingPanelSource from '../OrganizationSharingPanel.tsx?raw'; import organizationSharingCreateSectionSource from '../OrganizationSharingCreateSection.tsx?raw'; import organizationSharingLoadingStateSource from '../OrganizationSharingLoadingState.tsx?raw'; import organizationOutgoingSharesSectionSource from '../OrganizationOutgoingSharesSection.tsx?raw'; import organizationIncomingSharesSectionSource from '../OrganizationIncomingSharesSection.tsx?raw'; import organizationAccessStateSource from '../useOrganizationAccessPanelState.ts?raw'; +import organizationOverviewStateSource from '../useOrganizationOverviewPanelState.ts?raw'; import organizationSharingStateSource from '../useOrganizationSharingPanelState.ts?raw'; import canonicalResourceTypesSource from '@/utils/canonicalResourceTypes.ts?raw'; import organizationRolePresentationSource from '@/utils/organizationRolePresentation.ts?raw'; @@ -610,7 +614,6 @@ describe('monitored-system model guardrails', () => { expect(organizationAccessPanelSource).toContain('./OrganizationAccessLoadingState'); expect(organizationAccessPanelSource).toContain('./OrganizationAccessManagementSection'); expect(organizationAccessPanelSource).toContain('./OrganizationAccessMembersSection'); - expect(organizationOverviewPanelSource).toContain('@/utils/organizationSettingsPresentation'); expect(organizationAccessManagementSectionSource).toContain( '@/utils/organizationRolePresentation', ); @@ -651,12 +654,37 @@ describe('monitored-system model guardrails', () => { expect(organizationAccessStateSource).not.toContain("notificationStore.success(`Updated "); expect(organizationAccessStateSource).not.toContain("notificationStore.success(`Added "); expect(organizationAccessStateSource).not.toContain("notificationStore.success(`Removed "); - expect(organizationOverviewPanelSource).not.toContain( + expect(organizationOverviewPanelSource).toContain('./useOrganizationOverviewPanelState'); + expect(organizationOverviewPanelSource).toContain('./OrganizationOverviewLoadingState'); + expect(organizationOverviewPanelSource).toContain('./OrganizationOverviewDetailsSection'); + expect(organizationOverviewPanelSource).toContain('./OrganizationOverviewMembersSection'); + expect(organizationOverviewDetailsSectionSource).toContain( + '@/utils/organizationSettingsPresentation', + ); + expect(organizationOverviewMembersSectionSource).toContain( + '@/utils/organizationSettingsPresentation', + ); + expect(organizationOverviewDetailsSectionSource).toContain('formatOrgDate'); + expect(organizationOverviewMembersSectionSource).toContain('roleBadgeClass'); + expect(organizationOverviewMembersSectionSource).toContain('formatOrgDate'); + expect(organizationOverviewMembersSectionSource).toContain( + 'getOrganizationOverviewMembersEmptyState', + ); + expect(organizationOverviewLoadingStateSource).toContain('animate-pulse'); + expect(organizationOverviewStateSource).toContain('normalizeOrgScope(getOrgID())'); + expect(organizationOverviewStateSource).toContain('OrgsAPI.get'); + expect(organizationOverviewStateSource).toContain('OrgsAPI.listMembers'); + expect(organizationOverviewStateSource).toContain('OrgsAPI.update'); + expect(organizationOverviewStateSource).toContain('getOrganizationDisplayNameUpdatedMessage'); + expect(organizationOverviewStateSource).not.toContain( "notificationStore.error('Display name is required')", ); - expect(organizationOverviewPanelSource).not.toContain( + expect(organizationOverviewStateSource).not.toContain( "error instanceof Error ? error.message : 'Failed to update organization name'", ); + expect(organizationOverviewStateSource).not.toContain( + "notificationStore.success('Organization name updated')", + ); expect(organizationSharingCreateSectionSource).toContain('@/utils/organizationRolePresentation'); expect(organizationSharingStateSource).toContain('@/utils/organizationSettingsPresentation'); expect(organizationSharingCreateSectionSource).toContain('ORGANIZATION_SHARE_ROLE_OPTIONS'); @@ -716,6 +744,12 @@ describe('monitored-system model guardrails', () => { expect(organizationSettingsPresentationSource).toContain( 'export function getOrganizationDisplayNameRequiredMessage', ); + expect(organizationSettingsPresentationSource).toContain( + 'export function getOrganizationDisplayNameUpdatedMessage', + ); + expect(organizationSettingsPresentationSource).toContain( + 'export function getOrganizationOverviewManageRequiredMessage', + ); }); it('keeps alerts agent thresholds sourced from unified agent resources', () => { diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 1a284c072d..afebfad7c5 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -47,6 +47,9 @@ import commercialBillingSectionsSource from '../CommercialBillingSections.tsx?ra import selfHostedCommercialActivationSectionSource from '../SelfHostedCommercialActivationSection.tsx?raw'; import commercialBillingModelSource from '@/utils/commercialBillingModel.ts?raw'; import organizationOverviewPanelSource from '../OrganizationOverviewPanel.tsx?raw'; +import organizationOverviewLoadingStateSource from '../OrganizationOverviewLoadingState.tsx?raw'; +import organizationOverviewDetailsSectionSource from '../OrganizationOverviewDetailsSection.tsx?raw'; +import organizationOverviewMembersSectionSource from '../OrganizationOverviewMembersSection.tsx?raw'; import organizationAccessPanelSource from '../OrganizationAccessPanel.tsx?raw'; import organizationAccessLoadingStateSource from '../OrganizationAccessLoadingState.tsx?raw'; import organizationAccessManagementSectionSource from '../OrganizationAccessManagementSection.tsx?raw'; @@ -57,6 +60,7 @@ import organizationSharingLoadingStateSource from '../OrganizationSharingLoading import organizationOutgoingSharesSectionSource from '../OrganizationOutgoingSharesSection.tsx?raw'; import organizationIncomingSharesSectionSource from '../OrganizationIncomingSharesSection.tsx?raw'; import organizationAccessStateSource from '../useOrganizationAccessPanelState.ts?raw'; +import organizationOverviewStateSource from '../useOrganizationOverviewPanelState.ts?raw'; import organizationSharingStateSource from '../useOrganizationSharingPanelState.ts?raw'; import organizationBillingPanelSource from '../OrganizationBillingPanel.tsx?raw'; import proxmoxDeleteNodeDialogSource from '../ProxmoxDeleteNodeDialog.tsx?raw'; @@ -123,6 +127,9 @@ const extractedModules = [ '../UpdateInstallGuide.tsx', '../ReportingPanel.tsx', '../reportingPanelModel.ts', + '../OrganizationOverviewLoadingState.tsx', + '../OrganizationOverviewDetailsSection.tsx', + '../OrganizationOverviewMembersSection.tsx', '../OrganizationAccessLoadingState.tsx', '../OrganizationAccessManagementSection.tsx', '../OrganizationAccessMembersSection.tsx', @@ -131,6 +138,7 @@ const extractedModules = [ '../OrganizationOutgoingSharesSection.tsx', '../OrganizationIncomingSharesSection.tsx', '../useOrganizationAccessPanelState.ts', + '../useOrganizationOverviewPanelState.ts', '../useOrganizationSharingPanelState.ts', '../RBACFeatureGateSection.tsx', '../RolesEditorDialog.tsx', @@ -434,6 +442,27 @@ describe('Settings architecture guardrails', () => { expect(organizationAccessLoadingStateSource).toContain('animate-pulse'); }); + it('keeps organization overview split into shell, state, and section owners', () => { + expect(organizationOverviewPanelSource).toContain('./useOrganizationOverviewPanelState'); + expect(organizationOverviewPanelSource).toContain('./OrganizationOverviewLoadingState'); + expect(organizationOverviewPanelSource).toContain('./OrganizationOverviewDetailsSection'); + expect(organizationOverviewPanelSource).toContain('./OrganizationOverviewMembersSection'); + expect(organizationOverviewPanelSource).not.toContain('const loadOrganization = async'); + expect(organizationOverviewPanelSource).not.toContain('const saveDisplayName = async'); + expect(organizationOverviewStateSource).toContain('OrgsAPI.get'); + expect(organizationOverviewStateSource).toContain('OrgsAPI.listMembers'); + expect(organizationOverviewStateSource).toContain('OrgsAPI.update'); + expect(organizationOverviewStateSource).toContain('normalizeOrgScope(getOrgID())'); + expect(organizationOverviewDetailsSectionSource).toContain( + 'getOrganizationOverviewManageRequiredMessage', + ); + expect(organizationOverviewDetailsSectionSource).toContain('formatOrgDate'); + expect(organizationOverviewMembersSectionSource).toContain( + 'getOrganizationOverviewMembersEmptyState', + ); + expect(organizationOverviewLoadingStateSource).toContain('animate-pulse'); + }); + it('keeps RBAC settings panels split into gate, state, and dialog owners', () => { expect(rolesPanelSource).toContain('./RBACFeatureGateSection'); expect(rolesPanelSource).toContain('./RolesEditorDialog'); diff --git a/frontend-modern/src/components/Settings/useOrganizationOverviewPanelState.ts b/frontend-modern/src/components/Settings/useOrganizationOverviewPanelState.ts new file mode 100644 index 0000000000..cbe744163b --- /dev/null +++ b/frontend-modern/src/components/Settings/useOrganizationOverviewPanelState.ts @@ -0,0 +1,99 @@ +import { createMemo, createSignal, onCleanup, onMount } from 'solid-js'; +import { OrgsAPI, type Organization, type OrganizationMember } from '@/api/orgs'; +import { eventBus } from '@/stores/events'; +import { isMultiTenantEnabled } from '@/stores/license'; +import { notificationStore } from '@/stores/notifications'; +import { getOrgID } from '@/utils/apiClient'; +import { logger } from '@/utils/logger'; +import { normalizeOrgScope } from '@/utils/orgScope'; +import { + getOrganizationDisplayNameRequiredMessage, + getOrganizationDisplayNameUpdateErrorMessage, + getOrganizationDisplayNameUpdatedMessage, + getOrganizationSettingsLoadErrorMessage, +} from '@/utils/organizationSettingsPresentation'; +import { canManageOrg } from '@/utils/orgUtils'; + +interface OrganizationOverviewPanelProps { + currentUser?: string; +} + +export function useOrganizationOverviewPanelState(props: OrganizationOverviewPanelProps) { + const [loading, setLoading] = createSignal(true); + const [saving, setSaving] = createSignal(false); + const [org, setOrg] = createSignal(null); + const [members, setMembers] = createSignal([]); + const [displayNameDraft, setDisplayNameDraft] = createSignal(''); + + const activeOrgId = () => normalizeOrgScope(getOrgID()); + const canManageCurrentOrg = createMemo(() => canManageOrg(org(), props.currentUser)); + + const loadOrganization = async () => { + setLoading(true); + try { + const orgId = activeOrgId(); + const [orgData, memberData] = await Promise.all([ + OrgsAPI.get(orgId), + OrgsAPI.listMembers(orgId), + ]); + setOrg(orgData); + setMembers(memberData); + setDisplayNameDraft(orgData.displayName || ''); + } catch (error) { + logger.error('Failed to load organization overview', error); + const message = error instanceof Error ? error.message : ''; + notificationStore.error(getOrganizationSettingsLoadErrorMessage(message, 'overview')); + } finally { + setLoading(false); + } + }; + + const saveDisplayName = async () => { + const currentOrg = org(); + if (!currentOrg) return; + const nextName = displayNameDraft().trim(); + if (!nextName) { + notificationStore.error(getOrganizationDisplayNameRequiredMessage()); + return; + } + if (nextName === currentOrg.displayName) return; + + setSaving(true); + try { + const updated = await OrgsAPI.update(currentOrg.id, { displayName: nextName }); + setOrg(updated); + setDisplayNameDraft(updated.displayName || ''); + notificationStore.success(getOrganizationDisplayNameUpdatedMessage()); + } catch (error) { + logger.error('Failed to update organization display name', error); + notificationStore.error( + getOrganizationDisplayNameUpdateErrorMessage( + error instanceof Error ? error.message : undefined, + ), + ); + } finally { + setSaving(false); + } + }; + + onMount(() => { + if (!isMultiTenantEnabled()) return; + void loadOrganization(); + + const unsubscribe = eventBus.on('org_switched', () => { + void loadOrganization(); + }); + onCleanup(unsubscribe); + }); + + return { + canManageCurrentOrg, + displayNameDraft, + loading, + members, + org, + saveDisplayName, + saving, + setDisplayNameDraft, + }; +} diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index 04556be65b..88f6fe29f9 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -287,11 +287,13 @@ import organizationAccessManagementSectionSource from '@/components/Settings/Org import organizationAccessMembersSectionSource from '@/components/Settings/OrganizationAccessMembersSection.tsx?raw'; import billingAdminPanelSource from '@/components/Settings/BillingAdminPanel.tsx?raw'; import organizationBillingPanelSource from '@/components/Settings/OrganizationBillingPanel.tsx?raw'; -import organizationOverviewPanelSource from '@/components/Settings/OrganizationOverviewPanel.tsx?raw'; +import organizationOverviewLoadingStateSource from '@/components/Settings/OrganizationOverviewLoadingState.tsx?raw'; +import organizationOverviewMembersSectionSource from '@/components/Settings/OrganizationOverviewMembersSection.tsx?raw'; import organizationSharingCreateSectionSource from '@/components/Settings/OrganizationSharingCreateSection.tsx?raw'; import organizationOutgoingSharesSectionSource from '@/components/Settings/OrganizationOutgoingSharesSection.tsx?raw'; import organizationIncomingSharesSectionSource from '@/components/Settings/OrganizationIncomingSharesSection.tsx?raw'; import organizationAccessStateSource from '@/components/Settings/useOrganizationAccessPanelState.ts?raw'; +import organizationOverviewStateSource from '@/components/Settings/useOrganizationOverviewPanelState.ts?raw'; import organizationSharingStateSource from '@/components/Settings/useOrganizationSharingPanelState.ts?raw'; import organizationRolePresentationSource from '@/utils/organizationRolePresentation.ts?raw'; import organizationSettingsPresentationSource from '@/utils/organizationSettingsPresentation.ts?raw'; @@ -868,13 +870,13 @@ describe('frontend resource type boundaries', () => { 'export function normalizeOrganizationShareRole', ); expect(organizationAccessStateSource).toContain('normalizeOrgScope(getOrgID())'); - expect(organizationOverviewPanelSource).toContain('normalizeOrgScope(getOrgID())'); + expect(organizationOverviewStateSource).toContain('normalizeOrgScope(getOrgID())'); expect(organizationSharingStateSource).toContain('normalizeOrgScope(getOrgID())'); expect(organizationBillingPanelSource).toContain('normalizeOrgScope(getOrgID())'); expect(organizationAccessStateSource).toContain('@/utils/organizationSettingsPresentation'); expect(billingAdminPanelSource).toContain('@/utils/organizationSettingsPresentation'); expect(billingAdminPanelSource).toContain('@/utils/licensePresentation'); - expect(organizationOverviewPanelSource).toContain('@/utils/organizationSettingsPresentation'); + expect(organizationOverviewStateSource).toContain('@/utils/organizationSettingsPresentation'); expect(organizationSharingStateSource).toContain('@/utils/organizationSettingsPresentation'); expect(organizationBillingPanelSource).toContain('@/utils/organizationSettingsPresentation'); expect(organizationBillingPanelSource).toContain('@/utils/licensePresentation'); @@ -883,7 +885,7 @@ describe('frontend resource type boundaries', () => { expect(organizationAccessPanelSource).not.toContain( 'Multi-tenant requires an Enterprise license', ); - expect(organizationOverviewPanelSource).not.toContain( + expect(organizationOverviewStateSource).not.toContain( 'Multi-tenant is not enabled on this server', ); expect(organizationSharingStateSource).not.toContain( @@ -891,7 +893,10 @@ describe('frontend resource type boundaries', () => { ); expect(organizationAccessMembersSectionSource).toContain('getOrganizationAccessEmptyState'); expect(organizationAccessLoadingStateSource).toContain('animate-pulse'); - expect(organizationOverviewPanelSource).toContain('getOrganizationOverviewMembersEmptyState'); + expect(organizationOverviewMembersSectionSource).toContain( + 'getOrganizationOverviewMembersEmptyState', + ); + expect(organizationOverviewLoadingStateSource).toContain('animate-pulse'); expect(organizationOutgoingSharesSectionSource).toContain( 'getOrganizationOutgoingSharesEmptyState', ); @@ -903,7 +908,7 @@ describe('frontend resource type boundaries', () => { ); expect(organizationSharingStateSource).toContain('getOrganizationShareCreateSuccessMessage'); expect(organizationAccessMembersSectionSource).not.toContain('No organization members found.'); - expect(organizationOverviewPanelSource).not.toContain('No members found.'); + expect(organizationOverviewMembersSectionSource).not.toContain('No members found.'); expect(organizationOutgoingSharesSectionSource).not.toContain('No outgoing shares configured.'); expect(organizationIncomingSharesSectionSource).not.toContain( 'No incoming shares from other organizations.', @@ -944,9 +949,15 @@ describe('frontend resource type boundaries', () => { expect(organizationSettingsPresentationSource).toContain( 'export function getOrganizationAccessRoleUpdatedMessage', ); + expect(organizationSettingsPresentationSource).toContain( + 'export function getOrganizationDisplayNameUpdatedMessage', + ); expect(organizationSettingsPresentationSource).toContain( 'export function getOrganizationOverviewMembersEmptyState', ); + expect(organizationSettingsPresentationSource).toContain( + 'export function getOrganizationOverviewManageRequiredMessage', + ); expect(organizationSettingsPresentationSource).toContain( 'export function getOrganizationOutgoingSharesEmptyState', ); diff --git a/frontend-modern/src/utils/__tests__/organizationSettingsPresentation.test.ts b/frontend-modern/src/utils/__tests__/organizationSettingsPresentation.test.ts index a359e93a72..ff7c830233 100644 --- a/frontend-modern/src/utils/__tests__/organizationSettingsPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/organizationSettingsPresentation.test.ts @@ -8,10 +8,12 @@ import { getOrganizationAddMemberErrorMessage, getOrganizationDisplayNameRequiredMessage, getOrganizationDisplayNameUpdateErrorMessage, + getOrganizationDisplayNameUpdatedMessage, getOrganizationIncomingSharesEmptyState, getOrganizationMemberRemoveConfirmMessage, getOrganizationMemberRoleUpdateErrorMessage, getOrganizationMemberUserIdRequiredMessage, + getOrganizationOverviewManageRequiredMessage, getOrganizationOwnerRoleLockedMessage, getOrganizationOutgoingSharesEmptyState, getOrganizationOverviewMembersEmptyState, @@ -76,6 +78,10 @@ describe('organizationSettingsPresentation', () => { 'Failed to update organization name', ); expect(getOrganizationDisplayNameUpdateErrorMessage('bad request')).toBe('bad request'); + expect(getOrganizationDisplayNameUpdatedMessage()).toBe('Organization name updated'); + expect(getOrganizationOverviewManageRequiredMessage()).toBe( + 'Admin or owner role required to update organization details.', + ); expect(getOrganizationOwnerRoleLockedMessage()).toBe( 'Current owner can only remain owner. Transfer ownership instead.', ); diff --git a/frontend-modern/src/utils/organizationSettingsPresentation.ts b/frontend-modern/src/utils/organizationSettingsPresentation.ts index 3f4a3547fd..a850b5b391 100644 --- a/frontend-modern/src/utils/organizationSettingsPresentation.ts +++ b/frontend-modern/src/utils/organizationSettingsPresentation.ts @@ -47,6 +47,14 @@ export function getOrganizationDisplayNameUpdateErrorMessage(message?: string): return message || 'Failed to update organization name'; } +export function getOrganizationDisplayNameUpdatedMessage(): string { + return 'Organization name updated'; +} + +export function getOrganizationOverviewManageRequiredMessage(): string { + return 'Admin or owner role required to update organization details.'; +} + export function getOrganizationOwnerRoleLockedMessage(): string { return 'Current owner can only remain owner. Transfer ownership instead.'; } diff --git a/scripts/release_control/subsystem_lookup_test.py b/scripts/release_control/subsystem_lookup_test.py index ac724dc02e..596c85ada4 100644 --- a/scripts/release_control/subsystem_lookup_test.py +++ b/scripts/release_control/subsystem_lookup_test.py @@ -1830,6 +1830,7 @@ class SubsystemLookupTest(unittest.TestCase): [ "frontend-modern/src/api/__tests__/orgs.test.ts", "frontend-modern/src/components/Settings/__tests__/OrganizationAccessPanel.test.tsx", + "frontend-modern/src/components/Settings/__tests__/OrganizationOverviewPanel.test.tsx", "frontend-modern/src/components/Settings/__tests__/OrganizationSharingPanel.test.tsx", "frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx", "frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts", @@ -1920,6 +1921,60 @@ class SubsystemLookupTest(unittest.TestCase): "organization-settings-surface", ) + def test_lookup_paths_assigns_organization_overview_state_to_organization_settings(self) -> None: + result = lookup_paths( + ["frontend-modern/src/components/Settings/useOrganizationOverviewPanelState.ts"] + ) + self.assertEqual(result["unowned_runtime_files"], []) + self.assertEqual( + {item["subsystem"] for item in result["impacted_subsystems"]}, + {"organization-settings"}, + ) + file_entry = result["files"][0] + self.assertEqual(file_entry["classification"], "runtime") + self.assertEqual( + {match["subsystem"] for match in file_entry["matches"]}, + {"organization-settings"}, + ) + match = file_entry["matches"][0] + self.assertEqual( + match["contract"], + "docs/release-control/v6/internal/subsystems/organization-settings.md", + ) + self.assertEqual(match["lane_context"]["lane_id"], "L14") + self.assertEqual( + match["verification_requirement"]["id"], + "organization-settings-surface", + ) + + def test_lookup_paths_assigns_organization_overview_members_section_to_organization_settings( + self, + ) -> None: + result = lookup_paths( + ["frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx"] + ) + self.assertEqual(result["unowned_runtime_files"], []) + self.assertEqual( + {item["subsystem"] for item in result["impacted_subsystems"]}, + {"organization-settings"}, + ) + file_entry = result["files"][0] + self.assertEqual(file_entry["classification"], "runtime") + self.assertEqual( + {match["subsystem"] for match in file_entry["matches"]}, + {"organization-settings"}, + ) + match = file_entry["matches"][0] + self.assertEqual( + match["contract"], + "docs/release-control/v6/internal/subsystems/organization-settings.md", + ) + self.assertEqual(match["lane_context"]["lane_id"], "L14") + self.assertEqual( + match["verification_requirement"]["id"], + "organization-settings-surface", + ) + def test_lookup_paths_assigns_organization_outgoing_shares_section_to_organization_settings( self, ) -> None: @@ -1976,6 +2031,7 @@ class SubsystemLookupTest(unittest.TestCase): [ "frontend-modern/src/api/__tests__/orgs.test.ts", "frontend-modern/src/components/Settings/__tests__/OrganizationAccessPanel.test.tsx", + "frontend-modern/src/components/Settings/__tests__/OrganizationOverviewPanel.test.tsx", "frontend-modern/src/components/Settings/__tests__/OrganizationSharingPanel.test.tsx", "frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx", "frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts", @@ -2014,6 +2070,7 @@ class SubsystemLookupTest(unittest.TestCase): [ "frontend-modern/src/api/__tests__/orgs.test.ts", "frontend-modern/src/components/Settings/__tests__/OrganizationAccessPanel.test.tsx", + "frontend-modern/src/components/Settings/__tests__/OrganizationOverviewPanel.test.tsx", "frontend-modern/src/components/Settings/__tests__/OrganizationSharingPanel.test.tsx", "frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx", "frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts",