TopNav: Updates to create service account page and invite user (#52480)

* Simplify logic to support both navs

* Added new file
This commit is contained in:
Torkel Ödegaard 2022-07-20 16:02:25 +02:00 committed by GitHub
parent b42ac8a211
commit 01d561224c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 105 additions and 90 deletions

View File

@ -0,0 +1,10 @@
import React from 'react';
interface Props {
children: React.ReactNode;
}
/** Remove after topnav feature toggle is removed */
export function OldNavOnly({ children }: Props): React.ReactElement | null {
return <>{children}</>;
}

View File

@ -2,7 +2,7 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { GrafanaTheme2, NavModel, NavModelItem } from '@grafana/data';
import { config } from '@grafana/runtime';
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
@ -10,6 +10,7 @@ import { Footer } from '../Footer/Footer';
import { PageHeader } from '../PageHeader/PageHeader';
import { Page as NewPage } from '../PageNew/Page';
import { OldNavOnly } from './OldNavOnly';
import { PageContents } from './PageContents';
import { PageLayoutType, PageType } from './types';
import { usePageNav } from './usePageNav';
@ -31,7 +32,7 @@ export const OldPage: PageType = ({
usePageTitle(navModel, pageNav);
const pageHeaderNav = pageNav ?? navModel?.main;
const pageHeaderNav = getPageHeaderNav(navModel, pageNav);
return (
<div className={cx(styles.wrapper, className)}>
@ -58,8 +59,17 @@ export const OldPage: PageType = ({
);
};
function getPageHeaderNav(navModel?: NavModel, pageNav?: NavModelItem): NavModelItem | undefined {
if (pageNav?.children && pageNav.children.length > 0) {
return pageNav;
}
return navModel?.main;
}
OldPage.Header = PageHeader;
OldPage.Contents = PageContents;
OldPage.OldNavOnly = OldNavOnly;
export const Page: PageType = config.featureToggles.topnav ? NewPage : OldPage;

View File

@ -1,9 +1,10 @@
import { FC, HTMLAttributes, RefCallback } from 'react';
import React, { FC, HTMLAttributes, RefCallback } from 'react';
import { NavModel, NavModelItem } from '@grafana/data';
import { PageHeader } from '../PageHeader/PageHeader';
import { OldNavOnly } from './OldNavOnly';
import { PageContents } from './PageContents';
export interface PageProps extends HTMLAttributes<HTMLDivElement> {
@ -28,5 +29,6 @@ export enum PageLayoutType {
export interface PageType extends FC<PageProps> {
Header: typeof PageHeader;
OldNavOnly: typeof OldNavOnly;
Contents: typeof PageContents;
}

View File

@ -76,6 +76,7 @@ export const Page: PageType = ({
Page.Header = PageHeader;
Page.Contents = PageContents;
Page.OldNavOnly = () => null;
const getStyles = (theme: GrafanaTheme2) => {
const shadow = theme.isDark

View File

@ -1,18 +1,9 @@
import React from 'react';
import { locationUtil } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { locationService } from '@grafana/runtime';
import {
HorizontalGroup,
Button,
LinkButton,
Input,
Switch,
RadioButtonGroup,
Form,
Field,
InputControl,
} from '@grafana/ui';
import { Button, LinkButton, Input, Switch, RadioButtonGroup, Form, Field, InputControl, FieldSet } from '@grafana/ui';
import { getConfig } from 'app/core/config';
import { OrgRole, useDispatch } from 'app/types';
@ -52,32 +43,34 @@ export const UserInviteForm = () => {
{({ register, control, errors }) => {
return (
<>
<Field
invalid={!!errors.loginOrEmail}
error={!!errors.loginOrEmail ? 'Email or username is required' : undefined}
label="Email or username"
>
<Input {...register('loginOrEmail', { required: true })} placeholder="email@example.com" />
</Field>
<Field invalid={!!errors.name} label="Name">
<Input {...register('name')} placeholder="(optional)" />
</Field>
<Field invalid={!!errors.role} label="Role">
<InputControl
render={({ field: { ref, ...field } }) => <RadioButtonGroup {...field} options={roles} />}
control={control}
name="role"
/>
</Field>
<Field label="Send invite email">
<Switch id="send-email-switch" {...register('sendEmail')} />
</Field>
<HorizontalGroup>
<FieldSet>
<Field
invalid={!!errors.loginOrEmail}
error={!!errors.loginOrEmail ? 'Email or username is required' : undefined}
label="Email or username"
>
<Input {...register('loginOrEmail', { required: true })} placeholder="email@example.com" />
</Field>
<Field invalid={!!errors.name} label="Name">
<Input {...register('name')} placeholder="(optional)" />
</Field>
<Field invalid={!!errors.role} label="Role">
<InputControl
render={({ field: { ref, ...field } }) => <RadioButtonGroup {...field} options={roles} />}
control={control}
name="role"
/>
</Field>
<Field label="Send invite email">
<Switch id="send-email-switch" {...register('sendEmail')} />
</Field>
</FieldSet>
<Stack>
<Button type="submit">Submit</Button>
<LinkButton href={locationUtil.assureBaseUrl(getConfig().appSubUrl + '/org/users')} variant="secondary">
Back
</LinkButton>
</HorizontalGroup>
</Stack>
</>
);
}}

View File

@ -1,35 +1,29 @@
import React, { FC } from 'react';
import { connect } from 'react-redux';
import React from 'react';
import { NavModel } from '@grafana/data';
import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types/store';
import UserInviteForm from './UserInviteForm';
interface Props {
navModel: NavModel;
}
export function UserInvitePage() {
const subTitle = (
<>
Send invitation or add existing Grafana user to the organization.
<span className="highlight-word"> {contextSrv.user.orgName}</span>
</>
);
export const UserInvitePage: FC<Props> = ({ navModel }) => {
return (
<Page navModel={navModel}>
<Page navId="users" pageNav={{ text: 'Invite user' }} subTitle={subTitle}>
<Page.Contents>
<h3 className="page-sub-heading">Invite user</h3>
<div className="p-b-2">
Send invitation or add existing Grafana user to the organization.
<span className="highlight-word"> {contextSrv.user.orgName}</span>
</div>
<Page.OldNavOnly>
<h3 className="page-sub-heading">Invite user</h3>
<div className="p-b-2">{subTitle}</div>
</Page.OldNavOnly>
<UserInviteForm />
</Page.Contents>
</Page>
);
};
}
const mapStateToProps = (state: StoreState) => ({
navModel: getNavModel(state.navIndex, 'users'),
});
export default connect(mapStateToProps)(UserInvitePage);
export default UserInvitePage;

View File

@ -36,7 +36,9 @@ export function ChangePasswordPage({ loadUser, isUpdating, user, changePassword
<Page.Contents isLoading={!Boolean(user)}>
{user ? (
<>
<h3 className="page-heading">Change Your Password</h3>
<Page.OldNavOnly>
<h3 className="page-sub-heading">Change Your Password</h3>
</Page.OldNavOnly>
<ChangePasswordForm user={user} onChangePassword={changePassword} isSaving={isUpdating} />
</>
) : null}

View File

@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { getBackendSrv } from '@grafana/runtime';
import { Form, Button, Input, Field } from '@grafana/ui';
import { Form, Button, Input, Field, FieldSet } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
import { fetchBuiltinRoles, fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api';
@ -110,39 +110,43 @@ export const ServiceAccountCreatePage = ({}: Props): JSX.Element => {
};
return (
<Page navId="serviceaccounts">
<Page navId="serviceaccounts" pageNav={{ text: 'Create service account' }}>
<Page.Contents>
<h1>Create service account</h1>
<Page.OldNavOnly>
<h3 className="page-sub-heading">Create service account</h3>
</Page.OldNavOnly>
<Form onSubmit={onSubmit} validateOn="onSubmit">
{({ register, errors }) => {
return (
<>
<Field
label="Display name"
required
invalid={!!errors.name}
error={errors.name ? 'Display name is required' : undefined}
>
<Input id="display-name-input" {...register('name', { required: true })} autoFocus />
</Field>
<Field label="Role">
{contextSrv.licensedAccessControlEnabled() ? (
<UserRolePicker
userId={serviceAccount.id || 0}
orgId={serviceAccount.orgId}
builtInRole={serviceAccount.role}
builtInRoles={builtinRoles}
onBuiltinRoleChange={onRoleChange}
builtinRolesDisabled={false}
roleOptions={roleOptions}
updateDisabled={true}
onApplyRoles={onPendingRolesUpdate}
pendingRoles={pendingRoles}
/>
) : (
<OrgRolePicker aria-label="Role" value={serviceAccount.role} onChange={onRoleChange} />
)}
</Field>
<FieldSet>
<Field
label="Display name"
required
invalid={!!errors.name}
error={errors.name ? 'Display name is required' : undefined}
>
<Input id="display-name-input" {...register('name', { required: true })} autoFocus />
</Field>
<Field label="Role">
{contextSrv.licensedAccessControlEnabled() ? (
<UserRolePicker
userId={serviceAccount.id || 0}
orgId={serviceAccount.orgId}
builtInRole={serviceAccount.role}
builtInRoles={builtinRoles}
onBuiltinRoleChange={onRoleChange}
builtinRolesDisabled={false}
roleOptions={roleOptions}
updateDisabled={true}
onApplyRoles={onPendingRolesUpdate}
pendingRoles={pendingRoles}
/>
) : (
<OrgRolePicker aria-label="Role" value={serviceAccount.role} onChange={onRoleChange} />
)}
</Field>
</FieldSet>
<Button type="submit">Create</Button>
</>
);

View File

@ -4,7 +4,6 @@ import React, { useEffect, useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { GrafanaTheme2, OrgRole } from '@grafana/data';
import { config } from '@grafana/runtime';
import { Alert, ConfirmModal, FilterInput, Icon, LinkButton, RadioButtonGroup, Tooltip, useStyles2 } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { Page } from 'app/core/components/Page/Page';
@ -193,7 +192,7 @@ export const ServiceAccountsListPageUnconnected = ({
onRemove={onMigrationInfoClose}
></Alert>
)}
{!config.featureToggles.topnav && (
<Page.OldNavOnly>
<div className={styles.pageHeader}>
<h2>Service accounts</h2>
<div className={styles.apiKeyInfoLabel}>
@ -207,7 +206,7 @@ export const ServiceAccountsListPageUnconnected = ({
<span>Looking for API keys?</span>
</div>
</div>
)}
</Page.OldNavOnly>
<div className="page-action-bar">
<div className="gf-form gf-form--grow">
<FilterInput