Skip to content

Commit

Permalink
feat: context based login with ansattporten (#14014)
Browse files Browse the repository at this point in the history
Co-authored-by: William Thorenfeldt <[email protected]>
Co-authored-by: Mirko Sekulic <[email protected]>
  • Loading branch information
3 people authored Nov 21, 2024
1 parent c811226 commit 498673f
Show file tree
Hide file tree
Showing 15 changed files with 232 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -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<boolean>({
queryKey: [QueryKey.IsLoggedInWithAnsattporten],
queryFn: () => getIsLoggedInWithAnsattporten(),
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,26 @@ 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';
import { useSettingsModalMenuTabConfigs } from '../layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs';

export const queryParamKey: string = 'openSettingsModalWithTab';

export function useOpenSettingsModalBasedQueryParam(): void {
const [searchParams] = useSearchParams();
const { settingsRef } = useSettingsModalContext();
const settingsModalTabs = useSettingsModalMenuTabConfigs();

const tabIds = settingsModalTabs.map(({ tabId }) => tabId);

useEffect((): void => {
const tabToOpen: SettingsModalTabId = searchParams.get(queryParamKey) as SettingsModalTabId;
const shouldOpenModal: boolean = isValidTab(tabToOpen);
const shouldOpenModal: boolean = isValidTab(tabToOpen, tabIds);
if (shouldOpenModal) {
settingsRef.current.openSettings(tabToOpen);
}
}, [searchParams, settingsRef]);
}, [searchParams, settingsRef, tabIds]);
}

function isValidTab(tabId: SettingsModalTabId): boolean {
return allSettingsModalTabs.includes(tabId);
function isValidTab(tabId: SettingsModalTabId, tabIds: Array<SettingsModalTabId>): boolean {
return tabIds.includes(tabId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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<string[]>('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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
type StudioContentMenuButtonTabProps,
} from '@studio/components';
import type { SettingsModalTabId } from '../../../../../types/SettingsModalTabId';
import { useTranslation } from 'react-i18next';
import { PolicyTab } from './components/Tabs/PolicyTab';
Expand All @@ -11,6 +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 { Maskinporten } from './components/Tabs/Maskinporten';
import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils';

export const SettingsModal = forwardRef<SettingsModalHandle, {}>(({}, ref): ReactElement => {
const { t } = useTranslation();
Expand All @@ -19,6 +25,8 @@ export const SettingsModal = forwardRef<SettingsModalHandle, {}>(({}, ref): Reac
const dialogRef = useRef<HTMLDialogElement>();
const menuTabConfigs = useSettingsModalMenuTabConfigs();

const menuTabsToRender = filterFeatureFlag(menuTabConfigs);

const openSettings = useCallback(
(tab: SettingsModalTabId = currentTab) => {
setCurrentTab(tab);
Expand All @@ -45,6 +53,9 @@ export const SettingsModal = forwardRef<SettingsModalHandle, {}>(({}, ref): Reac
case 'access_control': {
return <AccessControlTab />;
}
case 'maskinporten': {
return shouldDisplayFeature('maskinporten') ? <Maskinporten /> : null;
}
}
};

Expand All @@ -63,7 +74,7 @@ export const SettingsModal = forwardRef<SettingsModalHandle, {}>(({}, ref): Reac
selectedTabId={currentTab}
onChangeTab={(tabId: SettingsModalTabId) => setCurrentTab(tabId)}
>
{menuTabConfigs.map((contentTab) => (
{menuTabsToRender.map((contentTab) => (
<StudioContentMenu.ButtonTab
key={contentTab.tabId}
tabName={contentTab.tabName}
Expand All @@ -79,3 +90,11 @@ export const SettingsModal = forwardRef<SettingsModalHandle, {}>(({}, ref): Reac
});

SettingsModal.displayName = 'SettingsModal';

function filterFeatureFlag(
menuTabConfigs: Array<StudioContentMenuButtonTabProps<SettingsModalTabId>>,
) {
return shouldDisplayFeature('maskinporten')
? menuTabConfigs
: menuTabConfigs.filter((tab) => tab.tabId !== 'maskinporten');
}
Original file line number Diff line number Diff line change
@@ -0,0 +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', () => {
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<typeof queriesMock>;
};
const renderMaskinporten = ({ queries = queriesMock }: RenderMaskinporten = {}) => {
const queryClient = createQueryClientMock();
renderWithProviders({ ...queriesMock, ...queries }, queryClient)(<Maskinporten />);
};

async function waitForLoggedInStatusCheckIsDone() {
await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading')));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { type ReactElement } from 'react';
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 = (): ReactElement => {
const { data: isLoggedInWithAnsattporten, isPending: isPendingAuthStatus } =
useIsLoggedInWithAnsattportenQuery();

const { t } = useTranslation();

const handleLoginWithAnsattporten = (): void => {
console.log('Will be implemented in next iteration when backend is ready');
};

if (isPendingAuthStatus) {
return <StudioSpinner spinnerTitle={t('general.loading')} />;
}

if (isLoggedInWithAnsattporten) {
return <div>View when logged in comes here</div>;
}

return (
<TabContent>
<StudioHeading level={2} size='sm' spacing>
{t('settings_modal.maskinporten_tab_title')}
</StudioHeading>
<StudioParagraph spacing>{t('settings_modal.maskinporten_tab_description')}</StudioParagraph>
<StudioButton onClick={handleLoginWithAnsattporten}>
{t('settings_modal.maskinporten_tab_login_with_ansattporten')}
</StudioButton>
</TabContent>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Maskinporten } from './Maskinporten';
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
SidebarBothIcon,
ShieldLockIcon,
TimerStartIcon,
CogIcon,
} from '@studio/icons';
import { useTranslation } from 'react-i18next';
import type { StudioContentMenuButtonTabProps } from '@studio/components';
Expand All @@ -13,13 +14,7 @@ const aboutTabId: SettingsModalTabId = 'about';
const setupTabId: SettingsModalTabId = 'setup';
const policyTabId: SettingsModalTabId = 'policy';
const accessControlTabId: SettingsModalTabId = 'access_control';

export const allSettingsModalTabs: Array<SettingsModalTabId> = [
aboutTabId,
setupTabId,
policyTabId,
accessControlTabId,
];
const maskinportenTabId: SettingsModalTabId = 'maskinporten';

export const useSettingsModalMenuTabConfigs =
(): StudioContentMenuButtonTabProps<SettingsModalTabId>[] => {
Expand All @@ -46,5 +41,10 @@ export const useSettingsModalMenuTabConfigs =
tabName: t(`settings_modal.left_nav_tab_${accessControlTabId}`),
icon: <TimerStartIcon />,
},
{
tabId: maskinportenTabId,
tabName: t(`settings_modal.left_nav_tab_${maskinportenTabId}`),
icon: <CogIcon />,
},
];
};
2 changes: 1 addition & 1 deletion frontend/app-development/types/SettingsModalTabId.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export type SettingsModalTabId = 'about' | 'setup' | 'policy' | 'access_control';
export type SettingsModalTabId = 'about' | 'setup' | 'policy' | 'access_control' | 'maskinporten';
4 changes: 4 additions & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -1002,9 +1002,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",
Expand Down
13 changes: 11 additions & 2 deletions frontend/packages/shared/src/api/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,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
Expand Down Expand Up @@ -89,7 +93,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
Expand Down
7 changes: 7 additions & 0 deletions frontend/packages/shared/src/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,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<boolean> =>
// 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<string[]>(appMetadataModelIdsPath(org, app, onlyUnReferenced));
export const getAppReleases = (owner: string, app: string) => get<AppReleasesResponse>(releasesPath(owner, app, 'Descending'));
export const getAppVersion = (org: string, app: string) => get<AppVersion>(appVersionPath(org, app));
Expand Down
3 changes: 3 additions & 0 deletions frontend/packages/shared/src/mocks/queriesMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ export const queriesMock: ServicesContextProps = {
// Queries - PrgetBpmnFile
getBpmnFile: jest.fn().mockImplementation(() => Promise.resolve<string>('')),
getProcessTaskType: jest.fn().mockImplementation(() => Promise.resolve<string>('')),
getIsLoggedInWithAnsattporten: jest
.fn()
.mockImplementation(() => Promise.resolve<boolean>(false)),

// Mutations
addAppAttachmentMetadata: jest.fn().mockImplementation(() => Promise.resolve()),
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/shared/src/types/QueryKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export enum QueryKey {
TextResources = 'TextResources',
Widgets = 'Widgets',
AppConfig = 'AppConfig',
IsLoggedInWithAnsattporten = 'IsLoggedInWithAnsattporten',

// Resourceadm
ResourceList = 'ResourceList',
Expand Down
4 changes: 3 additions & 1 deletion frontend/packages/shared/src/utils/featureToggleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export type SupportedFeatureFlags =
| 'addComponentModal'
| 'subform'
| 'summary2'
| 'optionListEditor';
| 'codeListEditor'
| 'optionListEditor'
| 'maskinporten';

/*
* Please add all the features that you want to be toggle on by default here.
Expand Down

0 comments on commit 498673f

Please sign in to comment.