diff --git a/src/Designer/frontend/admin/features/apps/pages/appDetails/AppDetails.tsx b/src/Designer/frontend/admin/features/apps/pages/appDetails/AppDetails.tsx index 2520e401edf..14992c7fecf 100644 --- a/src/Designer/frontend/admin/features/apps/pages/appDetails/AppDetails.tsx +++ b/src/Designer/frontend/admin/features/apps/pages/appDetails/AppDetails.tsx @@ -1,4 +1,4 @@ -import { StudioHeading } from '@studio/components'; +import { StudioAlert, StudioHeading } from '@studio/components'; import { AppMetrics } from './components/AppMetrics'; import { useQueryParamState } from 'admin/features/apps/hooks/useQueryParamState'; import classes from './AppDetails.module.css'; @@ -7,8 +7,14 @@ import { AppInfo } from './components/AppInfo'; import { Breadcrumbs } from 'admin/features/apps/components/Breadcrumbs/Breadcrumbs'; import { DEFAULT_SEARCH_PARAMS } from 'admin/constants/constants'; import { useRequiredRoutePathsParams } from 'admin/hooks/useRequiredRoutePathsParams'; +import { useAppHealthMetricsQuery } from 'admin/features/apps/hooks/queries/useAppHealthMetricsQuery'; +import { isAxiosError } from 'axios'; +import { useCurrentOrg } from 'admin/contexts/OrgContext'; +import { useEnvironmentTitle } from 'admin/features/apps/hooks/useEnvironmentTitle'; +import { useTranslation } from 'react-i18next'; export const AppsDetails = () => { + const { t } = useTranslation(); const { owner: org, environment, @@ -17,6 +23,21 @@ export const AppsDetails = () => { const defaultRange = DEFAULT_SEARCH_PARAMS.range; const [range, setRange] = useQueryParamState('range', defaultRange); + const currentOrg = useCurrentOrg(); + const orgName = currentOrg.full_name || currentOrg.username; + const envTitle = useEnvironmentTitle(environment); + + const { isError: healthIsError, error: healthError } = useAppHealthMetricsQuery( + org, + environment, + app, + { + hideDefaultError: true, + }, + ); + const hasNoAccess = + healthIsError && isAxiosError(healthError) && healthError.response?.status === 403; + return (
{ /> {app} -
- -
-
- -
+ {hasNoAccess ? ( + + {t('admin.app.missing_rights', { envTitle, orgName })} + + ) : ( + <> +
+ +
+
+ +
+ + )}
); }; diff --git a/src/Designer/frontend/admin/features/apps/pages/appDetails/components/AppMetrics.test.tsx b/src/Designer/frontend/admin/features/apps/pages/appDetails/components/AppMetrics.test.tsx index 53dc9364c81..4aa148c9c11 100644 --- a/src/Designer/frontend/admin/features/apps/pages/appDetails/components/AppMetrics.test.tsx +++ b/src/Designer/frontend/admin/features/apps/pages/appDetails/components/AppMetrics.test.tsx @@ -49,55 +49,50 @@ const defaultProps: AppMetricsProps = { describe('AppMetrics', () => { afterEach(jest.clearAllMocks); - describe('app health metrics', () => { - it('should render loading state', () => { - renderAppMetrics(); + it('should render merged info alert when all queries return 403', async () => { + const axiosError = createApiErrorMock(ServerCodes.Forbidden); + (axios.get as jest.Mock).mockRejectedValue(axiosError); + + renderAppMetrics(); + await waitFor(() => { expect( - screen.getByLabelText(textMock('admin.metrics.app.health.loading')), - ).toBeInTheDocument(); + screen.queryByLabelText(textMock('admin.metrics.app.health.loading')), + ).not.toBeInTheDocument(); }); - it('should render info alert when missing rights', async () => { - const axiosError = createApiErrorMock(ServerCodes.Forbidden); - (axios.get as jest.Mock).mockRejectedValue(axiosError); + expect( + screen.getByText( + textMock('admin.metrics.missing_rights', { envTitle, orgName: orgFullName }), + ), + ).toBeInTheDocument(); + }); - renderAppMetrics(); + it('should use org username when full name is missing in merged missing rights alert', async () => { + const axiosError = createApiErrorMock(ServerCodes.Forbidden); + (axios.get as jest.Mock).mockRejectedValue(axiosError); - await waitFor(() => { - expect( - screen.queryByLabelText(textMock('admin.metrics.app.health.loading')), - ).not.toBeInTheDocument(); - }); + renderAppMetrics(createQueryClientMock(), defaultProps, orgMockWithoutFullName); + await waitFor(() => { expect( - screen.getByText( - textMock('admin.metrics.app.health.missing_rights', { envTitle, orgName: orgFullName }), - ), - ).toBeInTheDocument(); + screen.queryByLabelText(textMock('admin.metrics.app.health.loading')), + ).not.toBeInTheDocument(); }); - it.each([ - ['health', 'admin.metrics.app.health.missing_rights', 'admin.metrics.app.health.loading'], - ['errors', 'admin.metrics.app.errors.missing_rights', 'admin.metrics.app.errors.loading'], - ['app', 'admin.metrics.app.missing_rights', 'admin.metrics.app.loading'], - ])( - 'should use org username when full name is missing in %s missing rights alert', - async (_section, missingRightsKey, loadingKey) => { - const axiosError = createApiErrorMock(ServerCodes.Forbidden); - (axios.get as jest.Mock).mockRejectedValue(axiosError); - - renderAppMetrics(createQueryClientMock(), defaultProps, orgMockWithoutFullName); + expect( + screen.getByText(textMock('admin.metrics.missing_rights', { envTitle, orgName: org })), + ).toBeInTheDocument(); + }); - await waitFor(() => { - expect(screen.queryByLabelText(textMock(loadingKey))).not.toBeInTheDocument(); - }); + describe('app health metrics', () => { + it('should render loading state', () => { + renderAppMetrics(); - expect( - screen.getByText(textMock(missingRightsKey, { envTitle, orgName: org })), - ).toBeInTheDocument(); - }, - ); + expect( + screen.getByLabelText(textMock('admin.metrics.app.health.loading')), + ).toBeInTheDocument(); + }); it('should render error state', async () => { const axiosError = createApiErrorMock(ServerCodes.InternalServerError); @@ -175,25 +170,6 @@ describe('AppMetrics', () => { ).toBeInTheDocument(); }); - it('should render info alert when missing rights', async () => { - const axiosError = createApiErrorMock(ServerCodes.Forbidden); - (axios.get as jest.Mock).mockRejectedValue(axiosError); - - renderAppMetrics(); - - await waitFor(() => { - expect( - screen.queryByLabelText(textMock('admin.metrics.app.errors.loading')), - ).not.toBeInTheDocument(); - }); - - expect( - screen.getByText( - textMock('admin.metrics.app.errors.missing_rights', { envTitle, orgName: orgFullName }), - ), - ).toBeInTheDocument(); - }); - it('should render error state', async () => { const axiosError = createApiErrorMock(ServerCodes.InternalServerError); (axios.get as jest.Mock).mockRejectedValue(axiosError); @@ -294,25 +270,6 @@ describe('AppMetrics', () => { expect(screen.getByLabelText(textMock('admin.metrics.app.loading'))).toBeInTheDocument(); }); - it('should render info alert when missing rights', async () => { - const axiosError = createApiErrorMock(ServerCodes.Forbidden); - (axios.get as jest.Mock).mockRejectedValue(axiosError); - - renderAppMetrics(); - - await waitFor(() => { - expect( - screen.queryByLabelText(textMock('admin.metrics.app.loading')), - ).not.toBeInTheDocument(); - }); - - expect( - screen.getByText( - textMock('admin.metrics.app.missing_rights', { envTitle, orgName: orgFullName }), - ), - ).toBeInTheDocument(); - }); - it('should render error state', async () => { const axiosError = createApiErrorMock(ServerCodes.InternalServerError); (axios.get as jest.Mock).mockRejectedValue(axiosError); diff --git a/src/Designer/frontend/admin/features/apps/pages/appDetails/components/AppMetrics.tsx b/src/Designer/frontend/admin/features/apps/pages/appDetails/components/AppMetrics.tsx index 3a6d557c35a..293fb028fea 100644 --- a/src/Designer/frontend/admin/features/apps/pages/appDetails/components/AppMetrics.tsx +++ b/src/Designer/frontend/admin/features/apps/pages/appDetails/components/AppMetrics.tsx @@ -163,6 +163,17 @@ export const AppMetrics = ({ range, setRange }: AppMetricsProps) => { )); }; + const allMissingRights = + appHealthMetricsIsError && + isAxiosError(appHealthMetricsError) && + appHealthMetricsError.response?.status === 403 && + appErrorMetricsIsError && + isAxiosError(appErrorMetricsError) && + appErrorMetricsError.response?.status === 403 && + appMetricsIsError && + isAxiosError(appMetricsError) && + appMetricsError.response?.status === 403; + return ( @@ -173,9 +184,17 @@ export const AppMetrics = ({ range, setRange }: AppMetricsProps) => { />
- {renderAppHealthMetrics()} - {renderAppErrorMetrics()} - {renderAppMetrics()} + {allMissingRights ? ( + + {t('admin.metrics.missing_rights', { envTitle, orgName })} + + ) : ( + <> + {renderAppHealthMetrics()} + {renderAppErrorMetrics()} + {renderAppMetrics()} + + )}
); diff --git a/src/Designer/frontend/admin/features/apps/pages/instances/Instances.test.tsx b/src/Designer/frontend/admin/features/apps/pages/instances/Instances.test.tsx new file mode 100644 index 00000000000..b3b732ff9fa --- /dev/null +++ b/src/Designer/frontend/admin/features/apps/pages/instances/Instances.test.tsx @@ -0,0 +1,81 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { Instances } from './Instances'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { MemoryRouter } from 'react-router-dom'; +import { textMock } from '@studio/testing/mocks/i18nMock'; +import { app, org } from '@studio/testing/testids'; +import axios from 'axios'; +import { createApiErrorMock } from 'app-shared/mocks/apiErrorMock'; +import { ServerCodes } from 'app-shared/enums/ServerCodes'; +import { useQueryParamState } from 'admin/features/apps/hooks/useQueryParamState'; +import { OrgContext } from 'admin/contexts/OrgContext'; + +const env = 'test'; + +const orgMock = { + username: org, + full_name: 'Test Org', + avatar_url: '', + id: 1, +}; + +jest.mock('admin/hooks/useRequiredRoutePathsParams', () => ({ + useRequiredRoutePathsParams: () => ({ owner: org, environment: env, app }), +})); +jest.mock('axios', () => ({ + ...jest.requireActual('axios'), + get: jest.fn(), +})); +jest.mock('admin/features/apps/hooks/useQueryParamState'); + +describe('Instances', () => { + beforeEach(() => { + jest + .mocked(useQueryParamState) + .mockImplementation((_key, defaultValue) => [defaultValue, jest.fn()]); + }); + afterEach(jest.clearAllMocks); + + it('should render filters while loading', () => { + (axios.get as jest.Mock).mockReturnValue(new Promise(() => {})); + + renderInstances(); + + expect( + screen.getByLabelText(textMock('admin.instances.archive_reference')), + ).toBeInTheDocument(); + expect( + screen.getByRole('combobox', { name: textMock('admin.instances.status.completed') }), + ).toBeInTheDocument(); + }); + + it('should hide filters when instances query returns 403', async () => { + const axiosError = createApiErrorMock(ServerCodes.Forbidden); + (axios.get as jest.Mock).mockRejectedValue(axiosError); + + renderInstances(); + + await waitFor(() => { + expect( + screen.queryByLabelText(textMock('admin.instances.archive_reference')), + ).not.toBeInTheDocument(); + }); + + expect( + screen.queryByRole('combobox', { name: textMock('admin.instances.status.completed') }), + ).not.toBeInTheDocument(); + }); +}); + +const renderInstances = () => { + render( + + + + + + + , + ); +}; diff --git a/src/Designer/frontend/admin/features/apps/pages/instances/Instances.tsx b/src/Designer/frontend/admin/features/apps/pages/instances/Instances.tsx index 7b8e789393a..16207a9ee20 100644 --- a/src/Designer/frontend/admin/features/apps/pages/instances/Instances.tsx +++ b/src/Designer/frontend/admin/features/apps/pages/instances/Instances.tsx @@ -7,6 +7,8 @@ import { useQueryParamState } from 'admin/features/apps/hooks/useQueryParamState import { ProcessTaskFilter } from './components/ProcessTaskFilter'; import { useTranslation } from 'react-i18next'; import { useRequiredRoutePathsParams } from 'admin/hooks/useRequiredRoutePathsParams'; +import { useAppInstancesQuery } from 'admin/features/apps/hooks/queries/useAppInstancesQuery'; +import { isAxiosError } from 'axios'; const YES_NO_ALL_OPTIONS = [ { label: 'admin.instances.filter.all', value: undefined }, @@ -52,45 +54,62 @@ export const Instances = () => { undefined, ); + const createdBefore = getCurrentDateOnlyStringMinusDays(createdBeforeDays); + const { error: instancesError } = useAppInstancesQuery( + org, + environment, + app, + currentTask, + isArchived, + archiveReference, + isConfirmed, + isSoftDeleted, + undefined, + createdBefore, + ); + const hasNoAccess = isAxiosError(instancesError) && instancesError.response?.status === 403; + return ( {t('admin.instances.title')} -
- - - - - - -
+ {!hasNoAccess && ( +
+ + + + + + +
+ )} { archiveReference={archiveReference} confirmed={isConfirmed} isSoftDeleted={isSoftDeleted} - createdBefore={getCurrentDateOnlyStringMinusDays(createdBeforeDays)} + createdBefore={createdBefore} />
); diff --git a/src/Designer/frontend/language/src/nb.json b/src/Designer/frontend/language/src/nb.json index 44c5c6de76c..3570f831997 100644 --- a/src/Designer/frontend/language/src/nb.json +++ b/src/Designer/frontend/language/src/nb.json @@ -7,6 +7,7 @@ "admin.app.info.publish_date": "Publiseringsdato", "admin.app.info.published_by": "Publisert av", "admin.app.info.title": "Informasjon om appen", + "admin.app.missing_rights": "Du har ikke rettigheter til å se applikasjonsdata i {{envTitle}}. Be eierne i {{orgName}} om å gi deg tilgang.", "admin.apps.alert_no_org_selected": "For å vise publiserte apper, må du velge en organisasjon i menyen øverst til høyre.", "admin.apps.alert_no_org_selected_no_access": "Hvis du ikke har tilgang til noen organisasjoner, må du be om tilgang fra en tjenesteeier.", "admin.apps.name": "Navn", @@ -65,6 +66,7 @@ "admin.metrics.app.health.missing_rights": "Du har ikke rettigheter til å se applikasjonenshelse i {{envTitle}}. Be eierne i {{orgName}} om å gi deg tilgang.", "admin.metrics.app.loading": "Laster inn applikasjonsmetrikker for denne appen...", "admin.metrics.app.missing_rights": "Du har ikke rettigheter til å se applikasjonsmetrikker i {{envTitle}}. Be eierne i {{orgName}} om å gi deg tilgang.", + "admin.metrics.missing_rights": "Du har ikke rettigheter til å se applikasjonsovervåking i {{envTitle}}. Be eierne i {{orgName}} om å gi deg tilgang.", "admin.metrics.app.no_data": "Fant ingen statistikk om appen for valgt periode.", "admin.metrics.app.no_data_link": "Her kan du se hvordan du setter en innstilling i koden for å samle inn statistikk.", "admin.metrics.errors": "Feil fra siste",