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 { css, cx } from '@emotion/css';
import React from 'react'; import React from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2, NavModel, NavModelItem } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { CustomScrollbar, useStyles2 } from '@grafana/ui'; import { CustomScrollbar, useStyles2 } from '@grafana/ui';
@ -10,6 +10,7 @@ import { Footer } from '../Footer/Footer';
import { PageHeader } from '../PageHeader/PageHeader'; import { PageHeader } from '../PageHeader/PageHeader';
import { Page as NewPage } from '../PageNew/Page'; import { Page as NewPage } from '../PageNew/Page';
import { OldNavOnly } from './OldNavOnly';
import { PageContents } from './PageContents'; import { PageContents } from './PageContents';
import { PageLayoutType, PageType } from './types'; import { PageLayoutType, PageType } from './types';
import { usePageNav } from './usePageNav'; import { usePageNav } from './usePageNav';
@ -31,7 +32,7 @@ export const OldPage: PageType = ({
usePageTitle(navModel, pageNav); usePageTitle(navModel, pageNav);
const pageHeaderNav = pageNav ?? navModel?.main; const pageHeaderNav = getPageHeaderNav(navModel, pageNav);
return ( return (
<div className={cx(styles.wrapper, className)}> <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.Header = PageHeader;
OldPage.Contents = PageContents; OldPage.Contents = PageContents;
OldPage.OldNavOnly = OldNavOnly;
export const Page: PageType = config.featureToggles.topnav ? NewPage : OldPage; 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 { NavModel, NavModelItem } from '@grafana/data';
import { PageHeader } from '../PageHeader/PageHeader'; import { PageHeader } from '../PageHeader/PageHeader';
import { OldNavOnly } from './OldNavOnly';
import { PageContents } from './PageContents'; import { PageContents } from './PageContents';
export interface PageProps extends HTMLAttributes<HTMLDivElement> { export interface PageProps extends HTMLAttributes<HTMLDivElement> {
@ -28,5 +29,6 @@ export enum PageLayoutType {
export interface PageType extends FC<PageProps> { export interface PageType extends FC<PageProps> {
Header: typeof PageHeader; Header: typeof PageHeader;
OldNavOnly: typeof OldNavOnly;
Contents: typeof PageContents; Contents: typeof PageContents;
} }

View File

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

View File

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

View File

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

View File

@ -36,7 +36,9 @@ export function ChangePasswordPage({ loadUser, isUpdating, user, changePassword
<Page.Contents isLoading={!Boolean(user)}> <Page.Contents isLoading={!Boolean(user)}>
{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} /> <ChangePasswordForm user={user} onChangePassword={changePassword} isSaving={isUpdating} />
</> </>
) : null} ) : null}

View File

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

View File

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