Skip to content

Commit

Permalink
feat: show alert when no scope is available (#14059)
Browse files Browse the repository at this point in the history
Co-authored-by: davidovrelid.com <[email protected]>
Co-authored-by: David Ovrelid <[email protected]>
Co-authored-by: Mirko Sekulic <[email protected]>
  • Loading branch information
4 people authored Nov 21, 2024
1 parent 2664fed commit 6a1351e
Show file tree
Hide file tree
Showing 12 changed files with 176 additions and 9 deletions.
12 changes: 12 additions & 0 deletions frontend/app-development/hooks/queries/useGetScopesQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query';
import { QueryKey } from 'app-shared/types/QueryKey';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { type MaskinportenScope } from 'app-shared/types/MaskinportenScope';

export const useGetScopesQuery = () => {
const { getMaskinportenScopes } = useServicesContext();
return useQuery<MaskinportenScope[]>({
queryKey: [QueryKey.AppScopes],
queryFn: () => getMaskinportenScopes(),
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,10 @@ describe('Maskinporten', () => {

await waitForLoggedInStatusCheckIsDone();

const temporaryLoggedInContent = screen.getByText('View when logged in comes here');
expect(temporaryLoggedInContent).toBeInTheDocument();
const loginButton = screen.queryByRole('button', {
name: textMock('settings_modal.maskinporten_tab_login_with_ansattporten'),
});
expect(loginButton).not.toBeInTheDocument();
});

it('should invoke "handleLoginWithAnsattPorten" when login button is clicked', async () => {
Expand All @@ -82,6 +84,27 @@ describe('Maskinporten', () => {
'Will be implemented in next iteration when backend is ready',
);
});

it('should show an alert with text that no scopes are available for user', async () => {
const getIsLoggedInWithAnsattportenMock = jest
.fn()
.mockImplementation(() => Promise.resolve(true));

const mockGetMaskinportenScopes = jest.fn().mockImplementation(() => Promise.resolve([]));

renderMaskinporten({
queries: {
getIsLoggedInWithAnsattporten: getIsLoggedInWithAnsattportenMock,
getMaskinportenScopes: mockGetMaskinportenScopes,
},
});

await waitForLoggedInStatusCheckIsDone();

expect(
screen.getByText(textMock('settings_modal.maskinporten_no_scopes_available')),
).toBeInTheDocument();
});
});

type RenderMaskinporten = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { type ReactElement } from 'react';
import React, { type ReactNode, 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';
import { useIsLoggedInWithAnsattportenQuery } from 'app-development/hooks/queries/useIsLoggedInWithAnsattportenQuery';
import { ScopeList } from './ScopeList';

export const Maskinporten = (): ReactElement => {
const { data: isLoggedInWithAnsattporten, isPending: isPendingAuthStatus } =
Expand All @@ -19,18 +20,35 @@ export const Maskinporten = (): ReactElement => {
}

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

return (
<TabContent>
<StudioHeading level={2} size='sm' spacing>
{t('settings_modal.maskinporten_tab_title')}
</StudioHeading>
<MaskinportenPageTemplate>
<StudioParagraph spacing>{t('settings_modal.maskinporten_tab_description')}</StudioParagraph>
<StudioButton onClick={handleLoginWithAnsattporten}>
{t('settings_modal.maskinporten_tab_login_with_ansattporten')}
</StudioButton>
</MaskinportenPageTemplate>
);
};

type MaskinportenPageTemplateProps = {
children: ReactNode;
};

const MaskinportenPageTemplate = ({ children }: MaskinportenPageTemplateProps): ReactElement => {
const { t } = useTranslation();
return (
<TabContent>
<StudioHeading level={2} size='sm' spacing>
{t('settings_modal.maskinporten_tab_title')}
</StudioHeading>
{children}
</TabContent>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.noScopeAlert {
margin-top: var(--fds-spacing-8);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import { screen, waitForElementToBeRemoved } from '@testing-library/react';
import { ScopeList } from './ScopeList';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { queriesMock } from 'app-shared/mocks/queriesMock';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { renderWithProviders } from 'app-development/test/mocks';
import { type MaskinportenScope } from 'app-shared/types/MaskinportenScope';

const scopesMock: MaskinportenScope = {
scope: 'label',
description: 'description',
};

describe('ScopeList', () => {
it('should display a spinner while loading', () => {
renderScopeList();
expect(screen.getByTitle(textMock('general.loading'))).toBeInTheDocument();
});

it('should display a list of scopes if scopes are available', async () => {
const mockGetMaskinportenScopes = jest
.fn()
.mockImplementation(() => Promise.resolve([scopesMock]));

renderScopeList({
queries: {
getMaskinportenScopes: mockGetMaskinportenScopes,
},
});

await waitForGetScopesCheckIsDone();

expect(
screen.getByText('List of scopes and possibility to select scope comes here'),
).toBeInTheDocument();
});

it('should display an alert if no scopes are available', async () => {
const mockGetMaskinportenScopes = jest.fn().mockImplementation(() => Promise.resolve([]));

renderScopeList({
queries: {
getMaskinportenScopes: mockGetMaskinportenScopes,
},
});
await waitForGetScopesCheckIsDone();

expect(
screen.getByText(textMock('settings_modal.maskinporten_no_scopes_available')),
).toBeInTheDocument();
});
});

type RenderScopeListProps = {
queries?: Partial<typeof queriesMock>;
};
const renderScopeList = ({ queries = queriesMock }: Partial<RenderScopeListProps> = {}) => {
const queryClient = createQueryClientMock();

renderWithProviders({ ...queriesMock, ...queries }, queryClient)(<ScopeList />);
};

async function waitForGetScopesCheckIsDone() {
await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading')));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { type ReactElement } from 'react';
import classes from './ScopeList.module.css';
import { StudioAlert, StudioSpinner } from '@studio/components';
import { useGetScopesQuery } from 'app-development/hooks/queries/useGetScopesQuery';
import { useTranslation } from 'react-i18next';

export const ScopeList = (): ReactElement => {
const { t } = useTranslation();
const { data: scopes, isPending: isPendingScopes } = useGetScopesQuery();
const hasScopes: boolean = scopes?.length > 0;

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

if (hasScopes) {
return <div>List of scopes and possibility to select scope comes here</div>;
}

return (
<StudioAlert severity='info' className={classes.noScopeAlert}>
{t('settings_modal.maskinporten_no_scopes_available')}
</StudioAlert>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ScopeList } from './ScopeList';
1 change: 1 addition & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,7 @@
"settings_modal.left_nav_tab_policy": "Tilganger",
"settings_modal.left_nav_tab_setup": "Oppsett",
"settings_modal.loading_content": "Laster inn data",
"settings_modal.maskinporten_no_scopes_available": "Du har ingen tilgjengelige scopes å velge mellom fordi din bruker ikke er tilknyttet noen scopes i Maskinporten.",
"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",
Expand Down
9 changes: 9 additions & 0 deletions frontend/packages/shared/src/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import type { FormLayoutsResponseV3 } from 'app-shared/types/api/FormLayoutsResp
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';
import type { MaskinportenScope } from 'app-shared/types/MaskinportenScope';
import type { OptionsLists } from 'app-shared/types/api/OptionsLists';

export const getIsLoggedInWithAnsattporten = async (): Promise<boolean> =>
Expand All @@ -91,6 +92,14 @@ export const getIsLoggedInWithAnsattporten = async (): Promise<boolean> =>
return resolve(false);
}, 1000);
});
export const getMaskinportenScopes = async (): Promise<MaskinportenScope[]> =>
// TODO: replace with endpoint when it's ready in the backend.
new Promise((resolve) => {
setTimeout(() => {
return resolve([]);
}, 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
4 changes: 4 additions & 0 deletions frontend/packages/shared/src/mocks/queriesMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import type { FormLayoutsResponseV3 } from 'app-shared/types/api/FormLayoutsResp
import type { DeploymentsResponse } from 'app-shared/types/api/DeploymentsResponse';
import type { RepoDiffResponse } from 'app-shared/types/api/RepoDiffResponse';
import type { ExternalImageUrlValidationResponse } from 'app-shared/types/api/ExternalImageUrlValidationResponse';
import type { MaskinportenScope } from 'app-shared/types/MaskinportenScope';
import type { OptionsLists } from 'app-shared/types/api/OptionsLists';
import type { Option } from 'app-shared/types/Option';

Expand Down Expand Up @@ -172,6 +173,9 @@ export const queriesMock: ServicesContextProps = {
getIsLoggedInWithAnsattporten: jest
.fn()
.mockImplementation(() => Promise.resolve<boolean>(false)),
getMaskinportenScopes: jest
.fn()
.mockImplementation(() => Promise.resolve<MaskinportenScope[]>([])),

// Mutations
addAppAttachmentMetadata: jest.fn().mockImplementation(() => Promise.resolve()),
Expand Down
4 changes: 4 additions & 0 deletions frontend/packages/shared/src/types/MaskinportenScope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type MaskinportenScope = {
scope: string;
description: string;
};
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 @@ -45,6 +45,7 @@ export enum QueryKey {
Widgets = 'Widgets',
AppConfig = 'AppConfig',
IsLoggedInWithAnsattporten = 'IsLoggedInWithAnsattporten',
AppScopes = 'AppScopes',

// Resourceadm
ResourceList = 'ResourceList',
Expand Down

0 comments on commit 6a1351e

Please sign in to comment.