Skip to content

Commit

Permalink
refactor(resource-adm): use new StudioPageHeader component in resourc…
Browse files Browse the repository at this point in the history
…eadm (#13699)
  • Loading branch information
mgunnerud authored Oct 4, 2024
1 parent 939464c commit c3f6f5e
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -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(
<MemoryRouter>
<ServicesContextProvider {...queriesMock} client={createQueryClientMock()}>
<ResourceAdmHeader organizations={organizations} user={testUser} />
</ServicesContextProvider>
</MemoryRouter>,
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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, resourceId } = useUrlParams();
const resourcePath = resourceId ? ` / ${resourceId}` : '';
const pageHeaderTitle: string = `${getOrgNameByUsername(org, organizations)}${resourcePath}`;

return (
<StudioPageHeader>
<StudioPageHeader.Main>
<StudioPageHeader.Left title={pageHeaderTitle} showTitle />
<StudioPageHeader.Right>
<DashboardHeaderMenu organizations={organizations} user={user} />
</StudioPageHeader.Right>
</StudioPageHeader.Main>
</StudioPageHeader>
);
};

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 (
<StudioPageHeader.ProfileMenu
triggerButtonText={showButtonText && triggerButtonText}
ariaLabelTriggerButton={triggerButtonText}
color='dark'
variant='regular'
profileImage={
<StudioAvatar
src={user?.avatar_url}
alt={t('general.profile_icon')}
title={t('shared.header_profile_icon_text')}
/>
}
profileMenuGroups={profileMenuGroups}
/>
);
};
1 change: 1 addition & 0 deletions frontend/resourceadm/components/ResourceAdmHeader/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ResourceAdmHeader } from './ResourceAdmHeader';
3 changes: 2 additions & 1 deletion frontend/resourceadm/hooks/useUrlParams/useUrlParams.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useParams } from 'react-router-dom';
import { getAppName } from '../../utils/stringUtils';

interface ResourceAdminUrlParams {
org: string;
Expand All @@ -14,7 +15,7 @@ export const useUrlParams = (): Readonly<ResourceAdminUrlParams> => {

return {
org: params.org,
app: `${params.org}-resources`,
app: getAppName(params.org),
env: params.env,
resourceId: params.resourceId,
accessListId: params.accessListId,
Expand Down
27 changes: 6 additions & 21 deletions frontend/resourceadm/pages/PageLayout/PageLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
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';
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
Expand All @@ -26,7 +22,7 @@ export const PageLayout = (): React.JSX.Element => {
const { data: organizations } = useOrganizationsQuery();
const mergeConflictModalRef = useRef<HTMLDialogElement>(null);

const { org = SelectedContextType.Self, app } = useUrlParams();
const { org, app } = useUrlParams();
const { data: repoStatus } = useRepoStatusQuery(org, app);

const navigate = useNavigate();
Expand Down Expand Up @@ -63,22 +59,11 @@ export const PageLayout = (): React.JSX.Element => {
};
}, [mergeConflictModalRef]);

const headerContextValue: IHeaderContext = useMemo(
() => ({
selectableOrgs: organizations,
user,
}),
[organizations, user],
);

return (
<>
<HeaderContext.Provider value={headerContextValue}>
<MergeConflictModal ref={mergeConflictModalRef} org={org} repo={app} />
{/* TODO - Find out if <AppHeader /> should be replaced to be the same as studio */}
<AppHeader />
<GiteaHeader menuOnlyHasRepository rightContentClassName={classes.extraPadding} />
</HeaderContext.Provider>
<MergeConflictModal ref={mergeConflictModalRef} org={org} repo={app} />
{organizations && user && <ResourceAdmHeader organizations={organizations} user={user} />}
<GiteaHeader menuOnlyHasRepository rightContentClassName={classes.extraPadding} />
<Outlet />
</>
);
Expand Down
3 changes: 2 additions & 1 deletion frontend/resourceadm/pages/RedirectPage/RedirectPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,7 +20,7 @@ export const RedirectPage = (): React.JSX.Element => {
// Error page if user has chosen "Alle"
<ErrorPage />
) : (
<Navigate to={`${org}-resources/`} replace={true} />
<Navigate to={`${getAppName(org)}/`} replace={true} />
)}
</div>
);
Expand Down
1 change: 1 addition & 0 deletions frontend/resourceadm/utils/stringUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export {
isSePrefix,
stringNumberToAriaLabel,
isOrgNrString,
getAppName,
} from './stringUtils';
4 changes: 4 additions & 0 deletions frontend/resourceadm/utils/stringUtils/stringUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
};
2 changes: 1 addition & 1 deletion frontend/resourceadm/utils/userUtils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { userHasAccessToOrganization } from './userUtils';
export { userHasAccessToOrganization, getOrgNameByUsername } from './userUtils';
5 changes: 5 additions & 0 deletions frontend/resourceadm/utils/userUtils/userUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

0 comments on commit c3f6f5e

Please sign in to comment.