From a4f596aea11fae3be6d86e4e635271e43e1b80e5 Mon Sep 17 00:00:00 2001 From: Martin Gunnerud Date: Thu, 4 Apr 2024 11:56:08 +0200 Subject: [PATCH 1/7] show error message if user does not have permission to read or edit resource accsslists --- frontend/language/src/nb.json | 1 + .../AccessListEnvLinks/AccessListEnvLinks.tsx | 2 +- .../NewAccessListModal/NewAccessListModal.tsx | 7 +++-- .../ResourceAccessLists.test.tsx | 22 +++++++++++++- .../ResourceAccessLists.tsx | 14 +++++++-- .../ResourceDeployEnvCard.test.tsx | 2 +- .../ResourceDeployEnvCard.tsx | 2 +- .../AboutResourcePage/AboutResourcePage.tsx | 2 +- .../DeployResourcePage/DeployResourcePage.tsx | 2 +- .../ListAdminPage/ListAdminPage.test.tsx | 29 +++++++++++++++---- .../pages/ListAdminPage/ListAdminPage.tsx | 29 +++++++++++++------ .../pages/MigrationPage/MigrationPage.tsx | 4 +-- .../resourceadm/utils/resourceUtils/index.ts | 8 +++++ .../utils/resourceUtils/resourceUtils.ts | 3 ++ 14 files changed, 100 insertions(+), 27 deletions(-) diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 7db294998bc..04064515e03 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -1021,6 +1021,7 @@ "resourceadm.listadmin_type": "Type", "resourceadm.listadmin_undo_remove_from_list": "Angre fjern", "resourceadm.loading_access_list": "Laster inn tilgangsliste", + "resourceadm.loading_access_list_permission_denied": "Du har ikke tilgang til å administrere tilgangslister i {{envName}}", "resourceadm.loading_app": "Laster inn ressursregisteret", "resourceadm.loading_env_list": "Laster inn miljølister", "resourceadm.loading_lists": "Laster inn lister", diff --git a/frontend/resourceadm/components/AccessListEnvLinks/AccessListEnvLinks.tsx b/frontend/resourceadm/components/AccessListEnvLinks/AccessListEnvLinks.tsx index b0bc958e20f..77c23d4acc7 100644 --- a/frontend/resourceadm/components/AccessListEnvLinks/AccessListEnvLinks.tsx +++ b/frontend/resourceadm/components/AccessListEnvLinks/AccessListEnvLinks.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { Alert, Button, List, Paragraph } from '@digdir/design-system-react'; import { getResourcePageURL } from '../../utils/urlUtils'; import { useUrlParams } from '../../hooks/useSelectedContext'; -import { getAvailableEnvironments } from '../../utils/resourceUtils/resourceUtils'; +import { getAvailableEnvironments } from '../../utils/resourceUtils'; import { useResourcePolicyPublishStatusQuery } from '../../hooks/queries'; import { StudioSpinner } from '@studio/components'; import { ArrowForwardIcon } from '@studio/icons'; diff --git a/frontend/resourceadm/components/NewAccessListModal/NewAccessListModal.tsx b/frontend/resourceadm/components/NewAccessListModal/NewAccessListModal.tsx index 8e1d1e2dc60..df0bf2dfc4a 100644 --- a/frontend/resourceadm/components/NewAccessListModal/NewAccessListModal.tsx +++ b/frontend/resourceadm/components/NewAccessListModal/NewAccessListModal.tsx @@ -7,11 +7,12 @@ import { Modal, Paragraph } from '@digdir/design-system-react'; import { ResourceNameAndId } from '../../components/ResourceNameAndId'; import { ServerCodes } from 'app-shared/enums/ServerCodes'; import { StudioButton } from '@studio/components'; -import { getAvailableEnvironments } from '../../utils/resourceUtils/resourceUtils'; +import { getEnvLabel } from '../../utils/resourceUtils'; +import type { EnvId } from '../../utils/resourceUtils'; export interface NewAccessListModalProps { org: string; - env: string; + env: EnvId; navigateUrl: string; onClose: () => void; } @@ -57,7 +58,7 @@ export const NewAccessListModal = forwardRef {t('resourceadm.listadmin_create_list_header', { - env: t(getAvailableEnvironments(org).find((listEnv) => listEnv.id === env).label), + env: t(getEnvLabel(org, env)), })} diff --git a/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.test.tsx b/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.test.tsx index 7517a105595..6ed8d0368c9 100644 --- a/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.test.tsx +++ b/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.test.tsx @@ -160,7 +160,9 @@ describe('ResourceAccessLists', () => { }); it('should show error when loading fails', async () => { - const getResourceAccessListsMock = jest.fn().mockImplementation(() => Promise.reject({})); + const getResourceAccessListsMock = jest + .fn() + .mockImplementation(() => Promise.reject({ response: { status: 500 } })); renderResourceAccessLists(getResourceAccessListsMock); const spinnerTitle = screen.queryByText(textMock('resourceadm.loading_lists')); @@ -168,6 +170,24 @@ describe('ResourceAccessLists', () => { expect(screen.getByText(textMock('resourceadm.listadmin_load_list_error'))).toBeInTheDocument(); }); + + it('should show error when user does not have permission to change access lists', async () => { + const getResourceAccessListsMock = jest + .fn() + .mockImplementation(() => Promise.reject({ response: { status: 403 } })); + renderResourceAccessLists(getResourceAccessListsMock); + + const spinnerTitle = screen.queryByText(textMock('resourceadm.loading_lists')); + await waitForElementToBeRemoved(spinnerTitle); + + expect( + screen.getByText( + textMock('resourceadm.loading_access_list_permission_denied', { + envName: textMock('resourceadm.deploy_test_env'), + }), + ), + ).toBeInTheDocument(); + }); }); const renderResourceAccessLists = (getResourceAccessListsMock?: jest.Mock) => { diff --git a/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.tsx b/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.tsx index c9b00bd3c50..96d3fef2ee3 100644 --- a/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.tsx +++ b/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState, useRef } from 'react'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import type { AxiosError } from 'axios'; import { Alert, Checkbox, Heading, Link as DigdirLink, Button } from '@digdir/design-system-react'; import classes from './ResourceAccessLists.module.css'; import { StudioSpinner, StudioButton } from '@studio/components'; @@ -12,9 +13,11 @@ import { getResourcePageURL } from '../../utils/urlUtils'; import { NewAccessListModal } from '../NewAccessListModal'; import type { Resource } from 'app-shared/types/ResourceAdm'; import { useUrlParams } from '../../hooks/useSelectedContext'; +import { getEnvLabel } from '../../utils/resourceUtils'; +import type { EnvId } from '../../utils/resourceUtils'; export interface ResourceAccessListsProps { - env: string; + env: EnvId; resourceData: Resource; } @@ -74,7 +77,14 @@ export const ResourceAccessLists = ({ } if (accessListsError) { - return {t('resourceadm.listadmin_load_list_error')}; + const errorMessage = + (accessListsError as AxiosError)?.response.status === 403 + ? t('resourceadm.loading_access_list_permission_denied', { + envName: t(getEnvLabel(selectedContext, env)), + }) + : t('resourceadm.listadmin_load_list_error'); + + return {errorMessage}; } return ( diff --git a/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.test.tsx b/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.test.tsx index b11ee6b465b..78ea63ed00f 100644 --- a/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.test.tsx +++ b/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.test.tsx @@ -6,7 +6,7 @@ import { textMock } from '../../../testing/mocks/i18nMock'; import userEvent from '@testing-library/user-event'; import { act } from 'react-dom/test-utils'; import type { QueryClient } from '@tanstack/react-query'; -import type { Environment } from '../../utils/resourceUtils/resourceUtils'; +import type { Environment } from '../../utils/resourceUtils'; import { queriesMock } from 'app-shared/mocks/queriesMock'; import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext'; diff --git a/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.tsx b/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.tsx index fc2ae8d52a1..ea344fa6863 100644 --- a/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.tsx +++ b/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.tsx @@ -6,7 +6,7 @@ import classes from './ResourceDeployEnvCard.module.css'; import { ArrowRightIcon } from '@studio/icons'; import { StudioButton } from '@studio/components'; import { usePublishResourceMutation } from '../../hooks/mutations'; -import type { Environment } from '../../utils/resourceUtils/resourceUtils'; +import type { Environment } from '../../utils/resourceUtils'; import { useUrlParams } from '../../hooks/useSelectedContext'; export type ResourceDeployEnvCardProps = { diff --git a/frontend/resourceadm/pages/AboutResourcePage/AboutResourcePage.tsx b/frontend/resourceadm/pages/AboutResourcePage/AboutResourcePage.tsx index 257c3d279e5..d2b981d914f 100644 --- a/frontend/resourceadm/pages/AboutResourcePage/AboutResourcePage.tsx +++ b/frontend/resourceadm/pages/AboutResourcePage/AboutResourcePage.tsx @@ -17,7 +17,7 @@ import { mapKeywordStringToKeywordTypeArray, mapKeywordsArrayToString, resourceTypeMap, -} from '../../utils/resourceUtils/resourceUtils'; +} from '../../utils/resourceUtils'; import { useTranslation } from 'react-i18next'; import { ResourceCheckboxGroup, diff --git a/frontend/resourceadm/pages/DeployResourcePage/DeployResourcePage.tsx b/frontend/resourceadm/pages/DeployResourcePage/DeployResourcePage.tsx index 12a5d307e71..1b069b3ea3d 100644 --- a/frontend/resourceadm/pages/DeployResourcePage/DeployResourcePage.tsx +++ b/frontend/resourceadm/pages/DeployResourcePage/DeployResourcePage.tsx @@ -23,7 +23,7 @@ import { useRepoStatusQuery } from 'app-shared/hooks/queries'; import { useTranslation, Trans } from 'react-i18next'; import { mergeQueryStatuses } from 'app-shared/utils/tanstackQueryUtils'; import { useUrlParams } from '../../hooks/useSelectedContext'; -import { getAvailableEnvironments } from '../../utils/resourceUtils/resourceUtils'; +import { getAvailableEnvironments } from '../../utils/resourceUtils'; export type DeployResourcePageProps = { navigateToPageWithError: (page: NavigationBarPage) => void; diff --git a/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.test.tsx b/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.test.tsx index 53c616a6597..6ffbd010ad2 100644 --- a/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.test.tsx +++ b/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.test.tsx @@ -109,15 +109,34 @@ describe('ListAdminPage', () => { expect(await screen.findByText('Test-list2')).toBeInTheDocument(); }); + + it('should show error when user does not have permission to edit access lists', async () => { + (useParams as jest.Mock).mockReturnValue({ + selectedContext: 'ttd', + env: 'tt02', + }); + + renderListAdminPage(true); + + expect( + await screen.findByText( + textMock('resourceadm.loading_access_list_permission_denied', { + envName: textMock('resourceadm.deploy_test_env'), + }), + ), + ).toBeInTheDocument(); + }); }); -const renderListAdminPage = () => { +const renderListAdminPage = (isError?: boolean) => { const allQueries: ServicesContextProps = { ...queriesMock, - getAccessLists: jest - .fn() - .mockImplementationOnce(() => Promise.resolve(accessListResults)) - .mockImplementationOnce(() => Promise.resolve(accessListResultsPage2)), + getAccessLists: isError + ? jest.fn().mockImplementationOnce(() => Promise.reject({ response: { status: 403 } })) + : jest + .fn() + .mockImplementationOnce(() => Promise.resolve(accessListResults)) + .mockImplementationOnce(() => Promise.resolve(accessListResultsPage2)), }; return render( diff --git a/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.tsx b/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.tsx index 5c983cbddc2..509b2c9d6d8 100644 --- a/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.tsx +++ b/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.tsx @@ -1,7 +1,14 @@ import React, { useRef, useEffect, useCallback } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Heading, Link as DigdirLink, ToggleGroup, Button } from '@digdir/design-system-react'; +import type { AxiosError } from 'axios'; +import { + Heading, + Link as DigdirLink, + ToggleGroup, + Button, + Alert, +} from '@digdir/design-system-react'; import { StudioSpinner, StudioButton } from '@studio/components'; import { PencilWritingIcon, PlusIcon } from '@studio/icons'; import classes from './ListAdminPage.module.css'; @@ -9,8 +16,8 @@ import { useGetAccessListsQuery } from '../../hooks/queries/useGetAccessListsQue import { NewAccessListModal } from '../../components/NewAccessListModal'; import { getAccessListPageUrl, getResourceDashboardURL } from '../../utils/urlUtils'; import { useUrlParams } from '../../hooks/useSelectedContext'; -import type { EnvId } from '../../utils/resourceUtils/resourceUtils'; -import { getAvailableEnvironments } from '../../utils/resourceUtils/resourceUtils'; +import type { EnvId } from '../../utils/resourceUtils'; +import { getAvailableEnvironments, getEnvLabel } from '../../utils/resourceUtils'; export const ListAdminPage = (): React.JSX.Element => { const { t } = useTranslation(); @@ -22,6 +29,7 @@ export const ListAdminPage = (): React.JSX.Element => { isLoading: isLoadingEnvListData, hasNextPage, isFetchingNextPage, + error: listFetchError, fetchNextPage, } = useGetAccessListsQuery(selectedContext, selectedEnv); @@ -66,10 +74,17 @@ export const ListAdminPage = (): React.JSX.Element => { createAccessListModalRef.current?.close()} /> + {(listFetchError as AxiosError)?.response.status === 403 && ( + + {t('resourceadm.loading_access_list_permission_denied', { + envName: t(getEnvLabel(selectedContext, selectedEnv as EnvId)), + })} + + )} {isLoadingEnvListData && ( {
{t('resourceadm.listadmin_lists_in', { - environment: t( - getAvailableEnvironments(selectedContext).find( - (listEnv) => listEnv.id === selectedEnv, - ).label, - ), + environment: t(getEnvLabel(selectedContext, selectedEnv as EnvId)), })} {envListData.pages.map((list) => { diff --git a/frontend/resourceadm/pages/MigrationPage/MigrationPage.tsx b/frontend/resourceadm/pages/MigrationPage/MigrationPage.tsx index 892c1f5258f..560b58a0d0a 100644 --- a/frontend/resourceadm/pages/MigrationPage/MigrationPage.tsx +++ b/frontend/resourceadm/pages/MigrationPage/MigrationPage.tsx @@ -19,8 +19,8 @@ import type { NavigationBarPage } from '../../types/NavigationBarPage'; import { useTranslation } from 'react-i18next'; import { useUrlParams } from '../../hooks/useSelectedContext'; import { StudioButton } from '@studio/components'; -import type { EnvId } from '../../utils/resourceUtils/resourceUtils'; -import { getAvailableEnvironments } from '../../utils/resourceUtils/resourceUtils'; +import type { EnvId } from '../../utils/resourceUtils'; +import { getAvailableEnvironments } from '../../utils/resourceUtils'; export type MigrationPageProps = { navigateToPageWithError: (page: NavigationBarPage) => void; diff --git a/frontend/resourceadm/utils/resourceUtils/index.ts b/frontend/resourceadm/utils/resourceUtils/index.ts index e6ab505b652..a46f9dbbad8 100644 --- a/frontend/resourceadm/utils/resourceUtils/index.ts +++ b/frontend/resourceadm/utils/resourceUtils/index.ts @@ -5,4 +5,12 @@ export { createNavigationTab, getResourceIdentifierErrorMessage, deepCompare, + getAvailableEnvironments, + getEnvLabel, + availableForTypeMap, + resourceStatusMap, + mapKeywordStringToKeywordTypeArray, + mapKeywordsArrayToString, + resourceTypeMap, } from './resourceUtils'; +export type { EnvId, Environment } from './resourceUtils'; diff --git a/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts b/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts index ed3ca450fd8..19664a7c122 100644 --- a/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts +++ b/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts @@ -78,6 +78,9 @@ export const getAvailableEnvironments = (org: string): Environment[] => { } return availableEnvs; }; +export const getEnvLabel = (org: string, env: EnvId): string => { + return getAvailableEnvironments(org).find((listEnv) => listEnv.id === env).label; +}; /** * Maps the language key to the text From 0e9f0595f4f629b7fb0da61c165b734975788fdd Mon Sep 17 00:00:00 2001 From: Martin Gunnerud Date: Thu, 4 Apr 2024 13:44:11 +0200 Subject: [PATCH 2/7] fix type error --- frontend/resourceadm/pages/ResourcePage/ResourcePage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/resourceadm/pages/ResourcePage/ResourcePage.tsx b/frontend/resourceadm/pages/ResourcePage/ResourcePage.tsx index 63818ad93a6..363ae6ee18f 100644 --- a/frontend/resourceadm/pages/ResourcePage/ResourcePage.tsx +++ b/frontend/resourceadm/pages/ResourcePage/ResourcePage.tsx @@ -28,6 +28,7 @@ import { } from '@studio/icons'; import { LeftNavigationBar } from 'app-shared/components/LeftNavigationBar'; import { createNavigationTab, deepCompare } from '../../utils/resourceUtils'; +import type { EnvId } from '../../utils/resourceUtils'; import { ResourceAccessLists } from '../../components/ResourceAccessLists'; import { AccessListDetail } from '../../components/AccessListDetails'; import { useGetAccessListQuery } from '../../hooks/queries/useGetAccessListQuery'; @@ -298,7 +299,7 @@ export const ResourcePage = (): React.JSX.Element => { /> )} {currentPage === accessListsPageId && env && !accessListId && ( - + )} {currentPage === accessListsPageId && env && accessList && ( Date: Thu, 4 Apr 2024 13:44:48 +0200 Subject: [PATCH 3/7] add better feedback when publishing resources --- frontend/language/src/nb.json | 3 +- .../ResourceDeployEnvCard.test.tsx | 36 +++++++++++++++++-- .../ResourceDeployEnvCard.tsx | 28 ++++++++++++--- 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 04064515e03..49c2639b628 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -1065,7 +1065,8 @@ "resourceadm.resource_navigation_modal_text": "Noen felt på siden har manglende informasjon eller feil i utfylling. Du kan endre eller fikse informasjonen når som helst før ressursen publiseres.", "resourceadm.resource_navigation_modal_title_policy": "Manglende informasjon i tilgangsregler", "resourceadm.resource_navigation_modal_title_resource": "Manglende informasjon i ressurs", - "resourceadm.resource_published_success": "Ressursen ble publisert.", + "resourceadm.resource_publish_no_access": "Du har ikke tilgang til å publisere ressurser i {{envName}}.", + "resourceadm.resource_published_success": "Ressursen ble publisert i {{envName}}.", "resourceadm.right_translation_bar_alert": "For å kunne publisere ressursen må du legge til nynorsk og engelsk oversettelse.", "resourceadm.right_translation_bar_title": "Oversettelse", "resourceadm.switch_should": "skal", diff --git a/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.test.tsx b/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.test.tsx index 78ea63ed00f..d85a9d01d36 100644 --- a/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.test.tsx +++ b/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.test.tsx @@ -40,7 +40,11 @@ describe('ResourceDeployEnvCard', () => { await act(() => user.click(deployButton)); await waitFor(() => { expect( - screen.getByText(textMock('resourceadm.resource_published_success')), + screen.getByText( + textMock('resourceadm.resource_published_success', { + envName: textMock(mockTestEnv.label), + }), + ), ).toBeInTheDocument(); }); }); @@ -89,11 +93,39 @@ describe('ResourceDeployEnvCard', () => { await act(() => user.click(deployButton)); expect(queriesMock.publishResource).toHaveBeenCalledTimes(1); }); + + it('should show error if publish fails with error 403', async () => { + const user = userEvent.setup(); + renderResourceDeployEnvCard( + {}, + { + publishResource: jest + .fn() + .mockImplementation(() => Promise.reject({ response: { status: 403 } })), + }, + ); + + const deployButton = screen.getByRole('button', { + name: textMock('resourceadm.deploy_card_publish', { env: textMock(mockTestEnv.label) }), + }); + + await act(() => user.click(deployButton)); + + await waitFor(() => { + expect( + screen.getByText( + textMock('resourceadm.resource_publish_no_access', { + envName: textMock(mockTestEnv.label), + }), + ), + ).toBeInTheDocument(); + }); + }); }); const renderResourceDeployEnvCard = ( props: Partial = {}, - queries: Partial = {}, + queries: Partial = {}, queryClient: QueryClient = createQueryClientMock(), ) => { const allQueries: ServicesContextProps = { diff --git a/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.tsx b/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.tsx index ea344fa6863..c87fd4d1866 100644 --- a/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.tsx +++ b/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.tsx @@ -1,12 +1,13 @@ -import React from 'react'; +import React, { useState } from 'react'; import { toast } from 'react-toastify'; import { useTranslation } from 'react-i18next'; -import { Tag, Paragraph, Spinner } from '@digdir/design-system-react'; +import type { AxiosError } from 'axios'; +import { Tag, Paragraph, Spinner, Alert } from '@digdir/design-system-react'; import classes from './ResourceDeployEnvCard.module.css'; import { ArrowRightIcon } from '@studio/icons'; import { StudioButton } from '@studio/components'; import { usePublishResourceMutation } from '../../hooks/mutations'; -import type { Environment } from '../../utils/resourceUtils'; +import { type Environment } from '../../utils/resourceUtils'; import { useUrlParams } from '../../hooks/useSelectedContext'; export type ResourceDeployEnvCardProps = { @@ -36,6 +37,7 @@ export const ResourceDeployEnvCard = ({ }: ResourceDeployEnvCardProps): React.JSX.Element => { const { t } = useTranslation(); + const [hasNoPublishAccess, setHasNoPublishAccess] = useState(false); const { selectedContext, repo, resourceId } = useUrlParams(); // Query function for publishing a resource @@ -45,7 +47,12 @@ export const ResourceDeployEnvCard = ({ const handlePublish = () => { publishResource(env.id, { onSuccess: () => { - toast.success(t('resourceadm.resource_published_success')); + toast.success(t('resourceadm.resource_published_success', { envName: t(env.label) })); + }, + onError: (error: Error) => { + if ((error as AxiosError).response.status === 403) { + setHasNoPublishAccess(true); + } }, }); }; @@ -76,9 +83,20 @@ export const ResourceDeployEnvCard = ({ )}
- + {t('resourceadm.deploy_card_publish', { env: t(env.label) })} + {hasNoPublishAccess && ( + + + {t('resourceadm.resource_publish_no_access', { envName: t(env.label) })} + + + )} )} From a8cf7a01e6164d5547ef56157100e8f388ffdd55 Mon Sep 17 00:00:00 2001 From: Martin Gunnerud Date: Thu, 4 Apr 2024 14:34:32 +0200 Subject: [PATCH 4/7] change description text of resource name field --- frontend/language/src/nb.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 49c2639b628..2745903081d 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -864,7 +864,7 @@ "resourceadm.about_resource_resource_description_label": "Beskrivelse (Bokmål)", "resourceadm.about_resource_resource_description_text": "Teksten kan bli synlig på flere områder på tvers av offentlige nettløsninger.", "resourceadm.about_resource_resource_title_label": "Navn på tjenesten (Bokmål)", - "resourceadm.about_resource_resource_title_text": "Navnet vil synes for brukerne, og bør være beskrivende for hva tjenesten handler om. Pass på at navnet er forståelig og gjenkjennbart.", + "resourceadm.about_resource_resource_title_text": "Navnet vil synes for brukere, og må være gjenkjennelig og beskrivende for hva tjenesten handler om. Pass på å skille tjenester som har like eller nesten like navn, slik at det blir lett for brukere å forstå.", "resourceadm.about_resource_resource_type": "Ressurstype", "resourceadm.about_resource_resource_type_error": "Du mangler å legge til ressurstype.", "resourceadm.about_resource_resource_type_generic_access_resource": "Generisk tilgangsressurs", From 488865f8a98671765572baeac9b8102ab61efc26 Mon Sep 17 00:00:00 2001 From: Martin Gunnerud Date: Thu, 4 Apr 2024 16:47:27 +0200 Subject: [PATCH 5/7] refactor: add new error component for access list errors. Refactor environments to object, and remove org argument from getEnvLabel function --- .../AccessListErrorMessage.tsx | 26 +++++++++ .../AccessListErrorMessage/index.ts | 1 + .../NewAccessListModal/NewAccessListModal.tsx | 2 +- .../ResourceAccessLists.tsx | 13 ++--- .../pages/ListAdminPage/ListAdminPage.tsx | 22 +++----- .../utils/resourceUtils/resourceUtils.ts | 54 ++++++++++--------- 6 files changed, 67 insertions(+), 51 deletions(-) create mode 100644 frontend/resourceadm/components/AccessListErrorMessage/AccessListErrorMessage.tsx create mode 100644 frontend/resourceadm/components/AccessListErrorMessage/index.ts diff --git a/frontend/resourceadm/components/AccessListErrorMessage/AccessListErrorMessage.tsx b/frontend/resourceadm/components/AccessListErrorMessage/AccessListErrorMessage.tsx new file mode 100644 index 00000000000..a179cc22b9b --- /dev/null +++ b/frontend/resourceadm/components/AccessListErrorMessage/AccessListErrorMessage.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import type { AxiosError } from 'axios'; +import { Alert } from '@digdir/design-system-react'; +import { getEnvLabel } from 'resourceadm/utils/resourceUtils'; +import { type EnvId } from 'resourceadm/utils/resourceUtils'; + +interface AccessListErrorMessageProps { + error: AxiosError; + env?: EnvId; +} + +export const AccessListErrorMessage = ({ + error, + env, +}: AccessListErrorMessageProps): React.JSX.Element => { + const { t } = useTranslation(); + + let errorMessage = t('resourceadm.listadmin_load_list_error'); + if (error?.response?.status === 403) + errorMessage = t('resourceadm.loading_access_list_permission_denied', { + envName: t(getEnvLabel(env)), + }); + + return {errorMessage}; +}; diff --git a/frontend/resourceadm/components/AccessListErrorMessage/index.ts b/frontend/resourceadm/components/AccessListErrorMessage/index.ts new file mode 100644 index 00000000000..245f51831a4 --- /dev/null +++ b/frontend/resourceadm/components/AccessListErrorMessage/index.ts @@ -0,0 +1 @@ +export { AccessListErrorMessage } from './AccessListErrorMessage'; diff --git a/frontend/resourceadm/components/NewAccessListModal/NewAccessListModal.tsx b/frontend/resourceadm/components/NewAccessListModal/NewAccessListModal.tsx index df0bf2dfc4a..bfe3dc9fe43 100644 --- a/frontend/resourceadm/components/NewAccessListModal/NewAccessListModal.tsx +++ b/frontend/resourceadm/components/NewAccessListModal/NewAccessListModal.tsx @@ -58,7 +58,7 @@ export const NewAccessListModal = forwardRef {t('resourceadm.listadmin_create_list_header', { - env: t(getEnvLabel(org, env)), + env: t(getEnvLabel(env)), })} diff --git a/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.tsx b/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.tsx index 96d3fef2ee3..860d254cd67 100644 --- a/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.tsx +++ b/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef } from 'react'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import type { AxiosError } from 'axios'; -import { Alert, Checkbox, Heading, Link as DigdirLink, Button } from '@digdir/design-system-react'; +import { Checkbox, Heading, Link as DigdirLink, Button } from '@digdir/design-system-react'; import classes from './ResourceAccessLists.module.css'; import { StudioSpinner, StudioButton } from '@studio/components'; import { PencilWritingIcon, PlusIcon } from '@studio/icons'; @@ -13,8 +13,8 @@ import { getResourcePageURL } from '../../utils/urlUtils'; import { NewAccessListModal } from '../NewAccessListModal'; import type { Resource } from 'app-shared/types/ResourceAdm'; import { useUrlParams } from '../../hooks/useSelectedContext'; -import { getEnvLabel } from '../../utils/resourceUtils'; import type { EnvId } from '../../utils/resourceUtils'; +import { AccessListErrorMessage } from '../AccessListErrorMessage'; export interface ResourceAccessListsProps { env: EnvId; @@ -77,14 +77,7 @@ export const ResourceAccessLists = ({ } if (accessListsError) { - const errorMessage = - (accessListsError as AxiosError)?.response.status === 403 - ? t('resourceadm.loading_access_list_permission_denied', { - envName: t(getEnvLabel(selectedContext, env)), - }) - : t('resourceadm.listadmin_load_list_error'); - - return {errorMessage}; + return ; } return ( diff --git a/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.tsx b/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.tsx index 509b2c9d6d8..ab4249494fd 100644 --- a/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.tsx +++ b/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.tsx @@ -2,13 +2,7 @@ import React, { useRef, useEffect, useCallback } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import type { AxiosError } from 'axios'; -import { - Heading, - Link as DigdirLink, - ToggleGroup, - Button, - Alert, -} from '@digdir/design-system-react'; +import { Heading, Link as DigdirLink, ToggleGroup, Button } from '@digdir/design-system-react'; import { StudioSpinner, StudioButton } from '@studio/components'; import { PencilWritingIcon, PlusIcon } from '@studio/icons'; import classes from './ListAdminPage.module.css'; @@ -18,6 +12,7 @@ import { getAccessListPageUrl, getResourceDashboardURL } from '../../utils/urlUt import { useUrlParams } from '../../hooks/useSelectedContext'; import type { EnvId } from '../../utils/resourceUtils'; import { getAvailableEnvironments, getEnvLabel } from '../../utils/resourceUtils'; +import { AccessListErrorMessage } from 'resourceadm/components/AccessListErrorMessage'; export const ListAdminPage = (): React.JSX.Element => { const { t } = useTranslation(); @@ -78,12 +73,11 @@ export const ListAdminPage = (): React.JSX.Element => { navigateUrl={getAccessListPageUrl(selectedContext, repo, selectedEnv)} onClose={() => createAccessListModalRef.current?.close()} /> - {(listFetchError as AxiosError)?.response.status === 403 && ( - - {t('resourceadm.loading_access_list_permission_denied', { - envName: t(getEnvLabel(selectedContext, selectedEnv as EnvId)), - })} - + {listFetchError && ( + )} {isLoadingEnvListData && ( {
{t('resourceadm.listadmin_lists_in', { - environment: t(getEnvLabel(selectedContext, selectedEnv as EnvId)), + environment: t(getEnvLabel(selectedEnv as EnvId)), })} {envListData.pages.map((list) => { diff --git a/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts b/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts index 19664a7c122..5c0da2b5f55 100644 --- a/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts +++ b/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts @@ -49,37 +49,39 @@ export type Environment = { label: string; envType: EnvType; }; + +const environments: Record = { + ['at22']: { + id: 'at22' as EnvId, + label: 'resourceadm.deploy_at22_env', + envType: 'test' as EnvType, + }, + ['at23']: { + id: 'at23' as EnvId, + label: 'resourceadm.deploy_at23_env', + envType: 'test' as EnvType, + }, + ['tt02']: { + id: 'tt02' as EnvId, + label: 'resourceadm.deploy_test_env', + envType: 'test' as EnvType, + }, + ['prod']: { + id: 'prod' as EnvId, + label: 'resourceadm.deploy_prod_env', + envType: 'prod' as EnvType, + }, +}; + export const getAvailableEnvironments = (org: string): Environment[] => { - const availableEnvs = [ - { - id: 'tt02' as EnvId, - label: 'resourceadm.deploy_test_env', - envType: 'test' as EnvType, - }, - { - id: 'prod' as EnvId, - label: 'resourceadm.deploy_prod_env', - envType: 'prod' as EnvType, - }, - ]; + const availableEnvs = [environments['tt02'], environments['prod']]; if (org === 'ttd') { - availableEnvs.push( - { - id: 'at22' as EnvId, - label: 'resourceadm.deploy_at22_env', - envType: 'test' as EnvType, - }, - { - id: 'at23' as EnvId, - label: 'resourceadm.deploy_at23_env', - envType: 'test' as EnvType, - }, - ); + availableEnvs.push(environments['at22'], environments['at23']); } return availableEnvs; }; -export const getEnvLabel = (org: string, env: EnvId): string => { - return getAvailableEnvironments(org).find((listEnv) => listEnv.id === env).label; +export const getEnvLabel = (env: EnvId): string => { + return environments[env]?.label || ''; }; /** From 0ad337da8c50637acce64657599e7d4343e3d9d6 Mon Sep 17 00:00:00 2001 From: Martin Gunnerud Date: Thu, 4 Apr 2024 17:00:38 +0200 Subject: [PATCH 6/7] env prop is not optional --- .../AccessListErrorMessage/AccessListErrorMessage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/resourceadm/components/AccessListErrorMessage/AccessListErrorMessage.tsx b/frontend/resourceadm/components/AccessListErrorMessage/AccessListErrorMessage.tsx index a179cc22b9b..0a016b81441 100644 --- a/frontend/resourceadm/components/AccessListErrorMessage/AccessListErrorMessage.tsx +++ b/frontend/resourceadm/components/AccessListErrorMessage/AccessListErrorMessage.tsx @@ -7,7 +7,7 @@ import { type EnvId } from 'resourceadm/utils/resourceUtils'; interface AccessListErrorMessageProps { error: AxiosError; - env?: EnvId; + env: EnvId; } export const AccessListErrorMessage = ({ From a0ec0c2e624356a0896b84d5461e3d0a53b86447 Mon Sep 17 00:00:00 2001 From: Martin Gunnerud Date: Fri, 5 Apr 2024 08:55:13 +0200 Subject: [PATCH 7/7] add test --- .../utils/resourceUtils/resourceUtils.test.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/frontend/resourceadm/utils/resourceUtils/resourceUtils.test.tsx b/frontend/resourceadm/utils/resourceUtils/resourceUtils.test.tsx index 5a001bc5197..4d454aa7312 100644 --- a/frontend/resourceadm/utils/resourceUtils/resourceUtils.test.tsx +++ b/frontend/resourceadm/utils/resourceUtils/resourceUtils.test.tsx @@ -4,8 +4,10 @@ import { getMissingInputLanguageString, mapLanguageKeyToLanguageText, deepCompare, + getEnvLabel, + mapKeywordStringToKeywordTypeArray, } from './'; -import { mapKeywordStringToKeywordTypeArray } from './resourceUtils'; +import type { EnvId } from './resourceUtils'; import type { LeftNavigationTab } from 'app-shared/types/LeftNavigationTab'; import { TestFlaskIcon } from '@studio/icons'; import React from 'react'; @@ -192,4 +194,16 @@ describe('deepCompare', () => { const areEqual = deepCompare([], {}); expect(areEqual).toBeFalsy(); }); + + describe('getEnvLabel', () => { + it('should return label for selected environment when environment exists', () => { + const envLabel = getEnvLabel('tt02'); + expect(envLabel).toEqual('resourceadm.deploy_test_env'); + }); + + it('should return empty label for selected environment when environment with given id does not exist', () => { + const envLabel = getEnvLabel('mu01' as EnvId); + expect(envLabel).toEqual(''); + }); + }); });