diff --git a/libs/ui-components/src/components/Device/DeviceDetails/TerminalTab.tsx b/libs/ui-components/src/components/Device/DeviceDetails/TerminalTab.tsx index 9c1b88275..5223cfa78 100644 --- a/libs/ui-components/src/components/Device/DeviceDetails/TerminalTab.tsx +++ b/libs/ui-components/src/components/Device/DeviceDetails/TerminalTab.tsx @@ -83,7 +83,7 @@ const TerminalTab = ({ device }: TerminalTabProps) => { const { sendMessage, isClosed, error, reconnect, isConnecting } = useWebSocket( device.metadata.name || '', - currentOrganization?.metadata?.name || undefined, + currentOrganization?.id || undefined, onMsgReceived, wsMeta, ); diff --git a/libs/ui-components/src/components/common/OrganizationGuard.tsx b/libs/ui-components/src/components/common/OrganizationGuard.tsx index e80d677cf..9a5edfebb 100644 --- a/libs/ui-components/src/components/common/OrganizationGuard.tsx +++ b/libs/ui-components/src/components/common/OrganizationGuard.tsx @@ -5,11 +5,46 @@ import { getErrorMessage } from '../../utils/error'; import { getCurrentOrganizationId, storeCurrentOrganizationId } from '../../utils/organizationStorage'; import { showSpinnerBriefly } from '../../utils/time'; +export type OrganizationItem = { + id: string; + label?: string; + description?: string; +}; + +// Returns the list of UserOrganizations ready to be displayed. +// Whenever two organizations have the same displayName, the description is set to the id. +const toOrganizationItems = (apiOrgs: Organization[]): OrganizationItem[] => { + const displayNameCounts = new Map(); + for (const org of apiOrgs) { + const displayName = org.spec?.displayName; + if (displayName) { + const prevCount = displayNameCounts.get(displayName) ?? 0; + displayNameCounts.set(displayName, prevCount + 1); + } + } + return apiOrgs.map((org) => { + const id = org.metadata?.name as string; + const displayName = org.spec?.displayName; + const sameNameCount = displayNameCounts.get(displayName || '') ?? 0; + if (sameNameCount > 1) { + return { + id, + label: displayName, + description: id, + }; + } + return { + id, + label: displayName, + }; + }); +}; + interface OrganizationContextType { - currentOrganization?: Organization; - availableOrganizations: Organization[]; + currentOrganization?: OrganizationItem; + availableOrganizations: OrganizationItem[]; mustShowOrganizationSelector: boolean; - selectOrganization: (org: Organization) => void; + selectOrganization: (org: OrganizationItem) => void; selectionError?: string; isReloading: boolean; isEmptyOrganizations: boolean; @@ -29,20 +64,17 @@ export const useOrganizationGuardContext = (): OrganizationContextType => { const OrganizationGuard = ({ children }: React.PropsWithChildren) => { const { fetch } = useAppContext(); - const [currentOrganization, setCurrentOrganization] = React.useState(); - const [availableOrganizations, setAvailableOrganizations] = React.useState([]); + const [currentOrganization, setCurrentOrganization] = React.useState(); + const [availableOrganizations, setAvailableOrganizations] = React.useState([]); const [organizationsLoaded, setOrganizationsLoaded] = React.useState(false); const [selectionError, setSelectionError] = React.useState(); const [isEmptyOrganizations, setIsEmptyOrganizations] = React.useState(false); const [isReloading, setIsReloading] = React.useState(false); const initializationStartedRef = React.useRef(false); - const selectOrganization = React.useCallback((org: Organization) => { - const organizationId = org.metadata?.name || ''; - + const selectOrganization = React.useCallback((org: OrganizationItem) => { try { - // Store organization in localStorage - headers will handle the rest - storeCurrentOrganizationId(organizationId); + storeCurrentOrganizationId(org.id); setCurrentOrganization(org); } catch (error) { setSelectionError(getErrorMessage(error)); @@ -52,10 +84,11 @@ const OrganizationGuard = ({ children }: React.PropsWithChildren) => { const fetchOrganizations = React.useCallback(async () => { try { const organizations = await fetch.get('organizations'); - setAvailableOrganizations(organizations.items); + const orgItems = toOrganizationItems(organizations.items ?? []); + setAvailableOrganizations(orgItems); // Treat empty organizations list as an error - if (!organizations.items || organizations.items.length === 0) { + if (!orgItems.length) { setSelectionError('No organizations available'); setIsEmptyOrganizations(true); setOrganizationsLoaded(true); @@ -65,17 +98,15 @@ const OrganizationGuard = ({ children }: React.PropsWithChildren) => { const currentOrgId = getCurrentOrganizationId(); // Validate current organization against available organizations - const currentOrg = currentOrgId - ? organizations.items.find((org) => org.metadata?.name === currentOrgId) - : undefined; + const currentOrg = currentOrgId ? orgItems.find((org) => org.id === currentOrgId) : undefined; if (currentOrg) { // The previously selected organization exists - use it selectOrganization(currentOrg); } else { - if (organizations.items?.length === 1) { + if (orgItems.length === 1) { // Only one organization available - select it automatically - selectOrganization(organizations.items[0]); + selectOrganization(orgItems[0]); } else if (currentOrgId) { // Previously set organization does not exist anymore - remove it from localStorage so the user can select a new organization setCurrentOrganization(undefined); diff --git a/libs/ui-components/src/components/common/OrganizationSelector.tsx b/libs/ui-components/src/components/common/OrganizationSelector.tsx index 91dd1426a..d03809460 100644 --- a/libs/ui-components/src/components/common/OrganizationSelector.tsx +++ b/libs/ui-components/src/components/common/OrganizationSelector.tsx @@ -25,14 +25,13 @@ import { Title, } from '@patternfly/react-core'; -import { Organization } from '@flightctl/types'; import { useTranslation } from '../../hooks/useTranslation'; -import { useOrganizationGuardContext } from './OrganizationGuard'; +import { type OrganizationItem, useOrganizationGuardContext } from './OrganizationGuard'; import { ORGANIZATION_STORAGE_KEY } from '../../utils/organizationStorage'; interface OrganizationSelectorContentProps { defaultOrganizationId?: string; - organizations: Organization[]; + organizations: OrganizationItem[]; onSelect: (orgId: string) => void; onCancel?: () => void; allowCancel?: boolean; @@ -73,14 +72,11 @@ const OrganizationSelectorContent = ({ > - {organizations.map((org) => { - const orgId = org.metadata?.name as string; - return ( - - {org.spec?.displayName || orgId} - - ); - })} + {organizations.map((org) => ( + + {org.label || org.id} + + ))} @@ -155,7 +151,7 @@ const OrganizationSelector = ({ onClose, isFirstLogin = true }: OrganizationSele const getLastSelectedOrganization = React.useCallback(() => { try { const savedOrgId = localStorage.getItem(ORGANIZATION_STORAGE_KEY); - if (savedOrgId && availableOrganizations.some((org) => org.metadata?.name === savedOrgId)) { + if (savedOrgId && availableOrganizations.some((org) => org.id === savedOrgId)) { return savedOrgId; } } catch (error) { @@ -166,7 +162,7 @@ const OrganizationSelector = ({ onClose, isFirstLogin = true }: OrganizationSele const handleSelect = React.useCallback( (orgId: string) => { - const org = availableOrganizations.find((org) => org.metadata?.name === orgId); + const org = availableOrganizations.find((o) => o.id === orgId); if (org) { try { selectOrganization(org); diff --git a/libs/ui-components/src/components/common/PageNavigation.tsx b/libs/ui-components/src/components/common/PageNavigation.tsx index 45ac70ae5..7bb004fb3 100644 --- a/libs/ui-components/src/components/common/PageNavigation.tsx +++ b/libs/ui-components/src/components/common/PageNavigation.tsx @@ -109,7 +109,7 @@ const PageNavigation = ({ showSettings = true }: { showSettings?: boolean }) => const [showOrganizationModal, setShowOrganizationModal] = React.useState(false); const [showLoginCommandModal, setShowLoginCommandModal] = React.useState(false); const showOrganizationSelection = availableOrganizations.length > 1; - const currentOrgDisplayName = currentOrganization?.spec?.displayName || currentOrganization?.metadata?.name || ''; + const currentOrgDisplayName = currentOrganization?.label || currentOrganization?.id; return ( <> diff --git a/libs/ui-components/src/components/common/PermissionsContext.tsx b/libs/ui-components/src/components/common/PermissionsContext.tsx index 2239c41b6..33f1ddf5a 100644 --- a/libs/ui-components/src/components/common/PermissionsContext.tsx +++ b/libs/ui-components/src/components/common/PermissionsContext.tsx @@ -43,7 +43,7 @@ export const PermissionsContextProvider = ({ children }: React.PropsWithChildren const [error, setError] = React.useState(); const { currentOrganization } = useOrganizationGuardContext(); - const orgId = currentOrganization?.metadata?.name; + const orgId = currentOrganization?.id; const { get } = useFetch();