From da60ad5148ec44d6361788455761aa64cc57827a Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:11:43 +0200 Subject: [PATCH 001/126] Resuable storage interface --- .../src/browser-storage/ScopedStorage.test.ts | 119 ++++++++++++++++++ .../src/browser-storage/ScopedStorage.ts | 71 +++++++++++ 2 files changed, 190 insertions(+) create mode 100644 frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts create mode 100644 frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts new file mode 100644 index 00000000000..4a2d8a1f542 --- /dev/null +++ b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts @@ -0,0 +1,119 @@ +import { ScopedStorage, ScopedStorageImpl } from './ScopedStorage'; + +describe('ScopedStorage', () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + describe('add new key', () => { + it('should create a single scoped key with the provided key-value pair as its value', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstName', 'Random Value'); + expect(scopedStorage.getItem('firstName')).toBe('Random Value'); + }); + }); + + describe('get item', () => { + it('should return "null" if key does not exist', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + expect(scopedStorage.getItem('firstName', 'Random Value')).toBeNull(); + }); + }); + + describe('parsing', () => { + const consoleErrorMock = jest.fn(); + const originalConsoleError = console.error; + beforeEach(() => { + console.error = consoleErrorMock; + }); + + afterEach(() => { + console.error = originalConsoleError; + }); + + it('should console.error when parsing the storage fails', () => { + window.localStorage.setItem('unit/test', '{"person";{"name":"tester"}}'); + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + expect(scopedStorage.getItem('person')).toBeNull(); + expect(consoleErrorMock).toHaveBeenCalledWith( + expect.stringContaining( + 'Failed to parse storage with key unit/test. Ensure that the storage is a valid JSON string. Error: SyntaxError:', + ), + ); + }); + }); + + describe('update existing key', () => { + it('should append a new key-value pair to the existing scoped key', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstKey', 'first value'); + scopedStorage.setItem('secondKey', 'secondValue'); + + expect(scopedStorage.getItem('firstKey')).toBe('first value'); + expect(scopedStorage.getItem('secondKey')).toBe('secondValue'); + }); + + it('should update the value of an existing key-value pair within the scoped key if the value has changed', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstKey', 'first value'); + scopedStorage.setItem('firstKey', 'first value is updated'); + expect(scopedStorage.getItem('firstKey')).toBe('first value is updated'); + }); + }); + + describe('delete values from key', () => { + it('should remove a specific key-value pair from the existing scoped key', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstKey', 'first value'); + expect(scopedStorage.getItem('firstKey')).toBeDefined(); + + scopedStorage.removeItem('firstKey'); + expect(scopedStorage.getItem('firstKey')).toBeUndefined(); + }); + + it('should not remove key if it does not exist', () => { + const removeItemMock = jest.fn(); + const customStorage = { + getItem: jest.fn().mockImplementation(() => null), + removeItem: removeItemMock, + }; + + const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); + scopedStorage.removeItem('keyDoesNotExist'); + + expect(removeItemMock).not.toHaveBeenCalled(); + }); + }); + + // Verify that Dependency Inversion works as expected + describe('when using localStorage', () => { + it('should store and retrieve values using localStorage', () => { + const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'local/storage'); + scopedStorage.setItem('firstNameInSession', 'Random Session Value'); + expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); + }); + }); + + describe('when using sessionStorage', () => { + it('should store and retrieve values using sessionStorage', () => { + const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'session/storage'); + scopedStorage.setItem('firstNameInSession', 'Random Session Value'); + expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); + }); + }); + + describe('when using a custom storage implementation', () => { + it('should store and retrieve values using the provided custom storage', () => { + const setItemMock = jest.fn(); + + const customStorage: ScopedStorage = { + setItem: setItemMock, + getItem: jest.fn(), + removeItem: jest.fn(), + }; + + const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); + scopedStorage.setItem('testKey', 'testValue'); + }); + }); +}); diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts new file mode 100644 index 00000000000..7acab02c9e2 --- /dev/null +++ b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts @@ -0,0 +1,71 @@ +type StorageKey = string; + +export interface ScopedStorage extends Pick {} + +export class ScopedStorageImpl implements ScopedStorage { + private readonly storageKey: StorageKey; + private readonly scopedStorage: ScopedStorage; + + constructor( + private storage: ScopedStorage, + private key: StorageKey, + ) { + this.storageKey = this.key; + this.scopedStorage = this.storage; + } + + public setItem(key: string, value: T): void { + const storageRecords: T = this.getAllRecordsInStorage(); + this.saveToStorage( + JSON.stringify({ + ...storageRecords, + [key]: value, + }), + ); + } + + public getItem(key: keyof T): T { + const records: T = this.getAllRecordsInStorage(); + + if (!records) { + return null; + } + + return records[key] as unknown as T; + } + + public removeItem(key: keyof T): void { + const storageRecords: T | null = this.getAllRecordsInStorage(); + + if (!storageRecords) { + return; + } + + const storageCopy = { ...storageRecords }; + delete storageCopy[key]; + this.saveToStorage(JSON.stringify({ ...storageCopy })); + } + + private getAllRecordsInStorage(): T | null { + return this.parseStorageData(this.scopedStorage.getItem(this.storageKey)); + } + + private saveToStorage(value: string) { + this.storage.setItem(this.storageKey, value); + } + + private parseStorageData(storage: string | null): T | null { + if (!storage) { + return null; + } + + try { + return JSON.parse(storage) satisfies T; + } catch (error) { + console.error( + `Failed to parse storage with key ${this.storageKey}. Ensure that the storage is a valid JSON string. Error: ${error}`, + ); + return null; + } + } +} From 03ad0042e1fc4e0b89a34c94b9176ac595090727 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:13:01 +0200 Subject: [PATCH 002/126] moved the reusable scoped storage to pure-funciton --- .../src/ScopedStorage}/ScopedStorage.test.ts | 0 .../studio-pure-functions/src/ScopedStorage}/ScopedStorage.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename frontend/{packages/ux-editor/src/browser-storage => libs/studio-pure-functions/src/ScopedStorage}/ScopedStorage.test.ts (100%) rename frontend/{packages/ux-editor/src/browser-storage => libs/studio-pure-functions/src/ScopedStorage}/ScopedStorage.ts (100%) diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts similarity index 100% rename from frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts rename to frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts similarity index 100% rename from frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts rename to frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts From 13a3d2ab43f267ed0d2c8d6a8d4fba46d8b4fe51 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:15:08 +0200 Subject: [PATCH 003/126] spelling --- .../src/ScopedStorage/ScopedStorage.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts index 4a2d8a1f542..54278ec6027 100644 --- a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts +++ b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts @@ -20,7 +20,7 @@ describe('ScopedStorage', () => { }); }); - describe('parsing', () => { + describe('Storage parsing', () => { const consoleErrorMock = jest.fn(); const originalConsoleError = console.error; beforeEach(() => { From e2c8b73a90bd7c2ee9616afee2e88f69c3f4027f Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:20:15 +0200 Subject: [PATCH 004/126] added missing method --- .../src/ScopedStorage/ScopedStorage.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts index 54278ec6027..0d695bd9890 100644 --- a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts +++ b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts @@ -76,6 +76,7 @@ describe('ScopedStorage', () => { const customStorage = { getItem: jest.fn().mockImplementation(() => null), removeItem: removeItemMock, + setItem: jest.fn(), }; const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); From 50fd70d7d83de64be6f30dab305e3f95fbde5188 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:21:15 +0200 Subject: [PATCH 005/126] syntax --- .../src/ScopedStorage/ScopedStorage.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts index 0d695bd9890..d5356d80854 100644 --- a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts +++ b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts @@ -16,7 +16,7 @@ describe('ScopedStorage', () => { describe('get item', () => { it('should return "null" if key does not exist', () => { const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - expect(scopedStorage.getItem('firstName', 'Random Value')).toBeNull(); + expect(scopedStorage.getItem('firstName')).toBeNull(); }); }); From 8ca15da01023a2dd7a07a354dfc8c3d36a3e46d0 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:21:55 +0200 Subject: [PATCH 006/126] added more precise return type --- .../studio-pure-functions/src/ScopedStorage/ScopedStorage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts index 7acab02c9e2..0424bbb3003 100644 --- a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts +++ b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts @@ -24,7 +24,7 @@ export class ScopedStorageImpl implements ScopedStorage { ); } - public getItem(key: keyof T): T { + public getItem(key: keyof T): T | null { const records: T = this.getAllRecordsInStorage(); if (!records) { From d64d4cf6b781b25b0a750e1335b86eb450b8b701 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:30:17 +0200 Subject: [PATCH 007/126] typecheck --- .../studio-pure-functions/src/ScopedStorage/ScopedStorage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts index 0424bbb3003..38d5dc386ae 100644 --- a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts +++ b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts @@ -24,7 +24,7 @@ export class ScopedStorageImpl implements ScopedStorage { ); } - public getItem(key: keyof T): T | null { + public getItem(key: string) { const records: T = this.getAllRecordsInStorage(); if (!records) { @@ -34,7 +34,7 @@ export class ScopedStorageImpl implements ScopedStorage { return records[key] as unknown as T; } - public removeItem(key: keyof T): void { + public removeItem(key: string): void { const storageRecords: T | null = this.getAllRecordsInStorage(); if (!storageRecords) { From 1893117f80005bbcffd348c17a60d7d7bac476ee Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:34:24 +0200 Subject: [PATCH 008/126] eslint --- .../src/ScopedStorage/ScopedStorage.test.ts | 2 +- .../studio-pure-functions/src/ScopedStorage/ScopedStorage.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts index d5356d80854..42c631d465d 100644 --- a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts +++ b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts @@ -1,4 +1,4 @@ -import { ScopedStorage, ScopedStorageImpl } from './ScopedStorage'; +import { type ScopedStorage, ScopedStorageImpl } from './ScopedStorage'; describe('ScopedStorage', () => { beforeEach(() => { diff --git a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts index 38d5dc386ae..ffbaaea8996 100644 --- a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts +++ b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts @@ -31,7 +31,7 @@ export class ScopedStorageImpl implements ScopedStorage { return null; } - return records[key] as unknown as T; + return records[key] as T; } public removeItem(key: string): void { From d1994eeea48e96b9aeec5b46a53a2a30b07bbdfa Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:36:53 +0200 Subject: [PATCH 009/126] added assertion --- .../src/ScopedStorage/ScopedStorage.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts index 42c631d465d..02046ba4a44 100644 --- a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts +++ b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts @@ -115,6 +115,7 @@ describe('ScopedStorage', () => { const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); scopedStorage.setItem('testKey', 'testValue'); + expect(setItemMock).toHaveBeenCalledWith('unit/test', '{"testKey":"testValue"}'); }); }); }); From 7c50e306ae4bef146b6d7f5e84466ae53f702333 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:42:07 +0200 Subject: [PATCH 010/126] moved test section to be more readable --- .../src/ScopedStorage/ScopedStorage.test.ts | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts index 02046ba4a44..6163672b265 100644 --- a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts +++ b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts @@ -20,29 +20,6 @@ describe('ScopedStorage', () => { }); }); - describe('Storage parsing', () => { - const consoleErrorMock = jest.fn(); - const originalConsoleError = console.error; - beforeEach(() => { - console.error = consoleErrorMock; - }); - - afterEach(() => { - console.error = originalConsoleError; - }); - - it('should console.error when parsing the storage fails', () => { - window.localStorage.setItem('unit/test', '{"person";{"name":"tester"}}'); - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - expect(scopedStorage.getItem('person')).toBeNull(); - expect(consoleErrorMock).toHaveBeenCalledWith( - expect.stringContaining( - 'Failed to parse storage with key unit/test. Ensure that the storage is a valid JSON string. Error: SyntaxError:', - ), - ); - }); - }); - describe('update existing key', () => { it('should append a new key-value pair to the existing scoped key', () => { const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); @@ -86,6 +63,29 @@ describe('ScopedStorage', () => { }); }); + describe('Storage parsing', () => { + const consoleErrorMock = jest.fn(); + const originalConsoleError = console.error; + beforeEach(() => { + console.error = consoleErrorMock; + }); + + afterEach(() => { + console.error = originalConsoleError; + }); + + it('should console.error when parsing the storage fails', () => { + window.localStorage.setItem('unit/test', '{"person";{"name":"tester"}}'); + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + expect(scopedStorage.getItem('person')).toBeNull(); + expect(consoleErrorMock).toHaveBeenCalledWith( + expect.stringContaining( + 'Failed to parse storage with key unit/test. Ensure that the storage is a valid JSON string. Error: SyntaxError:', + ), + ); + }); + }); + // Verify that Dependency Inversion works as expected describe('when using localStorage', () => { it('should store and retrieve values using localStorage', () => { From 210f60b621d8c41bb19b237a57372bdfccb20001 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:16:33 +0100 Subject: [PATCH 011/126] feat(settings): make it possible to open settings modal based on query-params --- ...seOpenSettingsModalBasedQueryParam.test.ts | 57 +++++++++++++++++++ .../useOpenSettingsModalBasedQueryParam.ts | 24 ++++++++ .../app-development/layout/PageLayout.tsx | 40 +++++++------ 3 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts create mode 100644 frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts new file mode 100644 index 00000000000..3e46752dfad --- /dev/null +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts @@ -0,0 +1,57 @@ +import { renderHookWithProviders } from '../test/mocks'; +import { + queryParamKey, + useOpenSettingsModalBasedQueryParam, +} from './useOpenSettingsModalBasedQueryParam'; +import { useSearchParams } from 'react-router-dom'; +import { useSettingsModalContext } from '../contexts/SettingsModalContext'; +import { waitFor } from '@testing-library/react'; + +jest.mock('../contexts/SettingsModalContext'); +jest.mock('react-router-dom', () => ({ + useSearchParams: jest.fn(), +})); + +describe('useOpenSettingsModalBasedQueryParam', () => { + it('should open dialog if query params has valid tab id', async () => { + const searchParams = buildSearchParams('about'); + setupSearchParamMock(searchParams); + + const openSettingsMock = jest.fn(); + setupUseSettingsModalContextMock(openSettingsMock); + + renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); + await waitFor(() => expect(openSettingsMock).toHaveBeenCalledWith('about')); + }); + + it('should not open dialog of query params has invalid tab id', async () => { + const searchParams = buildSearchParams('doestNotExistTab'); + setupSearchParamMock(searchParams); + + const openSettingsMock = jest.fn(); + setupUseSettingsModalContextMock(openSettingsMock); + + renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); + await waitFor(() => expect(openSettingsMock).not.toHaveBeenCalledWith('doestNotExistTab')); + }); +}); + +function setupSearchParamMock(searchParams: URLSearchParams): jest.Mock { + return (useSearchParams as jest.Mock).mockReturnValue([searchParams, jest.fn()]); +} + +function buildSearchParams(queryParamValue: string): URLSearchParams { + const searchParams: URLSearchParams = new URLSearchParams(); + searchParams.set(queryParamKey, queryParamValue); + return searchParams; +} + +function setupUseSettingsModalContextMock(openSettingsMock: jest.fn): jest.Mock { + return (useSettingsModalContext as jest.Mock).mockReturnValue({ + settingsRef: { + current: { + openSettings: openSettingsMock, + }, + }, + }); +} diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts new file mode 100644 index 00000000000..a7da1eaee81 --- /dev/null +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts @@ -0,0 +1,24 @@ +import { useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useSettingsModalContext } from '../contexts/SettingsModalContext'; +import type { SettingsModalTabId } from '../types/SettingsModalTabId'; +import { allSettingsModalTabs } from '../layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs'; + +export const queryParamKey: string = 'openSettingsModalWithTab'; + +export function useOpenSettingsModalBasedQueryParam(): void { + const [searchParams] = useSearchParams(); + const { settingsRef } = useSettingsModalContext(); + + useEffect((): void => { + const tabToOpen: SettingsModalTabId = searchParams.get(queryParamKey) as SettingsModalTabId; + const shouldOpenModal: boolean = isValidTab(tabToOpen); + if (shouldOpenModal) { + settingsRef.current.openSettings(tabToOpen); + } + }, [searchParams]); +} + +function isValidTab(tabId: SettingsModalTabId): boolean { + return allSettingsModalTabs.includes(tabId); +} diff --git a/frontend/app-development/layout/PageLayout.tsx b/frontend/app-development/layout/PageLayout.tsx index d4cb27107f5..b6d904117e7 100644 --- a/frontend/app-development/layout/PageLayout.tsx +++ b/frontend/app-development/layout/PageLayout.tsx @@ -10,13 +10,15 @@ import { NotFoundPage } from './NotFoundPage'; import { useTranslation } from 'react-i18next'; import { WebSocketSyncWrapper } from '../components'; import { PageHeaderContextProvider } from 'app-development/contexts/PageHeaderContext'; +import { useOpenSettingsModalBasedQueryParam } from '../hooks/useOpenSettingsModalBasedQueryParam'; +import { type AxiosError } from 'axios'; +import { type RepoStatus } from 'app-shared/types/RepoStatus'; /** * Displays the layout for the app development pages */ export const PageLayout = (): React.ReactNode => { const { t } = useTranslation(); - const { pathname } = useLocation(); const match = matchPath({ path: '/:org/:app', caseSensitive: true, end: false }, pathname); const { org, app } = match.params; @@ -41,20 +43,6 @@ export const PageLayout = (): React.ReactNode => { ); } - const renderPages = () => { - if (repoStatusError?.response?.status === ServerCodes.NotFound) { - return ; - } - if (repoStatus?.hasMergeConflict) { - return ; - } - return ( - - - - ); - }; - return ( <> @@ -63,7 +51,27 @@ export const PageLayout = (): React.ReactNode => { isRepoError={repoStatusError !== null} /> - {renderPages()} + ); }; + +type PagesToRenderProps = { + repoStatusError: AxiosError; + repoStatus: RepoStatus; +}; +const PagesToRender = ({ repoStatusError, repoStatus }: PagesToRenderProps) => { + // Listen to URL-search params and opens settings-modal if params matches. + useOpenSettingsModalBasedQueryParam(); + if (repoStatusError?.response?.status === ServerCodes.NotFound) { + return ; + } + if (repoStatus?.hasMergeConflict) { + return ; + } + return ( + + + + ); +}; From 7a5266c7323f8b1f61ba2671634b4ff25d634814 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:20:24 +0100 Subject: [PATCH 012/126] fixes --- .../hooks/useOpenSettingsModalBasedQueryParam.test.ts | 2 +- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts index 3e46752dfad..24cd57bea97 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts @@ -46,7 +46,7 @@ function buildSearchParams(queryParamValue: string): URLSearchParams { return searchParams; } -function setupUseSettingsModalContextMock(openSettingsMock: jest.fn): jest.Mock { +function setupUseSettingsModalContextMock(openSettingsMock: typeof jest.fn): jest.Mock { return (useSettingsModalContext as jest.Mock).mockReturnValue({ settingsRef: { current: { diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index 7d598dd3bb8..adddd53110d 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -14,6 +14,13 @@ const setupTabId: SettingsModalTabId = 'setup'; const policyTabId: SettingsModalTabId = 'policy'; const accessControlTabId: SettingsModalTabId = 'access_control'; +export const allSettingsModalTabs: Array = [ + aboutTabId, + setupTabId, + policyTabId, + accessControlTabId, +]; + export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); From 1424bb6f0e016e38b532b097a2cddc421ea24aae Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:24:36 +0100 Subject: [PATCH 013/126] improved test description --- .../hooks/useOpenSettingsModalBasedQueryParam.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts index 24cd57bea97..38a732ddcfd 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts @@ -13,7 +13,7 @@ jest.mock('react-router-dom', () => ({ })); describe('useOpenSettingsModalBasedQueryParam', () => { - it('should open dialog if query params has valid tab id', async () => { + it('should open "settingsModal" if query params has valid tab id', async () => { const searchParams = buildSearchParams('about'); setupSearchParamMock(searchParams); @@ -24,7 +24,7 @@ describe('useOpenSettingsModalBasedQueryParam', () => { await waitFor(() => expect(openSettingsMock).toHaveBeenCalledWith('about')); }); - it('should not open dialog of query params has invalid tab id', async () => { + it('should not open "settingsModal" if query params has an invalid tab id', async () => { const searchParams = buildSearchParams('doestNotExistTab'); setupSearchParamMock(searchParams); From b9a0731d7a6ea893e9a68439e9ab1ff7b8e698f6 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 20:45:29 +0100 Subject: [PATCH 014/126] PR-feedback + improve unit-tests --- .../hooks/useOpenSettingsModalBasedQueryParam.test.ts | 2 ++ .../hooks/useOpenSettingsModalBasedQueryParam.ts | 2 +- frontend/app-development/layout/PageLayout.tsx | 5 +++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts index 38a732ddcfd..60809622836 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts @@ -22,6 +22,7 @@ describe('useOpenSettingsModalBasedQueryParam', () => { renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); await waitFor(() => expect(openSettingsMock).toHaveBeenCalledWith('about')); + expect(openSettingsMock).toHaveBeenCalledTimes(1); }); it('should not open "settingsModal" if query params has an invalid tab id', async () => { @@ -33,6 +34,7 @@ describe('useOpenSettingsModalBasedQueryParam', () => { renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); await waitFor(() => expect(openSettingsMock).not.toHaveBeenCalledWith('doestNotExistTab')); + expect(openSettingsMock).toHaveBeenCalledTimes(0); }); }); diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts index a7da1eaee81..202a938b9f9 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts @@ -16,7 +16,7 @@ export function useOpenSettingsModalBasedQueryParam(): void { if (shouldOpenModal) { settingsRef.current.openSettings(tabToOpen); } - }, [searchParams]); + }, [searchParams, settingsRef]); } function isValidTab(tabId: SettingsModalTabId): boolean { diff --git a/frontend/app-development/layout/PageLayout.tsx b/frontend/app-development/layout/PageLayout.tsx index b6d904117e7..95ad155c170 100644 --- a/frontend/app-development/layout/PageLayout.tsx +++ b/frontend/app-development/layout/PageLayout.tsx @@ -51,7 +51,7 @@ export const PageLayout = (): React.ReactNode => { isRepoError={repoStatusError !== null} /> - + ); }; @@ -60,9 +60,10 @@ type PagesToRenderProps = { repoStatusError: AxiosError; repoStatus: RepoStatus; }; -const PagesToRender = ({ repoStatusError, repoStatus }: PagesToRenderProps) => { +const Pages = ({ repoStatusError, repoStatus }: PagesToRenderProps) => { // Listen to URL-search params and opens settings-modal if params matches. useOpenSettingsModalBasedQueryParam(); + if (repoStatusError?.response?.status === ServerCodes.NotFound) { return ; } From 7dd7f68455c4d9c99c6f10b986391b2396ca9b19 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 015/126] feat: context based login with ansattporten --- .../components/Tabs/Maskinporten/Maskinporten.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx new file mode 100644 index 00000000000..3319082978d --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -0,0 +1,5 @@ +describe('Maskinporten', () => { + it('should check and verify if the user is logged in', () => {}); + it('should display information about login, if user is not logged in', () => {}); + it('should display login with ansattporten if user is not logged in', () => {}); +}); From 07a617071744f06ebc1714ebd732163460e95418 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 016/126] feat: context based login with ansattporten --- .../SettingsModal/SettingsModal.tsx | 4 +++ .../Tabs/Maskinporten/Maskinporten.test.tsx | 5 ++++ .../Tabs/Maskinporten/Maskinporten.tsx | 25 +++++++++++++++++++ .../hooks/useSettingsModalMenuTabConfigs.tsx | 7 ++++++ .../types/SettingsModalTabId.ts | 2 +- frontend/language/src/nb.json | 4 +++ frontend/packages/shared/src/api/paths.js | 16 ++++++++++-- 7 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx create mode 100644 frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index 491acf72e32..a0fc88c8917 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -11,6 +11,7 @@ import { AccessControlTab } from './components/Tabs/AccessControlTab'; import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; +import { Maskinporten } from './components/Tabs/Maskinporten/Maskinporten'; export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); @@ -45,6 +46,9 @@ export const SettingsModal = forwardRef(({}, ref): Reac case 'access_control': { return ; } + case 'maskinporten': { + return ; + } } }; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx new file mode 100644 index 00000000000..3319082978d --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -0,0 +1,5 @@ +describe('Maskinporten', () => { + it('should check and verify if the user is logged in', () => {}); + it('should display information about login, if user is not logged in', () => {}); + it('should display login with ansattporten if user is not logged in', () => {}); +}); diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx new file mode 100644 index 00000000000..c646b4355aa --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { TabContent } from '../../TabContent'; +import { ansattportenLoginPath } from 'app-shared/api/paths'; +import { StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; +import { useTranslation } from 'react-i18next'; + +export const Maskinporten = (): React.ReactElement => { + const { t } = useTranslation(); + + const handleLoginWithAnsattporten = (): void => { + window.location.href = ansattportenLoginPath(); + }; + + return ( + + + {t('settings_modal.maskinporten_tab_title')} + + {t('settings_modal.maskinporten_tab_description')} + + {t('settings_modal.maskinporten_tab_login_with_ansattporten')} + + + ); +}; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index adddd53110d..263242e5485 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -5,6 +5,7 @@ import { SidebarBothIcon, ShieldLockIcon, TimerStartIcon, + CogIcon, } from '@studio/icons'; import { useTranslation } from 'react-i18next'; import type { StudioContentMenuButtonTabProps } from '@studio/components'; @@ -13,6 +14,7 @@ const aboutTabId: SettingsModalTabId = 'about'; const setupTabId: SettingsModalTabId = 'setup'; const policyTabId: SettingsModalTabId = 'policy'; const accessControlTabId: SettingsModalTabId = 'access_control'; +const maskinportenTabId: SettingsModalTabId = 'maskinporten'; export const allSettingsModalTabs: Array = [ aboutTabId, @@ -46,5 +48,10 @@ export const useSettingsModalMenuTabConfigs = tabName: t(`settings_modal.left_nav_tab_${accessControlTabId}`), icon: , }, + { + tabId: maskinportenTabId, + tabName: t(`settings_modal.left_nav_tab_${maskinportenTabId}`), + icon: , + }, ]; }; diff --git a/frontend/app-development/types/SettingsModalTabId.ts b/frontend/app-development/types/SettingsModalTabId.ts index c596b1e0d5b..3f65b015eae 100644 --- a/frontend/app-development/types/SettingsModalTabId.ts +++ b/frontend/app-development/types/SettingsModalTabId.ts @@ -1 +1 @@ -export type SettingsModalTabId = 'about' | 'setup' | 'policy' | 'access_control'; +export type SettingsModalTabId = 'about' | 'setup' | 'policy' | 'access_control' | 'maskinporten'; diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 17b292574af..333270f9134 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -977,9 +977,13 @@ "settings_modal.heading": "Innstillinger", "settings_modal.left_nav_tab_about": "Om appen", "settings_modal.left_nav_tab_access_control": "Oppstartskontroll", + "settings_modal.left_nav_tab_maskinporten": "Maskinporten", "settings_modal.left_nav_tab_policy": "Tilganger", "settings_modal.left_nav_tab_setup": "Oppsett", "settings_modal.loading_content": "Laster inn data", + "settings_modal.maskinporten_tab_description": "For å hente tilgjengelige scopes må du verifisere deg med Ansattporten", + "settings_modal.maskinporten_tab_login_with_ansattporten": "Logg inn med Ansattporten", + "settings_modal.maskinporten_tab_title": "Håndtering av Scopes fra Maskinporten", "settings_modal.policy_tab_heading": "Tilganger", "settings_modal.setup_tab_heading": "Oppsett", "settings_modal.setup_tab_switch_autoDeleteOnProcessEnd": "Automatisk sletting etter innsending", diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index fb6e9c76aa9..0280dd98e5c 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete @@ -16,7 +19,11 @@ export const serviceConfigPath = (org, app) => `${basePath}/${org}/${app}/config // DataModel export const createDataModelPath = (org, app) => `${basePath}/${org}/${app}/datamodels/new`; // Post -export const dataModelPath = (org, app, modelPath, saveOnly = false) => `${basePath}/${org}/${app}/datamodels/datamodel?${s({ modelPath, saveOnly })}`; // Get, Put, Delete +export const dataModelPath = (org, app, modelPath, saveOnly = false) => + `${basePath}/${org}/${app}/datamodels/datamodel?${s({ + modelPath, + saveOnly, + })}`; // Get, Put, Delete export const dataModelsPath = (org, app) => `${basePath}/${org}/${app}/datamodels/all-json`; // Get export const dataModelsXsdPath = (org, app) => `${basePath}/${org}/${app}/datamodels/all-xsd`; // Get export const dataModelsUploadPath = (org, app) => `${basePath}/${org}/${app}/datamodels/upload`; // Post @@ -88,7 +95,12 @@ export const envConfigPath = () => `${basePath}/environments`; export const abortmergePath = (org, app) => `${basePath}/repos/repo/${org}/${app}/abort-merge`; export const branchStatusPath = (org, app, branch) => `${basePath}/repos/repo/${org}/${app}/branches/branch?${s({ branch })}`; // Get export const cloneAppPath = (org, app) => `${basePath}/repos/repo/${org}/${app}/clone`; // Get -export const copyAppPath = (org, sourceRepository, targetRepository, targetOrg) => `${basePath}/repos/repo/${org}/copy-app?${s({ sourceRepository, targetRepository, targetOrg })}`; +export const copyAppPath = (org, sourceRepository, targetRepository, targetOrg) => + `${basePath}/repos/repo/${org}/copy-app?${s({ + sourceRepository, + targetRepository, + targetOrg, + })}`; export const createRepoPath = () => `${basePath}/repos/create-app`; // Post export const discardChangesPath = (org, app) => `${basePath}/repos/repo/${org}/${app}/discard`; // Get export const discardFileChangesPath = (org, app, filename) => `${basePath}/repos/repo/${org}/${app}/discard/${filename}`; // Get From facf0ccac6c8fbac9a4a5949d7d6bf164c04414d Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 017/126] frontend with mocks for maskinporten --- .../useIsLoggedInWithAnsattportenQuery.ts | 11 +++ .../Tabs/Maskinporten/Maskinporten.test.tsx | 98 ++++++++++++++++++- .../Tabs/Maskinporten/Maskinporten.tsx | 19 +++- .../hooks/useSettingsModalMenuTabConfigs.tsx | 1 + frontend/packages/shared/src/api/paths.js | 3 - frontend/packages/shared/src/api/queries.ts | 8 ++ .../packages/shared/src/mocks/queriesMock.ts | 3 + .../packages/shared/src/types/QueryKey.ts | 1 + 8 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts diff --git a/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts b/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts new file mode 100644 index 00000000000..b40dfd28ad7 --- /dev/null +++ b/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { QueryKey } from 'app-shared/types/QueryKey'; +import { useServicesContext } from 'app-shared/contexts/ServicesContext'; + +export const useIsLoggedInWithAnsattportenQuery = () => { + const { getIsLoggedInWithAnsattporten } = useServicesContext(); + return useQuery({ + queryKey: [QueryKey.IsLoggedInWithAnsattporten], + queryFn: () => getIsLoggedInWithAnsattporten(), + }); +}; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx index 3319082978d..747390d879c 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -1,5 +1,97 @@ +import React from 'react'; +import { screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import { Maskinporten } from './Maskinporten'; +import { renderWithProviders } from '../../../../../../../../test/mocks'; +import { textMock } from '@studio/testing/mocks/i18nMock'; +import { queriesMock } from 'app-shared/mocks/queriesMock'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; +import userEvent from '@testing-library/user-event'; + describe('Maskinporten', () => { - it('should check and verify if the user is logged in', () => {}); - it('should display information about login, if user is not logged in', () => {}); - it('should display login with ansattporten if user is not logged in', () => {}); + const consoleLogMock = jest.fn(); + const originalConsoleLog = console.log; + beforeEach(() => { + console.log = consoleLogMock; + }); + + afterEach(() => { + console.log = originalConsoleLog; + }); + + it('should check and verify if the user is logged in', async () => { + const getIsLoggedInWithAnsattportenMock = jest + .fn() + .mockImplementation(() => Promise.resolve(false)); + + renderMaskinporten({ + queries: { + getIsLoggedInWithAnsattporten: getIsLoggedInWithAnsattportenMock, + }, + }); + await waitForLoggedInStatusCheckIsDone(); + await waitFor(() => expect(getIsLoggedInWithAnsattportenMock).toHaveBeenCalledTimes(1)); + }); + + it('should display information about login and login button, if user is not logged in', async () => { + renderMaskinporten({}); + await waitForLoggedInStatusCheckIsDone(); + + const title = screen.getByRole('heading', { + level: 2, + name: textMock('settings_modal.maskinporten_tab_title'), + }); + expect(title).toBeInTheDocument(); + + const description = screen.getByText(textMock('settings_modal.maskinporten_tab_description')); + expect(description).toBeInTheDocument(); + + const loginButton = screen.getByRole('button', { + name: textMock('settings_modal.maskinporten_tab_login_with_ansattporten'), + }); + expect(loginButton).toBeInTheDocument(); + }); + + it('should display content if logged in', async () => { + const getIsLoggedInWithAnsattportenMock = jest + .fn() + .mockImplementation(() => Promise.resolve(true)); + renderMaskinporten({ + queries: { + getIsLoggedInWithAnsattporten: getIsLoggedInWithAnsattportenMock, + }, + }); + + await waitForLoggedInStatusCheckIsDone(); + + const temporaryLoggedInContent = screen.getByText('View when logged in comes here'); + expect(temporaryLoggedInContent).toBeInTheDocument(); + }); + + it('should invoke "handleLoginWithAnsattPorten" when login button is clicked', async () => { + const user = userEvent.setup(); + renderMaskinporten({}); + await waitForLoggedInStatusCheckIsDone(); + + const loginButton = screen.getByRole('button', { + name: textMock('settings_modal.maskinporten_tab_login_with_ansattporten'), + }); + + await user.click(loginButton); + + expect(consoleLogMock).toHaveBeenCalledWith( + 'Will be implemented in next iteration when backend is ready', + ); + }); }); + +type RenderMaskinporten = { + queries?: Partial; +}; +const renderMaskinporten = ({ queries = queriesMock }: RenderMaskinporten) => { + const queryClient = createQueryClientMock(); + renderWithProviders({ ...queriesMock, ...queries }, queryClient)(); +}; + +async function waitForLoggedInStatusCheckIsDone() { + await waitForElementToBeRemoved(() => screen.getByTitle(textMock('general.loading'))); +} diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx index c646b4355aa..cb3b33d5e1e 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx @@ -1,16 +1,27 @@ import React from 'react'; -import { TabContent } from '../../TabContent'; -import { ansattportenLoginPath } from 'app-shared/api/paths'; -import { StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; import { useTranslation } from 'react-i18next'; +import { TabContent } from '../../TabContent'; +import { StudioButton, StudioHeading, StudioParagraph, StudioSpinner } from '@studio/components'; +import { useIsLoggedInWithAnsattportenQuery } from '../../../../../../../../hooks/queries/useIsLoggedInWithAnsattportenQuery'; export const Maskinporten = (): React.ReactElement => { + const { data: isLoggedInWithAnsattporten, isPending: isPendingAuthStatus } = + useIsLoggedInWithAnsattportenQuery(); + const { t } = useTranslation(); const handleLoginWithAnsattporten = (): void => { - window.location.href = ansattportenLoginPath(); + console.log('Will be implemented in next iteration when backend is ready'); }; + if (isPendingAuthStatus) { + return ; + } + + if (isLoggedInWithAnsattporten) { + return
View when logged in comes here
; + } + return ( diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index 263242e5485..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -21,6 +21,7 @@ export const allSettingsModalTabs: Array = [ setupTabId, policyTabId, accessControlTabId, + maskinportenTabId, ]; export const useSettingsModalMenuTabConfigs = diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 0280dd98e5c..3bc11002f8f 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete diff --git a/frontend/packages/shared/src/api/queries.ts b/frontend/packages/shared/src/api/queries.ts index 0e5f047156b..451cd333f3d 100644 --- a/frontend/packages/shared/src/api/queries.ts +++ b/frontend/packages/shared/src/api/queries.ts @@ -54,6 +54,7 @@ import { getImageFileNamesPath, validateImageFromExternalUrlPath, } from './paths'; + import type { AppReleasesResponse, DataModelMetadataResponse, SearchRepoFilterParams, SearchRepositoryResponse } from 'app-shared/types/api'; import type { DeploymentsResponse } from 'app-shared/types/api/DeploymentsResponse'; import type { BranchStatus } from 'app-shared/types/BranchStatus'; @@ -83,6 +84,13 @@ import type { Policy } from 'app-shared/types/Policy'; import type { RepoDiffResponse } from 'app-shared/types/api/RepoDiffResponse'; import type { ExternalImageUrlValidationResponse } from 'app-shared/types/api/ExternalImageUrlValidationResponse'; +export const getIsLoggedInWithAnsattporten = async (): Promise => + // TODO: replace with endpoint when it's ready in the backend. + new Promise((resolve) => { + setTimeout(() => { + return resolve(false); + }, 1000); + }); export const getAppMetadataModelIds = (org: string, app: string, onlyUnReferenced: boolean) => get(appMetadataModelIdsPath(org, app, onlyUnReferenced)); export const getAppReleases = (owner: string, app: string) => get(releasesPath(owner, app, 'Descending')); export const getAppVersion = (org: string, app: string) => get(appVersionPath(org, app)); diff --git a/frontend/packages/shared/src/mocks/queriesMock.ts b/frontend/packages/shared/src/mocks/queriesMock.ts index 03f83e9c251..d878b75c9a9 100644 --- a/frontend/packages/shared/src/mocks/queriesMock.ts +++ b/frontend/packages/shared/src/mocks/queriesMock.ts @@ -166,6 +166,9 @@ export const queriesMock: ServicesContextProps = { // Queries - PrgetBpmnFile getBpmnFile: jest.fn().mockImplementation(() => Promise.resolve('')), getProcessTaskType: jest.fn().mockImplementation(() => Promise.resolve('')), + getIsLoggedInWithAnsattporten: jest + .fn() + .mockImplementation(() => Promise.resolve(false)), // Mutations addAppAttachmentMetadata: jest.fn().mockImplementation(() => Promise.resolve()), diff --git a/frontend/packages/shared/src/types/QueryKey.ts b/frontend/packages/shared/src/types/QueryKey.ts index f3542c6f927..5f164fe6843 100644 --- a/frontend/packages/shared/src/types/QueryKey.ts +++ b/frontend/packages/shared/src/types/QueryKey.ts @@ -44,6 +44,7 @@ export enum QueryKey { TextResources = 'TextResources', Widgets = 'Widgets', AppConfig = 'AppConfig', + IsLoggedInWithAnsattporten = 'IsLoggedInWithAnsattporten', // Resourceadm ResourceList = 'ResourceList', From c0738ee78a7c2820ee6d4b62556945123f7c8387 Mon Sep 17 00:00:00 2001 From: David Ovrelid <46874830+framitdavid@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:28:57 +0100 Subject: [PATCH 018/126] Update frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx Co-authored-by: William Thorenfeldt <48119543+wrt95@users.noreply.github.com> --- .../components/Tabs/Maskinporten/Maskinporten.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx index 747390d879c..25ed04969e6 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -93,5 +93,5 @@ const renderMaskinporten = ({ queries = queriesMock }: RenderMaskinporten) => { }; async function waitForLoggedInStatusCheckIsDone() { - await waitForElementToBeRemoved(() => screen.getByTitle(textMock('general.loading'))); + await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); } From ab1589778978b1d49c5c70a42083b367c9655e32 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 08:30:32 +0100 Subject: [PATCH 019/126] PR feedback --- .../SettingsModalButton/SettingsModal/SettingsModal.tsx | 2 +- .../SettingsModal/components/Tabs/Maskinporten/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index a0fc88c8917..77e3eaec68a 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -11,7 +11,7 @@ import { AccessControlTab } from './components/Tabs/AccessControlTab'; import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; -import { Maskinporten } from './components/Tabs/Maskinporten/Maskinporten'; +import { Maskinporten } from './components/Tabs/Maskinporten'; export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts new file mode 100644 index 00000000000..371b8f1a173 --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts @@ -0,0 +1 @@ +export { Maskinporten } from './Maskinporten'; From 34520cd0df2014d1da3d89133d86d7d19890d67d Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:16:33 +0100 Subject: [PATCH 020/126] feat(settings): make it possible to open settings modal based on query-params --- ...seOpenSettingsModalBasedQueryParam.test.ts | 57 +++++++++++++++++++ .../useOpenSettingsModalBasedQueryParam.ts | 24 ++++++++ .../app-development/layout/PageLayout.tsx | 40 +++++++------ 3 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts create mode 100644 frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts new file mode 100644 index 00000000000..3e46752dfad --- /dev/null +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts @@ -0,0 +1,57 @@ +import { renderHookWithProviders } from '../test/mocks'; +import { + queryParamKey, + useOpenSettingsModalBasedQueryParam, +} from './useOpenSettingsModalBasedQueryParam'; +import { useSearchParams } from 'react-router-dom'; +import { useSettingsModalContext } from '../contexts/SettingsModalContext'; +import { waitFor } from '@testing-library/react'; + +jest.mock('../contexts/SettingsModalContext'); +jest.mock('react-router-dom', () => ({ + useSearchParams: jest.fn(), +})); + +describe('useOpenSettingsModalBasedQueryParam', () => { + it('should open dialog if query params has valid tab id', async () => { + const searchParams = buildSearchParams('about'); + setupSearchParamMock(searchParams); + + const openSettingsMock = jest.fn(); + setupUseSettingsModalContextMock(openSettingsMock); + + renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); + await waitFor(() => expect(openSettingsMock).toHaveBeenCalledWith('about')); + }); + + it('should not open dialog of query params has invalid tab id', async () => { + const searchParams = buildSearchParams('doestNotExistTab'); + setupSearchParamMock(searchParams); + + const openSettingsMock = jest.fn(); + setupUseSettingsModalContextMock(openSettingsMock); + + renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); + await waitFor(() => expect(openSettingsMock).not.toHaveBeenCalledWith('doestNotExistTab')); + }); +}); + +function setupSearchParamMock(searchParams: URLSearchParams): jest.Mock { + return (useSearchParams as jest.Mock).mockReturnValue([searchParams, jest.fn()]); +} + +function buildSearchParams(queryParamValue: string): URLSearchParams { + const searchParams: URLSearchParams = new URLSearchParams(); + searchParams.set(queryParamKey, queryParamValue); + return searchParams; +} + +function setupUseSettingsModalContextMock(openSettingsMock: jest.fn): jest.Mock { + return (useSettingsModalContext as jest.Mock).mockReturnValue({ + settingsRef: { + current: { + openSettings: openSettingsMock, + }, + }, + }); +} diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts new file mode 100644 index 00000000000..a7da1eaee81 --- /dev/null +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts @@ -0,0 +1,24 @@ +import { useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useSettingsModalContext } from '../contexts/SettingsModalContext'; +import type { SettingsModalTabId } from '../types/SettingsModalTabId'; +import { allSettingsModalTabs } from '../layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs'; + +export const queryParamKey: string = 'openSettingsModalWithTab'; + +export function useOpenSettingsModalBasedQueryParam(): void { + const [searchParams] = useSearchParams(); + const { settingsRef } = useSettingsModalContext(); + + useEffect((): void => { + const tabToOpen: SettingsModalTabId = searchParams.get(queryParamKey) as SettingsModalTabId; + const shouldOpenModal: boolean = isValidTab(tabToOpen); + if (shouldOpenModal) { + settingsRef.current.openSettings(tabToOpen); + } + }, [searchParams]); +} + +function isValidTab(tabId: SettingsModalTabId): boolean { + return allSettingsModalTabs.includes(tabId); +} diff --git a/frontend/app-development/layout/PageLayout.tsx b/frontend/app-development/layout/PageLayout.tsx index d4cb27107f5..b6d904117e7 100644 --- a/frontend/app-development/layout/PageLayout.tsx +++ b/frontend/app-development/layout/PageLayout.tsx @@ -10,13 +10,15 @@ import { NotFoundPage } from './NotFoundPage'; import { useTranslation } from 'react-i18next'; import { WebSocketSyncWrapper } from '../components'; import { PageHeaderContextProvider } from 'app-development/contexts/PageHeaderContext'; +import { useOpenSettingsModalBasedQueryParam } from '../hooks/useOpenSettingsModalBasedQueryParam'; +import { type AxiosError } from 'axios'; +import { type RepoStatus } from 'app-shared/types/RepoStatus'; /** * Displays the layout for the app development pages */ export const PageLayout = (): React.ReactNode => { const { t } = useTranslation(); - const { pathname } = useLocation(); const match = matchPath({ path: '/:org/:app', caseSensitive: true, end: false }, pathname); const { org, app } = match.params; @@ -41,20 +43,6 @@ export const PageLayout = (): React.ReactNode => { ); } - const renderPages = () => { - if (repoStatusError?.response?.status === ServerCodes.NotFound) { - return ; - } - if (repoStatus?.hasMergeConflict) { - return ; - } - return ( - - - - ); - }; - return ( <> @@ -63,7 +51,27 @@ export const PageLayout = (): React.ReactNode => { isRepoError={repoStatusError !== null} /> - {renderPages()} + ); }; + +type PagesToRenderProps = { + repoStatusError: AxiosError; + repoStatus: RepoStatus; +}; +const PagesToRender = ({ repoStatusError, repoStatus }: PagesToRenderProps) => { + // Listen to URL-search params and opens settings-modal if params matches. + useOpenSettingsModalBasedQueryParam(); + if (repoStatusError?.response?.status === ServerCodes.NotFound) { + return ; + } + if (repoStatus?.hasMergeConflict) { + return ; + } + return ( + + + + ); +}; From c7a1be10af9a452286dd2d04bd803b7cfa513310 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:20:24 +0100 Subject: [PATCH 021/126] fixes --- .../hooks/useOpenSettingsModalBasedQueryParam.test.ts | 2 +- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts index 3e46752dfad..24cd57bea97 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts @@ -46,7 +46,7 @@ function buildSearchParams(queryParamValue: string): URLSearchParams { return searchParams; } -function setupUseSettingsModalContextMock(openSettingsMock: jest.fn): jest.Mock { +function setupUseSettingsModalContextMock(openSettingsMock: typeof jest.fn): jest.Mock { return (useSettingsModalContext as jest.Mock).mockReturnValue({ settingsRef: { current: { diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index 7d598dd3bb8..adddd53110d 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -14,6 +14,13 @@ const setupTabId: SettingsModalTabId = 'setup'; const policyTabId: SettingsModalTabId = 'policy'; const accessControlTabId: SettingsModalTabId = 'access_control'; +export const allSettingsModalTabs: Array = [ + aboutTabId, + setupTabId, + policyTabId, + accessControlTabId, +]; + export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); From f8bbc47038814945f0bde9cee4628416601d8a75 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:24:36 +0100 Subject: [PATCH 022/126] improved test description --- .../hooks/useOpenSettingsModalBasedQueryParam.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts index 24cd57bea97..38a732ddcfd 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts @@ -13,7 +13,7 @@ jest.mock('react-router-dom', () => ({ })); describe('useOpenSettingsModalBasedQueryParam', () => { - it('should open dialog if query params has valid tab id', async () => { + it('should open "settingsModal" if query params has valid tab id', async () => { const searchParams = buildSearchParams('about'); setupSearchParamMock(searchParams); @@ -24,7 +24,7 @@ describe('useOpenSettingsModalBasedQueryParam', () => { await waitFor(() => expect(openSettingsMock).toHaveBeenCalledWith('about')); }); - it('should not open dialog of query params has invalid tab id', async () => { + it('should not open "settingsModal" if query params has an invalid tab id', async () => { const searchParams = buildSearchParams('doestNotExistTab'); setupSearchParamMock(searchParams); From 28136667fc8ad8fe6e7df02c4b85366b0a9a0cdc Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 20:45:29 +0100 Subject: [PATCH 023/126] PR-feedback + improve unit-tests --- .../hooks/useOpenSettingsModalBasedQueryParam.test.ts | 2 ++ .../hooks/useOpenSettingsModalBasedQueryParam.ts | 2 +- frontend/app-development/layout/PageLayout.tsx | 5 +++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts index 38a732ddcfd..60809622836 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts @@ -22,6 +22,7 @@ describe('useOpenSettingsModalBasedQueryParam', () => { renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); await waitFor(() => expect(openSettingsMock).toHaveBeenCalledWith('about')); + expect(openSettingsMock).toHaveBeenCalledTimes(1); }); it('should not open "settingsModal" if query params has an invalid tab id', async () => { @@ -33,6 +34,7 @@ describe('useOpenSettingsModalBasedQueryParam', () => { renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); await waitFor(() => expect(openSettingsMock).not.toHaveBeenCalledWith('doestNotExistTab')); + expect(openSettingsMock).toHaveBeenCalledTimes(0); }); }); diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts index a7da1eaee81..202a938b9f9 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts @@ -16,7 +16,7 @@ export function useOpenSettingsModalBasedQueryParam(): void { if (shouldOpenModal) { settingsRef.current.openSettings(tabToOpen); } - }, [searchParams]); + }, [searchParams, settingsRef]); } function isValidTab(tabId: SettingsModalTabId): boolean { diff --git a/frontend/app-development/layout/PageLayout.tsx b/frontend/app-development/layout/PageLayout.tsx index b6d904117e7..95ad155c170 100644 --- a/frontend/app-development/layout/PageLayout.tsx +++ b/frontend/app-development/layout/PageLayout.tsx @@ -51,7 +51,7 @@ export const PageLayout = (): React.ReactNode => { isRepoError={repoStatusError !== null} /> - + ); }; @@ -60,9 +60,10 @@ type PagesToRenderProps = { repoStatusError: AxiosError; repoStatus: RepoStatus; }; -const PagesToRender = ({ repoStatusError, repoStatus }: PagesToRenderProps) => { +const Pages = ({ repoStatusError, repoStatus }: PagesToRenderProps) => { // Listen to URL-search params and opens settings-modal if params matches. useOpenSettingsModalBasedQueryParam(); + if (repoStatusError?.response?.status === ServerCodes.NotFound) { return ; } From 1fcb0bb84bb7620e934886c4419fa1ae625c3c1e Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 024/126] feat: context based login with ansattporten --- .../components/Tabs/Maskinporten/Maskinporten.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx new file mode 100644 index 00000000000..3319082978d --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -0,0 +1,5 @@ +describe('Maskinporten', () => { + it('should check and verify if the user is logged in', () => {}); + it('should display information about login, if user is not logged in', () => {}); + it('should display login with ansattporten if user is not logged in', () => {}); +}); From 1348b929c9c3e639c4fdf7c8a31812c8b6050641 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 025/126] feat: context based login with ansattporten --- .../SettingsModal/SettingsModal.tsx | 4 +++ .../Tabs/Maskinporten/Maskinporten.tsx | 25 +++++++++++++++++++ .../hooks/useSettingsModalMenuTabConfigs.tsx | 7 ++++++ .../types/SettingsModalTabId.ts | 2 +- frontend/language/src/nb.json | 4 +++ frontend/packages/shared/src/api/paths.js | 16 ++++++++++-- 6 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index 491acf72e32..a0fc88c8917 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -11,6 +11,7 @@ import { AccessControlTab } from './components/Tabs/AccessControlTab'; import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; +import { Maskinporten } from './components/Tabs/Maskinporten/Maskinporten'; export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); @@ -45,6 +46,9 @@ export const SettingsModal = forwardRef(({}, ref): Reac case 'access_control': { return ; } + case 'maskinporten': { + return ; + } } }; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx new file mode 100644 index 00000000000..c646b4355aa --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { TabContent } from '../../TabContent'; +import { ansattportenLoginPath } from 'app-shared/api/paths'; +import { StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; +import { useTranslation } from 'react-i18next'; + +export const Maskinporten = (): React.ReactElement => { + const { t } = useTranslation(); + + const handleLoginWithAnsattporten = (): void => { + window.location.href = ansattportenLoginPath(); + }; + + return ( + + + {t('settings_modal.maskinporten_tab_title')} + + {t('settings_modal.maskinporten_tab_description')} + + {t('settings_modal.maskinporten_tab_login_with_ansattporten')} + + + ); +}; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index adddd53110d..263242e5485 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -5,6 +5,7 @@ import { SidebarBothIcon, ShieldLockIcon, TimerStartIcon, + CogIcon, } from '@studio/icons'; import { useTranslation } from 'react-i18next'; import type { StudioContentMenuButtonTabProps } from '@studio/components'; @@ -13,6 +14,7 @@ const aboutTabId: SettingsModalTabId = 'about'; const setupTabId: SettingsModalTabId = 'setup'; const policyTabId: SettingsModalTabId = 'policy'; const accessControlTabId: SettingsModalTabId = 'access_control'; +const maskinportenTabId: SettingsModalTabId = 'maskinporten'; export const allSettingsModalTabs: Array = [ aboutTabId, @@ -46,5 +48,10 @@ export const useSettingsModalMenuTabConfigs = tabName: t(`settings_modal.left_nav_tab_${accessControlTabId}`), icon: , }, + { + tabId: maskinportenTabId, + tabName: t(`settings_modal.left_nav_tab_${maskinportenTabId}`), + icon: , + }, ]; }; diff --git a/frontend/app-development/types/SettingsModalTabId.ts b/frontend/app-development/types/SettingsModalTabId.ts index c596b1e0d5b..3f65b015eae 100644 --- a/frontend/app-development/types/SettingsModalTabId.ts +++ b/frontend/app-development/types/SettingsModalTabId.ts @@ -1 +1 @@ -export type SettingsModalTabId = 'about' | 'setup' | 'policy' | 'access_control'; +export type SettingsModalTabId = 'about' | 'setup' | 'policy' | 'access_control' | 'maskinporten'; diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index c3ae3486c97..76842784914 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -978,9 +978,13 @@ "settings_modal.heading": "Innstillinger", "settings_modal.left_nav_tab_about": "Om appen", "settings_modal.left_nav_tab_access_control": "Oppstartskontroll", + "settings_modal.left_nav_tab_maskinporten": "Maskinporten", "settings_modal.left_nav_tab_policy": "Tilganger", "settings_modal.left_nav_tab_setup": "Oppsett", "settings_modal.loading_content": "Laster inn data", + "settings_modal.maskinporten_tab_description": "For å hente tilgjengelige scopes må du verifisere deg med Ansattporten", + "settings_modal.maskinporten_tab_login_with_ansattporten": "Logg inn med Ansattporten", + "settings_modal.maskinporten_tab_title": "Håndtering av Scopes fra Maskinporten", "settings_modal.policy_tab_heading": "Tilganger", "settings_modal.setup_tab_heading": "Oppsett", "settings_modal.setup_tab_switch_autoDeleteOnProcessEnd": "Automatisk sletting etter innsending", diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index fb6e9c76aa9..0280dd98e5c 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete @@ -16,7 +19,11 @@ export const serviceConfigPath = (org, app) => `${basePath}/${org}/${app}/config // DataModel export const createDataModelPath = (org, app) => `${basePath}/${org}/${app}/datamodels/new`; // Post -export const dataModelPath = (org, app, modelPath, saveOnly = false) => `${basePath}/${org}/${app}/datamodels/datamodel?${s({ modelPath, saveOnly })}`; // Get, Put, Delete +export const dataModelPath = (org, app, modelPath, saveOnly = false) => + `${basePath}/${org}/${app}/datamodels/datamodel?${s({ + modelPath, + saveOnly, + })}`; // Get, Put, Delete export const dataModelsPath = (org, app) => `${basePath}/${org}/${app}/datamodels/all-json`; // Get export const dataModelsXsdPath = (org, app) => `${basePath}/${org}/${app}/datamodels/all-xsd`; // Get export const dataModelsUploadPath = (org, app) => `${basePath}/${org}/${app}/datamodels/upload`; // Post @@ -88,7 +95,12 @@ export const envConfigPath = () => `${basePath}/environments`; export const abortmergePath = (org, app) => `${basePath}/repos/repo/${org}/${app}/abort-merge`; export const branchStatusPath = (org, app, branch) => `${basePath}/repos/repo/${org}/${app}/branches/branch?${s({ branch })}`; // Get export const cloneAppPath = (org, app) => `${basePath}/repos/repo/${org}/${app}/clone`; // Get -export const copyAppPath = (org, sourceRepository, targetRepository, targetOrg) => `${basePath}/repos/repo/${org}/copy-app?${s({ sourceRepository, targetRepository, targetOrg })}`; +export const copyAppPath = (org, sourceRepository, targetRepository, targetOrg) => + `${basePath}/repos/repo/${org}/copy-app?${s({ + sourceRepository, + targetRepository, + targetOrg, + })}`; export const createRepoPath = () => `${basePath}/repos/create-app`; // Post export const discardChangesPath = (org, app) => `${basePath}/repos/repo/${org}/${app}/discard`; // Get export const discardFileChangesPath = (org, app, filename) => `${basePath}/repos/repo/${org}/${app}/discard/${filename}`; // Get From 2e8bb93a352e28a99ab0dd3df356309537b251d5 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 026/126] frontend with mocks for maskinporten --- .../useIsLoggedInWithAnsattportenQuery.ts | 11 +++ .../Tabs/Maskinporten/Maskinporten.test.tsx | 98 ++++++++++++++++++- .../Tabs/Maskinporten/Maskinporten.tsx | 19 +++- .../hooks/useSettingsModalMenuTabConfigs.tsx | 1 + frontend/packages/shared/src/api/paths.js | 3 - frontend/packages/shared/src/api/queries.ts | 8 ++ .../packages/shared/src/mocks/queriesMock.ts | 3 + .../packages/shared/src/types/QueryKey.ts | 1 + 8 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts diff --git a/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts b/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts new file mode 100644 index 00000000000..b40dfd28ad7 --- /dev/null +++ b/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { QueryKey } from 'app-shared/types/QueryKey'; +import { useServicesContext } from 'app-shared/contexts/ServicesContext'; + +export const useIsLoggedInWithAnsattportenQuery = () => { + const { getIsLoggedInWithAnsattporten } = useServicesContext(); + return useQuery({ + queryKey: [QueryKey.IsLoggedInWithAnsattporten], + queryFn: () => getIsLoggedInWithAnsattporten(), + }); +}; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx index 3319082978d..747390d879c 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -1,5 +1,97 @@ +import React from 'react'; +import { screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import { Maskinporten } from './Maskinporten'; +import { renderWithProviders } from '../../../../../../../../test/mocks'; +import { textMock } from '@studio/testing/mocks/i18nMock'; +import { queriesMock } from 'app-shared/mocks/queriesMock'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; +import userEvent from '@testing-library/user-event'; + describe('Maskinporten', () => { - it('should check and verify if the user is logged in', () => {}); - it('should display information about login, if user is not logged in', () => {}); - it('should display login with ansattporten if user is not logged in', () => {}); + const consoleLogMock = jest.fn(); + const originalConsoleLog = console.log; + beforeEach(() => { + console.log = consoleLogMock; + }); + + afterEach(() => { + console.log = originalConsoleLog; + }); + + it('should check and verify if the user is logged in', async () => { + const getIsLoggedInWithAnsattportenMock = jest + .fn() + .mockImplementation(() => Promise.resolve(false)); + + renderMaskinporten({ + queries: { + getIsLoggedInWithAnsattporten: getIsLoggedInWithAnsattportenMock, + }, + }); + await waitForLoggedInStatusCheckIsDone(); + await waitFor(() => expect(getIsLoggedInWithAnsattportenMock).toHaveBeenCalledTimes(1)); + }); + + it('should display information about login and login button, if user is not logged in', async () => { + renderMaskinporten({}); + await waitForLoggedInStatusCheckIsDone(); + + const title = screen.getByRole('heading', { + level: 2, + name: textMock('settings_modal.maskinporten_tab_title'), + }); + expect(title).toBeInTheDocument(); + + const description = screen.getByText(textMock('settings_modal.maskinporten_tab_description')); + expect(description).toBeInTheDocument(); + + const loginButton = screen.getByRole('button', { + name: textMock('settings_modal.maskinporten_tab_login_with_ansattporten'), + }); + expect(loginButton).toBeInTheDocument(); + }); + + it('should display content if logged in', async () => { + const getIsLoggedInWithAnsattportenMock = jest + .fn() + .mockImplementation(() => Promise.resolve(true)); + renderMaskinporten({ + queries: { + getIsLoggedInWithAnsattporten: getIsLoggedInWithAnsattportenMock, + }, + }); + + await waitForLoggedInStatusCheckIsDone(); + + const temporaryLoggedInContent = screen.getByText('View when logged in comes here'); + expect(temporaryLoggedInContent).toBeInTheDocument(); + }); + + it('should invoke "handleLoginWithAnsattPorten" when login button is clicked', async () => { + const user = userEvent.setup(); + renderMaskinporten({}); + await waitForLoggedInStatusCheckIsDone(); + + const loginButton = screen.getByRole('button', { + name: textMock('settings_modal.maskinporten_tab_login_with_ansattporten'), + }); + + await user.click(loginButton); + + expect(consoleLogMock).toHaveBeenCalledWith( + 'Will be implemented in next iteration when backend is ready', + ); + }); }); + +type RenderMaskinporten = { + queries?: Partial; +}; +const renderMaskinporten = ({ queries = queriesMock }: RenderMaskinporten) => { + const queryClient = createQueryClientMock(); + renderWithProviders({ ...queriesMock, ...queries }, queryClient)(); +}; + +async function waitForLoggedInStatusCheckIsDone() { + await waitForElementToBeRemoved(() => screen.getByTitle(textMock('general.loading'))); +} diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx index c646b4355aa..cb3b33d5e1e 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx @@ -1,16 +1,27 @@ import React from 'react'; -import { TabContent } from '../../TabContent'; -import { ansattportenLoginPath } from 'app-shared/api/paths'; -import { StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; import { useTranslation } from 'react-i18next'; +import { TabContent } from '../../TabContent'; +import { StudioButton, StudioHeading, StudioParagraph, StudioSpinner } from '@studio/components'; +import { useIsLoggedInWithAnsattportenQuery } from '../../../../../../../../hooks/queries/useIsLoggedInWithAnsattportenQuery'; export const Maskinporten = (): React.ReactElement => { + const { data: isLoggedInWithAnsattporten, isPending: isPendingAuthStatus } = + useIsLoggedInWithAnsattportenQuery(); + const { t } = useTranslation(); const handleLoginWithAnsattporten = (): void => { - window.location.href = ansattportenLoginPath(); + console.log('Will be implemented in next iteration when backend is ready'); }; + if (isPendingAuthStatus) { + return ; + } + + if (isLoggedInWithAnsattporten) { + return
View when logged in comes here
; + } + return ( diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index 263242e5485..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -21,6 +21,7 @@ export const allSettingsModalTabs: Array = [ setupTabId, policyTabId, accessControlTabId, + maskinportenTabId, ]; export const useSettingsModalMenuTabConfigs = diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 0280dd98e5c..3bc11002f8f 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete diff --git a/frontend/packages/shared/src/api/queries.ts b/frontend/packages/shared/src/api/queries.ts index 0e5f047156b..451cd333f3d 100644 --- a/frontend/packages/shared/src/api/queries.ts +++ b/frontend/packages/shared/src/api/queries.ts @@ -54,6 +54,7 @@ import { getImageFileNamesPath, validateImageFromExternalUrlPath, } from './paths'; + import type { AppReleasesResponse, DataModelMetadataResponse, SearchRepoFilterParams, SearchRepositoryResponse } from 'app-shared/types/api'; import type { DeploymentsResponse } from 'app-shared/types/api/DeploymentsResponse'; import type { BranchStatus } from 'app-shared/types/BranchStatus'; @@ -83,6 +84,13 @@ import type { Policy } from 'app-shared/types/Policy'; import type { RepoDiffResponse } from 'app-shared/types/api/RepoDiffResponse'; import type { ExternalImageUrlValidationResponse } from 'app-shared/types/api/ExternalImageUrlValidationResponse'; +export const getIsLoggedInWithAnsattporten = async (): Promise => + // TODO: replace with endpoint when it's ready in the backend. + new Promise((resolve) => { + setTimeout(() => { + return resolve(false); + }, 1000); + }); export const getAppMetadataModelIds = (org: string, app: string, onlyUnReferenced: boolean) => get(appMetadataModelIdsPath(org, app, onlyUnReferenced)); export const getAppReleases = (owner: string, app: string) => get(releasesPath(owner, app, 'Descending')); export const getAppVersion = (org: string, app: string) => get(appVersionPath(org, app)); diff --git a/frontend/packages/shared/src/mocks/queriesMock.ts b/frontend/packages/shared/src/mocks/queriesMock.ts index 03f83e9c251..d878b75c9a9 100644 --- a/frontend/packages/shared/src/mocks/queriesMock.ts +++ b/frontend/packages/shared/src/mocks/queriesMock.ts @@ -166,6 +166,9 @@ export const queriesMock: ServicesContextProps = { // Queries - PrgetBpmnFile getBpmnFile: jest.fn().mockImplementation(() => Promise.resolve('')), getProcessTaskType: jest.fn().mockImplementation(() => Promise.resolve('')), + getIsLoggedInWithAnsattporten: jest + .fn() + .mockImplementation(() => Promise.resolve(false)), // Mutations addAppAttachmentMetadata: jest.fn().mockImplementation(() => Promise.resolve()), diff --git a/frontend/packages/shared/src/types/QueryKey.ts b/frontend/packages/shared/src/types/QueryKey.ts index f3542c6f927..5f164fe6843 100644 --- a/frontend/packages/shared/src/types/QueryKey.ts +++ b/frontend/packages/shared/src/types/QueryKey.ts @@ -44,6 +44,7 @@ export enum QueryKey { TextResources = 'TextResources', Widgets = 'Widgets', AppConfig = 'AppConfig', + IsLoggedInWithAnsattporten = 'IsLoggedInWithAnsattporten', // Resourceadm ResourceList = 'ResourceList', From 54b3aea3e9c29b408e3e19819c19fcfa1f3502ec Mon Sep 17 00:00:00 2001 From: David Ovrelid <46874830+framitdavid@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:28:57 +0100 Subject: [PATCH 027/126] Update frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx Co-authored-by: William Thorenfeldt <48119543+wrt95@users.noreply.github.com> --- .../components/Tabs/Maskinporten/Maskinporten.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx index 747390d879c..25ed04969e6 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -93,5 +93,5 @@ const renderMaskinporten = ({ queries = queriesMock }: RenderMaskinporten) => { }; async function waitForLoggedInStatusCheckIsDone() { - await waitForElementToBeRemoved(() => screen.getByTitle(textMock('general.loading'))); + await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); } From 1d095264111a5dde0fbeb94875a67214837808a6 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 08:30:32 +0100 Subject: [PATCH 028/126] PR feedback --- .../SettingsModalButton/SettingsModal/SettingsModal.tsx | 2 +- .../SettingsModal/components/Tabs/Maskinporten/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index a0fc88c8917..77e3eaec68a 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -11,7 +11,7 @@ import { AccessControlTab } from './components/Tabs/AccessControlTab'; import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; -import { Maskinporten } from './components/Tabs/Maskinporten/Maskinporten'; +import { Maskinporten } from './components/Tabs/Maskinporten'; export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts new file mode 100644 index 00000000000..371b8f1a173 --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts @@ -0,0 +1 @@ +export { Maskinporten } from './Maskinporten'; From 3b12f25acc6c51fd6ca25917a54240afb5be09a0 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 15:40:41 +0100 Subject: [PATCH 029/126] hide maskinporten behind featureflag --- .../SettingsModal/SettingsModal.test.tsx | 21 +++++++++++++++++++ .../SettingsModal/SettingsModal.tsx | 21 ++++++++++++++++--- .../shared/src/utils/featureToggleUtils.ts | 3 ++- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.test.tsx index 237a8e6d81d..431bdb49396 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.test.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.test.tsx @@ -14,6 +14,7 @@ import { MemoryRouter } from 'react-router-dom'; import { SettingsModalContextProvider } from 'app-development/contexts/SettingsModalContext'; import { PreviewContextProvider } from 'app-development/contexts/PreviewContext'; import type { SettingsModalHandle } from 'app-development/types/SettingsModalHandle'; +import { typedLocalStorage } from '@studio/pure-functions'; jest.mock('app-development/hooks/mutations/useAppConfigMutation'); @@ -38,6 +39,26 @@ describe('SettingsModal', () => { await user.click(closeButton); }); + it('should hide the "Maskinporten" tab when the feature flag is not enabled', async () => { + await resolveAndWaitForSpinnerToDisappear(); + const maskinPortenTab = screen.queryByRole('tab', { + name: textMock('settings_modal.left_nav_tab_maskinporten'), + }); + + expect(maskinPortenTab).not.toBeInTheDocument(); + }); + + it('should display the "Maskinporten" tab when the feature flag is enabled.', async () => { + typedLocalStorage.setItem('featureFlags', ['maskinporten']); + await resolveAndWaitForSpinnerToDisappear(); + const maskinPortenTab = screen.getByRole('tab', { + name: textMock('settings_modal.left_nav_tab_maskinporten'), + }); + + expect(maskinPortenTab).toBeInTheDocument(); + typedLocalStorage.removeItem('featureFlags'); + }); + it('displays left navigation bar when promises resolve', async () => { await resolveAndWaitForSpinnerToDisappear(); expect(getAboutTab()).toBeInTheDocument(); diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index 77e3eaec68a..cf03a88d213 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -2,7 +2,11 @@ import type { ReactElement } from 'react'; import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react'; import classes from './SettingsModal.module.css'; import { CogIcon } from '@studio/icons'; -import { StudioModal, StudioContentMenu } from '@studio/components'; +import { + StudioModal, + StudioContentMenu, + StudioContentMenuButtonTabProps, +} from '@studio/components'; import type { SettingsModalTabId } from '../../../../../types/SettingsModalTabId'; import { useTranslation } from 'react-i18next'; import { PolicyTab } from './components/Tabs/PolicyTab'; @@ -12,6 +16,7 @@ import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; import { Maskinporten } from './components/Tabs/Maskinporten'; +import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); @@ -20,6 +25,8 @@ export const SettingsModal = forwardRef(({}, ref): Reac const dialogRef = useRef(); const menuTabConfigs = useSettingsModalMenuTabConfigs(); + const menuTabsToRender = filterFeatureFlag(menuTabConfigs); + const openSettings = useCallback( (tab: SettingsModalTabId = currentTab) => { setCurrentTab(tab); @@ -47,7 +54,7 @@ export const SettingsModal = forwardRef(({}, ref): Reac return ; } case 'maskinporten': { - return ; + return shouldDisplayFeature('maskinporten') ? : null; } } }; @@ -67,7 +74,7 @@ export const SettingsModal = forwardRef(({}, ref): Reac selectedTabId={currentTab} onChangeTab={(tabId: SettingsModalTabId) => setCurrentTab(tabId)} > - {menuTabConfigs.map((contentTab) => ( + {menuTabsToRender.map((contentTab) => ( (({}, ref): Reac }); SettingsModal.displayName = 'SettingsModal'; + +function filterFeatureFlag( + menuTabConfigs: Array>, +) { + return shouldDisplayFeature('maskinporten') + ? menuTabConfigs + : menuTabConfigs.filter((tab) => tab.tabId !== 'maskinporten'); +} diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.ts b/frontend/packages/shared/src/utils/featureToggleUtils.ts index 2410fb688db..52bb2f5a212 100644 --- a/frontend/packages/shared/src/utils/featureToggleUtils.ts +++ b/frontend/packages/shared/src/utils/featureToggleUtils.ts @@ -13,7 +13,8 @@ export type SupportedFeatureFlags = | 'addComponentModal' | 'subform' | 'summary2' - | 'codeListEditor'; + | 'codeListEditor' + | 'maskinporten'; /* * Please add all the features that you want to be toggle on by default here. From dcad4b6472e82dc100fb7b5148ebdcaeb9bf3389 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 15:44:43 +0100 Subject: [PATCH 030/126] eslint --- .../SettingsModalButton/SettingsModal/SettingsModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index cf03a88d213..c5dad2b42fe 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -5,7 +5,7 @@ import { CogIcon } from '@studio/icons'; import { StudioModal, StudioContentMenu, - StudioContentMenuButtonTabProps, + type StudioContentMenuButtonTabProps, } from '@studio/components'; import type { SettingsModalTabId } from '../../../../../types/SettingsModalTabId'; import { useTranslation } from 'react-i18next'; From 97b4a8fe7a7deeee5cd6e73b299b359638379019 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 15:50:55 +0100 Subject: [PATCH 031/126] fixed issue after merge --- frontend/packages/shared/src/utils/featureToggleUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.ts b/frontend/packages/shared/src/utils/featureToggleUtils.ts index 52bb2f5a212..6314d0254d2 100644 --- a/frontend/packages/shared/src/utils/featureToggleUtils.ts +++ b/frontend/packages/shared/src/utils/featureToggleUtils.ts @@ -14,6 +14,7 @@ export type SupportedFeatureFlags = | 'subform' | 'summary2' | 'codeListEditor' + | 'optionListEditor' | 'maskinporten'; /* From 41381f181f0428d807e1f5672ac478925b2702b2 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:16:33 +0100 Subject: [PATCH 032/126] feat(settings): make it possible to open settings modal based on query-params --- ...seOpenSettingsModalBasedQueryParam.test.ts | 57 +++++++++++++++++++ .../useOpenSettingsModalBasedQueryParam.ts | 24 ++++++++ .../app-development/layout/PageLayout.tsx | 40 +++++++------ 3 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts create mode 100644 frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts new file mode 100644 index 00000000000..3e46752dfad --- /dev/null +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts @@ -0,0 +1,57 @@ +import { renderHookWithProviders } from '../test/mocks'; +import { + queryParamKey, + useOpenSettingsModalBasedQueryParam, +} from './useOpenSettingsModalBasedQueryParam'; +import { useSearchParams } from 'react-router-dom'; +import { useSettingsModalContext } from '../contexts/SettingsModalContext'; +import { waitFor } from '@testing-library/react'; + +jest.mock('../contexts/SettingsModalContext'); +jest.mock('react-router-dom', () => ({ + useSearchParams: jest.fn(), +})); + +describe('useOpenSettingsModalBasedQueryParam', () => { + it('should open dialog if query params has valid tab id', async () => { + const searchParams = buildSearchParams('about'); + setupSearchParamMock(searchParams); + + const openSettingsMock = jest.fn(); + setupUseSettingsModalContextMock(openSettingsMock); + + renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); + await waitFor(() => expect(openSettingsMock).toHaveBeenCalledWith('about')); + }); + + it('should not open dialog of query params has invalid tab id', async () => { + const searchParams = buildSearchParams('doestNotExistTab'); + setupSearchParamMock(searchParams); + + const openSettingsMock = jest.fn(); + setupUseSettingsModalContextMock(openSettingsMock); + + renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); + await waitFor(() => expect(openSettingsMock).not.toHaveBeenCalledWith('doestNotExistTab')); + }); +}); + +function setupSearchParamMock(searchParams: URLSearchParams): jest.Mock { + return (useSearchParams as jest.Mock).mockReturnValue([searchParams, jest.fn()]); +} + +function buildSearchParams(queryParamValue: string): URLSearchParams { + const searchParams: URLSearchParams = new URLSearchParams(); + searchParams.set(queryParamKey, queryParamValue); + return searchParams; +} + +function setupUseSettingsModalContextMock(openSettingsMock: jest.fn): jest.Mock { + return (useSettingsModalContext as jest.Mock).mockReturnValue({ + settingsRef: { + current: { + openSettings: openSettingsMock, + }, + }, + }); +} diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts new file mode 100644 index 00000000000..a7da1eaee81 --- /dev/null +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts @@ -0,0 +1,24 @@ +import { useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useSettingsModalContext } from '../contexts/SettingsModalContext'; +import type { SettingsModalTabId } from '../types/SettingsModalTabId'; +import { allSettingsModalTabs } from '../layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs'; + +export const queryParamKey: string = 'openSettingsModalWithTab'; + +export function useOpenSettingsModalBasedQueryParam(): void { + const [searchParams] = useSearchParams(); + const { settingsRef } = useSettingsModalContext(); + + useEffect((): void => { + const tabToOpen: SettingsModalTabId = searchParams.get(queryParamKey) as SettingsModalTabId; + const shouldOpenModal: boolean = isValidTab(tabToOpen); + if (shouldOpenModal) { + settingsRef.current.openSettings(tabToOpen); + } + }, [searchParams]); +} + +function isValidTab(tabId: SettingsModalTabId): boolean { + return allSettingsModalTabs.includes(tabId); +} diff --git a/frontend/app-development/layout/PageLayout.tsx b/frontend/app-development/layout/PageLayout.tsx index d4cb27107f5..b6d904117e7 100644 --- a/frontend/app-development/layout/PageLayout.tsx +++ b/frontend/app-development/layout/PageLayout.tsx @@ -10,13 +10,15 @@ import { NotFoundPage } from './NotFoundPage'; import { useTranslation } from 'react-i18next'; import { WebSocketSyncWrapper } from '../components'; import { PageHeaderContextProvider } from 'app-development/contexts/PageHeaderContext'; +import { useOpenSettingsModalBasedQueryParam } from '../hooks/useOpenSettingsModalBasedQueryParam'; +import { type AxiosError } from 'axios'; +import { type RepoStatus } from 'app-shared/types/RepoStatus'; /** * Displays the layout for the app development pages */ export const PageLayout = (): React.ReactNode => { const { t } = useTranslation(); - const { pathname } = useLocation(); const match = matchPath({ path: '/:org/:app', caseSensitive: true, end: false }, pathname); const { org, app } = match.params; @@ -41,20 +43,6 @@ export const PageLayout = (): React.ReactNode => { ); } - const renderPages = () => { - if (repoStatusError?.response?.status === ServerCodes.NotFound) { - return ; - } - if (repoStatus?.hasMergeConflict) { - return ; - } - return ( - - - - ); - }; - return ( <> @@ -63,7 +51,27 @@ export const PageLayout = (): React.ReactNode => { isRepoError={repoStatusError !== null} /> - {renderPages()} + ); }; + +type PagesToRenderProps = { + repoStatusError: AxiosError; + repoStatus: RepoStatus; +}; +const PagesToRender = ({ repoStatusError, repoStatus }: PagesToRenderProps) => { + // Listen to URL-search params and opens settings-modal if params matches. + useOpenSettingsModalBasedQueryParam(); + if (repoStatusError?.response?.status === ServerCodes.NotFound) { + return ; + } + if (repoStatus?.hasMergeConflict) { + return ; + } + return ( + + + + ); +}; From 2b024b63f61ac385eb5ed9915e34d20d5bb78892 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:20:24 +0100 Subject: [PATCH 033/126] fixes --- .../hooks/useOpenSettingsModalBasedQueryParam.test.ts | 2 +- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts index 3e46752dfad..24cd57bea97 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts @@ -46,7 +46,7 @@ function buildSearchParams(queryParamValue: string): URLSearchParams { return searchParams; } -function setupUseSettingsModalContextMock(openSettingsMock: jest.fn): jest.Mock { +function setupUseSettingsModalContextMock(openSettingsMock: typeof jest.fn): jest.Mock { return (useSettingsModalContext as jest.Mock).mockReturnValue({ settingsRef: { current: { diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index 7d598dd3bb8..adddd53110d 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -14,6 +14,13 @@ const setupTabId: SettingsModalTabId = 'setup'; const policyTabId: SettingsModalTabId = 'policy'; const accessControlTabId: SettingsModalTabId = 'access_control'; +export const allSettingsModalTabs: Array = [ + aboutTabId, + setupTabId, + policyTabId, + accessControlTabId, +]; + export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); From a8e855f377f97f95386528a989e96b9960a30cca Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:24:36 +0100 Subject: [PATCH 034/126] improved test description --- .../hooks/useOpenSettingsModalBasedQueryParam.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts index 24cd57bea97..38a732ddcfd 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts @@ -13,7 +13,7 @@ jest.mock('react-router-dom', () => ({ })); describe('useOpenSettingsModalBasedQueryParam', () => { - it('should open dialog if query params has valid tab id', async () => { + it('should open "settingsModal" if query params has valid tab id', async () => { const searchParams = buildSearchParams('about'); setupSearchParamMock(searchParams); @@ -24,7 +24,7 @@ describe('useOpenSettingsModalBasedQueryParam', () => { await waitFor(() => expect(openSettingsMock).toHaveBeenCalledWith('about')); }); - it('should not open dialog of query params has invalid tab id', async () => { + it('should not open "settingsModal" if query params has an invalid tab id', async () => { const searchParams = buildSearchParams('doestNotExistTab'); setupSearchParamMock(searchParams); From 1521f6ed0b97ca401457756eb3830e9059aca6f4 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 20:45:29 +0100 Subject: [PATCH 035/126] PR-feedback + improve unit-tests --- .../hooks/useOpenSettingsModalBasedQueryParam.test.ts | 2 ++ .../hooks/useOpenSettingsModalBasedQueryParam.ts | 2 +- frontend/app-development/layout/PageLayout.tsx | 5 +++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts index 38a732ddcfd..60809622836 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts @@ -22,6 +22,7 @@ describe('useOpenSettingsModalBasedQueryParam', () => { renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); await waitFor(() => expect(openSettingsMock).toHaveBeenCalledWith('about')); + expect(openSettingsMock).toHaveBeenCalledTimes(1); }); it('should not open "settingsModal" if query params has an invalid tab id', async () => { @@ -33,6 +34,7 @@ describe('useOpenSettingsModalBasedQueryParam', () => { renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); await waitFor(() => expect(openSettingsMock).not.toHaveBeenCalledWith('doestNotExistTab')); + expect(openSettingsMock).toHaveBeenCalledTimes(0); }); }); diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts index a7da1eaee81..202a938b9f9 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts @@ -16,7 +16,7 @@ export function useOpenSettingsModalBasedQueryParam(): void { if (shouldOpenModal) { settingsRef.current.openSettings(tabToOpen); } - }, [searchParams]); + }, [searchParams, settingsRef]); } function isValidTab(tabId: SettingsModalTabId): boolean { diff --git a/frontend/app-development/layout/PageLayout.tsx b/frontend/app-development/layout/PageLayout.tsx index b6d904117e7..95ad155c170 100644 --- a/frontend/app-development/layout/PageLayout.tsx +++ b/frontend/app-development/layout/PageLayout.tsx @@ -51,7 +51,7 @@ export const PageLayout = (): React.ReactNode => { isRepoError={repoStatusError !== null} /> - + ); }; @@ -60,9 +60,10 @@ type PagesToRenderProps = { repoStatusError: AxiosError; repoStatus: RepoStatus; }; -const PagesToRender = ({ repoStatusError, repoStatus }: PagesToRenderProps) => { +const Pages = ({ repoStatusError, repoStatus }: PagesToRenderProps) => { // Listen to URL-search params and opens settings-modal if params matches. useOpenSettingsModalBasedQueryParam(); + if (repoStatusError?.response?.status === ServerCodes.NotFound) { return ; } From c5d41f0157654e76449774f6ccd4fcbc3500ded5 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 036/126] feat: context based login with ansattporten --- .../components/Tabs/Maskinporten/Maskinporten.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx new file mode 100644 index 00000000000..3319082978d --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -0,0 +1,5 @@ +describe('Maskinporten', () => { + it('should check and verify if the user is logged in', () => {}); + it('should display information about login, if user is not logged in', () => {}); + it('should display login with ansattporten if user is not logged in', () => {}); +}); From 1f588e11ada7cd77c979b305468d05d3e577ea3d Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 037/126] feat: context based login with ansattporten --- .../SettingsModal/SettingsModal.tsx | 4 +++ .../Tabs/Maskinporten/Maskinporten.tsx | 25 +++++++++++++++++++ .../hooks/useSettingsModalMenuTabConfigs.tsx | 7 ++++++ .../types/SettingsModalTabId.ts | 2 +- frontend/language/src/nb.json | 4 +++ frontend/packages/shared/src/api/paths.js | 16 ++++++++++-- 6 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index 491acf72e32..a0fc88c8917 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -11,6 +11,7 @@ import { AccessControlTab } from './components/Tabs/AccessControlTab'; import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; +import { Maskinporten } from './components/Tabs/Maskinporten/Maskinporten'; export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); @@ -45,6 +46,9 @@ export const SettingsModal = forwardRef(({}, ref): Reac case 'access_control': { return ; } + case 'maskinporten': { + return ; + } } }; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx new file mode 100644 index 00000000000..c646b4355aa --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { TabContent } from '../../TabContent'; +import { ansattportenLoginPath } from 'app-shared/api/paths'; +import { StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; +import { useTranslation } from 'react-i18next'; + +export const Maskinporten = (): React.ReactElement => { + const { t } = useTranslation(); + + const handleLoginWithAnsattporten = (): void => { + window.location.href = ansattportenLoginPath(); + }; + + return ( + + + {t('settings_modal.maskinporten_tab_title')} + + {t('settings_modal.maskinporten_tab_description')} + + {t('settings_modal.maskinporten_tab_login_with_ansattporten')} + + + ); +}; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index adddd53110d..263242e5485 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -5,6 +5,7 @@ import { SidebarBothIcon, ShieldLockIcon, TimerStartIcon, + CogIcon, } from '@studio/icons'; import { useTranslation } from 'react-i18next'; import type { StudioContentMenuButtonTabProps } from '@studio/components'; @@ -13,6 +14,7 @@ const aboutTabId: SettingsModalTabId = 'about'; const setupTabId: SettingsModalTabId = 'setup'; const policyTabId: SettingsModalTabId = 'policy'; const accessControlTabId: SettingsModalTabId = 'access_control'; +const maskinportenTabId: SettingsModalTabId = 'maskinporten'; export const allSettingsModalTabs: Array = [ aboutTabId, @@ -46,5 +48,10 @@ export const useSettingsModalMenuTabConfigs = tabName: t(`settings_modal.left_nav_tab_${accessControlTabId}`), icon: , }, + { + tabId: maskinportenTabId, + tabName: t(`settings_modal.left_nav_tab_${maskinportenTabId}`), + icon: , + }, ]; }; diff --git a/frontend/app-development/types/SettingsModalTabId.ts b/frontend/app-development/types/SettingsModalTabId.ts index c596b1e0d5b..3f65b015eae 100644 --- a/frontend/app-development/types/SettingsModalTabId.ts +++ b/frontend/app-development/types/SettingsModalTabId.ts @@ -1 +1 @@ -export type SettingsModalTabId = 'about' | 'setup' | 'policy' | 'access_control'; +export type SettingsModalTabId = 'about' | 'setup' | 'policy' | 'access_control' | 'maskinporten'; diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 7555f4e24b8..f6aee68deb9 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -986,9 +986,13 @@ "settings_modal.heading": "Innstillinger", "settings_modal.left_nav_tab_about": "Om appen", "settings_modal.left_nav_tab_access_control": "Oppstartskontroll", + "settings_modal.left_nav_tab_maskinporten": "Maskinporten", "settings_modal.left_nav_tab_policy": "Tilganger", "settings_modal.left_nav_tab_setup": "Oppsett", "settings_modal.loading_content": "Laster inn data", + "settings_modal.maskinporten_tab_description": "For å hente tilgjengelige scopes må du verifisere deg med Ansattporten", + "settings_modal.maskinporten_tab_login_with_ansattporten": "Logg inn med Ansattporten", + "settings_modal.maskinporten_tab_title": "Håndtering av Scopes fra Maskinporten", "settings_modal.policy_tab_heading": "Tilganger", "settings_modal.setup_tab_heading": "Oppsett", "settings_modal.setup_tab_switch_autoDeleteOnProcessEnd": "Automatisk sletting etter innsending", diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 0f3c86db231..ad102961501 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete @@ -16,7 +19,11 @@ export const serviceConfigPath = (org, app) => `${basePath}/${org}/${app}/config // DataModel export const createDataModelPath = (org, app) => `${basePath}/${org}/${app}/datamodels/new`; // Post -export const dataModelPath = (org, app, modelPath, saveOnly = false) => `${basePath}/${org}/${app}/datamodels/datamodel?${s({ modelPath, saveOnly })}`; // Get, Put, Delete +export const dataModelPath = (org, app, modelPath, saveOnly = false) => + `${basePath}/${org}/${app}/datamodels/datamodel?${s({ + modelPath, + saveOnly, + })}`; // Get, Put, Delete export const dataModelsPath = (org, app) => `${basePath}/${org}/${app}/datamodels/all-json`; // Get export const dataModelsXsdPath = (org, app) => `${basePath}/${org}/${app}/datamodels/all-xsd`; // Get export const dataModelsUploadPath = (org, app) => `${basePath}/${org}/${app}/datamodels/upload`; // Post @@ -89,7 +96,12 @@ export const envConfigPath = () => `${basePath}/environments`; export const abortmergePath = (org, app) => `${basePath}/repos/repo/${org}/${app}/abort-merge`; export const branchStatusPath = (org, app, branch) => `${basePath}/repos/repo/${org}/${app}/branches/branch?${s({ branch })}`; // Get export const cloneAppPath = (org, app) => `${basePath}/repos/repo/${org}/${app}/clone`; // Get -export const copyAppPath = (org, sourceRepository, targetRepository, targetOrg) => `${basePath}/repos/repo/${org}/copy-app?${s({ sourceRepository, targetRepository, targetOrg })}`; +export const copyAppPath = (org, sourceRepository, targetRepository, targetOrg) => + `${basePath}/repos/repo/${org}/copy-app?${s({ + sourceRepository, + targetRepository, + targetOrg, + })}`; export const createRepoPath = () => `${basePath}/repos/create-app`; // Post export const discardChangesPath = (org, app) => `${basePath}/repos/repo/${org}/${app}/discard`; // Get export const discardFileChangesPath = (org, app, filename) => `${basePath}/repos/repo/${org}/${app}/discard/${filename}`; // Get From b328101a55724d94320d005d78f4f56a3800fbb5 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 038/126] frontend with mocks for maskinporten --- .../useIsLoggedInWithAnsattportenQuery.ts | 11 +++ .../Tabs/Maskinporten/Maskinporten.test.tsx | 98 ++++++++++++++++++- .../Tabs/Maskinporten/Maskinporten.tsx | 19 +++- .../hooks/useSettingsModalMenuTabConfigs.tsx | 1 + frontend/packages/shared/src/api/paths.js | 3 - frontend/packages/shared/src/api/queries.ts | 8 ++ .../packages/shared/src/mocks/queriesMock.ts | 3 + .../packages/shared/src/types/QueryKey.ts | 1 + 8 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts diff --git a/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts b/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts new file mode 100644 index 00000000000..b40dfd28ad7 --- /dev/null +++ b/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { QueryKey } from 'app-shared/types/QueryKey'; +import { useServicesContext } from 'app-shared/contexts/ServicesContext'; + +export const useIsLoggedInWithAnsattportenQuery = () => { + const { getIsLoggedInWithAnsattporten } = useServicesContext(); + return useQuery({ + queryKey: [QueryKey.IsLoggedInWithAnsattporten], + queryFn: () => getIsLoggedInWithAnsattporten(), + }); +}; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx index 3319082978d..747390d879c 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -1,5 +1,97 @@ +import React from 'react'; +import { screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import { Maskinporten } from './Maskinporten'; +import { renderWithProviders } from '../../../../../../../../test/mocks'; +import { textMock } from '@studio/testing/mocks/i18nMock'; +import { queriesMock } from 'app-shared/mocks/queriesMock'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; +import userEvent from '@testing-library/user-event'; + describe('Maskinporten', () => { - it('should check and verify if the user is logged in', () => {}); - it('should display information about login, if user is not logged in', () => {}); - it('should display login with ansattporten if user is not logged in', () => {}); + const consoleLogMock = jest.fn(); + const originalConsoleLog = console.log; + beforeEach(() => { + console.log = consoleLogMock; + }); + + afterEach(() => { + console.log = originalConsoleLog; + }); + + it('should check and verify if the user is logged in', async () => { + const getIsLoggedInWithAnsattportenMock = jest + .fn() + .mockImplementation(() => Promise.resolve(false)); + + renderMaskinporten({ + queries: { + getIsLoggedInWithAnsattporten: getIsLoggedInWithAnsattportenMock, + }, + }); + await waitForLoggedInStatusCheckIsDone(); + await waitFor(() => expect(getIsLoggedInWithAnsattportenMock).toHaveBeenCalledTimes(1)); + }); + + it('should display information about login and login button, if user is not logged in', async () => { + renderMaskinporten({}); + await waitForLoggedInStatusCheckIsDone(); + + const title = screen.getByRole('heading', { + level: 2, + name: textMock('settings_modal.maskinporten_tab_title'), + }); + expect(title).toBeInTheDocument(); + + const description = screen.getByText(textMock('settings_modal.maskinporten_tab_description')); + expect(description).toBeInTheDocument(); + + const loginButton = screen.getByRole('button', { + name: textMock('settings_modal.maskinporten_tab_login_with_ansattporten'), + }); + expect(loginButton).toBeInTheDocument(); + }); + + it('should display content if logged in', async () => { + const getIsLoggedInWithAnsattportenMock = jest + .fn() + .mockImplementation(() => Promise.resolve(true)); + renderMaskinporten({ + queries: { + getIsLoggedInWithAnsattporten: getIsLoggedInWithAnsattportenMock, + }, + }); + + await waitForLoggedInStatusCheckIsDone(); + + const temporaryLoggedInContent = screen.getByText('View when logged in comes here'); + expect(temporaryLoggedInContent).toBeInTheDocument(); + }); + + it('should invoke "handleLoginWithAnsattPorten" when login button is clicked', async () => { + const user = userEvent.setup(); + renderMaskinporten({}); + await waitForLoggedInStatusCheckIsDone(); + + const loginButton = screen.getByRole('button', { + name: textMock('settings_modal.maskinporten_tab_login_with_ansattporten'), + }); + + await user.click(loginButton); + + expect(consoleLogMock).toHaveBeenCalledWith( + 'Will be implemented in next iteration when backend is ready', + ); + }); }); + +type RenderMaskinporten = { + queries?: Partial; +}; +const renderMaskinporten = ({ queries = queriesMock }: RenderMaskinporten) => { + const queryClient = createQueryClientMock(); + renderWithProviders({ ...queriesMock, ...queries }, queryClient)(); +}; + +async function waitForLoggedInStatusCheckIsDone() { + await waitForElementToBeRemoved(() => screen.getByTitle(textMock('general.loading'))); +} diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx index c646b4355aa..cb3b33d5e1e 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx @@ -1,16 +1,27 @@ import React from 'react'; -import { TabContent } from '../../TabContent'; -import { ansattportenLoginPath } from 'app-shared/api/paths'; -import { StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; import { useTranslation } from 'react-i18next'; +import { TabContent } from '../../TabContent'; +import { StudioButton, StudioHeading, StudioParagraph, StudioSpinner } from '@studio/components'; +import { useIsLoggedInWithAnsattportenQuery } from '../../../../../../../../hooks/queries/useIsLoggedInWithAnsattportenQuery'; export const Maskinporten = (): React.ReactElement => { + const { data: isLoggedInWithAnsattporten, isPending: isPendingAuthStatus } = + useIsLoggedInWithAnsattportenQuery(); + const { t } = useTranslation(); const handleLoginWithAnsattporten = (): void => { - window.location.href = ansattportenLoginPath(); + console.log('Will be implemented in next iteration when backend is ready'); }; + if (isPendingAuthStatus) { + return ; + } + + if (isLoggedInWithAnsattporten) { + return
View when logged in comes here
; + } + return ( diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index 263242e5485..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -21,6 +21,7 @@ export const allSettingsModalTabs: Array = [ setupTabId, policyTabId, accessControlTabId, + maskinportenTabId, ]; export const useSettingsModalMenuTabConfigs = diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index ad102961501..545ac846aaf 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete diff --git a/frontend/packages/shared/src/api/queries.ts b/frontend/packages/shared/src/api/queries.ts index c7d1ded93f8..6e96c93103b 100644 --- a/frontend/packages/shared/src/api/queries.ts +++ b/frontend/packages/shared/src/api/queries.ts @@ -54,6 +54,7 @@ import { getImageFileNamesPath, validateImageFromExternalUrlPath, } from './paths'; + import type { AppReleasesResponse, DataModelMetadataResponse, SearchRepoFilterParams, SearchRepositoryResponse } from 'app-shared/types/api'; import type { DeploymentsResponse } from 'app-shared/types/api/DeploymentsResponse'; import type { BranchStatus } from 'app-shared/types/BranchStatus'; @@ -84,6 +85,13 @@ import type { RepoDiffResponse } from 'app-shared/types/api/RepoDiffResponse'; import type { ExternalImageUrlValidationResponse } from 'app-shared/types/api/ExternalImageUrlValidationResponse'; import type { OptionsLists } from 'app-shared/types/api/OptionsLists'; +export const getIsLoggedInWithAnsattporten = async (): Promise => + // TODO: replace with endpoint when it's ready in the backend. + new Promise((resolve) => { + setTimeout(() => { + return resolve(false); + }, 1000); + }); export const getAppMetadataModelIds = (org: string, app: string, onlyUnReferenced: boolean) => get(appMetadataModelIdsPath(org, app, onlyUnReferenced)); export const getAppReleases = (owner: string, app: string) => get(releasesPath(owner, app, 'Descending')); export const getAppVersion = (org: string, app: string) => get(appVersionPath(org, app)); diff --git a/frontend/packages/shared/src/mocks/queriesMock.ts b/frontend/packages/shared/src/mocks/queriesMock.ts index 142f5ddde67..3ef7db6eae9 100644 --- a/frontend/packages/shared/src/mocks/queriesMock.ts +++ b/frontend/packages/shared/src/mocks/queriesMock.ts @@ -169,6 +169,9 @@ export const queriesMock: ServicesContextProps = { // Queries - PrgetBpmnFile getBpmnFile: jest.fn().mockImplementation(() => Promise.resolve('')), getProcessTaskType: jest.fn().mockImplementation(() => Promise.resolve('')), + getIsLoggedInWithAnsattporten: jest + .fn() + .mockImplementation(() => Promise.resolve(false)), // Mutations addAppAttachmentMetadata: jest.fn().mockImplementation(() => Promise.resolve()), diff --git a/frontend/packages/shared/src/types/QueryKey.ts b/frontend/packages/shared/src/types/QueryKey.ts index f3542c6f927..5f164fe6843 100644 --- a/frontend/packages/shared/src/types/QueryKey.ts +++ b/frontend/packages/shared/src/types/QueryKey.ts @@ -44,6 +44,7 @@ export enum QueryKey { TextResources = 'TextResources', Widgets = 'Widgets', AppConfig = 'AppConfig', + IsLoggedInWithAnsattporten = 'IsLoggedInWithAnsattporten', // Resourceadm ResourceList = 'ResourceList', From e27c80aedda021914993af47683c10f0f8f1a6b8 Mon Sep 17 00:00:00 2001 From: David Ovrelid <46874830+framitdavid@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:28:57 +0100 Subject: [PATCH 039/126] Update frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx Co-authored-by: William Thorenfeldt <48119543+wrt95@users.noreply.github.com> --- .../components/Tabs/Maskinporten/Maskinporten.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx index 747390d879c..25ed04969e6 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -93,5 +93,5 @@ const renderMaskinporten = ({ queries = queriesMock }: RenderMaskinporten) => { }; async function waitForLoggedInStatusCheckIsDone() { - await waitForElementToBeRemoved(() => screen.getByTitle(textMock('general.loading'))); + await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); } From a032f45d399c7c0fd6497079061740dfe4147e5e Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 08:30:32 +0100 Subject: [PATCH 040/126] PR feedback --- .../SettingsModalButton/SettingsModal/SettingsModal.tsx | 2 +- .../SettingsModal/components/Tabs/Maskinporten/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index a0fc88c8917..77e3eaec68a 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -11,7 +11,7 @@ import { AccessControlTab } from './components/Tabs/AccessControlTab'; import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; -import { Maskinporten } from './components/Tabs/Maskinporten/Maskinporten'; +import { Maskinporten } from './components/Tabs/Maskinporten'; export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts new file mode 100644 index 00000000000..371b8f1a173 --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts @@ -0,0 +1 @@ +export { Maskinporten } from './Maskinporten'; From c3dcc35072fa04c69cc4b47243f7ca1cb6d27571 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:20:24 +0100 Subject: [PATCH 041/126] fixes --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index d4420a5c5f8..cec49e79771 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,6 +24,13 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; +export const allSettingsModalTabs: Array = [ + aboutTabId, + setupTabId, + policyTabId, + accessControlTabId, +]; + export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); From 8e5f2dd3363143da6a17491dc5f0120db4d93779 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 042/126] feat: context based login with ansattporten --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 ------- frontend/packages/shared/src/api/paths.js | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index cec49e79771..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,13 +24,6 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; -export const allSettingsModalTabs: Array = [ - aboutTabId, - setupTabId, - policyTabId, - accessControlTabId, -]; - export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 545ac846aaf..ad102961501 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 0ed1716a143bd46379806d6c1d90e8f10115852a Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 043/126] frontend with mocks for maskinporten --- frontend/packages/shared/src/api/paths.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index ad102961501..545ac846aaf 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From b99c16b37ac6a73801fc8066f9b0a08eca576ca8 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 15:40:41 +0100 Subject: [PATCH 044/126] hide maskinporten behind featureflag --- .../SettingsModal/SettingsModal.test.tsx | 21 +++++++++++++++++++ .../SettingsModal/SettingsModal.tsx | 21 ++++++++++++++++--- .../shared/src/utils/featureToggleUtils.ts | 3 ++- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.test.tsx index 237a8e6d81d..431bdb49396 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.test.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.test.tsx @@ -14,6 +14,7 @@ import { MemoryRouter } from 'react-router-dom'; import { SettingsModalContextProvider } from 'app-development/contexts/SettingsModalContext'; import { PreviewContextProvider } from 'app-development/contexts/PreviewContext'; import type { SettingsModalHandle } from 'app-development/types/SettingsModalHandle'; +import { typedLocalStorage } from '@studio/pure-functions'; jest.mock('app-development/hooks/mutations/useAppConfigMutation'); @@ -38,6 +39,26 @@ describe('SettingsModal', () => { await user.click(closeButton); }); + it('should hide the "Maskinporten" tab when the feature flag is not enabled', async () => { + await resolveAndWaitForSpinnerToDisappear(); + const maskinPortenTab = screen.queryByRole('tab', { + name: textMock('settings_modal.left_nav_tab_maskinporten'), + }); + + expect(maskinPortenTab).not.toBeInTheDocument(); + }); + + it('should display the "Maskinporten" tab when the feature flag is enabled.', async () => { + typedLocalStorage.setItem('featureFlags', ['maskinporten']); + await resolveAndWaitForSpinnerToDisappear(); + const maskinPortenTab = screen.getByRole('tab', { + name: textMock('settings_modal.left_nav_tab_maskinporten'), + }); + + expect(maskinPortenTab).toBeInTheDocument(); + typedLocalStorage.removeItem('featureFlags'); + }); + it('displays left navigation bar when promises resolve', async () => { await resolveAndWaitForSpinnerToDisappear(); expect(getAboutTab()).toBeInTheDocument(); diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index 77e3eaec68a..cf03a88d213 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -2,7 +2,11 @@ import type { ReactElement } from 'react'; import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react'; import classes from './SettingsModal.module.css'; import { CogIcon } from '@studio/icons'; -import { StudioModal, StudioContentMenu } from '@studio/components'; +import { + StudioModal, + StudioContentMenu, + StudioContentMenuButtonTabProps, +} from '@studio/components'; import type { SettingsModalTabId } from '../../../../../types/SettingsModalTabId'; import { useTranslation } from 'react-i18next'; import { PolicyTab } from './components/Tabs/PolicyTab'; @@ -12,6 +16,7 @@ import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; import { Maskinporten } from './components/Tabs/Maskinporten'; +import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); @@ -20,6 +25,8 @@ export const SettingsModal = forwardRef(({}, ref): Reac const dialogRef = useRef(); const menuTabConfigs = useSettingsModalMenuTabConfigs(); + const menuTabsToRender = filterFeatureFlag(menuTabConfigs); + const openSettings = useCallback( (tab: SettingsModalTabId = currentTab) => { setCurrentTab(tab); @@ -47,7 +54,7 @@ export const SettingsModal = forwardRef(({}, ref): Reac return ; } case 'maskinporten': { - return ; + return shouldDisplayFeature('maskinporten') ? : null; } } }; @@ -67,7 +74,7 @@ export const SettingsModal = forwardRef(({}, ref): Reac selectedTabId={currentTab} onChangeTab={(tabId: SettingsModalTabId) => setCurrentTab(tabId)} > - {menuTabConfigs.map((contentTab) => ( + {menuTabsToRender.map((contentTab) => ( (({}, ref): Reac }); SettingsModal.displayName = 'SettingsModal'; + +function filterFeatureFlag( + menuTabConfigs: Array>, +) { + return shouldDisplayFeature('maskinporten') + ? menuTabConfigs + : menuTabConfigs.filter((tab) => tab.tabId !== 'maskinporten'); +} diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.ts b/frontend/packages/shared/src/utils/featureToggleUtils.ts index 9522a3ab11e..52bb2f5a212 100644 --- a/frontend/packages/shared/src/utils/featureToggleUtils.ts +++ b/frontend/packages/shared/src/utils/featureToggleUtils.ts @@ -13,7 +13,8 @@ export type SupportedFeatureFlags = | 'addComponentModal' | 'subform' | 'summary2' - | 'optionListEditor'; + | 'codeListEditor' + | 'maskinporten'; /* * Please add all the features that you want to be toggle on by default here. From 8ad841521dd957ca4afe520711ccf4860f43ae7f Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Tue, 12 Nov 2024 12:26:09 +0100 Subject: [PATCH 045/126] chore: scale designer in dev and staging (#14039) --- .github/workflows/deploy-designer.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/deploy-designer.yaml b/.github/workflows/deploy-designer.yaml index 34e66fe2784..6d71e0b2e67 100644 --- a/.github/workflows/deploy-designer.yaml +++ b/.github/workflows/deploy-designer.yaml @@ -106,7 +106,11 @@ jobs: artifact-environment: ${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }} config-chart-name: altinn-designer-config artifact-name: altinn-designer +<<<<<<< HEAD helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'dev' && 1 || 2 }} +======= + helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'preapproved-prod' && 1 || 2 }} +>>>>>>> 70f1511ca (chore: scale designer in dev and staging (#14039)) trace-workflow: true trace-team-name: 'team-studio' secrets: From 933a5b702c99d9f866b21af3aab8b15173b62430 Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Wed, 13 Nov 2024 14:43:32 +0100 Subject: [PATCH 046/126] feat: 2 replicas in prod/staging, 1 in dev (#14047) --- .github/workflows/deploy-designer.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/deploy-designer.yaml b/.github/workflows/deploy-designer.yaml index 6d71e0b2e67..34e66fe2784 100644 --- a/.github/workflows/deploy-designer.yaml +++ b/.github/workflows/deploy-designer.yaml @@ -106,11 +106,7 @@ jobs: artifact-environment: ${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }} config-chart-name: altinn-designer-config artifact-name: altinn-designer -<<<<<<< HEAD helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'dev' && 1 || 2 }} -======= - helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'preapproved-prod' && 1 || 2 }} ->>>>>>> 70f1511ca (chore: scale designer in dev and staging (#14039)) trace-workflow: true trace-team-name: 'team-studio' secrets: From ba7dc3b791cd77e89ec25c41e399bcdafeb3df8d Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 15:44:43 +0100 Subject: [PATCH 047/126] eslint --- .../SettingsModalButton/SettingsModal/SettingsModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index cf03a88d213..c5dad2b42fe 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -5,7 +5,7 @@ import { CogIcon } from '@studio/icons'; import { StudioModal, StudioContentMenu, - StudioContentMenuButtonTabProps, + type StudioContentMenuButtonTabProps, } from '@studio/components'; import type { SettingsModalTabId } from '../../../../../types/SettingsModalTabId'; import { useTranslation } from 'react-i18next'; From 96062f64701977506a039d33bade6acbac921307 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 15:50:55 +0100 Subject: [PATCH 048/126] fixed issue after merge --- frontend/packages/shared/src/utils/featureToggleUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.ts b/frontend/packages/shared/src/utils/featureToggleUtils.ts index 52bb2f5a212..6314d0254d2 100644 --- a/frontend/packages/shared/src/utils/featureToggleUtils.ts +++ b/frontend/packages/shared/src/utils/featureToggleUtils.ts @@ -14,6 +14,7 @@ export type SupportedFeatureFlags = | 'subform' | 'summary2' | 'codeListEditor' + | 'optionListEditor' | 'maskinporten'; /* From cdae4a63fd9982daffb095a3c03c564c3a3545a8 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 23:20:25 +0100 Subject: [PATCH 049/126] feat: split app-scopes endpoint into, status, login and app-scopes --- backend/src/Designer/Controllers/AppScopesController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/Designer/Controllers/AppScopesController.cs b/backend/src/Designer/Controllers/AppScopesController.cs index f3c699e9244..17baf9a3d4a 100644 --- a/backend/src/Designer/Controllers/AppScopesController.cs +++ b/backend/src/Designer/Controllers/AppScopesController.cs @@ -14,7 +14,7 @@ namespace Altinn.Studio.Designer.Controllers; - +// TODO split the endppoint [FeatureGate(StudioFeatureFlags.AnsattPorten)] [Route("designer/api/{org}/{app:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/app-scopes")] From ce5f840910fa31311dbb3d0fff3b741d713ae491 Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Tue, 19 Nov 2024 11:20:49 +0100 Subject: [PATCH 050/126] AnsattPorten controller --- .../Controllers/AnsattPortenController.cs | 47 +++++++++++++++++++ .../Controllers/AppScopesController.cs | 9 ++-- .../AnsattPorten/AnsattPortenExtensions.cs | 3 +- backend/src/Designer/Models/Dto/AuthStatus.cs | 6 +++ 4 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 backend/src/Designer/Controllers/AnsattPortenController.cs create mode 100644 backend/src/Designer/Models/Dto/AuthStatus.cs diff --git a/backend/src/Designer/Controllers/AnsattPortenController.cs b/backend/src/Designer/Controllers/AnsattPortenController.cs new file mode 100644 index 00000000000..a92945661fb --- /dev/null +++ b/backend/src/Designer/Controllers/AnsattPortenController.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using Altinn.Studio.Designer.Constants; +using Altinn.Studio.Designer.Models.Dto; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.FeatureManagement.Mvc; + +namespace Altinn.Studio.Designer.Controllers; + +[FeatureGate(StudioFeatureFlags.AnsattPorten)] +[Route("designer/api/[controller]")] +[ApiController] +public class AnsattPortenController(IAuthenticationService authService) : ControllerBase +{ + [Authorize(AnsattPortenConstants.AnsattportenAuthorizationPolicy)] + [HttpGet("login")] + public async Task Login([FromQuery] string redirectTo) + { + await Task.CompletedTask; + if (!Url.IsLocalUrl(redirectTo)) + { + return Forbid(); + } + + return LocalRedirect(redirectTo); + } + + [AllowAnonymous] + [HttpGet("auth-status")] + public async Task AuthStatus() + { + await Task.CompletedTask; + var authenticateResult = + await authService.AuthenticateAsync(HttpContext, + AnsattPortenConstants.AnsattportenAuthenticationScheme); + + var authStatus = new AuthStatus + { + IsLoggedIn = authenticateResult.Succeeded + }; + + return Ok(authStatus); + } + + +} diff --git a/backend/src/Designer/Controllers/AppScopesController.cs b/backend/src/Designer/Controllers/AppScopesController.cs index 17baf9a3d4a..4e8d88e0f87 100644 --- a/backend/src/Designer/Controllers/AppScopesController.cs +++ b/backend/src/Designer/Controllers/AppScopesController.cs @@ -14,7 +14,7 @@ namespace Altinn.Studio.Designer.Controllers; -// TODO split the endppoint + [FeatureGate(StudioFeatureFlags.AnsattPorten)] [Route("designer/api/{org}/{app:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/app-scopes")] @@ -27,7 +27,7 @@ public async Task GetScopesFromMaskinPorten(string org, string ap { var scopes = await maskinPortenHttpClient.GetAvailableScopes(cancellationToken); - var reponse = new AppScopesResponse() + var response = new AppScopesResponse() { Scopes = scopes.Select(x => new MaskinPortenScopeDto() { @@ -36,10 +36,9 @@ public async Task GetScopesFromMaskinPorten(string org, string ap }).ToHashSet() }; - return Ok(reponse); + return Ok(response); } - [Authorize] [HttpPut] public async Task UpsertAppScopes(string org, string app, [FromBody] AppScopesUpsertRequest appScopesUpsertRequest, @@ -55,7 +54,6 @@ public async Task UpsertAppScopes(string org, string app, [FromBody] AppScopesUp await appScopesService.UpsertScopesAsync(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer), scopes, cancellationToken); } - [Authorize] [HttpGet] public async Task GetAppScopes(string org, string app, CancellationToken cancellationToken) @@ -73,5 +71,4 @@ public async Task GetAppScopes(string org, string app, Cancellati return Ok(reponse); } - } diff --git a/backend/src/Designer/Infrastructure/AnsattPorten/AnsattPortenExtensions.cs b/backend/src/Designer/Infrastructure/AnsattPorten/AnsattPortenExtensions.cs index 8cfdfcc3881..073da38601c 100644 --- a/backend/src/Designer/Infrastructure/AnsattPorten/AnsattPortenExtensions.cs +++ b/backend/src/Designer/Infrastructure/AnsattPorten/AnsattPortenExtensions.cs @@ -75,8 +75,7 @@ private static IServiceCollection AddAnsattPortenAuthentication(this IServiceCol options.Events.OnRedirectToIdentityProvider = context => { - if (!context.Request.Path.StartsWithSegments("/designer/api") || - !context.Request.Path.Value!.Contains("/maskinporten")) + if (!context.Request.Path.StartsWithSegments("/designer/api/ansattporten/login")) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; context.HandleResponse(); diff --git a/backend/src/Designer/Models/Dto/AuthStatus.cs b/backend/src/Designer/Models/Dto/AuthStatus.cs new file mode 100644 index 00000000000..3ff49e8f39b --- /dev/null +++ b/backend/src/Designer/Models/Dto/AuthStatus.cs @@ -0,0 +1,6 @@ +namespace Altinn.Studio.Designer.Models.Dto; + +public class AuthStatus +{ + public bool IsLoggedIn { get; set; } +} From 383c4b2cd04c663b4b60011727ba23a1507eeaf1 Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Tue, 19 Nov 2024 12:05:55 +0100 Subject: [PATCH 051/126] rename redirect to param --- backend/src/Designer/Controllers/AnsattPortenController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/Designer/Controllers/AnsattPortenController.cs b/backend/src/Designer/Controllers/AnsattPortenController.cs index a92945661fb..8feff836cf4 100644 --- a/backend/src/Designer/Controllers/AnsattPortenController.cs +++ b/backend/src/Designer/Controllers/AnsattPortenController.cs @@ -15,7 +15,7 @@ public class AnsattPortenController(IAuthenticationService authService) : Contro { [Authorize(AnsattPortenConstants.AnsattportenAuthorizationPolicy)] [HttpGet("login")] - public async Task Login([FromQuery] string redirectTo) + public async Task Login([FromQuery(Name = "redirect_to")] string redirectTo) { await Task.CompletedTask; if (!Url.IsLocalUrl(redirectTo)) From 7253c5d621c4127ca4281fa24ffa57fe359ce576 Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Tue, 19 Nov 2024 13:24:05 +0100 Subject: [PATCH 052/126] Add tests for Ansattporten controller --- .../Controllers/AnsattPortenController.cs | 1 - .../Controllers/AppScopesController.cs | 1 - .../AnsattPortenController/AuthStatusTests.cs | 81 +++++++++++++++++++ .../AnsattPortenController/LoginTests.cs | 48 +++++++++++ .../Controllers/ApiTests/ApiTestsBase.cs | 6 +- .../Base/AppScopesControllerTestsBase.cs | 24 +++--- .../PreviewController/GetImageTests.cs | 2 +- 7 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 backend/tests/Designer.Tests/Controllers/AnsattPortenController/AuthStatusTests.cs create mode 100644 backend/tests/Designer.Tests/Controllers/AnsattPortenController/LoginTests.cs diff --git a/backend/src/Designer/Controllers/AnsattPortenController.cs b/backend/src/Designer/Controllers/AnsattPortenController.cs index 8feff836cf4..868fbb28240 100644 --- a/backend/src/Designer/Controllers/AnsattPortenController.cs +++ b/backend/src/Designer/Controllers/AnsattPortenController.cs @@ -30,7 +30,6 @@ public async Task Login([FromQuery(Name = "redirect_to")] string [HttpGet("auth-status")] public async Task AuthStatus() { - await Task.CompletedTask; var authenticateResult = await authService.AuthenticateAsync(HttpContext, AnsattPortenConstants.AnsattportenAuthenticationScheme); diff --git a/backend/src/Designer/Controllers/AppScopesController.cs b/backend/src/Designer/Controllers/AppScopesController.cs index 4e8d88e0f87..474157c14da 100644 --- a/backend/src/Designer/Controllers/AppScopesController.cs +++ b/backend/src/Designer/Controllers/AppScopesController.cs @@ -17,7 +17,6 @@ namespace Altinn.Studio.Designer.Controllers; [FeatureGate(StudioFeatureFlags.AnsattPorten)] [Route("designer/api/{org}/{app:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/app-scopes")] - public class AppScopesController(IMaskinPortenHttpClient maskinPortenHttpClient, IAppScopesService appScopesService) : ControllerBase { diff --git a/backend/tests/Designer.Tests/Controllers/AnsattPortenController/AuthStatusTests.cs b/backend/tests/Designer.Tests/Controllers/AnsattPortenController/AuthStatusTests.cs new file mode 100644 index 00000000000..7097a351233 --- /dev/null +++ b/backend/tests/Designer.Tests/Controllers/AnsattPortenController/AuthStatusTests.cs @@ -0,0 +1,81 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Altinn.Studio.Designer.Models.Dto; +using Designer.Tests.Controllers.ApiTests; +using FluentAssertions; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.Mvc.Testing.Handlers; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Designer.Tests.Controllers.AnsattPortenController; + +public class AuthStatusTest : DesignerEndpointsTestsBase, IClassFixture> +{ + private static string VersionPrefix => "/designer/api/ansattporten/auth-status"; + + // Setup unauthenticated http client + protected override HttpClient GetTestClient() + { + string configPath = GetConfigPath(); + IConfiguration configuration = new ConfigurationBuilder() + .AddJsonFile(configPath, false, false) + .AddJsonStream(GenerateJsonOverrideConfig()) + .AddEnvironmentVariables() + .Build(); + + return Factory.WithWebHostBuilder(builder => + { + builder.UseConfiguration(configuration); + builder.ConfigureAppConfiguration((_, conf) => + { + conf.AddJsonFile(configPath); + conf.AddJsonStream(GenerateJsonOverrideConfig()); + }); + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureServices(ConfigureTestServicesForSpecificTest); + }).CreateDefaultClient(new ApiTestsAuthAndCookieDelegatingHandler(), new CookieContainerHandler()); + } + + public AuthStatusTest(WebApplicationFactory factory) : base(factory) + { + } + + [Fact] + public async Task AuthStatus_Should_ReturnFalse_IfNotAuthenticated() + { + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, VersionPrefix); + + using var response = await HttpClient.SendAsync(httpRequestMessage); + response.StatusCode.Should().Be(HttpStatusCode.OK); + + AuthStatus authStatus = await response.Content.ReadAsAsync(); + authStatus.IsLoggedIn.Should().BeFalse(); + } + + [Fact] + public async Task AuthStatus_Should_ReturnTrue_IfAuthenticated() + { + // Setup test authentication + ConfigureTestServicesForSpecificTest = services => + { + services.AddAuthentication(defaultScheme: TestAuthConstants.TestAuthenticationScheme) + .AddScheme( + TestAuthConstants.TestAuthenticationScheme, options => { }); + services.AddTransient(); + }; + + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, VersionPrefix); + + using var response = await HttpClient.SendAsync(httpRequestMessage); + response.StatusCode.Should().Be(HttpStatusCode.OK); + + AuthStatus authStatus = await response.Content.ReadAsAsync(); + authStatus.IsLoggedIn.Should().BeTrue(); + } +} diff --git a/backend/tests/Designer.Tests/Controllers/AnsattPortenController/LoginTests.cs b/backend/tests/Designer.Tests/Controllers/AnsattPortenController/LoginTests.cs new file mode 100644 index 00000000000..1d928cf2b29 --- /dev/null +++ b/backend/tests/Designer.Tests/Controllers/AnsattPortenController/LoginTests.cs @@ -0,0 +1,48 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Altinn.Studio.Designer.Constants; +using Designer.Tests.Controllers.ApiTests; +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; + +namespace Designer.Tests.Controllers.AnsattPortenController; + +public class LoginTests : DesignerEndpointsTestsBase, IClassFixture> +{ + private static string VersionPrefix => "/designer/api/ansattporten/login"; + + public LoginTests(WebApplicationFactory factory) : base(factory) + { + JsonConfigOverrides.Add( + $$""" + { + "FeatureManagement": { + "{{StudioFeatureFlags.AnsattPorten}}": true + }, + "AnsattPortenLoginSettings": { + "ClientId": "non-empty-for-testing", + "ClientSecret": "non-empty-for-testing" + } + } + """); + } + + [Theory] + [InlineData("/test", HttpStatusCode.Redirect)] + [InlineData("/", HttpStatusCode.Redirect)] + [InlineData("https://docs.altinn.studio/", HttpStatusCode.Forbidden)] + public async Task LoginShouldReturn_ExpectedCode(string redirectTo, HttpStatusCode expectedStatusCode) + { + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get + , $"{VersionPrefix}?redirect_to={redirectTo}"); + + using var response = await HttpClient.SendAsync(httpRequestMessage); + Assert.Equal(expectedStatusCode, response.StatusCode); + + if (expectedStatusCode == HttpStatusCode.Redirect) + { + Assert.Equal(redirectTo, response.Headers.Location?.ToString()); + } + } +} diff --git a/backend/tests/Designer.Tests/Controllers/ApiTests/ApiTestsBase.cs b/backend/tests/Designer.Tests/Controllers/ApiTests/ApiTestsBase.cs index 109345e8f49..7b03c34ac9e 100644 --- a/backend/tests/Designer.Tests/Controllers/ApiTests/ApiTestsBase.cs +++ b/backend/tests/Designer.Tests/Controllers/ApiTests/ApiTestsBase.cs @@ -44,7 +44,7 @@ protected HttpClient HttpClient /// protected abstract void ConfigureTestServices(IServiceCollection services); - protected Action ConfigureTestForSpecificTest { get; set; } = delegate { }; + protected Action ConfigureTestServicesForSpecificTest { get; set; } = delegate { }; /// /// Location of the assembly of the executing unit test. @@ -97,7 +97,7 @@ protected virtual HttpClient GetTestClient() TestAuthConstants.TestAuthenticationScheme, options => { }); services.AddTransient(); }); - builder.ConfigureServices(ConfigureTestForSpecificTest); + builder.ConfigureServices(ConfigureTestServicesForSpecificTest); }).CreateDefaultClient(new ApiTestsAuthAndCookieDelegatingHandler(), new CookieContainerHandler()); } @@ -152,7 +152,7 @@ private void InitializeJsonConfigOverrides() } - private Stream GenerateJsonOverrideConfig() + protected Stream GenerateJsonOverrideConfig() { var overrideJson = Newtonsoft.Json.Linq.JObject.Parse(JsonConfigOverrides.First()); if (JsonConfigOverrides.Count > 1) diff --git a/backend/tests/Designer.Tests/Controllers/AppScopesController/Base/AppScopesControllerTestsBase.cs b/backend/tests/Designer.Tests/Controllers/AppScopesController/Base/AppScopesControllerTestsBase.cs index 4e6e468ae06..0398fe48dea 100644 --- a/backend/tests/Designer.Tests/Controllers/AppScopesController/Base/AppScopesControllerTestsBase.cs +++ b/backend/tests/Designer.Tests/Controllers/AppScopesController/Base/AppScopesControllerTestsBase.cs @@ -1,3 +1,4 @@ +using Altinn.Studio.Designer.Constants; using Designer.Tests.Controllers.ApiTests; using Designer.Tests.Fixtures; using Microsoft.AspNetCore.Mvc.Testing; @@ -9,16 +10,17 @@ public class AppScopesControllerTestsBase : DbDesignerEndpoints { public AppScopesControllerTestsBase(WebApplicationFactory factory, DesignerDbFixture designerDbFixture) : base(factory, designerDbFixture) { - JsonConfigOverrides.Add($@" - {{ - ""FeatureManagement"": {{ - ""AnsattPorten"": true - }}, - ""AnsattPortenLoginSettings"": {{ - ""ClientId"": ""non-empty-for-testing"", - ""ClientSecret"": ""non-empty-for-testing"" - }} - }} - "); + JsonConfigOverrides.Add( + $$""" + { + "FeatureManagement": { + "{{StudioFeatureFlags.AnsattPorten}}": true + }, + "AnsattPortenLoginSettings": { + "ClientId": "non-empty-for-testing", + "ClientSecret": "non-empty-for-testing" + } + } + """); } } diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/GetImageTests.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/GetImageTests.cs index afcc096ffcc..e314dfb04bb 100644 --- a/backend/tests/Designer.Tests/Controllers/PreviewController/GetImageTests.cs +++ b/backend/tests/Designer.Tests/Controllers/PreviewController/GetImageTests.cs @@ -84,7 +84,7 @@ public async Task Get_Image_Non_Existing_Image_Return_NotFound() public async Task Call_To_Get_Designer_Iframe_Does_Not_Hit_Image_EndPoint() { Mock factMock = new(); - ConfigureTestForSpecificTest = s => + ConfigureTestServicesForSpecificTest = s => { s.AddTransient(_ => factMock.Object); }; From ee5dfc42352b0b9183dced0f357546b8d63ee636 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:11:43 +0200 Subject: [PATCH 053/126] Resuable storage interface --- .../src/browser-storage/ScopedStorage.test.ts | 119 ++++++++++++++++++ .../src/browser-storage/ScopedStorage.ts | 71 +++++++++++ 2 files changed, 190 insertions(+) create mode 100644 frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts create mode 100644 frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts new file mode 100644 index 00000000000..4a2d8a1f542 --- /dev/null +++ b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts @@ -0,0 +1,119 @@ +import { ScopedStorage, ScopedStorageImpl } from './ScopedStorage'; + +describe('ScopedStorage', () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + describe('add new key', () => { + it('should create a single scoped key with the provided key-value pair as its value', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstName', 'Random Value'); + expect(scopedStorage.getItem('firstName')).toBe('Random Value'); + }); + }); + + describe('get item', () => { + it('should return "null" if key does not exist', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + expect(scopedStorage.getItem('firstName', 'Random Value')).toBeNull(); + }); + }); + + describe('parsing', () => { + const consoleErrorMock = jest.fn(); + const originalConsoleError = console.error; + beforeEach(() => { + console.error = consoleErrorMock; + }); + + afterEach(() => { + console.error = originalConsoleError; + }); + + it('should console.error when parsing the storage fails', () => { + window.localStorage.setItem('unit/test', '{"person";{"name":"tester"}}'); + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + expect(scopedStorage.getItem('person')).toBeNull(); + expect(consoleErrorMock).toHaveBeenCalledWith( + expect.stringContaining( + 'Failed to parse storage with key unit/test. Ensure that the storage is a valid JSON string. Error: SyntaxError:', + ), + ); + }); + }); + + describe('update existing key', () => { + it('should append a new key-value pair to the existing scoped key', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstKey', 'first value'); + scopedStorage.setItem('secondKey', 'secondValue'); + + expect(scopedStorage.getItem('firstKey')).toBe('first value'); + expect(scopedStorage.getItem('secondKey')).toBe('secondValue'); + }); + + it('should update the value of an existing key-value pair within the scoped key if the value has changed', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstKey', 'first value'); + scopedStorage.setItem('firstKey', 'first value is updated'); + expect(scopedStorage.getItem('firstKey')).toBe('first value is updated'); + }); + }); + + describe('delete values from key', () => { + it('should remove a specific key-value pair from the existing scoped key', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstKey', 'first value'); + expect(scopedStorage.getItem('firstKey')).toBeDefined(); + + scopedStorage.removeItem('firstKey'); + expect(scopedStorage.getItem('firstKey')).toBeUndefined(); + }); + + it('should not remove key if it does not exist', () => { + const removeItemMock = jest.fn(); + const customStorage = { + getItem: jest.fn().mockImplementation(() => null), + removeItem: removeItemMock, + }; + + const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); + scopedStorage.removeItem('keyDoesNotExist'); + + expect(removeItemMock).not.toHaveBeenCalled(); + }); + }); + + // Verify that Dependency Inversion works as expected + describe('when using localStorage', () => { + it('should store and retrieve values using localStorage', () => { + const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'local/storage'); + scopedStorage.setItem('firstNameInSession', 'Random Session Value'); + expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); + }); + }); + + describe('when using sessionStorage', () => { + it('should store and retrieve values using sessionStorage', () => { + const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'session/storage'); + scopedStorage.setItem('firstNameInSession', 'Random Session Value'); + expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); + }); + }); + + describe('when using a custom storage implementation', () => { + it('should store and retrieve values using the provided custom storage', () => { + const setItemMock = jest.fn(); + + const customStorage: ScopedStorage = { + setItem: setItemMock, + getItem: jest.fn(), + removeItem: jest.fn(), + }; + + const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); + scopedStorage.setItem('testKey', 'testValue'); + }); + }); +}); diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts new file mode 100644 index 00000000000..7acab02c9e2 --- /dev/null +++ b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts @@ -0,0 +1,71 @@ +type StorageKey = string; + +export interface ScopedStorage extends Pick {} + +export class ScopedStorageImpl implements ScopedStorage { + private readonly storageKey: StorageKey; + private readonly scopedStorage: ScopedStorage; + + constructor( + private storage: ScopedStorage, + private key: StorageKey, + ) { + this.storageKey = this.key; + this.scopedStorage = this.storage; + } + + public setItem(key: string, value: T): void { + const storageRecords: T = this.getAllRecordsInStorage(); + this.saveToStorage( + JSON.stringify({ + ...storageRecords, + [key]: value, + }), + ); + } + + public getItem(key: keyof T): T { + const records: T = this.getAllRecordsInStorage(); + + if (!records) { + return null; + } + + return records[key] as unknown as T; + } + + public removeItem(key: keyof T): void { + const storageRecords: T | null = this.getAllRecordsInStorage(); + + if (!storageRecords) { + return; + } + + const storageCopy = { ...storageRecords }; + delete storageCopy[key]; + this.saveToStorage(JSON.stringify({ ...storageCopy })); + } + + private getAllRecordsInStorage(): T | null { + return this.parseStorageData(this.scopedStorage.getItem(this.storageKey)); + } + + private saveToStorage(value: string) { + this.storage.setItem(this.storageKey, value); + } + + private parseStorageData(storage: string | null): T | null { + if (!storage) { + return null; + } + + try { + return JSON.parse(storage) satisfies T; + } catch (error) { + console.error( + `Failed to parse storage with key ${this.storageKey}. Ensure that the storage is a valid JSON string. Error: ${error}`, + ); + return null; + } + } +} From 8ec2c0247b229aba7b6f43d7cf4c273fcbdbe562 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:13:01 +0200 Subject: [PATCH 054/126] moved the reusable scoped storage to pure-funciton --- .../src/browser-storage/ScopedStorage.test.ts | 119 ------------------ .../src/browser-storage/ScopedStorage.ts | 71 ----------- 2 files changed, 190 deletions(-) delete mode 100644 frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts delete mode 100644 frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts deleted file mode 100644 index 4a2d8a1f542..00000000000 --- a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { ScopedStorage, ScopedStorageImpl } from './ScopedStorage'; - -describe('ScopedStorage', () => { - beforeEach(() => { - window.localStorage.clear(); - }); - - describe('add new key', () => { - it('should create a single scoped key with the provided key-value pair as its value', () => { - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - scopedStorage.setItem('firstName', 'Random Value'); - expect(scopedStorage.getItem('firstName')).toBe('Random Value'); - }); - }); - - describe('get item', () => { - it('should return "null" if key does not exist', () => { - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - expect(scopedStorage.getItem('firstName', 'Random Value')).toBeNull(); - }); - }); - - describe('parsing', () => { - const consoleErrorMock = jest.fn(); - const originalConsoleError = console.error; - beforeEach(() => { - console.error = consoleErrorMock; - }); - - afterEach(() => { - console.error = originalConsoleError; - }); - - it('should console.error when parsing the storage fails', () => { - window.localStorage.setItem('unit/test', '{"person";{"name":"tester"}}'); - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - expect(scopedStorage.getItem('person')).toBeNull(); - expect(consoleErrorMock).toHaveBeenCalledWith( - expect.stringContaining( - 'Failed to parse storage with key unit/test. Ensure that the storage is a valid JSON string. Error: SyntaxError:', - ), - ); - }); - }); - - describe('update existing key', () => { - it('should append a new key-value pair to the existing scoped key', () => { - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - scopedStorage.setItem('firstKey', 'first value'); - scopedStorage.setItem('secondKey', 'secondValue'); - - expect(scopedStorage.getItem('firstKey')).toBe('first value'); - expect(scopedStorage.getItem('secondKey')).toBe('secondValue'); - }); - - it('should update the value of an existing key-value pair within the scoped key if the value has changed', () => { - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - scopedStorage.setItem('firstKey', 'first value'); - scopedStorage.setItem('firstKey', 'first value is updated'); - expect(scopedStorage.getItem('firstKey')).toBe('first value is updated'); - }); - }); - - describe('delete values from key', () => { - it('should remove a specific key-value pair from the existing scoped key', () => { - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - scopedStorage.setItem('firstKey', 'first value'); - expect(scopedStorage.getItem('firstKey')).toBeDefined(); - - scopedStorage.removeItem('firstKey'); - expect(scopedStorage.getItem('firstKey')).toBeUndefined(); - }); - - it('should not remove key if it does not exist', () => { - const removeItemMock = jest.fn(); - const customStorage = { - getItem: jest.fn().mockImplementation(() => null), - removeItem: removeItemMock, - }; - - const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); - scopedStorage.removeItem('keyDoesNotExist'); - - expect(removeItemMock).not.toHaveBeenCalled(); - }); - }); - - // Verify that Dependency Inversion works as expected - describe('when using localStorage', () => { - it('should store and retrieve values using localStorage', () => { - const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'local/storage'); - scopedStorage.setItem('firstNameInSession', 'Random Session Value'); - expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); - }); - }); - - describe('when using sessionStorage', () => { - it('should store and retrieve values using sessionStorage', () => { - const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'session/storage'); - scopedStorage.setItem('firstNameInSession', 'Random Session Value'); - expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); - }); - }); - - describe('when using a custom storage implementation', () => { - it('should store and retrieve values using the provided custom storage', () => { - const setItemMock = jest.fn(); - - const customStorage: ScopedStorage = { - setItem: setItemMock, - getItem: jest.fn(), - removeItem: jest.fn(), - }; - - const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); - scopedStorage.setItem('testKey', 'testValue'); - }); - }); -}); diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts deleted file mode 100644 index 7acab02c9e2..00000000000 --- a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts +++ /dev/null @@ -1,71 +0,0 @@ -type StorageKey = string; - -export interface ScopedStorage extends Pick {} - -export class ScopedStorageImpl implements ScopedStorage { - private readonly storageKey: StorageKey; - private readonly scopedStorage: ScopedStorage; - - constructor( - private storage: ScopedStorage, - private key: StorageKey, - ) { - this.storageKey = this.key; - this.scopedStorage = this.storage; - } - - public setItem(key: string, value: T): void { - const storageRecords: T = this.getAllRecordsInStorage(); - this.saveToStorage( - JSON.stringify({ - ...storageRecords, - [key]: value, - }), - ); - } - - public getItem(key: keyof T): T { - const records: T = this.getAllRecordsInStorage(); - - if (!records) { - return null; - } - - return records[key] as unknown as T; - } - - public removeItem(key: keyof T): void { - const storageRecords: T | null = this.getAllRecordsInStorage(); - - if (!storageRecords) { - return; - } - - const storageCopy = { ...storageRecords }; - delete storageCopy[key]; - this.saveToStorage(JSON.stringify({ ...storageCopy })); - } - - private getAllRecordsInStorage(): T | null { - return this.parseStorageData(this.scopedStorage.getItem(this.storageKey)); - } - - private saveToStorage(value: string) { - this.storage.setItem(this.storageKey, value); - } - - private parseStorageData(storage: string | null): T | null { - if (!storage) { - return null; - } - - try { - return JSON.parse(storage) satisfies T; - } catch (error) { - console.error( - `Failed to parse storage with key ${this.storageKey}. Ensure that the storage is a valid JSON string. Error: ${error}`, - ); - return null; - } - } -} From 271b722655f37b9dfce7a834067096b595d3a812 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 055/126] feat: context based login with ansattporten --- .../components/Tabs/Maskinporten/Maskinporten.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx new file mode 100644 index 00000000000..3319082978d --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -0,0 +1,5 @@ +describe('Maskinporten', () => { + it('should check and verify if the user is logged in', () => {}); + it('should display information about login, if user is not logged in', () => {}); + it('should display login with ansattporten if user is not logged in', () => {}); +}); From 6850d92e919edeacb87518895bf6ba35c6560afe Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 056/126] feat: context based login with ansattporten --- .../SettingsModal/SettingsModal.tsx | 4 +++ .../Tabs/Maskinporten/Maskinporten.tsx | 25 +++++++++++++++++++ .../hooks/useSettingsModalMenuTabConfigs.tsx | 7 ++++++ .../types/SettingsModalTabId.ts | 2 +- frontend/language/src/nb.json | 4 +++ frontend/packages/shared/src/api/paths.js | 16 ++++++++++-- 6 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index 491acf72e32..a0fc88c8917 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -11,6 +11,7 @@ import { AccessControlTab } from './components/Tabs/AccessControlTab'; import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; +import { Maskinporten } from './components/Tabs/Maskinporten/Maskinporten'; export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); @@ -45,6 +46,9 @@ export const SettingsModal = forwardRef(({}, ref): Reac case 'access_control': { return ; } + case 'maskinporten': { + return ; + } } }; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx new file mode 100644 index 00000000000..c646b4355aa --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { TabContent } from '../../TabContent'; +import { ansattportenLoginPath } from 'app-shared/api/paths'; +import { StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; +import { useTranslation } from 'react-i18next'; + +export const Maskinporten = (): React.ReactElement => { + const { t } = useTranslation(); + + const handleLoginWithAnsattporten = (): void => { + window.location.href = ansattportenLoginPath(); + }; + + return ( + + + {t('settings_modal.maskinporten_tab_title')} + + {t('settings_modal.maskinporten_tab_description')} + + {t('settings_modal.maskinporten_tab_login_with_ansattporten')} + + + ); +}; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index adddd53110d..263242e5485 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -5,6 +5,7 @@ import { SidebarBothIcon, ShieldLockIcon, TimerStartIcon, + CogIcon, } from '@studio/icons'; import { useTranslation } from 'react-i18next'; import type { StudioContentMenuButtonTabProps } from '@studio/components'; @@ -13,6 +14,7 @@ const aboutTabId: SettingsModalTabId = 'about'; const setupTabId: SettingsModalTabId = 'setup'; const policyTabId: SettingsModalTabId = 'policy'; const accessControlTabId: SettingsModalTabId = 'access_control'; +const maskinportenTabId: SettingsModalTabId = 'maskinporten'; export const allSettingsModalTabs: Array = [ aboutTabId, @@ -46,5 +48,10 @@ export const useSettingsModalMenuTabConfigs = tabName: t(`settings_modal.left_nav_tab_${accessControlTabId}`), icon: , }, + { + tabId: maskinportenTabId, + tabName: t(`settings_modal.left_nav_tab_${maskinportenTabId}`), + icon: , + }, ]; }; diff --git a/frontend/app-development/types/SettingsModalTabId.ts b/frontend/app-development/types/SettingsModalTabId.ts index c596b1e0d5b..3f65b015eae 100644 --- a/frontend/app-development/types/SettingsModalTabId.ts +++ b/frontend/app-development/types/SettingsModalTabId.ts @@ -1 +1 @@ -export type SettingsModalTabId = 'about' | 'setup' | 'policy' | 'access_control'; +export type SettingsModalTabId = 'about' | 'setup' | 'policy' | 'access_control' | 'maskinporten'; diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index d534e2dc7a0..bd534edff9d 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -1001,9 +1001,13 @@ "settings_modal.heading": "Innstillinger", "settings_modal.left_nav_tab_about": "Om appen", "settings_modal.left_nav_tab_access_control": "Oppstartskontroll", + "settings_modal.left_nav_tab_maskinporten": "Maskinporten", "settings_modal.left_nav_tab_policy": "Tilganger", "settings_modal.left_nav_tab_setup": "Oppsett", "settings_modal.loading_content": "Laster inn data", + "settings_modal.maskinporten_tab_description": "For å hente tilgjengelige scopes må du verifisere deg med Ansattporten", + "settings_modal.maskinporten_tab_login_with_ansattporten": "Logg inn med Ansattporten", + "settings_modal.maskinporten_tab_title": "Håndtering av Scopes fra Maskinporten", "settings_modal.policy_tab_heading": "Tilganger", "settings_modal.setup_tab_heading": "Oppsett", "settings_modal.setup_tab_switch_autoDeleteOnProcessEnd": "Automatisk sletting etter innsending", diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 0f3c86db231..ad102961501 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete @@ -16,7 +19,11 @@ export const serviceConfigPath = (org, app) => `${basePath}/${org}/${app}/config // DataModel export const createDataModelPath = (org, app) => `${basePath}/${org}/${app}/datamodels/new`; // Post -export const dataModelPath = (org, app, modelPath, saveOnly = false) => `${basePath}/${org}/${app}/datamodels/datamodel?${s({ modelPath, saveOnly })}`; // Get, Put, Delete +export const dataModelPath = (org, app, modelPath, saveOnly = false) => + `${basePath}/${org}/${app}/datamodels/datamodel?${s({ + modelPath, + saveOnly, + })}`; // Get, Put, Delete export const dataModelsPath = (org, app) => `${basePath}/${org}/${app}/datamodels/all-json`; // Get export const dataModelsXsdPath = (org, app) => `${basePath}/${org}/${app}/datamodels/all-xsd`; // Get export const dataModelsUploadPath = (org, app) => `${basePath}/${org}/${app}/datamodels/upload`; // Post @@ -89,7 +96,12 @@ export const envConfigPath = () => `${basePath}/environments`; export const abortmergePath = (org, app) => `${basePath}/repos/repo/${org}/${app}/abort-merge`; export const branchStatusPath = (org, app, branch) => `${basePath}/repos/repo/${org}/${app}/branches/branch?${s({ branch })}`; // Get export const cloneAppPath = (org, app) => `${basePath}/repos/repo/${org}/${app}/clone`; // Get -export const copyAppPath = (org, sourceRepository, targetRepository, targetOrg) => `${basePath}/repos/repo/${org}/copy-app?${s({ sourceRepository, targetRepository, targetOrg })}`; +export const copyAppPath = (org, sourceRepository, targetRepository, targetOrg) => + `${basePath}/repos/repo/${org}/copy-app?${s({ + sourceRepository, + targetRepository, + targetOrg, + })}`; export const createRepoPath = () => `${basePath}/repos/create-app`; // Post export const discardChangesPath = (org, app) => `${basePath}/repos/repo/${org}/${app}/discard`; // Get export const discardFileChangesPath = (org, app, filename) => `${basePath}/repos/repo/${org}/${app}/discard/${filename}`; // Get From 65c62465d5879c5545c10a172bae62e3e32d270d Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 057/126] frontend with mocks for maskinporten --- .../useIsLoggedInWithAnsattportenQuery.ts | 11 +++ .../Tabs/Maskinporten/Maskinporten.test.tsx | 98 ++++++++++++++++++- .../Tabs/Maskinporten/Maskinporten.tsx | 19 +++- .../hooks/useSettingsModalMenuTabConfigs.tsx | 1 + frontend/packages/shared/src/api/paths.js | 3 - frontend/packages/shared/src/api/queries.ts | 8 ++ .../packages/shared/src/mocks/queriesMock.ts | 3 + .../packages/shared/src/types/QueryKey.ts | 1 + 8 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts diff --git a/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts b/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts new file mode 100644 index 00000000000..b40dfd28ad7 --- /dev/null +++ b/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { QueryKey } from 'app-shared/types/QueryKey'; +import { useServicesContext } from 'app-shared/contexts/ServicesContext'; + +export const useIsLoggedInWithAnsattportenQuery = () => { + const { getIsLoggedInWithAnsattporten } = useServicesContext(); + return useQuery({ + queryKey: [QueryKey.IsLoggedInWithAnsattporten], + queryFn: () => getIsLoggedInWithAnsattporten(), + }); +}; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx index 3319082978d..747390d879c 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -1,5 +1,97 @@ +import React from 'react'; +import { screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import { Maskinporten } from './Maskinporten'; +import { renderWithProviders } from '../../../../../../../../test/mocks'; +import { textMock } from '@studio/testing/mocks/i18nMock'; +import { queriesMock } from 'app-shared/mocks/queriesMock'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; +import userEvent from '@testing-library/user-event'; + describe('Maskinporten', () => { - it('should check and verify if the user is logged in', () => {}); - it('should display information about login, if user is not logged in', () => {}); - it('should display login with ansattporten if user is not logged in', () => {}); + const consoleLogMock = jest.fn(); + const originalConsoleLog = console.log; + beforeEach(() => { + console.log = consoleLogMock; + }); + + afterEach(() => { + console.log = originalConsoleLog; + }); + + it('should check and verify if the user is logged in', async () => { + const getIsLoggedInWithAnsattportenMock = jest + .fn() + .mockImplementation(() => Promise.resolve(false)); + + renderMaskinporten({ + queries: { + getIsLoggedInWithAnsattporten: getIsLoggedInWithAnsattportenMock, + }, + }); + await waitForLoggedInStatusCheckIsDone(); + await waitFor(() => expect(getIsLoggedInWithAnsattportenMock).toHaveBeenCalledTimes(1)); + }); + + it('should display information about login and login button, if user is not logged in', async () => { + renderMaskinporten({}); + await waitForLoggedInStatusCheckIsDone(); + + const title = screen.getByRole('heading', { + level: 2, + name: textMock('settings_modal.maskinporten_tab_title'), + }); + expect(title).toBeInTheDocument(); + + const description = screen.getByText(textMock('settings_modal.maskinporten_tab_description')); + expect(description).toBeInTheDocument(); + + const loginButton = screen.getByRole('button', { + name: textMock('settings_modal.maskinporten_tab_login_with_ansattporten'), + }); + expect(loginButton).toBeInTheDocument(); + }); + + it('should display content if logged in', async () => { + const getIsLoggedInWithAnsattportenMock = jest + .fn() + .mockImplementation(() => Promise.resolve(true)); + renderMaskinporten({ + queries: { + getIsLoggedInWithAnsattporten: getIsLoggedInWithAnsattportenMock, + }, + }); + + await waitForLoggedInStatusCheckIsDone(); + + const temporaryLoggedInContent = screen.getByText('View when logged in comes here'); + expect(temporaryLoggedInContent).toBeInTheDocument(); + }); + + it('should invoke "handleLoginWithAnsattPorten" when login button is clicked', async () => { + const user = userEvent.setup(); + renderMaskinporten({}); + await waitForLoggedInStatusCheckIsDone(); + + const loginButton = screen.getByRole('button', { + name: textMock('settings_modal.maskinporten_tab_login_with_ansattporten'), + }); + + await user.click(loginButton); + + expect(consoleLogMock).toHaveBeenCalledWith( + 'Will be implemented in next iteration when backend is ready', + ); + }); }); + +type RenderMaskinporten = { + queries?: Partial; +}; +const renderMaskinporten = ({ queries = queriesMock }: RenderMaskinporten) => { + const queryClient = createQueryClientMock(); + renderWithProviders({ ...queriesMock, ...queries }, queryClient)(); +}; + +async function waitForLoggedInStatusCheckIsDone() { + await waitForElementToBeRemoved(() => screen.getByTitle(textMock('general.loading'))); +} diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx index c646b4355aa..cb3b33d5e1e 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx @@ -1,16 +1,27 @@ import React from 'react'; -import { TabContent } from '../../TabContent'; -import { ansattportenLoginPath } from 'app-shared/api/paths'; -import { StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; import { useTranslation } from 'react-i18next'; +import { TabContent } from '../../TabContent'; +import { StudioButton, StudioHeading, StudioParagraph, StudioSpinner } from '@studio/components'; +import { useIsLoggedInWithAnsattportenQuery } from '../../../../../../../../hooks/queries/useIsLoggedInWithAnsattportenQuery'; export const Maskinporten = (): React.ReactElement => { + const { data: isLoggedInWithAnsattporten, isPending: isPendingAuthStatus } = + useIsLoggedInWithAnsattportenQuery(); + const { t } = useTranslation(); const handleLoginWithAnsattporten = (): void => { - window.location.href = ansattportenLoginPath(); + console.log('Will be implemented in next iteration when backend is ready'); }; + if (isPendingAuthStatus) { + return ; + } + + if (isLoggedInWithAnsattporten) { + return
View when logged in comes here
; + } + return ( diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index 263242e5485..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -21,6 +21,7 @@ export const allSettingsModalTabs: Array = [ setupTabId, policyTabId, accessControlTabId, + maskinportenTabId, ]; export const useSettingsModalMenuTabConfigs = diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index ad102961501..545ac846aaf 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete diff --git a/frontend/packages/shared/src/api/queries.ts b/frontend/packages/shared/src/api/queries.ts index c7d1ded93f8..6e96c93103b 100644 --- a/frontend/packages/shared/src/api/queries.ts +++ b/frontend/packages/shared/src/api/queries.ts @@ -54,6 +54,7 @@ import { getImageFileNamesPath, validateImageFromExternalUrlPath, } from './paths'; + import type { AppReleasesResponse, DataModelMetadataResponse, SearchRepoFilterParams, SearchRepositoryResponse } from 'app-shared/types/api'; import type { DeploymentsResponse } from 'app-shared/types/api/DeploymentsResponse'; import type { BranchStatus } from 'app-shared/types/BranchStatus'; @@ -84,6 +85,13 @@ import type { RepoDiffResponse } from 'app-shared/types/api/RepoDiffResponse'; import type { ExternalImageUrlValidationResponse } from 'app-shared/types/api/ExternalImageUrlValidationResponse'; import type { OptionsLists } from 'app-shared/types/api/OptionsLists'; +export const getIsLoggedInWithAnsattporten = async (): Promise => + // TODO: replace with endpoint when it's ready in the backend. + new Promise((resolve) => { + setTimeout(() => { + return resolve(false); + }, 1000); + }); export const getAppMetadataModelIds = (org: string, app: string, onlyUnReferenced: boolean) => get(appMetadataModelIdsPath(org, app, onlyUnReferenced)); export const getAppReleases = (owner: string, app: string) => get(releasesPath(owner, app, 'Descending')); export const getAppVersion = (org: string, app: string) => get(appVersionPath(org, app)); diff --git a/frontend/packages/shared/src/mocks/queriesMock.ts b/frontend/packages/shared/src/mocks/queriesMock.ts index 142f5ddde67..3ef7db6eae9 100644 --- a/frontend/packages/shared/src/mocks/queriesMock.ts +++ b/frontend/packages/shared/src/mocks/queriesMock.ts @@ -169,6 +169,9 @@ export const queriesMock: ServicesContextProps = { // Queries - PrgetBpmnFile getBpmnFile: jest.fn().mockImplementation(() => Promise.resolve('')), getProcessTaskType: jest.fn().mockImplementation(() => Promise.resolve('')), + getIsLoggedInWithAnsattporten: jest + .fn() + .mockImplementation(() => Promise.resolve(false)), // Mutations addAppAttachmentMetadata: jest.fn().mockImplementation(() => Promise.resolve()), diff --git a/frontend/packages/shared/src/types/QueryKey.ts b/frontend/packages/shared/src/types/QueryKey.ts index f3542c6f927..5f164fe6843 100644 --- a/frontend/packages/shared/src/types/QueryKey.ts +++ b/frontend/packages/shared/src/types/QueryKey.ts @@ -44,6 +44,7 @@ export enum QueryKey { TextResources = 'TextResources', Widgets = 'Widgets', AppConfig = 'AppConfig', + IsLoggedInWithAnsattporten = 'IsLoggedInWithAnsattporten', // Resourceadm ResourceList = 'ResourceList', From 2b6a2f6e434c6e90f2b8ad6f3f79b5bd9b2ea630 Mon Sep 17 00:00:00 2001 From: David Ovrelid <46874830+framitdavid@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:28:57 +0100 Subject: [PATCH 058/126] Update frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx Co-authored-by: William Thorenfeldt <48119543+wrt95@users.noreply.github.com> --- .../components/Tabs/Maskinporten/Maskinporten.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx index 747390d879c..25ed04969e6 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -93,5 +93,5 @@ const renderMaskinporten = ({ queries = queriesMock }: RenderMaskinporten) => { }; async function waitForLoggedInStatusCheckIsDone() { - await waitForElementToBeRemoved(() => screen.getByTitle(textMock('general.loading'))); + await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); } From 40e54369fd9a81a4a8b872af5f937844b699146a Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 08:30:32 +0100 Subject: [PATCH 059/126] PR feedback --- .../SettingsModalButton/SettingsModal/SettingsModal.tsx | 2 +- .../SettingsModal/components/Tabs/Maskinporten/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index a0fc88c8917..77e3eaec68a 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -11,7 +11,7 @@ import { AccessControlTab } from './components/Tabs/AccessControlTab'; import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; -import { Maskinporten } from './components/Tabs/Maskinporten/Maskinporten'; +import { Maskinporten } from './components/Tabs/Maskinporten'; export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts new file mode 100644 index 00000000000..371b8f1a173 --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/index.ts @@ -0,0 +1 @@ +export { Maskinporten } from './Maskinporten'; From fcebffab1117ad54cb9c01f4715eaf10254ad24a Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:20:24 +0100 Subject: [PATCH 060/126] fixes --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index d4420a5c5f8..cec49e79771 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,6 +24,13 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; +export const allSettingsModalTabs: Array = [ + aboutTabId, + setupTabId, + policyTabId, + accessControlTabId, +]; + export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); From 45e5f437dc08f7cc04bccbbfa58173df11ee19c5 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 061/126] feat: context based login with ansattporten --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 ------- frontend/packages/shared/src/api/paths.js | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index cec49e79771..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,13 +24,6 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; -export const allSettingsModalTabs: Array = [ - aboutTabId, - setupTabId, - policyTabId, - accessControlTabId, -]; - export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 545ac846aaf..ad102961501 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 7e3c4203854ee28b5d482107207dd713d60450a8 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 062/126] frontend with mocks for maskinporten --- frontend/packages/shared/src/api/paths.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index ad102961501..545ac846aaf 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 5596aa028815dc51e3d6d1f9a074fe5f6d812781 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 15:40:41 +0100 Subject: [PATCH 063/126] hide maskinporten behind featureflag --- .../SettingsModal/SettingsModal.test.tsx | 21 +++++++++++++++++++ .../SettingsModal/SettingsModal.tsx | 21 ++++++++++++++++--- .../shared/src/utils/featureToggleUtils.ts | 3 ++- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.test.tsx index 237a8e6d81d..431bdb49396 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.test.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.test.tsx @@ -14,6 +14,7 @@ import { MemoryRouter } from 'react-router-dom'; import { SettingsModalContextProvider } from 'app-development/contexts/SettingsModalContext'; import { PreviewContextProvider } from 'app-development/contexts/PreviewContext'; import type { SettingsModalHandle } from 'app-development/types/SettingsModalHandle'; +import { typedLocalStorage } from '@studio/pure-functions'; jest.mock('app-development/hooks/mutations/useAppConfigMutation'); @@ -38,6 +39,26 @@ describe('SettingsModal', () => { await user.click(closeButton); }); + it('should hide the "Maskinporten" tab when the feature flag is not enabled', async () => { + await resolveAndWaitForSpinnerToDisappear(); + const maskinPortenTab = screen.queryByRole('tab', { + name: textMock('settings_modal.left_nav_tab_maskinporten'), + }); + + expect(maskinPortenTab).not.toBeInTheDocument(); + }); + + it('should display the "Maskinporten" tab when the feature flag is enabled.', async () => { + typedLocalStorage.setItem('featureFlags', ['maskinporten']); + await resolveAndWaitForSpinnerToDisappear(); + const maskinPortenTab = screen.getByRole('tab', { + name: textMock('settings_modal.left_nav_tab_maskinporten'), + }); + + expect(maskinPortenTab).toBeInTheDocument(); + typedLocalStorage.removeItem('featureFlags'); + }); + it('displays left navigation bar when promises resolve', async () => { await resolveAndWaitForSpinnerToDisappear(); expect(getAboutTab()).toBeInTheDocument(); diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index 77e3eaec68a..cf03a88d213 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -2,7 +2,11 @@ import type { ReactElement } from 'react'; import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react'; import classes from './SettingsModal.module.css'; import { CogIcon } from '@studio/icons'; -import { StudioModal, StudioContentMenu } from '@studio/components'; +import { + StudioModal, + StudioContentMenu, + StudioContentMenuButtonTabProps, +} from '@studio/components'; import type { SettingsModalTabId } from '../../../../../types/SettingsModalTabId'; import { useTranslation } from 'react-i18next'; import { PolicyTab } from './components/Tabs/PolicyTab'; @@ -12,6 +16,7 @@ import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; import { Maskinporten } from './components/Tabs/Maskinporten'; +import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); @@ -20,6 +25,8 @@ export const SettingsModal = forwardRef(({}, ref): Reac const dialogRef = useRef(); const menuTabConfigs = useSettingsModalMenuTabConfigs(); + const menuTabsToRender = filterFeatureFlag(menuTabConfigs); + const openSettings = useCallback( (tab: SettingsModalTabId = currentTab) => { setCurrentTab(tab); @@ -47,7 +54,7 @@ export const SettingsModal = forwardRef(({}, ref): Reac return ; } case 'maskinporten': { - return ; + return shouldDisplayFeature('maskinporten') ? : null; } } }; @@ -67,7 +74,7 @@ export const SettingsModal = forwardRef(({}, ref): Reac selectedTabId={currentTab} onChangeTab={(tabId: SettingsModalTabId) => setCurrentTab(tabId)} > - {menuTabConfigs.map((contentTab) => ( + {menuTabsToRender.map((contentTab) => ( (({}, ref): Reac }); SettingsModal.displayName = 'SettingsModal'; + +function filterFeatureFlag( + menuTabConfigs: Array>, +) { + return shouldDisplayFeature('maskinporten') + ? menuTabConfigs + : menuTabConfigs.filter((tab) => tab.tabId !== 'maskinporten'); +} diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.ts b/frontend/packages/shared/src/utils/featureToggleUtils.ts index 9522a3ab11e..52bb2f5a212 100644 --- a/frontend/packages/shared/src/utils/featureToggleUtils.ts +++ b/frontend/packages/shared/src/utils/featureToggleUtils.ts @@ -13,7 +13,8 @@ export type SupportedFeatureFlags = | 'addComponentModal' | 'subform' | 'summary2' - | 'optionListEditor'; + | 'codeListEditor' + | 'maskinporten'; /* * Please add all the features that you want to be toggle on by default here. From e7ab2764a5680cec0ab89f346362f60039f9a12d Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Tue, 12 Nov 2024 12:26:09 +0100 Subject: [PATCH 064/126] chore: scale designer in dev and staging (#14039) --- .github/workflows/deploy-designer.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/deploy-designer.yaml b/.github/workflows/deploy-designer.yaml index 34e66fe2784..6d71e0b2e67 100644 --- a/.github/workflows/deploy-designer.yaml +++ b/.github/workflows/deploy-designer.yaml @@ -106,7 +106,11 @@ jobs: artifact-environment: ${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }} config-chart-name: altinn-designer-config artifact-name: altinn-designer +<<<<<<< HEAD helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'dev' && 1 || 2 }} +======= + helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'preapproved-prod' && 1 || 2 }} +>>>>>>> 70f1511ca (chore: scale designer in dev and staging (#14039)) trace-workflow: true trace-team-name: 'team-studio' secrets: From aafdaf3c3c839a52034dc61d0c9adf04749cab89 Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Wed, 13 Nov 2024 14:43:32 +0100 Subject: [PATCH 065/126] feat: 2 replicas in prod/staging, 1 in dev (#14047) --- .github/workflows/deploy-designer.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/deploy-designer.yaml b/.github/workflows/deploy-designer.yaml index 6d71e0b2e67..34e66fe2784 100644 --- a/.github/workflows/deploy-designer.yaml +++ b/.github/workflows/deploy-designer.yaml @@ -106,11 +106,7 @@ jobs: artifact-environment: ${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }} config-chart-name: altinn-designer-config artifact-name: altinn-designer -<<<<<<< HEAD helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'dev' && 1 || 2 }} -======= - helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'preapproved-prod' && 1 || 2 }} ->>>>>>> 70f1511ca (chore: scale designer in dev and staging (#14039)) trace-workflow: true trace-team-name: 'team-studio' secrets: From e5b8e022c9dbd0c9e3b17b6d49f9ed22ae070f9d Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 15:44:43 +0100 Subject: [PATCH 066/126] eslint --- .../SettingsModalButton/SettingsModal/SettingsModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index cf03a88d213..c5dad2b42fe 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -5,7 +5,7 @@ import { CogIcon } from '@studio/icons'; import { StudioModal, StudioContentMenu, - StudioContentMenuButtonTabProps, + type StudioContentMenuButtonTabProps, } from '@studio/components'; import type { SettingsModalTabId } from '../../../../../types/SettingsModalTabId'; import { useTranslation } from 'react-i18next'; From 685e8fc319e7b949bb6ce5566f68ab04b603fe07 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 15:50:55 +0100 Subject: [PATCH 067/126] fixed issue after merge --- frontend/packages/shared/src/utils/featureToggleUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.ts b/frontend/packages/shared/src/utils/featureToggleUtils.ts index 52bb2f5a212..6314d0254d2 100644 --- a/frontend/packages/shared/src/utils/featureToggleUtils.ts +++ b/frontend/packages/shared/src/utils/featureToggleUtils.ts @@ -14,6 +14,7 @@ export type SupportedFeatureFlags = | 'subform' | 'summary2' | 'codeListEditor' + | 'optionListEditor' | 'maskinporten'; /* From 486eb78b9d38a9832b2f97ad5265d5c1db3d0cc8 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:20:24 +0100 Subject: [PATCH 068/126] fixes --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index d4420a5c5f8..cec49e79771 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,6 +24,13 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; +export const allSettingsModalTabs: Array = [ + aboutTabId, + setupTabId, + policyTabId, + accessControlTabId, +]; + export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); From b01d7277f6f4a32ee5c1cbc74e5079e8f2cfe6f1 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 069/126] feat: context based login with ansattporten --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 ------- frontend/packages/shared/src/api/paths.js | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index cec49e79771..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,13 +24,6 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; -export const allSettingsModalTabs: Array = [ - aboutTabId, - setupTabId, - policyTabId, - accessControlTabId, -]; - export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 545ac846aaf..ad102961501 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 91c4109b22a75ae3b5c3b395e98d1b910879ff63 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 070/126] frontend with mocks for maskinporten --- frontend/packages/shared/src/api/paths.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index ad102961501..545ac846aaf 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 7a476b6af59055128cf796545ecd27e67478d98a Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:16:33 +0100 Subject: [PATCH 071/126] feat(settings): make it possible to open settings modal based on query-params --- .../hooks/useOpenSettingsModalBasedQueryParam.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts index 202a938b9f9..0a854340dba 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts @@ -16,7 +16,11 @@ export function useOpenSettingsModalBasedQueryParam(): void { if (shouldOpenModal) { settingsRef.current.openSettings(tabToOpen); } +<<<<<<< HEAD }, [searchParams, settingsRef]); +======= + }, [searchParams]); +>>>>>>> 210f60b62 (feat(settings): make it possible to open settings modal based on query-params) } function isValidTab(tabId: SettingsModalTabId): boolean { From 604d834d731f910633de4fa8ab76ea97897dbe09 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:20:24 +0100 Subject: [PATCH 072/126] fixes --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index d4420a5c5f8..cec49e79771 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,6 +24,13 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; +export const allSettingsModalTabs: Array = [ + aboutTabId, + setupTabId, + policyTabId, + accessControlTabId, +]; + export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); From 961e127fac7320ec295921f33f0d3489467a0002 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 20:45:29 +0100 Subject: [PATCH 073/126] PR-feedback + improve unit-tests --- .../hooks/useOpenSettingsModalBasedQueryParam.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts index 0a854340dba..202a938b9f9 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts @@ -16,11 +16,7 @@ export function useOpenSettingsModalBasedQueryParam(): void { if (shouldOpenModal) { settingsRef.current.openSettings(tabToOpen); } -<<<<<<< HEAD }, [searchParams, settingsRef]); -======= - }, [searchParams]); ->>>>>>> 210f60b62 (feat(settings): make it possible to open settings modal based on query-params) } function isValidTab(tabId: SettingsModalTabId): boolean { From db14f3364f7883f8ca150ae1125126d3241b9d32 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 074/126] feat: context based login with ansattporten --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 ------- frontend/packages/shared/src/api/paths.js | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index cec49e79771..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,13 +24,6 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; -export const allSettingsModalTabs: Array = [ - aboutTabId, - setupTabId, - policyTabId, - accessControlTabId, -]; - export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 545ac846aaf..ad102961501 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From f7987a3464eb8d3f444b36872957775cf6978996 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 075/126] frontend with mocks for maskinporten --- frontend/packages/shared/src/api/paths.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index ad102961501..545ac846aaf 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 302cfd0b3f5a5680eb4022e209165b051a3e0cae Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:11:43 +0200 Subject: [PATCH 076/126] Resuable storage interface --- .../src/browser-storage/ScopedStorage.test.ts | 119 ++++++++++++++++++ .../src/browser-storage/ScopedStorage.ts | 71 +++++++++++ 2 files changed, 190 insertions(+) create mode 100644 frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts create mode 100644 frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts new file mode 100644 index 00000000000..4a2d8a1f542 --- /dev/null +++ b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts @@ -0,0 +1,119 @@ +import { ScopedStorage, ScopedStorageImpl } from './ScopedStorage'; + +describe('ScopedStorage', () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + describe('add new key', () => { + it('should create a single scoped key with the provided key-value pair as its value', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstName', 'Random Value'); + expect(scopedStorage.getItem('firstName')).toBe('Random Value'); + }); + }); + + describe('get item', () => { + it('should return "null" if key does not exist', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + expect(scopedStorage.getItem('firstName', 'Random Value')).toBeNull(); + }); + }); + + describe('parsing', () => { + const consoleErrorMock = jest.fn(); + const originalConsoleError = console.error; + beforeEach(() => { + console.error = consoleErrorMock; + }); + + afterEach(() => { + console.error = originalConsoleError; + }); + + it('should console.error when parsing the storage fails', () => { + window.localStorage.setItem('unit/test', '{"person";{"name":"tester"}}'); + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + expect(scopedStorage.getItem('person')).toBeNull(); + expect(consoleErrorMock).toHaveBeenCalledWith( + expect.stringContaining( + 'Failed to parse storage with key unit/test. Ensure that the storage is a valid JSON string. Error: SyntaxError:', + ), + ); + }); + }); + + describe('update existing key', () => { + it('should append a new key-value pair to the existing scoped key', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstKey', 'first value'); + scopedStorage.setItem('secondKey', 'secondValue'); + + expect(scopedStorage.getItem('firstKey')).toBe('first value'); + expect(scopedStorage.getItem('secondKey')).toBe('secondValue'); + }); + + it('should update the value of an existing key-value pair within the scoped key if the value has changed', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstKey', 'first value'); + scopedStorage.setItem('firstKey', 'first value is updated'); + expect(scopedStorage.getItem('firstKey')).toBe('first value is updated'); + }); + }); + + describe('delete values from key', () => { + it('should remove a specific key-value pair from the existing scoped key', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstKey', 'first value'); + expect(scopedStorage.getItem('firstKey')).toBeDefined(); + + scopedStorage.removeItem('firstKey'); + expect(scopedStorage.getItem('firstKey')).toBeUndefined(); + }); + + it('should not remove key if it does not exist', () => { + const removeItemMock = jest.fn(); + const customStorage = { + getItem: jest.fn().mockImplementation(() => null), + removeItem: removeItemMock, + }; + + const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); + scopedStorage.removeItem('keyDoesNotExist'); + + expect(removeItemMock).not.toHaveBeenCalled(); + }); + }); + + // Verify that Dependency Inversion works as expected + describe('when using localStorage', () => { + it('should store and retrieve values using localStorage', () => { + const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'local/storage'); + scopedStorage.setItem('firstNameInSession', 'Random Session Value'); + expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); + }); + }); + + describe('when using sessionStorage', () => { + it('should store and retrieve values using sessionStorage', () => { + const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'session/storage'); + scopedStorage.setItem('firstNameInSession', 'Random Session Value'); + expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); + }); + }); + + describe('when using a custom storage implementation', () => { + it('should store and retrieve values using the provided custom storage', () => { + const setItemMock = jest.fn(); + + const customStorage: ScopedStorage = { + setItem: setItemMock, + getItem: jest.fn(), + removeItem: jest.fn(), + }; + + const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); + scopedStorage.setItem('testKey', 'testValue'); + }); + }); +}); diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts new file mode 100644 index 00000000000..7acab02c9e2 --- /dev/null +++ b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts @@ -0,0 +1,71 @@ +type StorageKey = string; + +export interface ScopedStorage extends Pick {} + +export class ScopedStorageImpl implements ScopedStorage { + private readonly storageKey: StorageKey; + private readonly scopedStorage: ScopedStorage; + + constructor( + private storage: ScopedStorage, + private key: StorageKey, + ) { + this.storageKey = this.key; + this.scopedStorage = this.storage; + } + + public setItem(key: string, value: T): void { + const storageRecords: T = this.getAllRecordsInStorage(); + this.saveToStorage( + JSON.stringify({ + ...storageRecords, + [key]: value, + }), + ); + } + + public getItem(key: keyof T): T { + const records: T = this.getAllRecordsInStorage(); + + if (!records) { + return null; + } + + return records[key] as unknown as T; + } + + public removeItem(key: keyof T): void { + const storageRecords: T | null = this.getAllRecordsInStorage(); + + if (!storageRecords) { + return; + } + + const storageCopy = { ...storageRecords }; + delete storageCopy[key]; + this.saveToStorage(JSON.stringify({ ...storageCopy })); + } + + private getAllRecordsInStorage(): T | null { + return this.parseStorageData(this.scopedStorage.getItem(this.storageKey)); + } + + private saveToStorage(value: string) { + this.storage.setItem(this.storageKey, value); + } + + private parseStorageData(storage: string | null): T | null { + if (!storage) { + return null; + } + + try { + return JSON.parse(storage) satisfies T; + } catch (error) { + console.error( + `Failed to parse storage with key ${this.storageKey}. Ensure that the storage is a valid JSON string. Error: ${error}`, + ); + return null; + } + } +} From dd6df579f2926a10911ca168df4478f3e1e478bd Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:13:01 +0200 Subject: [PATCH 077/126] moved the reusable scoped storage to pure-funciton --- .../src/browser-storage/ScopedStorage.test.ts | 119 ------------------ .../src/browser-storage/ScopedStorage.ts | 71 ----------- 2 files changed, 190 deletions(-) delete mode 100644 frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts delete mode 100644 frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts deleted file mode 100644 index 4a2d8a1f542..00000000000 --- a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { ScopedStorage, ScopedStorageImpl } from './ScopedStorage'; - -describe('ScopedStorage', () => { - beforeEach(() => { - window.localStorage.clear(); - }); - - describe('add new key', () => { - it('should create a single scoped key with the provided key-value pair as its value', () => { - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - scopedStorage.setItem('firstName', 'Random Value'); - expect(scopedStorage.getItem('firstName')).toBe('Random Value'); - }); - }); - - describe('get item', () => { - it('should return "null" if key does not exist', () => { - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - expect(scopedStorage.getItem('firstName', 'Random Value')).toBeNull(); - }); - }); - - describe('parsing', () => { - const consoleErrorMock = jest.fn(); - const originalConsoleError = console.error; - beforeEach(() => { - console.error = consoleErrorMock; - }); - - afterEach(() => { - console.error = originalConsoleError; - }); - - it('should console.error when parsing the storage fails', () => { - window.localStorage.setItem('unit/test', '{"person";{"name":"tester"}}'); - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - expect(scopedStorage.getItem('person')).toBeNull(); - expect(consoleErrorMock).toHaveBeenCalledWith( - expect.stringContaining( - 'Failed to parse storage with key unit/test. Ensure that the storage is a valid JSON string. Error: SyntaxError:', - ), - ); - }); - }); - - describe('update existing key', () => { - it('should append a new key-value pair to the existing scoped key', () => { - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - scopedStorage.setItem('firstKey', 'first value'); - scopedStorage.setItem('secondKey', 'secondValue'); - - expect(scopedStorage.getItem('firstKey')).toBe('first value'); - expect(scopedStorage.getItem('secondKey')).toBe('secondValue'); - }); - - it('should update the value of an existing key-value pair within the scoped key if the value has changed', () => { - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - scopedStorage.setItem('firstKey', 'first value'); - scopedStorage.setItem('firstKey', 'first value is updated'); - expect(scopedStorage.getItem('firstKey')).toBe('first value is updated'); - }); - }); - - describe('delete values from key', () => { - it('should remove a specific key-value pair from the existing scoped key', () => { - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - scopedStorage.setItem('firstKey', 'first value'); - expect(scopedStorage.getItem('firstKey')).toBeDefined(); - - scopedStorage.removeItem('firstKey'); - expect(scopedStorage.getItem('firstKey')).toBeUndefined(); - }); - - it('should not remove key if it does not exist', () => { - const removeItemMock = jest.fn(); - const customStorage = { - getItem: jest.fn().mockImplementation(() => null), - removeItem: removeItemMock, - }; - - const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); - scopedStorage.removeItem('keyDoesNotExist'); - - expect(removeItemMock).not.toHaveBeenCalled(); - }); - }); - - // Verify that Dependency Inversion works as expected - describe('when using localStorage', () => { - it('should store and retrieve values using localStorage', () => { - const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'local/storage'); - scopedStorage.setItem('firstNameInSession', 'Random Session Value'); - expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); - }); - }); - - describe('when using sessionStorage', () => { - it('should store and retrieve values using sessionStorage', () => { - const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'session/storage'); - scopedStorage.setItem('firstNameInSession', 'Random Session Value'); - expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); - }); - }); - - describe('when using a custom storage implementation', () => { - it('should store and retrieve values using the provided custom storage', () => { - const setItemMock = jest.fn(); - - const customStorage: ScopedStorage = { - setItem: setItemMock, - getItem: jest.fn(), - removeItem: jest.fn(), - }; - - const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); - scopedStorage.setItem('testKey', 'testValue'); - }); - }); -}); diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts deleted file mode 100644 index 7acab02c9e2..00000000000 --- a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts +++ /dev/null @@ -1,71 +0,0 @@ -type StorageKey = string; - -export interface ScopedStorage extends Pick {} - -export class ScopedStorageImpl implements ScopedStorage { - private readonly storageKey: StorageKey; - private readonly scopedStorage: ScopedStorage; - - constructor( - private storage: ScopedStorage, - private key: StorageKey, - ) { - this.storageKey = this.key; - this.scopedStorage = this.storage; - } - - public setItem(key: string, value: T): void { - const storageRecords: T = this.getAllRecordsInStorage(); - this.saveToStorage( - JSON.stringify({ - ...storageRecords, - [key]: value, - }), - ); - } - - public getItem(key: keyof T): T { - const records: T = this.getAllRecordsInStorage(); - - if (!records) { - return null; - } - - return records[key] as unknown as T; - } - - public removeItem(key: keyof T): void { - const storageRecords: T | null = this.getAllRecordsInStorage(); - - if (!storageRecords) { - return; - } - - const storageCopy = { ...storageRecords }; - delete storageCopy[key]; - this.saveToStorage(JSON.stringify({ ...storageCopy })); - } - - private getAllRecordsInStorage(): T | null { - return this.parseStorageData(this.scopedStorage.getItem(this.storageKey)); - } - - private saveToStorage(value: string) { - this.storage.setItem(this.storageKey, value); - } - - private parseStorageData(storage: string | null): T | null { - if (!storage) { - return null; - } - - try { - return JSON.parse(storage) satisfies T; - } catch (error) { - console.error( - `Failed to parse storage with key ${this.storageKey}. Ensure that the storage is a valid JSON string. Error: ${error}`, - ); - return null; - } - } -} From 7e530a711541a3c6c6fc537e2c00f25fac3fd8eb Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:16:33 +0100 Subject: [PATCH 078/126] feat(settings): make it possible to open settings modal based on query-params --- .../useOpenSettingsModalBasedQueryParam.test.ts | 17 +++++++++++++++++ .../useOpenSettingsModalBasedQueryParam.ts | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts index 60809622836..473df13b562 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts @@ -13,7 +13,11 @@ jest.mock('react-router-dom', () => ({ })); describe('useOpenSettingsModalBasedQueryParam', () => { +<<<<<<< HEAD it('should open "settingsModal" if query params has valid tab id', async () => { +======= + it('should open dialog if query params has valid tab id', async () => { +>>>>>>> 41381f181 (feat(settings): make it possible to open settings modal based on query-params) const searchParams = buildSearchParams('about'); setupSearchParamMock(searchParams); @@ -22,10 +26,16 @@ describe('useOpenSettingsModalBasedQueryParam', () => { renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); await waitFor(() => expect(openSettingsMock).toHaveBeenCalledWith('about')); +<<<<<<< HEAD expect(openSettingsMock).toHaveBeenCalledTimes(1); }); it('should not open "settingsModal" if query params has an invalid tab id', async () => { +======= + }); + + it('should not open dialog of query params has invalid tab id', async () => { +>>>>>>> 41381f181 (feat(settings): make it possible to open settings modal based on query-params) const searchParams = buildSearchParams('doestNotExistTab'); setupSearchParamMock(searchParams); @@ -34,7 +44,10 @@ describe('useOpenSettingsModalBasedQueryParam', () => { renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); await waitFor(() => expect(openSettingsMock).not.toHaveBeenCalledWith('doestNotExistTab')); +<<<<<<< HEAD expect(openSettingsMock).toHaveBeenCalledTimes(0); +======= +>>>>>>> 41381f181 (feat(settings): make it possible to open settings modal based on query-params) }); }); @@ -48,7 +61,11 @@ function buildSearchParams(queryParamValue: string): URLSearchParams { return searchParams; } +<<<<<<< HEAD function setupUseSettingsModalContextMock(openSettingsMock: typeof jest.fn): jest.Mock { +======= +function setupUseSettingsModalContextMock(openSettingsMock: jest.fn): jest.Mock { +>>>>>>> 41381f181 (feat(settings): make it possible to open settings modal based on query-params) return (useSettingsModalContext as jest.Mock).mockReturnValue({ settingsRef: { current: { diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts index 202a938b9f9..13e277d853c 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts @@ -16,7 +16,11 @@ export function useOpenSettingsModalBasedQueryParam(): void { if (shouldOpenModal) { settingsRef.current.openSettings(tabToOpen); } +<<<<<<< HEAD }, [searchParams, settingsRef]); +======= + }, [searchParams]); +>>>>>>> 41381f181 (feat(settings): make it possible to open settings modal based on query-params) } function isValidTab(tabId: SettingsModalTabId): boolean { From 5ab5a7aa7cdae3b37cb0feaef44eeaf4996b0bec Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:20:24 +0100 Subject: [PATCH 079/126] fixes --- .../useOpenSettingsModalBasedQueryParam.test.ts | 17 ----------------- .../hooks/useSettingsModalMenuTabConfigs.tsx | 7 +++++++ 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts index 473df13b562..60809622836 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.test.ts @@ -13,11 +13,7 @@ jest.mock('react-router-dom', () => ({ })); describe('useOpenSettingsModalBasedQueryParam', () => { -<<<<<<< HEAD it('should open "settingsModal" if query params has valid tab id', async () => { -======= - it('should open dialog if query params has valid tab id', async () => { ->>>>>>> 41381f181 (feat(settings): make it possible to open settings modal based on query-params) const searchParams = buildSearchParams('about'); setupSearchParamMock(searchParams); @@ -26,16 +22,10 @@ describe('useOpenSettingsModalBasedQueryParam', () => { renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); await waitFor(() => expect(openSettingsMock).toHaveBeenCalledWith('about')); -<<<<<<< HEAD expect(openSettingsMock).toHaveBeenCalledTimes(1); }); it('should not open "settingsModal" if query params has an invalid tab id', async () => { -======= - }); - - it('should not open dialog of query params has invalid tab id', async () => { ->>>>>>> 41381f181 (feat(settings): make it possible to open settings modal based on query-params) const searchParams = buildSearchParams('doestNotExistTab'); setupSearchParamMock(searchParams); @@ -44,10 +34,7 @@ describe('useOpenSettingsModalBasedQueryParam', () => { renderHookWithProviders()(() => useOpenSettingsModalBasedQueryParam()); await waitFor(() => expect(openSettingsMock).not.toHaveBeenCalledWith('doestNotExistTab')); -<<<<<<< HEAD expect(openSettingsMock).toHaveBeenCalledTimes(0); -======= ->>>>>>> 41381f181 (feat(settings): make it possible to open settings modal based on query-params) }); }); @@ -61,11 +48,7 @@ function buildSearchParams(queryParamValue: string): URLSearchParams { return searchParams; } -<<<<<<< HEAD function setupUseSettingsModalContextMock(openSettingsMock: typeof jest.fn): jest.Mock { -======= -function setupUseSettingsModalContextMock(openSettingsMock: jest.fn): jest.Mock { ->>>>>>> 41381f181 (feat(settings): make it possible to open settings modal based on query-params) return (useSettingsModalContext as jest.Mock).mockReturnValue({ settingsRef: { current: { diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index d4420a5c5f8..cec49e79771 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,6 +24,13 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; +export const allSettingsModalTabs: Array = [ + aboutTabId, + setupTabId, + policyTabId, + accessControlTabId, +]; + export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); From c4f78ea3fb3eee19ee3a2cbdf717c826f162e93b Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 20:45:29 +0100 Subject: [PATCH 080/126] PR-feedback + improve unit-tests --- .../hooks/useOpenSettingsModalBasedQueryParam.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts index 13e277d853c..202a938b9f9 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts @@ -16,11 +16,7 @@ export function useOpenSettingsModalBasedQueryParam(): void { if (shouldOpenModal) { settingsRef.current.openSettings(tabToOpen); } -<<<<<<< HEAD }, [searchParams, settingsRef]); -======= - }, [searchParams]); ->>>>>>> 41381f181 (feat(settings): make it possible to open settings modal based on query-params) } function isValidTab(tabId: SettingsModalTabId): boolean { From a050c194d424851d233f756935463bbce0555ff4 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 081/126] feat: context based login with ansattporten --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 ------- frontend/packages/shared/src/api/paths.js | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index cec49e79771..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,13 +24,6 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; -export const allSettingsModalTabs: Array = [ - aboutTabId, - setupTabId, - policyTabId, - accessControlTabId, -]; - export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 545ac846aaf..ad102961501 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From a71322d011ac9e46998f53bad820ec1bb2583b9b Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 082/126] frontend with mocks for maskinporten --- frontend/packages/shared/src/api/paths.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index ad102961501..545ac846aaf 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 4151365205253b55333f0e4f1ba44715434bf544 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:20:24 +0100 Subject: [PATCH 083/126] fixes --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index d4420a5c5f8..cec49e79771 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,6 +24,13 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; +export const allSettingsModalTabs: Array = [ + aboutTabId, + setupTabId, + policyTabId, + accessControlTabId, +]; + export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); From 16cd0d35beeeaf08d056d131058349aa68221602 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 084/126] feat: context based login with ansattporten --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 ------- frontend/packages/shared/src/api/paths.js | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index cec49e79771..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,13 +24,6 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; -export const allSettingsModalTabs: Array = [ - aboutTabId, - setupTabId, - policyTabId, - accessControlTabId, -]; - export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 545ac846aaf..ad102961501 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 58c632d90fb2172f1835523da9441a753c617295 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 085/126] frontend with mocks for maskinporten --- frontend/packages/shared/src/api/paths.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index ad102961501..545ac846aaf 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 9c38238f6f37c9059a7833764adab201305e26a3 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 15:40:41 +0100 Subject: [PATCH 086/126] hide maskinporten behind featureflag --- .../SettingsModalButton/SettingsModal/SettingsModal.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index c5dad2b42fe..6fb1f2240af 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -5,7 +5,11 @@ import { CogIcon } from '@studio/icons'; import { StudioModal, StudioContentMenu, +<<<<<<< HEAD type StudioContentMenuButtonTabProps, +======= + StudioContentMenuButtonTabProps, +>>>>>>> b99c16b37 (hide maskinporten behind featureflag) } from '@studio/components'; import type { SettingsModalTabId } from '../../../../../types/SettingsModalTabId'; import { useTranslation } from 'react-i18next'; From d67cc4802c7481110dc2d6f3e3aecb9a6d2cde25 Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Tue, 12 Nov 2024 12:26:09 +0100 Subject: [PATCH 087/126] chore: scale designer in dev and staging (#14039) --- .github/workflows/deploy-designer.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/deploy-designer.yaml b/.github/workflows/deploy-designer.yaml index 34e66fe2784..6d71e0b2e67 100644 --- a/.github/workflows/deploy-designer.yaml +++ b/.github/workflows/deploy-designer.yaml @@ -106,7 +106,11 @@ jobs: artifact-environment: ${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }} config-chart-name: altinn-designer-config artifact-name: altinn-designer +<<<<<<< HEAD helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'dev' && 1 || 2 }} +======= + helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'preapproved-prod' && 1 || 2 }} +>>>>>>> 70f1511ca (chore: scale designer in dev and staging (#14039)) trace-workflow: true trace-team-name: 'team-studio' secrets: From 8efd6e66b8dd9a4243a375f9da73422e4859698a Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Wed, 13 Nov 2024 14:43:32 +0100 Subject: [PATCH 088/126] feat: 2 replicas in prod/staging, 1 in dev (#14047) --- .github/workflows/deploy-designer.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/deploy-designer.yaml b/.github/workflows/deploy-designer.yaml index 6d71e0b2e67..34e66fe2784 100644 --- a/.github/workflows/deploy-designer.yaml +++ b/.github/workflows/deploy-designer.yaml @@ -106,11 +106,7 @@ jobs: artifact-environment: ${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }} config-chart-name: altinn-designer-config artifact-name: altinn-designer -<<<<<<< HEAD helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'dev' && 1 || 2 }} -======= - helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'preapproved-prod' && 1 || 2 }} ->>>>>>> 70f1511ca (chore: scale designer in dev and staging (#14039)) trace-workflow: true trace-team-name: 'team-studio' secrets: From 8f9941158cce287a8cc97580c5cb655564fa78db Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 15:44:43 +0100 Subject: [PATCH 089/126] eslint --- .../SettingsModalButton/SettingsModal/SettingsModal.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index 6fb1f2240af..c5dad2b42fe 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -5,11 +5,7 @@ import { CogIcon } from '@studio/icons'; import { StudioModal, StudioContentMenu, -<<<<<<< HEAD type StudioContentMenuButtonTabProps, -======= - StudioContentMenuButtonTabProps, ->>>>>>> b99c16b37 (hide maskinporten behind featureflag) } from '@studio/components'; import type { SettingsModalTabId } from '../../../../../types/SettingsModalTabId'; import { useTranslation } from 'react-i18next'; From cd7137834f6ec6020104757669fb4d16f896743c Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:20:24 +0100 Subject: [PATCH 090/126] fixes --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index d4420a5c5f8..cec49e79771 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,6 +24,13 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; +export const allSettingsModalTabs: Array = [ + aboutTabId, + setupTabId, + policyTabId, + accessControlTabId, +]; + export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); From ed6e6f084a074fd38f2f0ae78adc5de9b1403880 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 091/126] feat: context based login with ansattporten --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 ------- frontend/packages/shared/src/api/paths.js | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index cec49e79771..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,13 +24,6 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; -export const allSettingsModalTabs: Array = [ - aboutTabId, - setupTabId, - policyTabId, - accessControlTabId, -]; - export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 545ac846aaf..ad102961501 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From fdea28c7c79659039af1091f388ec3f70c0ef76a Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 092/126] frontend with mocks for maskinporten --- frontend/packages/shared/src/api/paths.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index ad102961501..545ac846aaf 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 0e3c5851867ecc18c3efa2ed7be6deb9eb551293 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:20:24 +0100 Subject: [PATCH 093/126] fixes --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index d4420a5c5f8..cec49e79771 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,6 +24,13 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; +export const allSettingsModalTabs: Array = [ + aboutTabId, + setupTabId, + policyTabId, + accessControlTabId, +]; + export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); From 5366666b928aa1e06cd0b328b4e8d6c6c80e0a98 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 094/126] feat: context based login with ansattporten --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 ------- frontend/packages/shared/src/api/paths.js | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index cec49e79771..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,13 +24,6 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; -export const allSettingsModalTabs: Array = [ - aboutTabId, - setupTabId, - policyTabId, - accessControlTabId, -]; - export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 545ac846aaf..ad102961501 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 609f1fecbecd1c3a4fdd33c159e5d1ac758afbe8 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 095/126] frontend with mocks for maskinporten --- frontend/packages/shared/src/api/paths.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index ad102961501..545ac846aaf 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From cda9b9165a29290338d2864e98f01ff4a55ff2da Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Tue, 19 Nov 2024 11:20:49 +0100 Subject: [PATCH 096/126] AnsattPorten controller --- .../Controllers/AnsattPortenController.cs | 47 +++++++++++++++++++ .../Controllers/AppScopesController.cs | 7 +-- .../AnsattPorten/AnsattPortenExtensions.cs | 3 +- backend/src/Designer/Models/Dto/AuthStatus.cs | 6 +++ 4 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 backend/src/Designer/Controllers/AnsattPortenController.cs create mode 100644 backend/src/Designer/Models/Dto/AuthStatus.cs diff --git a/backend/src/Designer/Controllers/AnsattPortenController.cs b/backend/src/Designer/Controllers/AnsattPortenController.cs new file mode 100644 index 00000000000..a92945661fb --- /dev/null +++ b/backend/src/Designer/Controllers/AnsattPortenController.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using Altinn.Studio.Designer.Constants; +using Altinn.Studio.Designer.Models.Dto; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.FeatureManagement.Mvc; + +namespace Altinn.Studio.Designer.Controllers; + +[FeatureGate(StudioFeatureFlags.AnsattPorten)] +[Route("designer/api/[controller]")] +[ApiController] +public class AnsattPortenController(IAuthenticationService authService) : ControllerBase +{ + [Authorize(AnsattPortenConstants.AnsattportenAuthorizationPolicy)] + [HttpGet("login")] + public async Task Login([FromQuery] string redirectTo) + { + await Task.CompletedTask; + if (!Url.IsLocalUrl(redirectTo)) + { + return Forbid(); + } + + return LocalRedirect(redirectTo); + } + + [AllowAnonymous] + [HttpGet("auth-status")] + public async Task AuthStatus() + { + await Task.CompletedTask; + var authenticateResult = + await authService.AuthenticateAsync(HttpContext, + AnsattPortenConstants.AnsattportenAuthenticationScheme); + + var authStatus = new AuthStatus + { + IsLoggedIn = authenticateResult.Succeeded + }; + + return Ok(authStatus); + } + + +} diff --git a/backend/src/Designer/Controllers/AppScopesController.cs b/backend/src/Designer/Controllers/AppScopesController.cs index b17e6fa8421..22feda80376 100644 --- a/backend/src/Designer/Controllers/AppScopesController.cs +++ b/backend/src/Designer/Controllers/AppScopesController.cs @@ -28,7 +28,7 @@ public async Task GetScopesFromMaskinPorten(string org, string ap { var scopes = await maskinPortenHttpClient.GetAvailableScopes(cancellationToken); - var reponse = new AppScopesResponse() + var response = new AppScopesResponse() { Scopes = scopes.Select(x => new MaskinPortenScopeDto() { @@ -37,10 +37,9 @@ public async Task GetScopesFromMaskinPorten(string org, string ap }).ToHashSet() }; - return Ok(reponse); + return Ok(response); } - [Authorize] [HttpPut] public async Task UpsertAppScopes(string org, string app, [FromBody] AppScopesUpsertRequest appScopesUpsertRequest, @@ -56,7 +55,6 @@ public async Task UpsertAppScopes(string org, string app, [FromBody] AppScopesUp await appScopesService.UpsertScopesAsync(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer), scopes, cancellationToken); } - [Authorize] [HttpGet] public async Task GetAppScopes(string org, string app, CancellationToken cancellationToken) @@ -74,5 +72,4 @@ public async Task GetAppScopes(string org, string app, Cancellati return Ok(reponse); } - } diff --git a/backend/src/Designer/Infrastructure/AnsattPorten/AnsattPortenExtensions.cs b/backend/src/Designer/Infrastructure/AnsattPorten/AnsattPortenExtensions.cs index 8cfdfcc3881..073da38601c 100644 --- a/backend/src/Designer/Infrastructure/AnsattPorten/AnsattPortenExtensions.cs +++ b/backend/src/Designer/Infrastructure/AnsattPorten/AnsattPortenExtensions.cs @@ -75,8 +75,7 @@ private static IServiceCollection AddAnsattPortenAuthentication(this IServiceCol options.Events.OnRedirectToIdentityProvider = context => { - if (!context.Request.Path.StartsWithSegments("/designer/api") || - !context.Request.Path.Value!.Contains("/maskinporten")) + if (!context.Request.Path.StartsWithSegments("/designer/api/ansattporten/login")) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; context.HandleResponse(); diff --git a/backend/src/Designer/Models/Dto/AuthStatus.cs b/backend/src/Designer/Models/Dto/AuthStatus.cs new file mode 100644 index 00000000000..3ff49e8f39b --- /dev/null +++ b/backend/src/Designer/Models/Dto/AuthStatus.cs @@ -0,0 +1,6 @@ +namespace Altinn.Studio.Designer.Models.Dto; + +public class AuthStatus +{ + public bool IsLoggedIn { get; set; } +} From 3dcbd84eafce6cd50d9ca25603f104db728f6c42 Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Tue, 19 Nov 2024 12:05:55 +0100 Subject: [PATCH 097/126] rename redirect to param --- backend/src/Designer/Controllers/AnsattPortenController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/Designer/Controllers/AnsattPortenController.cs b/backend/src/Designer/Controllers/AnsattPortenController.cs index a92945661fb..8feff836cf4 100644 --- a/backend/src/Designer/Controllers/AnsattPortenController.cs +++ b/backend/src/Designer/Controllers/AnsattPortenController.cs @@ -15,7 +15,7 @@ public class AnsattPortenController(IAuthenticationService authService) : Contro { [Authorize(AnsattPortenConstants.AnsattportenAuthorizationPolicy)] [HttpGet("login")] - public async Task Login([FromQuery] string redirectTo) + public async Task Login([FromQuery(Name = "redirect_to")] string redirectTo) { await Task.CompletedTask; if (!Url.IsLocalUrl(redirectTo)) From c42611cda17ada1c6dc488fc1216ae5f6b0647ba Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:11:43 +0200 Subject: [PATCH 098/126] Resuable storage interface --- .../src/browser-storage/ScopedStorage.test.ts | 119 ++++++++++++++++++ .../src/browser-storage/ScopedStorage.ts | 71 +++++++++++ 2 files changed, 190 insertions(+) create mode 100644 frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts create mode 100644 frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts new file mode 100644 index 00000000000..4a2d8a1f542 --- /dev/null +++ b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts @@ -0,0 +1,119 @@ +import { ScopedStorage, ScopedStorageImpl } from './ScopedStorage'; + +describe('ScopedStorage', () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + describe('add new key', () => { + it('should create a single scoped key with the provided key-value pair as its value', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstName', 'Random Value'); + expect(scopedStorage.getItem('firstName')).toBe('Random Value'); + }); + }); + + describe('get item', () => { + it('should return "null" if key does not exist', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + expect(scopedStorage.getItem('firstName', 'Random Value')).toBeNull(); + }); + }); + + describe('parsing', () => { + const consoleErrorMock = jest.fn(); + const originalConsoleError = console.error; + beforeEach(() => { + console.error = consoleErrorMock; + }); + + afterEach(() => { + console.error = originalConsoleError; + }); + + it('should console.error when parsing the storage fails', () => { + window.localStorage.setItem('unit/test', '{"person";{"name":"tester"}}'); + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + expect(scopedStorage.getItem('person')).toBeNull(); + expect(consoleErrorMock).toHaveBeenCalledWith( + expect.stringContaining( + 'Failed to parse storage with key unit/test. Ensure that the storage is a valid JSON string. Error: SyntaxError:', + ), + ); + }); + }); + + describe('update existing key', () => { + it('should append a new key-value pair to the existing scoped key', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstKey', 'first value'); + scopedStorage.setItem('secondKey', 'secondValue'); + + expect(scopedStorage.getItem('firstKey')).toBe('first value'); + expect(scopedStorage.getItem('secondKey')).toBe('secondValue'); + }); + + it('should update the value of an existing key-value pair within the scoped key if the value has changed', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstKey', 'first value'); + scopedStorage.setItem('firstKey', 'first value is updated'); + expect(scopedStorage.getItem('firstKey')).toBe('first value is updated'); + }); + }); + + describe('delete values from key', () => { + it('should remove a specific key-value pair from the existing scoped key', () => { + const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); + scopedStorage.setItem('firstKey', 'first value'); + expect(scopedStorage.getItem('firstKey')).toBeDefined(); + + scopedStorage.removeItem('firstKey'); + expect(scopedStorage.getItem('firstKey')).toBeUndefined(); + }); + + it('should not remove key if it does not exist', () => { + const removeItemMock = jest.fn(); + const customStorage = { + getItem: jest.fn().mockImplementation(() => null), + removeItem: removeItemMock, + }; + + const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); + scopedStorage.removeItem('keyDoesNotExist'); + + expect(removeItemMock).not.toHaveBeenCalled(); + }); + }); + + // Verify that Dependency Inversion works as expected + describe('when using localStorage', () => { + it('should store and retrieve values using localStorage', () => { + const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'local/storage'); + scopedStorage.setItem('firstNameInSession', 'Random Session Value'); + expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); + }); + }); + + describe('when using sessionStorage', () => { + it('should store and retrieve values using sessionStorage', () => { + const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'session/storage'); + scopedStorage.setItem('firstNameInSession', 'Random Session Value'); + expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); + }); + }); + + describe('when using a custom storage implementation', () => { + it('should store and retrieve values using the provided custom storage', () => { + const setItemMock = jest.fn(); + + const customStorage: ScopedStorage = { + setItem: setItemMock, + getItem: jest.fn(), + removeItem: jest.fn(), + }; + + const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); + scopedStorage.setItem('testKey', 'testValue'); + }); + }); +}); diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts new file mode 100644 index 00000000000..7acab02c9e2 --- /dev/null +++ b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts @@ -0,0 +1,71 @@ +type StorageKey = string; + +export interface ScopedStorage extends Pick {} + +export class ScopedStorageImpl implements ScopedStorage { + private readonly storageKey: StorageKey; + private readonly scopedStorage: ScopedStorage; + + constructor( + private storage: ScopedStorage, + private key: StorageKey, + ) { + this.storageKey = this.key; + this.scopedStorage = this.storage; + } + + public setItem(key: string, value: T): void { + const storageRecords: T = this.getAllRecordsInStorage(); + this.saveToStorage( + JSON.stringify({ + ...storageRecords, + [key]: value, + }), + ); + } + + public getItem(key: keyof T): T { + const records: T = this.getAllRecordsInStorage(); + + if (!records) { + return null; + } + + return records[key] as unknown as T; + } + + public removeItem(key: keyof T): void { + const storageRecords: T | null = this.getAllRecordsInStorage(); + + if (!storageRecords) { + return; + } + + const storageCopy = { ...storageRecords }; + delete storageCopy[key]; + this.saveToStorage(JSON.stringify({ ...storageCopy })); + } + + private getAllRecordsInStorage(): T | null { + return this.parseStorageData(this.scopedStorage.getItem(this.storageKey)); + } + + private saveToStorage(value: string) { + this.storage.setItem(this.storageKey, value); + } + + private parseStorageData(storage: string | null): T | null { + if (!storage) { + return null; + } + + try { + return JSON.parse(storage) satisfies T; + } catch (error) { + console.error( + `Failed to parse storage with key ${this.storageKey}. Ensure that the storage is a valid JSON string. Error: ${error}`, + ); + return null; + } + } +} From d5870526d59b92670f99918ba1a054ab3d1e80c8 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 25 Oct 2024 13:13:01 +0200 Subject: [PATCH 099/126] moved the reusable scoped storage to pure-funciton --- .../src/browser-storage/ScopedStorage.test.ts | 119 ------------------ .../src/browser-storage/ScopedStorage.ts | 71 ----------- 2 files changed, 190 deletions(-) delete mode 100644 frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts delete mode 100644 frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts deleted file mode 100644 index 4a2d8a1f542..00000000000 --- a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { ScopedStorage, ScopedStorageImpl } from './ScopedStorage'; - -describe('ScopedStorage', () => { - beforeEach(() => { - window.localStorage.clear(); - }); - - describe('add new key', () => { - it('should create a single scoped key with the provided key-value pair as its value', () => { - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - scopedStorage.setItem('firstName', 'Random Value'); - expect(scopedStorage.getItem('firstName')).toBe('Random Value'); - }); - }); - - describe('get item', () => { - it('should return "null" if key does not exist', () => { - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - expect(scopedStorage.getItem('firstName', 'Random Value')).toBeNull(); - }); - }); - - describe('parsing', () => { - const consoleErrorMock = jest.fn(); - const originalConsoleError = console.error; - beforeEach(() => { - console.error = consoleErrorMock; - }); - - afterEach(() => { - console.error = originalConsoleError; - }); - - it('should console.error when parsing the storage fails', () => { - window.localStorage.setItem('unit/test', '{"person";{"name":"tester"}}'); - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - expect(scopedStorage.getItem('person')).toBeNull(); - expect(consoleErrorMock).toHaveBeenCalledWith( - expect.stringContaining( - 'Failed to parse storage with key unit/test. Ensure that the storage is a valid JSON string. Error: SyntaxError:', - ), - ); - }); - }); - - describe('update existing key', () => { - it('should append a new key-value pair to the existing scoped key', () => { - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - scopedStorage.setItem('firstKey', 'first value'); - scopedStorage.setItem('secondKey', 'secondValue'); - - expect(scopedStorage.getItem('firstKey')).toBe('first value'); - expect(scopedStorage.getItem('secondKey')).toBe('secondValue'); - }); - - it('should update the value of an existing key-value pair within the scoped key if the value has changed', () => { - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - scopedStorage.setItem('firstKey', 'first value'); - scopedStorage.setItem('firstKey', 'first value is updated'); - expect(scopedStorage.getItem('firstKey')).toBe('first value is updated'); - }); - }); - - describe('delete values from key', () => { - it('should remove a specific key-value pair from the existing scoped key', () => { - const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); - scopedStorage.setItem('firstKey', 'first value'); - expect(scopedStorage.getItem('firstKey')).toBeDefined(); - - scopedStorage.removeItem('firstKey'); - expect(scopedStorage.getItem('firstKey')).toBeUndefined(); - }); - - it('should not remove key if it does not exist', () => { - const removeItemMock = jest.fn(); - const customStorage = { - getItem: jest.fn().mockImplementation(() => null), - removeItem: removeItemMock, - }; - - const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); - scopedStorage.removeItem('keyDoesNotExist'); - - expect(removeItemMock).not.toHaveBeenCalled(); - }); - }); - - // Verify that Dependency Inversion works as expected - describe('when using localStorage', () => { - it('should store and retrieve values using localStorage', () => { - const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'local/storage'); - scopedStorage.setItem('firstNameInSession', 'Random Session Value'); - expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); - }); - }); - - describe('when using sessionStorage', () => { - it('should store and retrieve values using sessionStorage', () => { - const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'session/storage'); - scopedStorage.setItem('firstNameInSession', 'Random Session Value'); - expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); - }); - }); - - describe('when using a custom storage implementation', () => { - it('should store and retrieve values using the provided custom storage', () => { - const setItemMock = jest.fn(); - - const customStorage: ScopedStorage = { - setItem: setItemMock, - getItem: jest.fn(), - removeItem: jest.fn(), - }; - - const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); - scopedStorage.setItem('testKey', 'testValue'); - }); - }); -}); diff --git a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts b/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts deleted file mode 100644 index 7acab02c9e2..00000000000 --- a/frontend/packages/ux-editor/src/browser-storage/ScopedStorage.ts +++ /dev/null @@ -1,71 +0,0 @@ -type StorageKey = string; - -export interface ScopedStorage extends Pick {} - -export class ScopedStorageImpl implements ScopedStorage { - private readonly storageKey: StorageKey; - private readonly scopedStorage: ScopedStorage; - - constructor( - private storage: ScopedStorage, - private key: StorageKey, - ) { - this.storageKey = this.key; - this.scopedStorage = this.storage; - } - - public setItem(key: string, value: T): void { - const storageRecords: T = this.getAllRecordsInStorage(); - this.saveToStorage( - JSON.stringify({ - ...storageRecords, - [key]: value, - }), - ); - } - - public getItem(key: keyof T): T { - const records: T = this.getAllRecordsInStorage(); - - if (!records) { - return null; - } - - return records[key] as unknown as T; - } - - public removeItem(key: keyof T): void { - const storageRecords: T | null = this.getAllRecordsInStorage(); - - if (!storageRecords) { - return; - } - - const storageCopy = { ...storageRecords }; - delete storageCopy[key]; - this.saveToStorage(JSON.stringify({ ...storageCopy })); - } - - private getAllRecordsInStorage(): T | null { - return this.parseStorageData(this.scopedStorage.getItem(this.storageKey)); - } - - private saveToStorage(value: string) { - this.storage.setItem(this.storageKey, value); - } - - private parseStorageData(storage: string | null): T | null { - if (!storage) { - return null; - } - - try { - return JSON.parse(storage) satisfies T; - } catch (error) { - console.error( - `Failed to parse storage with key ${this.storageKey}. Ensure that the storage is a valid JSON string. Error: ${error}`, - ); - return null; - } - } -} From 7a9f7353adb2f2fd1d1b143aee392c8a11b97503 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:20:24 +0100 Subject: [PATCH 100/126] fixes --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index d4420a5c5f8..cec49e79771 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,6 +24,13 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; +export const allSettingsModalTabs: Array = [ + aboutTabId, + setupTabId, + policyTabId, + accessControlTabId, +]; + export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); From 5096c7c1686ea66c4c77fa0ec69706d11c2043bc Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 101/126] feat: context based login with ansattporten --- .../SettingsModal/SettingsModal.tsx | 8 ++++++++ .../components/Tabs/Maskinporten/Maskinporten.tsx | 15 +++++++++++++++ .../hooks/useSettingsModalMenuTabConfigs.tsx | 7 ------- frontend/packages/shared/src/api/paths.js | 3 +++ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index c5dad2b42fe..72885c6bf76 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -15,8 +15,12 @@ import { AccessControlTab } from './components/Tabs/AccessControlTab'; import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; +<<<<<<< HEAD import { Maskinporten } from './components/Tabs/Maskinporten'; import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; +======= +import { Maskinporten } from './components/Tabs/Maskinporten/Maskinporten'; +>>>>>>> 1f588e11a (feat: context based login with ansattporten) export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); @@ -54,7 +58,11 @@ export const SettingsModal = forwardRef(({}, ref): Reac return ; } case 'maskinporten': { +<<<<<<< HEAD return shouldDisplayFeature('maskinporten') ? : null; +======= + return ; +>>>>>>> 1f588e11a (feat: context based login with ansattporten) } } }; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx index cb3b33d5e1e..25590ba74e6 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx @@ -1,4 +1,5 @@ import React from 'react'; +<<<<<<< HEAD import { useTranslation } from 'react-i18next'; import { TabContent } from '../../TabContent'; import { StudioButton, StudioHeading, StudioParagraph, StudioSpinner } from '@studio/components'; @@ -22,6 +23,20 @@ export const Maskinporten = (): React.ReactElement => { return
View when logged in comes here
; } +======= +import { TabContent } from '../../TabContent'; +import { ansattportenLoginPath } from 'app-shared/api/paths'; +import { StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; +import { useTranslation } from 'react-i18next'; + +export const Maskinporten = (): React.ReactElement => { + const { t } = useTranslation(); + + const handleLoginWithAnsattporten = (): void => { + window.location.href = ansattportenLoginPath(); + }; + +>>>>>>> 1f588e11a (feat: context based login with ansattporten) return ( diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index cec49e79771..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,13 +24,6 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; -export const allSettingsModalTabs: Array = [ - aboutTabId, - setupTabId, - policyTabId, - accessControlTabId, -]; - export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 545ac846aaf..ad102961501 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 4fc11dba0d70a4b50c6566ffe91741f4019e395b Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 102/126] frontend with mocks for maskinporten --- .../Tabs/Maskinporten/Maskinporten.test.tsx | 4 ++++ .../Tabs/Maskinporten/Maskinporten.tsx | 17 ++--------------- frontend/packages/shared/src/api/paths.js | 3 --- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx index 25ed04969e6..9ecdd5eff06 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -93,5 +93,9 @@ const renderMaskinporten = ({ queries = queriesMock }: RenderMaskinporten) => { }; async function waitForLoggedInStatusCheckIsDone() { +<<<<<<< HEAD await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); +======= + await waitForElementToBeRemoved(() => screen.getByTitle(textMock('general.loading'))); +>>>>>>> b328101a5 (frontend with mocks for maskinporten) } diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx index 25590ba74e6..1eff808704f 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx @@ -1,5 +1,6 @@ import React from 'react'; -<<<<<<< HEAD +import { TabContent } from '../../TabContent'; +import { StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; import { useTranslation } from 'react-i18next'; import { TabContent } from '../../TabContent'; import { StudioButton, StudioHeading, StudioParagraph, StudioSpinner } from '@studio/components'; @@ -23,20 +24,6 @@ export const Maskinporten = (): React.ReactElement => { return
View when logged in comes here
; } -======= -import { TabContent } from '../../TabContent'; -import { ansattportenLoginPath } from 'app-shared/api/paths'; -import { StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; -import { useTranslation } from 'react-i18next'; - -export const Maskinporten = (): React.ReactElement => { - const { t } = useTranslation(); - - const handleLoginWithAnsattporten = (): void => { - window.location.href = ansattportenLoginPath(); - }; - ->>>>>>> 1f588e11a (feat: context based login with ansattporten) return ( diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index ad102961501..545ac846aaf 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From da3469434f9391dc17316edce560d14e08f04e02 Mon Sep 17 00:00:00 2001 From: David Ovrelid <46874830+framitdavid@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:28:57 +0100 Subject: [PATCH 103/126] Update frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx Co-authored-by: William Thorenfeldt <48119543+wrt95@users.noreply.github.com> --- .../components/Tabs/Maskinporten/Maskinporten.test.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx index 9ecdd5eff06..25ed04969e6 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -93,9 +93,5 @@ const renderMaskinporten = ({ queries = queriesMock }: RenderMaskinporten) => { }; async function waitForLoggedInStatusCheckIsDone() { -<<<<<<< HEAD await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); -======= - await waitForElementToBeRemoved(() => screen.getByTitle(textMock('general.loading'))); ->>>>>>> b328101a5 (frontend with mocks for maskinporten) } From 95630eacd5d1839927e3eb5068c04ceb0d48a68c Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 08:30:32 +0100 Subject: [PATCH 104/126] PR feedback --- .../SettingsModal/SettingsModal.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index 72885c6bf76..d8228e6fb1e 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -15,12 +15,8 @@ import { AccessControlTab } from './components/Tabs/AccessControlTab'; import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; -<<<<<<< HEAD -import { Maskinporten } from './components/Tabs/Maskinporten'; import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; -======= -import { Maskinporten } from './components/Tabs/Maskinporten/Maskinporten'; ->>>>>>> 1f588e11a (feat: context based login with ansattporten) +import { Maskinporten } from './components/Tabs/Maskinporten'; export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); @@ -58,11 +54,7 @@ export const SettingsModal = forwardRef(({}, ref): Reac return ; } case 'maskinporten': { -<<<<<<< HEAD return shouldDisplayFeature('maskinporten') ? : null; -======= - return ; ->>>>>>> 1f588e11a (feat: context based login with ansattporten) } } }; From 1bed9515ff1a8d5dc5b304971576ecdf878bb636 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:20:24 +0100 Subject: [PATCH 105/126] fixes --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index d4420a5c5f8..cec49e79771 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,6 +24,13 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; +export const allSettingsModalTabs: Array = [ + aboutTabId, + setupTabId, + policyTabId, + accessControlTabId, +]; + export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); From 6584186385dde5e8ed808eb4a33b19b479992d97 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 106/126] feat: context based login with ansattporten --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 ------- frontend/packages/shared/src/api/paths.js | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index cec49e79771..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,13 +24,6 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; -export const allSettingsModalTabs: Array = [ - aboutTabId, - setupTabId, - policyTabId, - accessControlTabId, -]; - export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 545ac846aaf..ad102961501 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 6a717dbeeed63c0de1252d3d455c87f3fe93678b Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 107/126] frontend with mocks for maskinporten --- frontend/packages/shared/src/api/paths.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index ad102961501..545ac846aaf 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 4aada2bd7d8744dab11b2dab7dedaaebf8896277 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 15:40:41 +0100 Subject: [PATCH 108/126] hide maskinporten behind featureflag --- .../SettingsModalButton/SettingsModal/SettingsModal.tsx | 2 +- frontend/packages/shared/src/utils/featureToggleUtils.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index d8228e6fb1e..c5dad2b42fe 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -15,8 +15,8 @@ import { AccessControlTab } from './components/Tabs/AccessControlTab'; import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; -import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; import { Maskinporten } from './components/Tabs/Maskinporten'; +import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.ts b/frontend/packages/shared/src/utils/featureToggleUtils.ts index 6314d0254d2..ea899eb7ff3 100644 --- a/frontend/packages/shared/src/utils/featureToggleUtils.ts +++ b/frontend/packages/shared/src/utils/featureToggleUtils.ts @@ -14,7 +14,10 @@ export type SupportedFeatureFlags = | 'subform' | 'summary2' | 'codeListEditor' +<<<<<<< HEAD | 'optionListEditor' +======= +>>>>>>> b99c16b37 (hide maskinporten behind featureflag) | 'maskinporten'; /* From 1e1f257b065c3f96668d416f0dfc904c6f24dd85 Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Tue, 12 Nov 2024 12:26:09 +0100 Subject: [PATCH 109/126] chore: scale designer in dev and staging (#14039) --- .github/workflows/deploy-designer.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/deploy-designer.yaml b/.github/workflows/deploy-designer.yaml index 34e66fe2784..6d71e0b2e67 100644 --- a/.github/workflows/deploy-designer.yaml +++ b/.github/workflows/deploy-designer.yaml @@ -106,7 +106,11 @@ jobs: artifact-environment: ${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }} config-chart-name: altinn-designer-config artifact-name: altinn-designer +<<<<<<< HEAD helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'dev' && 1 || 2 }} +======= + helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'preapproved-prod' && 1 || 2 }} +>>>>>>> 70f1511ca (chore: scale designer in dev and staging (#14039)) trace-workflow: true trace-team-name: 'team-studio' secrets: From e2eddbd7a2067e691bfb452168de6d32a6f099f5 Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Wed, 13 Nov 2024 14:43:32 +0100 Subject: [PATCH 110/126] feat: 2 replicas in prod/staging, 1 in dev (#14047) --- .github/workflows/deploy-designer.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/deploy-designer.yaml b/.github/workflows/deploy-designer.yaml index 6d71e0b2e67..34e66fe2784 100644 --- a/.github/workflows/deploy-designer.yaml +++ b/.github/workflows/deploy-designer.yaml @@ -106,11 +106,7 @@ jobs: artifact-environment: ${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }} config-chart-name: altinn-designer-config artifact-name: altinn-designer -<<<<<<< HEAD helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'dev' && 1 || 2 }} -======= - helm-set-arguments: environmentName=${{ matrix.environment == 'preapproved-prod' && 'prod' || matrix.environment }},chartVersion=0.1.0+${{ needs.determine-tag.outputs.tag }},imageTag=${{ needs.determine-tag.outputs.tag }},dbMigrationsTag=${{ needs.determine-tag.outputs.tag }},replicas=${{ matrix.environment == 'preapproved-prod' && 1 || 2 }} ->>>>>>> 70f1511ca (chore: scale designer in dev and staging (#14039)) trace-workflow: true trace-team-name: 'team-studio' secrets: From f87227bff9f54a7e4d8fe93965a31927d1bb752c Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 15:50:55 +0100 Subject: [PATCH 111/126] fixed issue after merge --- frontend/packages/shared/src/utils/featureToggleUtils.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.ts b/frontend/packages/shared/src/utils/featureToggleUtils.ts index ea899eb7ff3..6314d0254d2 100644 --- a/frontend/packages/shared/src/utils/featureToggleUtils.ts +++ b/frontend/packages/shared/src/utils/featureToggleUtils.ts @@ -14,10 +14,7 @@ export type SupportedFeatureFlags = | 'subform' | 'summary2' | 'codeListEditor' -<<<<<<< HEAD | 'optionListEditor' -======= ->>>>>>> b99c16b37 (hide maskinporten behind featureflag) | 'maskinporten'; /* From d3dc3171031e5b6c00574112c2c18ba98670a57c Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:16:33 +0100 Subject: [PATCH 112/126] feat(settings): make it possible to open settings modal based on query-params --- .../hooks/useOpenSettingsModalBasedQueryParam.ts | 4 ++++ frontend/app-development/layout/PageLayout.tsx | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts index 202a938b9f9..b9cad0fa1ab 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts @@ -16,7 +16,11 @@ export function useOpenSettingsModalBasedQueryParam(): void { if (shouldOpenModal) { settingsRef.current.openSettings(tabToOpen); } +<<<<<<< HEAD }, [searchParams, settingsRef]); +======= + }, [searchParams]); +>>>>>>> 34520cd0d (feat(settings): make it possible to open settings modal based on query-params) } function isValidTab(tabId: SettingsModalTabId): boolean { diff --git a/frontend/app-development/layout/PageLayout.tsx b/frontend/app-development/layout/PageLayout.tsx index 95ad155c170..68ba98e8a56 100644 --- a/frontend/app-development/layout/PageLayout.tsx +++ b/frontend/app-development/layout/PageLayout.tsx @@ -51,7 +51,11 @@ export const PageLayout = (): React.ReactNode => { isRepoError={repoStatusError !== null} /> +<<<<<<< HEAD +======= + +>>>>>>> 34520cd0d (feat(settings): make it possible to open settings modal based on query-params) ); }; @@ -60,10 +64,16 @@ type PagesToRenderProps = { repoStatusError: AxiosError; repoStatus: RepoStatus; }; +<<<<<<< HEAD const Pages = ({ repoStatusError, repoStatus }: PagesToRenderProps) => { // Listen to URL-search params and opens settings-modal if params matches. useOpenSettingsModalBasedQueryParam(); +======= +const PagesToRender = ({ repoStatusError, repoStatus }: PagesToRenderProps) => { + // Listen to URL-search params and opens settings-modal if params matches. + useOpenSettingsModalBasedQueryParam(); +>>>>>>> 34520cd0d (feat(settings): make it possible to open settings modal based on query-params) if (repoStatusError?.response?.status === ServerCodes.NotFound) { return ; } From f21756c9dc5b4416b56662f8d864ee999c414576 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:20:24 +0100 Subject: [PATCH 113/126] fixes --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index d4420a5c5f8..cec49e79771 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,6 +24,13 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; +export const allSettingsModalTabs: Array = [ + aboutTabId, + setupTabId, + policyTabId, + accessControlTabId, +]; + export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); From fa8a401409fc49c3a34130f5b81e7d3663838702 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 20:45:29 +0100 Subject: [PATCH 114/126] PR-feedback + improve unit-tests --- .../hooks/useOpenSettingsModalBasedQueryParam.ts | 4 ---- frontend/app-development/layout/PageLayout.tsx | 11 +---------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts index b9cad0fa1ab..202a938b9f9 100644 --- a/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts +++ b/frontend/app-development/hooks/useOpenSettingsModalBasedQueryParam.ts @@ -16,11 +16,7 @@ export function useOpenSettingsModalBasedQueryParam(): void { if (shouldOpenModal) { settingsRef.current.openSettings(tabToOpen); } -<<<<<<< HEAD }, [searchParams, settingsRef]); -======= - }, [searchParams]); ->>>>>>> 34520cd0d (feat(settings): make it possible to open settings modal based on query-params) } function isValidTab(tabId: SettingsModalTabId): boolean { diff --git a/frontend/app-development/layout/PageLayout.tsx b/frontend/app-development/layout/PageLayout.tsx index 68ba98e8a56..2d47148e6b0 100644 --- a/frontend/app-development/layout/PageLayout.tsx +++ b/frontend/app-development/layout/PageLayout.tsx @@ -51,11 +51,7 @@ export const PageLayout = (): React.ReactNode => { isRepoError={repoStatusError !== null} /> -<<<<<<< HEAD -======= - ->>>>>>> 34520cd0d (feat(settings): make it possible to open settings modal based on query-params) ); }; @@ -64,16 +60,11 @@ type PagesToRenderProps = { repoStatusError: AxiosError; repoStatus: RepoStatus; }; -<<<<<<< HEAD + const Pages = ({ repoStatusError, repoStatus }: PagesToRenderProps) => { // Listen to URL-search params and opens settings-modal if params matches. useOpenSettingsModalBasedQueryParam(); -======= -const PagesToRender = ({ repoStatusError, repoStatus }: PagesToRenderProps) => { - // Listen to URL-search params and opens settings-modal if params matches. - useOpenSettingsModalBasedQueryParam(); ->>>>>>> 34520cd0d (feat(settings): make it possible to open settings modal based on query-params) if (repoStatusError?.response?.status === ServerCodes.NotFound) { return ; } From 5b8e333d878e3e169867f4018da94aaf93770656 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 115/126] feat: context based login with ansattporten --- .../SettingsModal/SettingsModal.tsx | 8 ++++++++ .../components/Tabs/Maskinporten/Maskinporten.tsx | 14 ++++++++++++++ .../hooks/useSettingsModalMenuTabConfigs.tsx | 7 ------- frontend/packages/shared/src/api/paths.js | 3 +++ 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index c5dad2b42fe..85189d200a5 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -15,8 +15,12 @@ import { AccessControlTab } from './components/Tabs/AccessControlTab'; import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; +<<<<<<< HEAD import { Maskinporten } from './components/Tabs/Maskinporten'; import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; +======= +import { Maskinporten } from './components/Tabs/Maskinporten/Maskinporten'; +>>>>>>> 1348b929c (feat: context based login with ansattporten) export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); @@ -54,7 +58,11 @@ export const SettingsModal = forwardRef(({}, ref): Reac return ; } case 'maskinporten': { +<<<<<<< HEAD return shouldDisplayFeature('maskinporten') ? : null; +======= + return ; +>>>>>>> 1348b929c (feat: context based login with ansattporten) } } }; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx index 1eff808704f..7de4f2cafd3 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { TabContent } from '../../TabContent'; +<<<<<<< HEAD import { StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; import { useTranslation } from 'react-i18next'; import { TabContent } from '../../TabContent'; @@ -24,6 +25,19 @@ export const Maskinporten = (): React.ReactElement => { return
View when logged in comes here
; } +======= +import { ansattportenLoginPath } from 'app-shared/api/paths'; +import { StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; +import { useTranslation } from 'react-i18next'; + +export const Maskinporten = (): React.ReactElement => { + const { t } = useTranslation(); + + const handleLoginWithAnsattporten = (): void => { + window.location.href = ansattportenLoginPath(); + }; + +>>>>>>> 1348b929c (feat: context based login with ansattporten) return ( diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index cec49e79771..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,13 +24,6 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; -export const allSettingsModalTabs: Array = [ - aboutTabId, - setupTabId, - policyTabId, - accessControlTabId, -]; - export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 545ac846aaf..ad102961501 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 37a71da94a8481004e84bd13517241b4f57e7ac6 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 116/126] frontend with mocks for maskinporten --- .../Tabs/Maskinporten/Maskinporten.tsx | 16 ---------------- frontend/packages/shared/src/api/paths.js | 3 --- 2 files changed, 19 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx index 7de4f2cafd3..cb3b33d5e1e 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx @@ -1,7 +1,4 @@ import React from 'react'; -import { TabContent } from '../../TabContent'; -<<<<<<< HEAD -import { StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; import { useTranslation } from 'react-i18next'; import { TabContent } from '../../TabContent'; import { StudioButton, StudioHeading, StudioParagraph, StudioSpinner } from '@studio/components'; @@ -25,19 +22,6 @@ export const Maskinporten = (): React.ReactElement => { return
View when logged in comes here
; } -======= -import { ansattportenLoginPath } from 'app-shared/api/paths'; -import { StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; -import { useTranslation } from 'react-i18next'; - -export const Maskinporten = (): React.ReactElement => { - const { t } = useTranslation(); - - const handleLoginWithAnsattporten = (): void => { - window.location.href = ansattportenLoginPath(); - }; - ->>>>>>> 1348b929c (feat: context based login with ansattporten) return ( diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index ad102961501..545ac846aaf 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 093a257dc0215940c7a1067779b86c55c0962e9f Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 14 Nov 2024 08:30:32 +0100 Subject: [PATCH 117/126] PR feedback --- .../SettingsModalButton/SettingsModal/SettingsModal.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index 85189d200a5..c5dad2b42fe 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -15,12 +15,8 @@ import { AccessControlTab } from './components/Tabs/AccessControlTab'; import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; -<<<<<<< HEAD import { Maskinporten } from './components/Tabs/Maskinporten'; import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; -======= -import { Maskinporten } from './components/Tabs/Maskinporten/Maskinporten'; ->>>>>>> 1348b929c (feat: context based login with ansattporten) export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); @@ -58,11 +54,7 @@ export const SettingsModal = forwardRef(({}, ref): Reac return ; } case 'maskinporten': { -<<<<<<< HEAD return shouldDisplayFeature('maskinporten') ? : null; -======= - return ; ->>>>>>> 1348b929c (feat: context based login with ansattporten) } } }; From 4b4a2731bdb4b754772c30ac3f80d5523069f573 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 15:20:24 +0100 Subject: [PATCH 118/126] fixes --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index d4420a5c5f8..cec49e79771 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,6 +24,13 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; +export const allSettingsModalTabs: Array = [ + aboutTabId, + setupTabId, + policyTabId, + accessControlTabId, +]; + export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); From 53c0119f11e8326ff613357db80cc3a8e2546bd7 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 6 Nov 2024 20:45:29 +0100 Subject: [PATCH 119/126] PR-feedback + improve unit-tests --- frontend/app-development/layout/PageLayout.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/app-development/layout/PageLayout.tsx b/frontend/app-development/layout/PageLayout.tsx index 2d47148e6b0..95ad155c170 100644 --- a/frontend/app-development/layout/PageLayout.tsx +++ b/frontend/app-development/layout/PageLayout.tsx @@ -60,7 +60,6 @@ type PagesToRenderProps = { repoStatusError: AxiosError; repoStatus: RepoStatus; }; - const Pages = ({ repoStatusError, repoStatus }: PagesToRenderProps) => { // Listen to URL-search params and opens settings-modal if params matches. useOpenSettingsModalBasedQueryParam(); From 93c32846dd43f2a16c781d545ea9af7dc510c461 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Fri, 8 Nov 2024 11:57:59 +0100 Subject: [PATCH 120/126] feat: context based login with ansattporten --- .../SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx | 7 ------- frontend/packages/shared/src/api/paths.js | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx index cec49e79771..d4420a5c5f8 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -24,13 +24,6 @@ export const allSettingsModalTabs: Array = [ maskinportenTabId, ]; -export const allSettingsModalTabs: Array = [ - aboutTabId, - setupTabId, - policyTabId, - accessControlTabId, -]; - export const useSettingsModalMenuTabConfigs = (): StudioContentMenuButtonTabProps[] => { const { t } = useTranslation(); diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 545ac846aaf..ad102961501 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,9 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From 9c929352a3cd7182f72364d3add70576059752a5 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 12 Nov 2024 15:15:23 +0100 Subject: [PATCH 121/126] frontend with mocks for maskinporten --- frontend/packages/shared/src/api/paths.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index ad102961501..545ac846aaf 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,9 +4,6 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; -// Ansattporten -export const ansattportenLoginPath = () => `${basePath}/ansattporten/login`; - // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete From bafa65f404db26fc19e0361aa038f869018f155c Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Thu, 21 Nov 2024 13:11:21 +0100 Subject: [PATCH 122/126] added login with maskinporten --- .../useIsLoggedInWithAnsattportenQuery.ts | 4 +-- .../Tabs/Maskinporten/Maskinporten.test.tsx | 25 +++++++------------ .../Tabs/Maskinporten/Maskinporten.tsx | 7 +++--- frontend/packages/shared/src/api/paths.js | 4 +++ frontend/packages/shared/src/api/queries.ts | 12 ++++----- .../packages/shared/src/mocks/queriesMock.ts | 2 +- 6 files changed, 25 insertions(+), 29 deletions(-) diff --git a/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts b/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts index b40dfd28ad7..c35de4a9bf8 100644 --- a/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts +++ b/frontend/app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery.ts @@ -4,8 +4,8 @@ import { useServicesContext } from 'app-shared/contexts/ServicesContext'; export const useIsLoggedInWithAnsattportenQuery = () => { const { getIsLoggedInWithAnsattporten } = useServicesContext(); - return useQuery({ + return useQuery<{ isLoggedIn: boolean }>({ queryKey: [QueryKey.IsLoggedInWithAnsattporten], - queryFn: () => getIsLoggedInWithAnsattporten(), + queryFn: getIsLoggedInWithAnsattporten, }); }; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx index 0e5cdb46bb7..f7747ce62fe 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -6,22 +6,15 @@ import { textMock } from '@studio/testing/mocks/i18nMock'; import { queriesMock } from 'app-shared/mocks/queriesMock'; import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; import userEvent from '@testing-library/user-event'; +import { loginWithAnsattPorten } from 'app-shared/api/paths'; -describe('Maskinporten', () => { - const consoleLogMock = jest.fn(); - const originalConsoleLog = console.log; - beforeEach(() => { - console.log = consoleLogMock; - }); - - afterEach(() => { - console.log = originalConsoleLog; - }); +jest.mock('app-shared/api/paths'); +describe('Maskinporten', () => { it('should check and verify if the user is logged in', async () => { const getIsLoggedInWithAnsattportenMock = jest .fn() - .mockImplementation(() => Promise.resolve(false)); + .mockImplementation(() => Promise.resolve({ isLoggedIn: false })); renderMaskinporten({ queries: { @@ -54,7 +47,7 @@ describe('Maskinporten', () => { it('should display content if logged in', async () => { const getIsLoggedInWithAnsattportenMock = jest .fn() - .mockImplementation(() => Promise.resolve(true)); + .mockImplementation(() => Promise.resolve({ isLoggedIn: true })); renderMaskinporten({ queries: { getIsLoggedInWithAnsattporten: getIsLoggedInWithAnsattportenMock, @@ -68,6 +61,9 @@ describe('Maskinporten', () => { }); it('should invoke "handleLoginWithAnsattPorten" when login button is clicked', async () => { + const loginWithAnsattPortenMock = jest.fn(); + (loginWithAnsattPorten as jest.Mock).mockImplementation(loginWithAnsattPortenMock); + const user = userEvent.setup(); renderMaskinporten(); await waitForLoggedInStatusCheckIsDone(); @@ -77,10 +73,7 @@ describe('Maskinporten', () => { }); await user.click(loginButton); - - expect(consoleLogMock).toHaveBeenCalledWith( - 'Will be implemented in next iteration when backend is ready', - ); + expect(loginWithAnsattPortenMock).toHaveBeenCalledTimes('/'); }); }); diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx index 3ec228a4b5d..d7a44cb3d36 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.tsx @@ -3,22 +3,23 @@ import { useTranslation } from 'react-i18next'; import { TabContent } from '../../TabContent'; import { StudioButton, StudioHeading, StudioParagraph, StudioSpinner } from '@studio/components'; import { useIsLoggedInWithAnsattportenQuery } from '../../../../../../../../hooks/queries/useIsLoggedInWithAnsattportenQuery'; +import { loginWithAnsattPorten } from 'app-shared/api/paths'; export const Maskinporten = (): ReactElement => { - const { data: isLoggedInWithAnsattporten, isPending: isPendingAuthStatus } = + const { data: ansattportenAuthStatus, isPending: isPendingAuthStatus } = useIsLoggedInWithAnsattportenQuery(); const { t } = useTranslation(); const handleLoginWithAnsattporten = (): void => { - console.log('Will be implemented in next iteration when backend is ready'); + window.location.href = loginWithAnsattPorten(window.location.pathname + window.location.search); }; if (isPendingAuthStatus) { return ; } - if (isLoggedInWithAnsattporten) { + if (ansattportenAuthStatus.isLoggedIn) { return
View when logged in comes here
; } diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index 545ac846aaf..0e7b416dd78 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -4,6 +4,10 @@ import { PREVIEW_MOCK_PARTY_ID, PREVIEW_MOCK_INSTANCE_GUID } from '../constants' // Base path const basePath = '/designer/api'; +// Ansattporten +export const authStatusAnsattporten = () => `${basePath}/ansattporten/auth-status`; +export const loginWithAnsattPorten = (redirectTo) => `${basePath}/ansattporten/login?redirect_to=${redirectTo}`; + // ApplicationMetadata export const appMetadataPath = (org, app) => `${basePath}/${org}/${app}/metadata`; // Get, Put, Post export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app}/metadata/attachment-component`; // Post, Put, Delete diff --git a/frontend/packages/shared/src/api/queries.ts b/frontend/packages/shared/src/api/queries.ts index 6e96c93103b..1dc376f9926 100644 --- a/frontend/packages/shared/src/api/queries.ts +++ b/frontend/packages/shared/src/api/queries.ts @@ -53,6 +53,7 @@ import { repoDiffPath, getImageFileNamesPath, validateImageFromExternalUrlPath, + authStatusAnsattporten, } from './paths'; import type { AppReleasesResponse, DataModelMetadataResponse, SearchRepoFilterParams, SearchRepositoryResponse } from 'app-shared/types/api'; @@ -85,13 +86,10 @@ import type { RepoDiffResponse } from 'app-shared/types/api/RepoDiffResponse'; import type { ExternalImageUrlValidationResponse } from 'app-shared/types/api/ExternalImageUrlValidationResponse'; import type { OptionsLists } from 'app-shared/types/api/OptionsLists'; -export const getIsLoggedInWithAnsattporten = async (): Promise => - // TODO: replace with endpoint when it's ready in the backend. - new Promise((resolve) => { - setTimeout(() => { - return resolve(false); - }, 1000); - }); +export const getIsLoggedInWithAnsattporten = () => + get<{ + isLoggedIn: boolean; + }>(authStatusAnsattporten()); export const getAppMetadataModelIds = (org: string, app: string, onlyUnReferenced: boolean) => get(appMetadataModelIdsPath(org, app, onlyUnReferenced)); export const getAppReleases = (owner: string, app: string) => get(releasesPath(owner, app, 'Descending')); export const getAppVersion = (org: string, app: string) => get(appVersionPath(org, app)); diff --git a/frontend/packages/shared/src/mocks/queriesMock.ts b/frontend/packages/shared/src/mocks/queriesMock.ts index 3ef7db6eae9..e8723d6c7bf 100644 --- a/frontend/packages/shared/src/mocks/queriesMock.ts +++ b/frontend/packages/shared/src/mocks/queriesMock.ts @@ -171,7 +171,7 @@ export const queriesMock: ServicesContextProps = { getProcessTaskType: jest.fn().mockImplementation(() => Promise.resolve('')), getIsLoggedInWithAnsattporten: jest .fn() - .mockImplementation(() => Promise.resolve(false)), + .mockImplementation(() => Promise.resolve<{ isLoggedIn: false }>({ isLoggedIn: false })), // Mutations addAppAttachmentMetadata: jest.fn().mockImplementation(() => Promise.resolve()), From fffd866ed4b36fa6db45c0dadfabfd881daeed06 Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Thu, 21 Nov 2024 15:09:48 +0100 Subject: [PATCH 123/126] fix redirection from AP to endpoints with default auth scheme --- .../Controllers/AnsattPortenController.cs | 6 ++--- .../AuthenticationConfiguration.cs | 5 +++- .../AnsattPortenController/AuthStatusTests.cs | 3 ++- .../Base/AnsattPortenControllerTestsBase.cs | 24 +++++++++++++++++++ .../AnsattPortenController/LoginTests.cs | 15 ++---------- 5 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 backend/tests/Designer.Tests/Controllers/AnsattPortenController/Base/AnsattPortenControllerTestsBase.cs diff --git a/backend/src/Designer/Controllers/AnsattPortenController.cs b/backend/src/Designer/Controllers/AnsattPortenController.cs index 8feff836cf4..95f7f73dee7 100644 --- a/backend/src/Designer/Controllers/AnsattPortenController.cs +++ b/backend/src/Designer/Controllers/AnsattPortenController.cs @@ -11,7 +11,7 @@ namespace Altinn.Studio.Designer.Controllers; [FeatureGate(StudioFeatureFlags.AnsattPorten)] [Route("designer/api/[controller]")] [ApiController] -public class AnsattPortenController(IAuthenticationService authService) : ControllerBase +public class AnsattPortenController : ControllerBase { [Authorize(AnsattPortenConstants.AnsattportenAuthorizationPolicy)] [HttpGet("login")] @@ -32,8 +32,7 @@ public async Task AuthStatus() { await Task.CompletedTask; var authenticateResult = - await authService.AuthenticateAsync(HttpContext, - AnsattPortenConstants.AnsattportenAuthenticationScheme); + await HttpContext.AuthenticateAsync(AnsattPortenConstants.AnsattportenAuthenticationScheme); var authStatus = new AuthStatus { @@ -43,5 +42,4 @@ await authService.AuthenticateAsync(HttpContext, return Ok(authStatus); } - } diff --git a/backend/src/Designer/Infrastructure/AuthenticationConfiguration.cs b/backend/src/Designer/Infrastructure/AuthenticationConfiguration.cs index 7c0230e33ce..b85b469c457 100644 --- a/backend/src/Designer/Infrastructure/AuthenticationConfiguration.cs +++ b/backend/src/Designer/Infrastructure/AuthenticationConfiguration.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using Altinn.Studio.Designer.Configuration; +using Altinn.Studio.Designer.Constants; using Altinn.Studio.Designer.Helpers; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; @@ -38,6 +39,7 @@ private static IServiceCollection AddGiteaOidcAuthentication(this IServiceCollec IConfiguration configuration, IWebHostEnvironment env) { var oidcSettings = FetchOidcSettingsFromConfiguration(configuration, env); + bool ansattPortenFeatureFlag = configuration.GetSection($"FeatureManagement:{StudioFeatureFlags.AnsattPorten}").Get(); services .AddAuthentication(options => @@ -49,7 +51,8 @@ private static IServiceCollection AddGiteaOidcAuthentication(this IServiceCollec { options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; - options.Cookie.SameSite = SameSiteMode.Strict; + options.Cookie.SameSite = ansattPortenFeatureFlag ? SameSiteMode.Lax : SameSiteMode.Strict; + options.Cookie.IsEssential = true; options.ExpireTimeSpan = TimeSpan.FromMinutes(oidcSettings.CookieExpiryTimeInMinutes); diff --git a/backend/tests/Designer.Tests/Controllers/AnsattPortenController/AuthStatusTests.cs b/backend/tests/Designer.Tests/Controllers/AnsattPortenController/AuthStatusTests.cs index 7097a351233..cf5e57d2c0b 100644 --- a/backend/tests/Designer.Tests/Controllers/AnsattPortenController/AuthStatusTests.cs +++ b/backend/tests/Designer.Tests/Controllers/AnsattPortenController/AuthStatusTests.cs @@ -2,6 +2,7 @@ using System.Net.Http; using System.Threading.Tasks; using Altinn.Studio.Designer.Models.Dto; +using Designer.Tests.Controllers.AnsattPortenController.Base; using Designer.Tests.Controllers.ApiTests; using FluentAssertions; using Microsoft.AspNetCore.Authentication; @@ -15,7 +16,7 @@ namespace Designer.Tests.Controllers.AnsattPortenController; -public class AuthStatusTest : DesignerEndpointsTestsBase, IClassFixture> +public class AuthStatusTest : AnsattPortenControllerTestsBase, IClassFixture> { private static string VersionPrefix => "/designer/api/ansattporten/auth-status"; diff --git a/backend/tests/Designer.Tests/Controllers/AnsattPortenController/Base/AnsattPortenControllerTestsBase.cs b/backend/tests/Designer.Tests/Controllers/AnsattPortenController/Base/AnsattPortenControllerTestsBase.cs new file mode 100644 index 00000000000..c305064be10 --- /dev/null +++ b/backend/tests/Designer.Tests/Controllers/AnsattPortenController/Base/AnsattPortenControllerTestsBase.cs @@ -0,0 +1,24 @@ +using Altinn.Studio.Designer.Constants; +using Designer.Tests.Controllers.ApiTests; +using Microsoft.AspNetCore.Mvc.Testing; + +namespace Designer.Tests.Controllers.AnsattPortenController.Base; + +public class AnsattPortenControllerTestsBase : DesignerEndpointsTestsBase where TControllerTest : class +{ + public AnsattPortenControllerTestsBase(WebApplicationFactory factory) : base(factory) + { + JsonConfigOverrides.Add( + $$""" + { + "FeatureManagement": { + "{{StudioFeatureFlags.AnsattPorten}}": true + }, + "AnsattPortenLoginSettings": { + "ClientId": "non-empty-for-testing", + "ClientSecret": "non-empty-for-testing" + } + } + """); + } +} diff --git a/backend/tests/Designer.Tests/Controllers/AnsattPortenController/LoginTests.cs b/backend/tests/Designer.Tests/Controllers/AnsattPortenController/LoginTests.cs index 1d928cf2b29..daa6b2f9339 100644 --- a/backend/tests/Designer.Tests/Controllers/AnsattPortenController/LoginTests.cs +++ b/backend/tests/Designer.Tests/Controllers/AnsattPortenController/LoginTests.cs @@ -2,30 +2,19 @@ using System.Net.Http; using System.Threading.Tasks; using Altinn.Studio.Designer.Constants; +using Designer.Tests.Controllers.AnsattPortenController.Base; using Designer.Tests.Controllers.ApiTests; using Microsoft.AspNetCore.Mvc.Testing; using Xunit; namespace Designer.Tests.Controllers.AnsattPortenController; -public class LoginTests : DesignerEndpointsTestsBase, IClassFixture> +public class LoginTests : AnsattPortenControllerTestsBase, IClassFixture> { private static string VersionPrefix => "/designer/api/ansattporten/login"; public LoginTests(WebApplicationFactory factory) : base(factory) { - JsonConfigOverrides.Add( - $$""" - { - "FeatureManagement": { - "{{StudioFeatureFlags.AnsattPorten}}": true - }, - "AnsattPortenLoginSettings": { - "ClientId": "non-empty-for-testing", - "ClientSecret": "non-empty-for-testing" - } - } - """); } [Theory] From 07c0e0c5fd94938ddbe64178cdb83f19377de4b7 Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Mon, 25 Nov 2024 12:40:55 +0100 Subject: [PATCH 124/126] set feature flag som true in dev --- charts/altinn-designer/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/altinn-designer/values.yaml b/charts/altinn-designer/values.yaml index f45614a2836..5e57f21f2f7 100644 --- a/charts/altinn-designer/values.yaml +++ b/charts/altinn-designer/values.yaml @@ -53,7 +53,7 @@ environmentVariables: - name: OidcLoginSettings__CookieExpiryTimeInMinutes value: 59 - name: FeatureManagement__AnsattPorten - value: "false" + value: "true" - name: FeatureManagement__EidLogging value: "true" staging: From f8f3c38b738dd657af859836bdb281263583ea46 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Mon, 25 Nov 2024 14:43:11 +0100 Subject: [PATCH 125/126] Fixed unit-test --- .../Tabs/Maskinporten/Maskinporten.test.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx index 1ed13508d60..c11d8119313 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/Maskinporten.test.tsx @@ -6,7 +6,6 @@ import { textMock } from '@studio/testing/mocks/i18nMock'; import { queriesMock } from 'app-shared/mocks/queriesMock'; import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; import userEvent from '@testing-library/user-event'; -import { loginWithAnsattPorten } from 'app-shared/api/paths'; jest.mock('app-shared/api/paths'); @@ -63,8 +62,8 @@ describe('Maskinporten', () => { }); it('should invoke "handleLoginWithAnsattPorten" when login button is clicked', async () => { - const loginWithAnsattPortenMock = jest.fn(); - (loginWithAnsattPorten as jest.Mock).mockImplementation(loginWithAnsattPortenMock); + // jsdom does not support .href navigation, therefore this mock is needed. + const hrefMock = mockWindowLocationHref(); const user = userEvent.setup(); renderMaskinporten(); @@ -75,13 +74,13 @@ describe('Maskinporten', () => { }); await user.click(loginButton); - expect(loginWithAnsattPortenMock).toHaveBeenCalledTimes('/'); + expect(hrefMock).toHaveBeenCalledTimes(1); }); it('should show an alert with text that no scopes are available for user', async () => { const getIsLoggedInWithAnsattportenMock = jest .fn() - .mockImplementation(() => Promise.resolve(true)); + .mockImplementation(() => Promise.resolve({ isLoggedIn: true })); const mockGetMaskinportenScopes = jest.fn().mockImplementation(() => Promise.resolve([])); @@ -111,3 +110,14 @@ const renderMaskinporten = ({ queries = queriesMock }: RenderMaskinporten = {}) async function waitForLoggedInStatusCheckIsDone() { await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); } + +function mockWindowLocationHref(): jest.Mock { + const hrefMock = jest.fn(); + delete window.location; + window.location = { href: '' } as Location; + Object.defineProperty(window.location, 'href', { + set: hrefMock, + }); + + return hrefMock; +} From a7dd8d15bb29ccb8d5b93a551863d9077dffee26 Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Mon, 25 Nov 2024 15:10:38 +0100 Subject: [PATCH 126/126] remove authdelegating handler for AP tests --- .../Controllers/AnsattPortenController/AuthStatusTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/Designer.Tests/Controllers/AnsattPortenController/AuthStatusTests.cs b/backend/tests/Designer.Tests/Controllers/AnsattPortenController/AuthStatusTests.cs index cf5e57d2c0b..59958508764 100644 --- a/backend/tests/Designer.Tests/Controllers/AnsattPortenController/AuthStatusTests.cs +++ b/backend/tests/Designer.Tests/Controllers/AnsattPortenController/AuthStatusTests.cs @@ -40,7 +40,7 @@ protected override HttpClient GetTestClient() }); builder.ConfigureTestServices(ConfigureTestServices); builder.ConfigureServices(ConfigureTestServicesForSpecificTest); - }).CreateDefaultClient(new ApiTestsAuthAndCookieDelegatingHandler(), new CookieContainerHandler()); + }).CreateDefaultClient(new CookieContainerHandler()); } public AuthStatusTest(WebApplicationFactory factory) : base(factory)