From 1ff3640c29a07725a3f67a6d95a761a43264c404 Mon Sep 17 00:00:00 2001 From: Martin Gunnerud Date: Fri, 4 Oct 2024 10:49:40 +0200 Subject: [PATCH 1/4] use new header component in resourceadm --- .../src/components/StudioPageHeader/index.ts | 8 +- frontend/packages/shared/src/constants.js | 2 + .../ResourceAdmHeader/ResourceAdmHeader.tsx | 96 +++++++++++++++++++ .../components/ResourceAdmHeader/index.ts | 1 + .../hooks/useUrlParams/useUrlParams.ts | 3 +- .../pages/PageLayout/PageLayout.tsx | 27 ++---- .../pages/RedirectPage/RedirectPage.tsx | 3 +- .../resourceadm/utils/stringUtils/index.ts | 1 + .../utils/stringUtils/stringUtils.ts | 4 + frontend/resourceadm/utils/userUtils/index.ts | 2 +- .../resourceadm/utils/userUtils/userUtils.ts | 5 + 11 files changed, 127 insertions(+), 25 deletions(-) create mode 100644 frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.tsx create mode 100644 frontend/resourceadm/components/ResourceAdmHeader/index.ts diff --git a/frontend/libs/studio-components/src/components/StudioPageHeader/index.ts b/frontend/libs/studio-components/src/components/StudioPageHeader/index.ts index 629646839a3..e89744278cc 100644 --- a/frontend/libs/studio-components/src/components/StudioPageHeader/index.ts +++ b/frontend/libs/studio-components/src/components/StudioPageHeader/index.ts @@ -5,6 +5,7 @@ import { import { StudioPageHeaderButton } from './StudioPageHeaderButton'; import { StudioPageHeaderProfileMenu, + type StudioProfileMenuItem, type StudioProfileMenuGroup, } from './StudioPageHeaderProfileMenu'; import { StudioPageHeaderCenter } from './StudioPageHeaderCenter'; @@ -33,4 +34,9 @@ StudioPageHeader.Sub = StudioPageHeaderSub; StudioPageHeader.HeaderButton = StudioPageHeaderButton; StudioPageHeader.ProfileMenu = StudioPageHeaderProfileMenu; -export { StudioPageHeader, type StudioPageHeaderProps, type StudioProfileMenuGroup }; +export { + StudioPageHeader, + type StudioPageHeaderProps, + type StudioProfileMenuGroup, + type StudioProfileMenuItem, +}; diff --git a/frontend/packages/shared/src/constants.js b/frontend/packages/shared/src/constants.js index fe14363a732..a4dea619350 100644 --- a/frontend/packages/shared/src/constants.js +++ b/frontend/packages/shared/src/constants.js @@ -18,3 +18,5 @@ export const PROD_ENV_TYPE = 'production'; export const PROTECTED_TASK_NAME_CUSTOM_RECEIPT = 'CustomReceipt'; export const PREVIEW_MOCK_PARTY_ID = '51001'; export const PREVIEW_MOCK_INSTANCE_GUID = 'f1e23d45-6789-1bcd-8c34-56789abcdef0'; + +export const MEDIA_QUERY_MAX_WIDTH = '(max-width: 1024px)'; diff --git a/frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.tsx b/frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.tsx new file mode 100644 index 00000000000..e91ce3a1fa5 --- /dev/null +++ b/frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { + StudioAvatar, + StudioPageHeader, + type StudioProfileMenuGroup, + useMediaQuery, + type StudioProfileMenuItem, +} from '@studio/components'; +import { getOrgNameByUsername } from '../../utils/userUtils'; +import { type Organization } from 'app-shared/types/Organization'; +import { MEDIA_QUERY_MAX_WIDTH } from 'app-shared/constants'; +import { useLogoutMutation } from 'app-shared/hooks/mutations/useLogoutMutation'; +import type { User } from 'app-shared/types/Repository'; +import { useUrlParams } from '../../hooks/useUrlParams'; +import { getAppName } from '../../utils/stringUtils'; + +interface ResourceAdmHeaderProps { + organizations: Organization[]; + user: User; +} + +export const ResourceAdmHeader = ({ organizations, user }: ResourceAdmHeaderProps) => { + const { org } = useUrlParams(); + const pageHeaderTitle: string = getOrgNameByUsername(org, organizations); + + return ( + + + + + + + + + ); +}; + +const DashboardHeaderMenu = ({ organizations, user }: ResourceAdmHeaderProps) => { + const { t } = useTranslation(); + const showButtonText = !useMediaQuery(MEDIA_QUERY_MAX_WIDTH); + const { org, app } = useUrlParams(); + const { mutate: logout } = useLogoutMutation(); + const navigate = useNavigate(); + const selectableOrgs = organizations; + + const triggerButtonText = t('shared.header_user_for_org', { + user: user?.full_name || user?.login, + org: getOrgNameByUsername(org, selectableOrgs), + }); + const repoPath = `/repos/${org}/${app}`; + + const handleSetSelectedContext = (context: string) => { + navigate(`/${context}/${getAppName(context)}${location.search}`); + }; + + const selectableOrgMenuItems: StudioProfileMenuItem[] = + selectableOrgs.map((selectableOrg: Organization) => ({ + action: { type: 'button', onClick: () => handleSetSelectedContext(selectableOrg.username) }, + itemName: selectableOrg?.full_name || selectableOrg.username, + isActive: org === selectableOrg.username, + })) ?? []; + + const giteaMenuItem: StudioProfileMenuItem = { + action: { type: 'link', href: repoPath }, + itemName: t('shared.header_go_to_gitea'), + }; + + const logOutMenuItem: StudioProfileMenuItem = { + action: { type: 'button', onClick: logout }, + itemName: t('shared.header_logout'), + }; + + const profileMenuGroups: StudioProfileMenuGroup[] = [ + { items: selectableOrgMenuItems }, + { items: [giteaMenuItem, logOutMenuItem] }, + ]; + + return ( + + } + profileMenuGroups={profileMenuGroups} + /> + ); +}; diff --git a/frontend/resourceadm/components/ResourceAdmHeader/index.ts b/frontend/resourceadm/components/ResourceAdmHeader/index.ts new file mode 100644 index 00000000000..c10ac33b2c8 --- /dev/null +++ b/frontend/resourceadm/components/ResourceAdmHeader/index.ts @@ -0,0 +1 @@ +export { ResourceAdmHeader } from './ResourceAdmHeader'; diff --git a/frontend/resourceadm/hooks/useUrlParams/useUrlParams.ts b/frontend/resourceadm/hooks/useUrlParams/useUrlParams.ts index 70a3147557e..2170785d108 100644 --- a/frontend/resourceadm/hooks/useUrlParams/useUrlParams.ts +++ b/frontend/resourceadm/hooks/useUrlParams/useUrlParams.ts @@ -1,4 +1,5 @@ import { useParams } from 'react-router-dom'; +import { getAppName } from '../../utils/stringUtils'; interface ResourceAdminUrlParams { org: string; @@ -14,7 +15,7 @@ export const useUrlParams = (): Readonly => { return { org: params.org, - app: `${params.org}-resources`, + app: getAppName(params.org), env: params.env, resourceId: params.resourceId, accessListId: params.accessListId, diff --git a/frontend/resourceadm/pages/PageLayout/PageLayout.tsx b/frontend/resourceadm/pages/PageLayout/PageLayout.tsx index 2751be58d65..76600ed2b19 100644 --- a/frontend/resourceadm/pages/PageLayout/PageLayout.tsx +++ b/frontend/resourceadm/pages/PageLayout/PageLayout.tsx @@ -1,11 +1,6 @@ -import React, { useEffect, useMemo, useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import classes from './PageLayout.module.css'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; -import AppHeader, { - HeaderContext, - SelectedContextType, -} from 'app-shared/navigation/main-header/Header'; -import type { IHeaderContext } from 'app-shared/navigation/main-header/Header'; import { userHasAccessToOrganization } from '../../utils/userUtils'; import { useOrganizationsQuery } from '../../hooks/queries'; import { useRepoStatusQuery, useUserQuery } from 'app-shared/hooks/queries'; @@ -13,6 +8,7 @@ import { GiteaHeader } from 'app-shared/components/GiteaHeader'; import { useUrlParams } from '../../hooks/useUrlParams'; import postMessages from 'app-shared/utils/postMessages'; import { MergeConflictModal } from '../../components/MergeConflictModal'; +import { ResourceAdmHeader } from '../../components/ResourceAdmHeader'; /** * @component @@ -26,7 +22,7 @@ export const PageLayout = (): React.JSX.Element => { const { data: organizations } = useOrganizationsQuery(); const mergeConflictModalRef = useRef(null); - const { org = SelectedContextType.Self, app } = useUrlParams(); + const { org, app } = useUrlParams(); const { data: repoStatus } = useRepoStatusQuery(org, app); const navigate = useNavigate(); @@ -63,22 +59,11 @@ export const PageLayout = (): React.JSX.Element => { }; }, [mergeConflictModalRef]); - const headerContextValue: IHeaderContext = useMemo( - () => ({ - selectableOrgs: organizations, - user, - }), - [organizations, user], - ); - return ( <> - - - {/* TODO - Find out if should be replaced to be the same as studio */} - - - + + {organizations && user && } + ); diff --git a/frontend/resourceadm/pages/RedirectPage/RedirectPage.tsx b/frontend/resourceadm/pages/RedirectPage/RedirectPage.tsx index 4f73093d85d..a7e44e2efaf 100644 --- a/frontend/resourceadm/pages/RedirectPage/RedirectPage.tsx +++ b/frontend/resourceadm/pages/RedirectPage/RedirectPage.tsx @@ -3,6 +3,7 @@ import { Navigate } from 'react-router-dom'; import classes from './RedirectPage.module.css'; import { ErrorPage } from '../ErrorPage'; import { useUrlParams } from '../../hooks/useUrlParams'; +import { getAppName } from '../../utils/stringUtils'; /** * @component @@ -19,7 +20,7 @@ export const RedirectPage = (): React.JSX.Element => { // Error page if user has chosen "Alle" ) : ( - + )} ); diff --git a/frontend/resourceadm/utils/stringUtils/index.ts b/frontend/resourceadm/utils/stringUtils/index.ts index a8b3f0dc76f..4326770255e 100644 --- a/frontend/resourceadm/utils/stringUtils/index.ts +++ b/frontend/resourceadm/utils/stringUtils/index.ts @@ -4,4 +4,5 @@ export { isSePrefix, stringNumberToAriaLabel, isOrgNrString, + getAppName, } from './stringUtils'; diff --git a/frontend/resourceadm/utils/stringUtils/stringUtils.ts b/frontend/resourceadm/utils/stringUtils/stringUtils.ts index eb9f7be6f85..8d319fb4fbc 100644 --- a/frontend/resourceadm/utils/stringUtils/stringUtils.ts +++ b/frontend/resourceadm/utils/stringUtils/stringUtils.ts @@ -30,3 +30,7 @@ export const stringNumberToAriaLabel = (s: string): string => { export const isOrgNrString = (s: string): boolean => { return /^\d{9}$/.test(s); // regex for search string is exactly 9 digits }; + +export const getAppName = (org: string): string => { + return `${org}-resources`; +}; diff --git a/frontend/resourceadm/utils/userUtils/index.ts b/frontend/resourceadm/utils/userUtils/index.ts index 20865c094f2..c1b0eaffa3b 100644 --- a/frontend/resourceadm/utils/userUtils/index.ts +++ b/frontend/resourceadm/utils/userUtils/index.ts @@ -1 +1 @@ -export { userHasAccessToOrganization } from './userUtils'; +export { userHasAccessToOrganization, getOrgNameByUsername } from './userUtils'; diff --git a/frontend/resourceadm/utils/userUtils/userUtils.ts b/frontend/resourceadm/utils/userUtils/userUtils.ts index 57ee2512e92..9373106c2b9 100644 --- a/frontend/resourceadm/utils/userUtils/userUtils.ts +++ b/frontend/resourceadm/utils/userUtils/userUtils.ts @@ -14,3 +14,8 @@ export const userHasAccessToOrganization = ({ return Boolean(orgs.find((x) => x.username === org)); }; + +export const getOrgNameByUsername = (username: string, orgs: Organization[]) => { + const org = orgs?.find((o) => o.username === username); + return org?.full_name || org?.username; +}; From 517cbb90611084e13d7beb8282475c85dcca4ceb Mon Sep 17 00:00:00 2001 From: Martin Gunnerud Date: Fri, 4 Oct 2024 14:23:11 +0200 Subject: [PATCH 2/4] show if of chosen resource in header --- .../components/ResourceAdmHeader/ResourceAdmHeader.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.tsx b/frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.tsx index e91ce3a1fa5..d5a63cc1d5b 100644 --- a/frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.tsx +++ b/frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.tsx @@ -22,8 +22,9 @@ interface ResourceAdmHeaderProps { } export const ResourceAdmHeader = ({ organizations, user }: ResourceAdmHeaderProps) => { - const { org } = useUrlParams(); - const pageHeaderTitle: string = getOrgNameByUsername(org, organizations); + const { org, resourceId } = useUrlParams(); + const resourcePath = resourceId ? ` / ${resourceId}` : ''; + const pageHeaderTitle: string = `${getOrgNameByUsername(org, organizations)}${resourcePath}`; return ( From 343ab8a8e130459ff45e0a89e0069dab424bca01 Mon Sep 17 00:00:00 2001 From: Martin Gunnerud Date: Fri, 4 Oct 2024 14:23:42 +0200 Subject: [PATCH 3/4] add tests for ResourceAdmHeader --- .../ResourceAdmHeader.test.tsx | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.test.tsx diff --git a/frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.test.tsx b/frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.test.tsx new file mode 100644 index 00000000000..69989ead373 --- /dev/null +++ b/frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.test.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { render, screen } from '@testing-library/react'; +import { textMock } from '@studio/testing/mocks/i18nMock'; +import userEvent from '@testing-library/user-event'; +import { queriesMock } from 'app-shared/mocks/queriesMock'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; +import { ServicesContextProvider } from 'app-shared/contexts/ServicesContext'; +import { ResourceAdmHeader } from './ResourceAdmHeader'; + +const mainOrganization = { + avatar_url: '', + id: 1, + username: 'ttd', + full_name: 'Testdepartementet', +}; +const otherOrganization = { + avatar_url: '', + id: 2, + username: 'skd', + full_name: 'Skatteetaten', +}; +const organizations = [mainOrganization, otherOrganization]; + +const testUser = { + avatar_url: '', + email: 'test@test.no', + full_name: 'Test Testersen', + id: 11, + login: 'test', + userType: 1, +}; + +const resourceId = 'res-id'; + +const navigateMock = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => navigateMock, + useParams: () => ({ + org: mainOrganization.username, + resourceId: resourceId, + }), +})); + +describe('ResourceAdmHeader', () => { + afterEach(jest.clearAllMocks); + + it('should show org name and resource id in header', () => { + renderResourceAdmHeader(); + + expect(screen.getByText(`${mainOrganization.full_name} / ${resourceId}`)).toBeInTheDocument(); + }); + + it('should navigate to new org when another org is chosen in menu', async () => { + const user = userEvent.setup(); + renderResourceAdmHeader(); + + const menuTrigger = screen.getByRole('button', { + name: textMock('shared.header_user_for_org', { + user: testUser.full_name, + org: mainOrganization.full_name, + }), + }); + await user.click(menuTrigger); + + const otherOrgButton = screen.getByRole('menuitemradio', { + name: otherOrganization.full_name, + }); + await user.click(otherOrgButton); + + expect(navigateMock).toHaveBeenCalled(); + }); +}); + +const renderResourceAdmHeader = () => { + return render( + + + + + , + ); +}; From edbe6a513f5f9c351912ea2db7195b78de840b80 Mon Sep 17 00:00:00 2001 From: Martin Gunnerud Date: Fri, 4 Oct 2024 14:38:52 +0200 Subject: [PATCH 4/4] refactor --- .../components/ResourceAdmHeader/ResourceAdmHeader.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.tsx b/frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.tsx index d5a63cc1d5b..ff7c2ae54e6 100644 --- a/frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.tsx +++ b/frontend/resourceadm/components/ResourceAdmHeader/ResourceAdmHeader.tsx @@ -56,12 +56,13 @@ const DashboardHeaderMenu = ({ organizations, user }: ResourceAdmHeaderProps) => navigate(`/${context}/${getAppName(context)}${location.search}`); }; - const selectableOrgMenuItems: StudioProfileMenuItem[] = - selectableOrgs.map((selectableOrg: Organization) => ({ + const selectableOrgMenuItems: StudioProfileMenuItem[] = selectableOrgs.map( + (selectableOrg: Organization) => ({ action: { type: 'button', onClick: () => handleSetSelectedContext(selectableOrg.username) }, itemName: selectableOrg?.full_name || selectableOrg.username, isActive: org === selectableOrg.username, - })) ?? []; + }), + ); const giteaMenuItem: StudioProfileMenuItem = { action: { type: 'link', href: repoPath }, @@ -86,7 +87,7 @@ const DashboardHeaderMenu = ({ organizations, user }: ResourceAdmHeaderProps) => variant='regular' profileImage={