Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(resource-adm): use new StudioPageHeader component in resourceadm #13699

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
};
Loading