From 1c1a86296738c2c2702ba22469d2aa36ff516a3c Mon Sep 17 00:00:00 2001 From: Vojtech Szocs Date: Mon, 22 Jun 2026 18:35:45 +0000 Subject: [PATCH] Fix single perspective icon and text in NavHeader --- frontend/@types/console/window.d.ts | 1 + .../providers/perspective-state-provider.ts | 11 +-- .../PerspectiveConfiguration.tsx | 31 ++----- .../__tests__/PerspectiveDetector.spec.tsx | 20 ++-- .../src/components/nav/NavHeader.tsx | 43 +++++---- .../nav/__tests__/NavHeader.spec.tsx | 31 +++++-- .../hooks/__tests__/usePerspectives.spec.ts | 50 ++++++---- .../__tests__/usePinnedResources.spec.ts | 91 +++++++++++++++---- .../src/hooks/usePerspectives.ts | 50 +++------- .../src/hooks/usePinnedResources.ts | 8 +- .../src/utils/override-perspectives.ts | 56 ++++++++++++ .../catalog/PinnedResourcesConfiguration.tsx | 10 +- pkg/server/server.go | 12 ++- 13 files changed, 261 insertions(+), 153 deletions(-) create mode 100644 frontend/packages/console-shared/src/utils/override-perspectives.ts diff --git a/frontend/@types/console/window.d.ts b/frontend/@types/console/window.d.ts index 5baf55c51a6..a0fc0b463ae 100644 --- a/frontend/@types/console/window.d.ts +++ b/frontend/@types/console/window.d.ts @@ -31,6 +31,7 @@ declare interface Window { GOOS: string; graphqlBaseURL: string; developerCatalogCategories: string; + /** JSON encoded configuration for the console's perspectives override */ perspectives: string; developerCatalogTypes: string; userSettingsLocation: string; diff --git a/frontend/packages/console-app/src/actions/providers/perspective-state-provider.ts b/frontend/packages/console-app/src/actions/providers/perspective-state-provider.ts index a04559a9588..47d62c7a022 100644 --- a/frontend/packages/console-app/src/actions/providers/perspective-state-provider.ts +++ b/frontend/packages/console-app/src/actions/providers/perspective-state-provider.ts @@ -1,17 +1,16 @@ import type { SetFeatureFlag } from '@console/dynamic-plugin-sdk'; -import type { Perspective } from '@console/shared/src/hooks/usePerspectives'; +import { hasReviewAccess } from '@console/shared/src/hooks/usePerspectives'; import { - hasReviewAccess, PerspectiveVisibilityState, -} from '@console/shared/src/hooks/usePerspectives'; + overridePerspectives, +} from '@console/shared/src/utils/override-perspectives'; import { FLAG_DEVELOPER_PERSPECTIVE } from '../../consts'; export const useDeveloperPerspectiveStateProvider = (setFeatureFlag: SetFeatureFlag) => { - if (!window.SERVER_FLAGS.perspectives) { + if (!overridePerspectives) { setFeatureFlag(FLAG_DEVELOPER_PERSPECTIVE, true); } else { - const perspectives: Perspective[] = JSON.parse(window.SERVER_FLAGS.perspectives); - const devPerspective = perspectives?.find((p) => p.id === 'dev'); + const devPerspective = overridePerspectives.find((p) => p.id === 'dev'); if (!devPerspective) { setFeatureFlag(FLAG_DEVELOPER_PERSPECTIVE, true); } else if (devPerspective.visibility.state === PerspectiveVisibilityState.Disabled) { diff --git a/frontend/packages/console-app/src/components/detect-context/PerspectiveConfiguration.tsx b/frontend/packages/console-app/src/components/detect-context/PerspectiveConfiguration.tsx index 976ca5cb6ac..ae64986f02d 100644 --- a/frontend/packages/console-app/src/components/detect-context/PerspectiveConfiguration.tsx +++ b/frontend/packages/console-app/src/components/detect-context/PerspectiveConfiguration.tsx @@ -14,10 +14,7 @@ import { } from '@patternfly/react-core'; import { safeDump } from 'js-yaml'; import { useTranslation } from 'react-i18next'; -import type { - Perspective as PerspectiveExtension, - AccessReviewResourceAttributes, -} from '@console/dynamic-plugin-sdk/src'; +import type { Perspective as PerspectiveExtension } from '@console/dynamic-plugin-sdk/src'; import { isPerspective } from '@console/dynamic-plugin-sdk/src'; import type { K8sResourceKind } from '@console/internal/module/k8s'; import { useExtensions } from '@console/plugin-sdk/src/api/useExtensions'; @@ -29,27 +26,11 @@ import { SaveStatus } from '@console/shared/src/components/cluster-configuration import { useConsoleOperatorConfig } from '@console/shared/src/components/cluster-configuration/useConsoleOperatorConfig'; import { useDebounceCallback } from '@console/shared/src/hooks/useDebounceCallback'; import { useTelemetry } from '@console/shared/src/hooks/useTelemetry'; - -enum PerspectiveVisibilityState { - Enabled = 'Enabled', - Disabled = 'Disabled', - AccessReview = 'AccessReview', -} - -type PerspectiveAccessReview = { - required?: AccessReviewResourceAttributes[]; - missing?: AccessReviewResourceAttributes[]; -}; - -type PerspectiveVisibility = { - state: PerspectiveVisibilityState; - accessReview?: PerspectiveAccessReview; -}; - -type Perspective = { - id: string; - visibility: PerspectiveVisibility; -}; +import type { + PerspectiveVisibility, + Perspective, +} from '@console/shared/src/utils/override-perspectives'; +import { PerspectiveVisibilityState } from '@console/shared/src/utils/override-perspectives'; type PerspectivesConsoleConfig = K8sResourceKind & { spec: { diff --git a/frontend/packages/console-app/src/components/detect-context/__tests__/PerspectiveDetector.spec.tsx b/frontend/packages/console-app/src/components/detect-context/__tests__/PerspectiveDetector.spec.tsx index 3a786c08d69..60abfe93786 100644 --- a/frontend/packages/console-app/src/components/detect-context/__tests__/PerspectiveDetector.spec.tsx +++ b/frontend/packages/console-app/src/components/detect-context/__tests__/PerspectiveDetector.spec.tsx @@ -2,13 +2,20 @@ import { render, waitFor } from '@testing-library/react'; import { useLocation } from 'react-router'; import type { Perspective } from '@console/dynamic-plugin-sdk'; import type { LoadedExtension } from '@console/dynamic-plugin-sdk/src/types'; -import { - usePerspectives, - PerspectiveVisibilityState, -} from '@console/shared/src/hooks/usePerspectives'; -import type { Perspective as PerspectiveType } from '@console/shared/src/hooks/usePerspectives'; +import { usePerspectives } from '@console/shared/src/hooks/usePerspectives'; +import type { Perspective as PerspectiveType } from '@console/shared/src/utils/override-perspectives'; +import { PerspectiveVisibilityState } from '@console/shared/src/utils/override-perspectives'; import PerspectiveDetector from '../PerspectiveDetector'; +let mockOverridePerspectives: PerspectiveType[] | undefined; + +jest.mock('@console/shared/src/utils/override-perspectives', () => ({ + ...jest.requireActual('@console/shared/src/utils/override-perspectives'), + get overridePerspectives() { + return mockOverridePerspectives; + }, +})); + jest.mock('@console/shared/src/hooks/usePerspectives', () => ({ ...jest.requireActual('@console/shared/src/hooks/usePerspectives'), usePerspectives: jest.fn(), @@ -94,7 +101,7 @@ describe('PerspectiveDetector', () => { }); it('should set admin as default perspective when all perspectives are disabled', async () => { - const perspectives: PerspectiveType[] = [ + mockOverridePerspectives = [ { id: 'dev', visibility: { @@ -122,7 +129,6 @@ describe('PerspectiveDetector', () => { }, }, ]; - window.SERVER_FLAGS.perspectives = JSON.stringify(perspectives); let promiseResolver: (value: () => [boolean, boolean]) => void; const testPromise = new Promise<() => [boolean, boolean]>( diff --git a/frontend/packages/console-app/src/components/nav/NavHeader.tsx b/frontend/packages/console-app/src/components/nav/NavHeader.tsx index 266fe771a2b..d872b0912b4 100644 --- a/frontend/packages/console-app/src/components/nav/NavHeader.tsx +++ b/frontend/packages/console-app/src/components/nav/NavHeader.tsx @@ -74,11 +74,23 @@ const NavHeader: FC = ({ onPerspectiveSelected }) => { /> )); - const { icon, name } = useMemo( + const { icon, name } = useMemo<{ + icon: Perspective['properties']['icon']; + name: Perspective['properties']['name']; + }>( () => perspectiveExtensions.find((p) => p?.properties?.id === activePerspective)?.properties ?? - perspectiveExtensions[0]?.properties ?? { icon: null, name: null }, - [activePerspective, perspectiveExtensions], + perspectiveExtensions[0]?.properties ?? { icon: null, name: t('Core platform') }, + [activePerspective, perspectiveExtensions, t], + ); + + const ActivePerspectiveIcon = icon ? ( + icon().then((m) => m.default)} + LoadingComponent={IconLoadingComponent} + /> + ) : ( + ); return perspectiveDropdownItems.length > 1 ? ( @@ -99,20 +111,11 @@ const NavHeader: FC = ({ onPerspectiveSelected }) => { isExpanded={isPerspectiveDropdownOpen} ref={toggleRef} onClick={() => togglePerspectiveOpen()} - icon={ - icon && ( - icon().then((m) => m.default)} - LoadingComponent={IconLoadingComponent} - /> - ) - } + icon={ActivePerspectiveIcon} > - {name && ( - - {name} - - )} + + {name} + )} popperProps={{ @@ -123,13 +126,9 @@ const NavHeader: FC = ({ onPerspectiveSelected }) => { ) : ( -
+
- <RhUiGearGroupFillIcon /> {t('Core platform')} + {ActivePerspectiveIcon} {name}
); diff --git a/frontend/packages/console-app/src/components/nav/__tests__/NavHeader.spec.tsx b/frontend/packages/console-app/src/components/nav/__tests__/NavHeader.spec.tsx index 6db823b2d4c..de9833abfe3 100644 --- a/frontend/packages/console-app/src/components/nav/__tests__/NavHeader.spec.tsx +++ b/frontend/packages/console-app/src/components/nav/__tests__/NavHeader.spec.tsx @@ -1,9 +1,20 @@ import { screen, waitForElementToBeRemoved } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; +import type { Perspective } from '@console/shared/src/utils/override-perspectives'; +import { PerspectiveVisibilityState } from '@console/shared/src/utils/override-perspectives'; import NavHeader from '../NavHeader'; import { renderWithPerspective } from './navTestUtils'; +let mockOverridePerspectives: Perspective[]; + +jest.mock('@console/shared/src/utils/override-perspectives', () => ({ + ...jest.requireActual('@console/shared/src/utils/override-perspectives'), + get overridePerspectives() { + return mockOverridePerspectives; + }, +})); + jest.mock('@console/internal/components/utils/async', () => ({ AsyncComponent: () => null, })); @@ -89,14 +100,14 @@ describe('NavHeader', () => { describe('when only one perspective is available', () => { beforeEach(() => { - window.SERVER_FLAGS.perspectives = JSON.stringify([ - { id: 'admin', visibility: { state: 'Enabled' } }, - { id: 'dev', visibility: { state: 'Disabled' } }, - ]); + mockOverridePerspectives = [ + { id: 'admin', visibility: { state: PerspectiveVisibilityState.Enabled } }, + { id: 'dev', visibility: { state: PerspectiveVisibilityState.Disabled } }, + ]; }); afterEach(() => { - delete window.SERVER_FLAGS.perspectives; + mockOverridePerspectives = undefined; }); it('should render static label instead of dropdown', () => { @@ -109,14 +120,14 @@ describe('NavHeader', () => { describe('when all perspectives are disabled', () => { beforeEach(() => { - window.SERVER_FLAGS.perspectives = JSON.stringify([ - { id: 'admin', visibility: { state: 'Disabled' } }, - { id: 'dev', visibility: { state: 'Disabled' } }, - ]); + mockOverridePerspectives = [ + { id: 'admin', visibility: { state: PerspectiveVisibilityState.Disabled } }, + { id: 'dev', visibility: { state: PerspectiveVisibilityState.Disabled } }, + ]; }); afterEach(() => { - delete window.SERVER_FLAGS.perspectives; + mockOverridePerspectives = undefined; }); it('should fall back to static label for admin perspective', () => { diff --git a/frontend/packages/console-shared/src/hooks/__tests__/usePerspectives.spec.ts b/frontend/packages/console-shared/src/hooks/__tests__/usePerspectives.spec.ts index 89976fa46ed..ca486a6f3da 100644 --- a/frontend/packages/console-shared/src/hooks/__tests__/usePerspectives.spec.ts +++ b/frontend/packages/console-shared/src/hooks/__tests__/usePerspectives.spec.ts @@ -1,19 +1,31 @@ import { renderHook, waitFor } from '@testing-library/react'; import { checkAccess } from '@console/dynamic-plugin-sdk/src/app/components/utils/rbac'; import { useExtensions } from '@console/plugin-sdk/src/api/useExtensions'; -import { usePerspectives, PerspectiveVisibilityState } from '../usePerspectives'; -import type { Perspective } from '../usePerspectives'; +import type { Perspective } from '@console/shared/src/utils/override-perspectives'; +import { PerspectiveVisibilityState } from '@console/shared/src/utils/override-perspectives'; +import { usePerspectives } from '../usePerspectives'; const useExtensionsMock = useExtensions as jest.Mock; +let mockOverridePerspectives: Perspective[]; + +jest.mock('@console/shared/src/utils/override-perspectives', () => ({ + ...jest.requireActual('@console/shared/src/utils/override-perspectives'), + get overridePerspectives() { + return mockOverridePerspectives; + }, +})); + jest.mock('@console/plugin-sdk/src/api/useExtensions', () => ({ useExtensions: jest.fn() })); jest.mock('@console/dynamic-plugin-sdk/src/app/components/utils/rbac', () => ({ checkAccess: jest.fn(), })); + describe('usePerspectives', () => { beforeEach(() => { - window.SERVER_FLAGS.perspectives = undefined; + mockOverridePerspectives = undefined; + useExtensionsMock.mockClear(); useExtensionsMock.mockReturnValue([ { @@ -43,7 +55,7 @@ describe('usePerspectives', () => { }); it('should return all the available perspectives if perspectives are not set in the server flags', async () => { - window.SERVER_FLAGS.perspectives = undefined; + mockOverridePerspectives = undefined; const { result } = renderHook(() => usePerspectives()); @@ -75,7 +87,7 @@ describe('usePerspectives', () => { }); it('should return all the available perspectives if perspectives are not configured in the server flags', async () => { - window.SERVER_FLAGS.perspectives = ''; + mockOverridePerspectives = undefined; const { result } = renderHook(() => usePerspectives()); @@ -107,7 +119,7 @@ describe('usePerspectives', () => { }); it('should return only the enabled perspectives and the perspectives that satisfy the missing accessreview checks that are set in the server flags', async () => { - const perspectives: Perspective[] = [ + mockOverridePerspectives = [ { id: 'dev', visibility: { @@ -135,7 +147,7 @@ describe('usePerspectives', () => { }, }, ]; - window.SERVER_FLAGS.perspectives = JSON.stringify(perspectives); + (checkAccess as jest.Mock).mockReturnValue(Promise.resolve({ status: { allowed: true } })); const { result } = renderHook(() => usePerspectives()); @@ -154,7 +166,7 @@ describe('usePerspectives', () => { }); it('should return the admin perspective as default if all the perspectives are disabled', async () => { - const perspectives: Perspective[] = [ + mockOverridePerspectives = [ { id: 'dev', visibility: { @@ -182,7 +194,7 @@ describe('usePerspectives', () => { }, }, ]; - window.SERVER_FLAGS.perspectives = JSON.stringify(perspectives); + (checkAccess as jest.Mock).mockReturnValue(Promise.resolve({ status: { allowed: true } })); const { result } = renderHook(() => usePerspectives()); @@ -201,7 +213,7 @@ describe('usePerspectives', () => { }); it('should return only the enabled perspectives and the perspectives that satisfy the required accessreview checks that are set in the server flags', async () => { - const perspectives: Perspective[] = [ + mockOverridePerspectives = [ { id: 'dev', visibility: { @@ -229,7 +241,7 @@ describe('usePerspectives', () => { }, }, ]; - window.SERVER_FLAGS.perspectives = JSON.stringify(perspectives); + (checkAccess as jest.Mock).mockReturnValue(Promise.resolve({ status: { allowed: true } })); const { result } = renderHook(() => usePerspectives()); @@ -256,7 +268,7 @@ describe('usePerspectives', () => { }); it('should handle perspectives with accessReview checks', async () => { - const perspectives: Perspective[] = [ + mockOverridePerspectives = [ { id: 'dev', visibility: { @@ -285,7 +297,7 @@ describe('usePerspectives', () => { }, }, ]; - window.SERVER_FLAGS.perspectives = JSON.stringify(perspectives); + (checkAccess as jest.Mock).mockReturnValue(Promise.resolve({ status: { allowed: true } })); const { result } = renderHook(() => usePerspectives()); @@ -304,7 +316,7 @@ describe('usePerspectives', () => { }); it('should return only the enabled perspectives and the perspectives that satisfy the required accessreview checks that are set in the server flags for user with limited access', async () => { - const perspectives: Perspective[] = [ + mockOverridePerspectives = [ { id: 'dev', visibility: { @@ -332,7 +344,7 @@ describe('usePerspectives', () => { }, }, ]; - window.SERVER_FLAGS.perspectives = JSON.stringify(perspectives); + (checkAccess as jest.Mock).mockReturnValue(Promise.resolve({ status: { allowed: false } })); const { result } = renderHook(() => usePerspectives()); @@ -351,7 +363,7 @@ describe('usePerspectives', () => { }); it('should not return perspective when required accessreview check throws an error', async () => { - const perspectives: Perspective[] = [ + mockOverridePerspectives = [ { id: 'dev', visibility: { @@ -379,7 +391,7 @@ describe('usePerspectives', () => { }, }, ]; - window.SERVER_FLAGS.perspectives = JSON.stringify(perspectives); + (checkAccess as jest.Mock).mockReturnValue(Promise.reject(new Error('Unexpected error'))); const { result } = renderHook(() => usePerspectives()); @@ -398,7 +410,7 @@ describe('usePerspectives', () => { }); it('should also return perspectives that are not configured', async () => { - const perspectives: Perspective[] = [ + mockOverridePerspectives = [ { id: 'dev', visibility: { @@ -414,7 +426,7 @@ describe('usePerspectives', () => { }, }, ]; - window.SERVER_FLAGS.perspectives = JSON.stringify(perspectives); + (checkAccess as jest.Mock).mockReturnValue(Promise.resolve({ status: { allowed: true } })); const { result } = renderHook(() => usePerspectives()); diff --git a/frontend/packages/console-shared/src/hooks/__tests__/usePinnedResources.spec.ts b/frontend/packages/console-shared/src/hooks/__tests__/usePinnedResources.spec.ts index 1072f5b64b7..af88bcf8b46 100644 --- a/frontend/packages/console-shared/src/hooks/__tests__/usePinnedResources.spec.ts +++ b/frontend/packages/console-shared/src/hooks/__tests__/usePinnedResources.spec.ts @@ -4,6 +4,8 @@ import { DeploymentModel } from '@console/dynamic-plugin-sdk/src/utils/k8s/__moc import { ConfigMapModel } from '@console/internal/models'; import { useModelFinder } from '@console/internal/module/k8s/k8s-models'; import { usePerspectives } from '@console/shared/src/hooks/usePerspectives'; +import type { Perspective } from '../../utils/override-perspectives'; +import { PerspectiveVisibilityState } from '../../utils/override-perspectives'; import { usePinnedResources } from '../usePinnedResources'; import { useUserPreference } from '../useUserPreference'; @@ -13,6 +15,15 @@ const useUserPreferenceMock = useUserPreference as jest.Mock; const useModelFinderMock = useModelFinder as jest.Mock; const setPinnedResourcesMock = jest.fn(); +let mockOverridePerspectives: Perspective[]; + +jest.mock('@console/shared/src/utils/override-perspectives', () => ({ + ...jest.requireActual('@console/shared/src/utils/override-perspectives'), + get overridePerspectives() { + return mockOverridePerspectives; + }, +})); + jest.mock('@console/shared/src/hooks/usePerspectives', () => ({ usePerspectives: jest.fn() })); jest.mock('@console/dynamic-plugin-sdk/src/perspective/useActivePerspective', () => ({ default: jest.fn(), @@ -63,7 +74,7 @@ describe('usePinnedResources', () => { }); it('should return default pins from extension if perspectives are not configured', async () => { - window.SERVER_FLAGS.perspectives = ''; + mockOverridePerspectives = undefined; useActivePerspectiveMock.mockReturnValue(['dev']); useUserPreferenceMock.mockImplementation((configKey, defaultPins) => [ defaultPins, @@ -85,8 +96,14 @@ describe('usePinnedResources', () => { }); it('should return an empty array if user settings are not loaded yet', async () => { - window.SERVER_FLAGS.perspectives = - '[{ "id" : "dev", "visibility": {"state" : "Enabled" }, "pinnedResources" : [{"version" : "v1", "resource" : "deployments"}]}]'; + mockOverridePerspectives = [ + { + id: 'dev', + visibility: { state: PerspectiveVisibilityState.Enabled }, + pinnedResources: [{ version: 'v1', resource: 'deployments' }], + }, + ]; + // Mock user preference data useUserPreferenceMock.mockReturnValue([null, setPinnedResourcesMock, false]); @@ -100,7 +117,10 @@ describe('usePinnedResources', () => { }); it('should not return any pins if no pins are configured and no extension could be found', async () => { - window.SERVER_FLAGS.perspectives = '[{ "id" : "dev", "visibility": {"state" : "Enabled" }}]'; + mockOverridePerspectives = [ + { id: 'dev', visibility: { state: PerspectiveVisibilityState.Enabled } }, + ]; + // Mock empty old data useUserPreferenceMock.mockReturnValue([{}, setPinnedResourcesMock, true]); usePerspectivesMock.mockClear(); @@ -115,8 +135,11 @@ describe('usePinnedResources', () => { expect(setPinnedResourcesMock).toHaveBeenCalledTimes(0); }); - it('should not return any pins if no pins are configured and extension donot have default pins', async () => { - window.SERVER_FLAGS.perspectives = '[{ "id" : "dev", "visibility": {"state" : "Enabled" }}]'; + it('should not return any pins if no pins are configured and extension do not have default pins', async () => { + mockOverridePerspectives = [ + { id: 'dev', visibility: { state: PerspectiveVisibilityState.Enabled } }, + ]; + // Mock empty old data useUserPreferenceMock.mockReturnValue([{}, setPinnedResourcesMock, true]); usePerspectivesMock.mockClear(); @@ -140,8 +163,10 @@ describe('usePinnedResources', () => { }); it('should not return any pins if pins configured is an empty array and the extension has default pins', async () => { - window.SERVER_FLAGS.perspectives = - '[{ "id" : "dev", "visibility": {"state" : "Enabled" }, "pinnedResources": []}]'; + mockOverridePerspectives = [ + { id: 'dev', visibility: { state: PerspectiveVisibilityState.Enabled }, pinnedResources: [] }, + ]; + // Mock empty old data useActivePerspectiveMock.mockReturnValue(['dev']); useUserPreferenceMock.mockImplementation((configKey, defaultPins) => [ @@ -160,8 +185,14 @@ describe('usePinnedResources', () => { }); it('should return default pins if pins configured is null and the extension has default pins', async () => { - window.SERVER_FLAGS.perspectives = - '[{ "id" : "dev", "visibility": {"state" : "Enabled" }, "pinnedResources": null}]'; + mockOverridePerspectives = [ + { + id: 'dev', + visibility: { state: PerspectiveVisibilityState.Enabled }, + pinnedResources: null, + }, + ]; + // Mock empty old data useActivePerspectiveMock.mockReturnValue(['dev']); useUserPreferenceMock.mockImplementation((configKey, storageKey, defaultPins) => [ @@ -184,7 +215,10 @@ describe('usePinnedResources', () => { }); it('should return default pins from extension if there are no pinned resources configured by and the extension has default pins', async () => { - window.SERVER_FLAGS.perspectives = '[{ "id" : "dev", "visibility": {"state" : "Enabled" }}]'; + mockOverridePerspectives = [ + { id: 'dev', visibility: { state: PerspectiveVisibilityState.Enabled } }, + ]; + // Mock empty old data useActivePerspectiveMock.mockReturnValue(['dev']); useUserPreferenceMock.mockImplementation((configKey, storageKey, defaultPins) => [ @@ -207,8 +241,14 @@ describe('usePinnedResources', () => { }); it('should return customized pins if the pins are not customized by the user and the extension has default pins', async () => { - window.SERVER_FLAGS.perspectives = - '[{ "id" : "dev", "visibility": {"state" : "Enabled" }, "pinnedResources" : [{"version" : "v1", "resource" : "deployments", "group": "apps"}]}]'; + mockOverridePerspectives = [ + { + id: 'dev', + visibility: { state: PerspectiveVisibilityState.Enabled }, + pinnedResources: [{ version: 'v1', resource: 'deployments', group: 'apps' }], + }, + ]; + // Mock empty old data useActivePerspectiveMock.mockReturnValue(['dev']); useUserPreferenceMock.mockImplementation((configKey, storageKey, defaultPins) => [ @@ -227,8 +267,14 @@ describe('usePinnedResources', () => { }); it('should return an array of pins saved in user settings for the current perspective', async () => { - window.SERVER_FLAGS.perspectives = - '[{ "id" : "dev", "visibility": {"state" : "Enabled" }, "pinnedResources" : [{"version" : "v1", "resource" : "deployments"}]}]'; + mockOverridePerspectives = [ + { + id: 'dev', + visibility: { state: PerspectiveVisibilityState.Enabled }, + pinnedResources: [{ version: 'v1', resource: 'deployments' }], + }, + ]; + // Mock user settings data useActivePerspectiveMock.mockReturnValue(['dev']); useUserPreferenceMock.mockReturnValue([ @@ -250,9 +296,18 @@ describe('usePinnedResources', () => { expect(setPinnedResourcesMock).toHaveBeenCalledTimes(0); }); - it('should return configured pins and filter out pins with resources that donot exist', async () => { - window.SERVER_FLAGS.perspectives = - '[{ "id" : "dev", "visibility": {"state" : "Enabled" }, "pinnedResources" : [{"version" : "v1", "resource" : "deploymentss", "group" : "apps" },{"version" : "v1", "resource" : "configmaps", "group" : "" } ]}]'; + it('should return configured pins and filter out pins with resources that do not exist', async () => { + mockOverridePerspectives = [ + { + id: 'dev', + visibility: { state: PerspectiveVisibilityState.Enabled }, + pinnedResources: [ + { version: 'v1', resource: 'deploymentss', group: 'apps' }, + { version: 'v1', resource: 'configmaps', group: '' }, + ], + }, + ]; + // Mock user settings data useActivePerspectiveMock.mockReturnValue(['dev']); useUserPreferenceMock.mockReturnValue([{}, setPinnedResourcesMock, true]); diff --git a/frontend/packages/console-shared/src/hooks/usePerspectives.ts b/frontend/packages/console-shared/src/hooks/usePerspectives.ts index 3f92a3cec5d..a4c78714dac 100644 --- a/frontend/packages/console-shared/src/hooks/usePerspectives.ts +++ b/frontend/packages/console-shared/src/hooks/usePerspectives.ts @@ -2,42 +2,16 @@ import { useState, useCallback, useEffect, useMemo } from 'react'; import type { Perspective as PerspectiveExtension, PerspectiveType, - AccessReviewResourceAttributes, } from '@console/dynamic-plugin-sdk'; import { isPerspective, checkAccess } from '@console/dynamic-plugin-sdk'; import type { LoadedExtension } from '@console/dynamic-plugin-sdk/src/types'; import { useExtensions } from '@console/plugin-sdk/src/api/useExtensions'; import { USER_PREFERENCE_PREFIX } from '../constants/common'; +import type { PerspectiveAccessReview } from '../utils/override-perspectives'; +import { PerspectiveVisibilityState, overridePerspectives } from '../utils/override-perspectives'; const PERSPECTIVE_VISITED_FEATURE_KEY = 'perspective.visited'; -export enum PerspectiveVisibilityState { - Enabled = 'Enabled', - Disabled = 'Disabled', - AccessReview = 'AccessReview', -} - -type PerspectiveAccessReview = { - required?: AccessReviewResourceAttributes[]; - missing?: AccessReviewResourceAttributes[]; -}; - -type PerspectiveVisibility = { - state: PerspectiveVisibilityState; - accessReview?: PerspectiveAccessReview; -}; - -export type PerspectivePinnedResource = { - group: string; - version: string; - resource: string; -}; -export type Perspective = { - id: string; - visibility: PerspectiveVisibility; - pinnedResources?: PerspectivePinnedResource[]; -}; - export const getPerspectiveVisitedKey = (perspective: PerspectiveType): string => `${USER_PREFERENCE_PREFIX}.${PERSPECTIVE_VISITED_FEATURE_KEY}.${perspective}`; @@ -83,7 +57,8 @@ export const usePerspectives = (): LoadedExtension[] => { const perspectiveExtensions = useExtensions(isPerspective); const [results, setResults] = useState>(() => { let obj: Record = {}; - if (!window.SERVER_FLAGS.perspectives) { + + if (!overridePerspectives) { obj = perspectiveExtensions.reduce( (acc: Record, ex: LoadedExtension) => { acc[ex.properties.id] = true; @@ -92,9 +67,10 @@ export const usePerspectives = (): LoadedExtension[] => { {}, ); } else { - const perspectives: Perspective[] = JSON.parse(window.SERVER_FLAGS.perspectives); obj = perspectiveExtensions.reduce((acc, perspectiveExtension) => { - const perspective = perspectives?.find((p) => p.id === perspectiveExtension.properties.id); + const perspective = overridePerspectives.find( + (p) => p.id === perspectiveExtension.properties.id, + ); if ( !perspective?.visibility?.state || @@ -124,11 +100,13 @@ export const usePerspectives = (): LoadedExtension[] => { }, [setResults], ); + useEffect(() => { - if (window.SERVER_FLAGS.perspectives) { - const perspectives: Perspective[] = JSON.parse(window.SERVER_FLAGS.perspectives); + if (overridePerspectives) { perspectiveExtensions.forEach((perspectiveExtension) => { - const perspective = perspectives?.find((p) => p.id === perspectiveExtension.properties.id); + const perspective = overridePerspectives.find( + (p) => p.id === perspectiveExtension.properties.id, + ); if ( !perspective || @@ -156,8 +134,9 @@ export const usePerspectives = (): LoadedExtension[] => { }); } }, [perspectiveExtensions, handleResults]); + const perspectives = useMemo(() => { - if (!window.SERVER_FLAGS.perspectives) { + if (!overridePerspectives) { return perspectiveExtensions; } @@ -168,6 +147,7 @@ export const usePerspectives = (): LoadedExtension[] => { ? perspectiveExtensions.filter((p) => p.properties.id === 'admin') : filteredExtensions; }, [perspectiveExtensions, results]); + return perspectives; }; diff --git a/frontend/packages/console-shared/src/hooks/usePinnedResources.ts b/frontend/packages/console-shared/src/hooks/usePinnedResources.ts index d6d62c8143b..4fc12a98594 100644 --- a/frontend/packages/console-shared/src/hooks/usePinnedResources.ts +++ b/frontend/packages/console-shared/src/hooks/usePinnedResources.ts @@ -3,7 +3,8 @@ import * as _ from 'lodash'; import type { ExtensionK8sModel, K8sModel } from '@console/dynamic-plugin-sdk'; import { useActivePerspective } from '@console/dynamic-plugin-sdk'; import { referenceForExtensionModel, useModelFinder } from '@console/internal/module/k8s'; -import type { Perspective } from './usePerspectives'; +import type { Perspective } from '../utils/override-perspectives'; +import { overridePerspectives } from '../utils/override-perspectives'; import { usePerspectives } from './usePerspectives'; import { useTelemetry } from './useTelemetry'; import { useUserPreference } from './useUserPreference'; @@ -23,9 +24,8 @@ export const usePinnedResources = (): [string[], (pinnedResources: string[]) => const getPins = useCallback( (id: string, defaultPins: ExtensionK8sModel[]): ExtensionK8sModel[] => { let customizedPins: ExtensionK8sModel[] = null; - if (window.SERVER_FLAGS.perspectives) { - const perspectives: Perspective[] = JSON.parse(window.SERVER_FLAGS.perspectives); - const perspective = perspectives.find((p: Perspective) => p.id === id); + if (overridePerspectives) { + const perspective = overridePerspectives.find((p: Perspective) => p.id === id); customizedPins = perspective?.pinnedResources?.map((pr) => { const model: K8sModel = findModel(pr.group, pr.resource); return ( diff --git a/frontend/packages/console-shared/src/utils/override-perspectives.ts b/frontend/packages/console-shared/src/utils/override-perspectives.ts new file mode 100644 index 00000000000..a7735274c2c --- /dev/null +++ b/frontend/packages/console-shared/src/utils/override-perspectives.ts @@ -0,0 +1,56 @@ +import type { AccessReviewResourceAttributes } from '@console/dynamic-plugin-sdk'; + +export enum PerspectiveVisibilityState { + Enabled = 'Enabled', + Disabled = 'Disabled', + AccessReview = 'AccessReview', +} + +export type PerspectiveAccessReview = { + required?: AccessReviewResourceAttributes[]; + missing?: AccessReviewResourceAttributes[]; +}; + +export type PerspectiveVisibility = { + state: PerspectiveVisibilityState; + accessReview?: PerspectiveAccessReview; +}; + +export type PerspectivePinnedResource = { + group?: string; + version: string; + resource: string; +}; + +export type Perspective = { + id: string; + visibility: PerspectiveVisibility; + pinnedResources?: PerspectivePinnedResource[]; +}; + +/** + * If {@link window.SERVER_FLAGS.perspectives} is defined, return the parsed array. + * + * Otherwise, return `undefined` (no perspective overrides specified). + */ +const getOverridePerspectives = (): Perspective[] => { + if (window.SERVER_FLAGS.perspectives) { + try { + const value = JSON.parse(window.SERVER_FLAGS.perspectives); + + if (!Array.isArray(value)) { + throw new Error('Parsed value must be an array', value); + } + + return value as Perspective[]; + } catch (e) { + // eslint-disable-next-line no-console + console.warn('Failed to parse perspectives override', e); + } + } + + return undefined; +}; + +// Evaluate once at runtime +export const overridePerspectives = getOverridePerspectives(); diff --git a/frontend/packages/dev-console/src/components/catalog/PinnedResourcesConfiguration.tsx b/frontend/packages/dev-console/src/components/catalog/PinnedResourcesConfiguration.tsx index 72c49a9dab3..a8eca6d3601 100644 --- a/frontend/packages/dev-console/src/components/catalog/PinnedResourcesConfiguration.tsx +++ b/frontend/packages/dev-console/src/components/catalog/PinnedResourcesConfiguration.tsx @@ -28,15 +28,13 @@ import { SaveStatus } from '@console/shared/src/components/cluster-configuration import { useConsoleOperatorConfig } from '@console/shared/src/components/cluster-configuration/useConsoleOperatorConfig'; import { YellowExclamationTriangleIcon } from '@console/shared/src/components/status/icons'; import { useDebounceCallback } from '@console/shared/src/hooks/useDebounceCallback'; +import { usePerspectives } from '@console/shared/src/hooks/usePerspectives'; +import { useTelemetry } from '@console/shared/src/hooks/useTelemetry'; import type { Perspective, PerspectivePinnedResource, -} from '@console/shared/src/hooks/usePerspectives'; -import { - PerspectiveVisibilityState, - usePerspectives, -} from '@console/shared/src/hooks/usePerspectives'; -import { useTelemetry } from '@console/shared/src/hooks/useTelemetry'; +} from '@console/shared/src/utils/override-perspectives'; +import { PerspectiveVisibilityState } from '@console/shared/src/utils/override-perspectives'; import './PinnedResourcesConfiguration.scss'; // skip duplicate resources. diff --git a/pkg/server/server.go b/pkg/server/server.go index b403a5c494a..4519042fb73 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -627,10 +627,20 @@ func (s *Server) HTTPHandler() (http.Handler, error) { Perspectives: []serverconfig.Perspective{}, }, } + if len(s.Perspectives) > 0 { err := json.Unmarshal([]byte(s.Perspectives), &config.Customization.Perspectives) if err != nil { - klog.Errorf("Unable to parse perspective JSON: %v", err) + klog.Errorf("Unable to parse perspectives JSON: %v", err) + } else if len(config.Customization.Perspectives) > 0 { + klog.Infoln("Using console perspective overrides:") + grouped := make(map[string][]string) + for _, perspective := range config.Customization.Perspectives { + grouped[string(perspective.Visibility.State)] = append(grouped[string(perspective.Visibility.State)], perspective.ID) + } + for visibility, ids := range grouped { + klog.Infof(" - [%s]: %s\n", visibility, strings.Join(ids, ", ")) + } } }