Pages: update react components to use v2 theme (#33413)

* chore: expose theme types / functions

* fix(grafana-ui): withTheme2 extends themeable2

* feat: migrate page components to use new theme

* refactor(pages): replace legacy form components with latest form components

* refactor(dashboardimport): update page component to use theme spacing

* refactor(alerting-ng): update page component to use v2 theme

* test(dashboardpage): update test for v2 theme

* test(apikeyspage): update test to select InlineSwitch component

* test(createteam): update snapshot

* refactor(playlist): update page components to use v2 theme

* refactor(page): put back classes on page-container and background colors
This commit is contained in:
Jack Westbrook 2021-04-28 16:05:00 +02:00 committed by GitHub
parent 22ac0fc3cd
commit 249004ebef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 108 additions and 148 deletions

View File

@ -1,7 +1,7 @@
export { createTheme } from './createTheme';
export { ThemeRichColor, GrafanaThemeV2 } from './types';
export { ThemeColors } from './createColors';
export { ThemeBreakpoints } from './breakpoints';
export { ThemeBreakpoints, ThemeBreakpointsKey } from './breakpoints';
export { ThemeShadows } from './createShadows';
export { ThemeShape } from './createShape';
export { ThemeTypography, ThemeTypographyVariant } from './createTypography';

View File

@ -1,7 +1,7 @@
import { createTheme, GrafanaTheme, GrafanaThemeV2 } from '@grafana/data';
import hoistNonReactStatics from 'hoist-non-react-statics';
import React, { useContext } from 'react';
import { Themeable } from '../types/theme';
import { Themeable, Themeable2 } from '../types/theme';
import { stylesFactory } from './stylesFactory';
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
@ -42,8 +42,8 @@ export const withTheme = <P extends Themeable, S extends {} = {}>(Component: Rea
};
/** @alpha */
export const withTheme2 = <P extends Themeable, S extends {} = {}>(Component: React.ComponentType<P>) => {
const WithTheme: React.FunctionComponent<Subtract<P, Themeable>> = (props) => {
export const withTheme2 = <P extends Themeable2, S extends {} = {}>(Component: React.ComponentType<P>) => {
const WithTheme: React.FunctionComponent<Subtract<P, Themeable2>> = (props) => {
/**
* If theme context is mocked, let's use it instead of the original context
* This is used in tests when mocking theme using mockThemeContext function defined below

View File

@ -1,4 +1,13 @@
export { ThemeContext, withTheme, useTheme, useTheme2, useStyles, useStyles2, mockThemeContext } from './ThemeContext';
export {
ThemeContext,
withTheme,
withTheme2,
useTheme,
useTheme2,
useStyles,
useStyles2,
mockThemeContext,
} from './ThemeContext';
export { getTheme, mockTheme } from './getTheme';
export { stylesFactory } from './stylesFactory';
export { GlobalStyles } from './GlobalStyles/GlobalStyles';

View File

@ -6,15 +6,15 @@ import { getTitleFromNavModel } from 'app/core/selectors/navModel';
import PageHeader from '../PageHeader/PageHeader';
import { Footer } from '../Footer/Footer';
import { PageContents } from './PageContents';
import { CustomScrollbar, useStyles } from '@grafana/ui';
import { GrafanaTheme, NavModel } from '@grafana/data';
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
import { GrafanaThemeV2, NavModel, ThemeBreakpointsKey } from '@grafana/data';
import { Branding } from '../Branding/Branding';
import { css, cx } from '@emotion/css';
interface Props extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
navModel: NavModel;
contentWidth?: keyof GrafanaTheme['breakpoints'];
contentWidth?: ThemeBreakpointsKey;
}
export interface PageType extends FC<Props> {
@ -23,7 +23,7 @@ export interface PageType extends FC<Props> {
}
export const Page: PageType = ({ navModel, children, className, contentWidth, ...otherProps }) => {
const styles = useStyles(getStyles);
const styles = useStyles2(getStyles);
useEffect(() => {
const title = getTitleFromNavModel(navModel);
@ -51,17 +51,17 @@ Page.Contents = PageContents;
export default Page;
const getStyles = (theme: GrafanaTheme) => ({
const getStyles = (theme: GrafanaThemeV2) => ({
wrapper: css`
background: ${theme.colors.background.primary};
bottom: 0;
position: absolute;
top: 0;
bottom: 0;
width: 100%;
background: ${theme.colors.bg1};
`,
contentWidth: (size: keyof GrafanaTheme['breakpoints']) => css`
contentWidth: (size: ThemeBreakpointsKey) => css`
.page-container {
max-width: ${theme.breakpoints[size]};
max-width: ${theme.breakpoints.values[size]}px;
}
`,
});

View File

@ -2,9 +2,9 @@ import React, { FormEvent, PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect, ConnectedProps } from 'react-redux';
import { css } from '@emotion/css';
import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { GrafanaThemeV2, SelectableValue } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { PageToolbar, stylesFactory, ToolbarButton } from '@grafana/ui';
import { PageToolbar, stylesFactory, ToolbarButton, withTheme2, Themeable2 } from '@grafana/ui';
import { config } from 'app/core/config';
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper';
import { AlertingQueryEditor } from './components/AlertingQueryEditor';
@ -48,13 +48,13 @@ const connector = connect(mapStateToProps, mapDispatchToProps);
interface RouteProps extends GrafanaRouteComponentProps<{ id: string }> {}
interface OwnProps {
interface OwnProps extends Themeable2 {
saveDefinition: typeof createAlertDefinition | typeof updateAlertDefinition;
}
type Props = OwnProps & ConnectedProps<typeof connector>;
class NextGenAlertingPageUnconnected extends PureComponent<Props> {
class UnthemedNextGenAlertingPage extends PureComponent<Props> {
componentDidMount() {
const { getAlertDefinition, pageId } = this.props;
@ -122,9 +122,9 @@ class NextGenAlertingPageUnconnected extends PureComponent<Props> {
}
render() {
const { alertDefinition, uiState, updateAlertDefinitionUiState, getInstances } = this.props;
const { alertDefinition, uiState, updateAlertDefinitionUiState, getInstances, theme } = this.props;
const styles = getStyles(config.theme);
const styles = getStyles(theme);
return (
<div className={styles.wrapper}>
@ -154,16 +154,18 @@ class NextGenAlertingPageUnconnected extends PureComponent<Props> {
}
}
const NextGenAlertingPageUnconnected = withTheme2(UnthemedNextGenAlertingPage);
export default hot(module)(connector(NextGenAlertingPageUnconnected));
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
const getStyles = stylesFactory((theme: GrafanaThemeV2) => ({
wrapper: css`
width: calc(100% - 55px);
height: 100%;
position: fixed;
top: 0;
bottom: 0;
background: ${theme.colors.dashboardBg};
background: ${theme.colors.background.canvas};
display: flex;
flex-direction: column;
`,

View File

@ -187,8 +187,8 @@ describe('ApiKeysPage', () => {
});
function toggleShowExpired() {
expect(screen.getByText(/show expired/i)).toBeInTheDocument();
userEvent.click(screen.getByText(/show expired/i));
expect(screen.queryByLabelText(/show expired/i)).toBeInTheDocument();
userEvent.click(screen.getByLabelText(/show expired/i));
}
async function addAndVerifyApiKey(addApiKeyMock: jest.Mock, includeExpired: boolean) {

View File

@ -11,7 +11,7 @@ import { ApiKeysAddedModal } from './ApiKeysAddedModal';
import config from 'app/core/config';
import appEvents from 'app/core/app_events';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { LegacyForms } from '@grafana/ui';
import { InlineField, InlineSwitch } from '@grafana/ui';
import { rangeUtil } from '@grafana/data';
import { getTimeZone } from 'app/features/profile/state/selectors';
import { setSearchQuery } from './state/reducers';
@ -21,8 +21,6 @@ import { ApiKeysTable } from './ApiKeysTable';
import { ApiKeysController } from './ApiKeysController';
import { ShowModalReactEvent } from 'app/types/events';
const { Switch } = LegacyForms;
function mapStateToProps(state: StoreState) {
return {
navModel: getNavModel(state.navIndex, 'apikeys'),
@ -154,7 +152,9 @@ export class ApiKeysPageUnconnected extends PureComponent<Props, State> {
{showTable ? (
<>
<h3 className="page-heading">Existing API keys</h3>
<Switch label="Show expired" checked={includeExpired} onChange={this.onIncludeExpiredChange} />
<InlineField label="Show expired">
<InlineSwitch id="showExpired" value={includeExpired} onChange={this.onIncludeExpiredChange} />
</InlineField>
<ApiKeysTable apiKeys={apiKeys} timeZone={timeZone} onDelete={this.onDeleteApiKey} />
</>
) : null}

View File

@ -8,7 +8,7 @@ import { notifyApp } from 'app/core/actions';
import { cleanUpDashboardAndVariables } from '../state/actions';
import { selectors } from '@grafana/e2e-selectors';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
import { getTheme } from '@grafana/ui';
import { createTheme } from '@grafana/data';
jest.mock('app/features/dashboard/components/DashboardSettings/GeneralSettings', () => ({}));
@ -68,7 +68,7 @@ function dashboardPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
cancelVariables: jest.fn(),
templateVarsChangedInUrl: jest.fn(),
dashboard: null,
theme: getTheme(),
theme: createTheme(),
};
Object.assign(props, propOverrides);

View File

@ -5,7 +5,7 @@ import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import { locationService } from '@grafana/runtime';
import { selectors } from '@grafana/e2e-selectors';
import { CustomScrollbar, stylesFactory, Themeable, withTheme } from '@grafana/ui';
import { CustomScrollbar, stylesFactory, Themeable2, withTheme2 } from '@grafana/ui';
import { createErrorNotification } from 'app/core/copy/appNotification';
import { Branding } from 'app/core/components/Branding/Branding';
@ -26,7 +26,7 @@ import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getTimeSrv } from '../services/TimeSrv';
import { getKioskMode } from 'app/core/navigation/kiosk';
import { GrafanaTheme, UrlQueryValue } from '@grafana/data';
import { GrafanaThemeV2, UrlQueryValue } from '@grafana/data';
import { DashboardLoading } from '../components/DashboardLoading/DashboardLoading';
import { DashboardFailed } from '../components/DashboardLoading/DashboardFailed';
@ -50,7 +50,7 @@ type DashboardPageRouteSearchParams = {
};
export interface Props
extends Themeable,
extends Themeable2,
GrafanaRouteComponentProps<DashboardPageRouteParams, DashboardPageRouteSearchParams> {
initPhase: DashboardInitPhase;
isInitSlow: boolean;
@ -373,7 +373,7 @@ const mapDispatchToProps = {
/*
* Styles
*/
export const getStyles = stylesFactory((theme: GrafanaTheme) => {
export const getStyles = stylesFactory((theme: GrafanaThemeV2) => {
return {
dashboardContainer: css`
position: absolute;
@ -392,13 +392,13 @@ export const getStyles = stylesFactory((theme: GrafanaTheme) => {
display: flex;
`,
dashboardContent: css`
padding: ${theme.spacing.md};
padding: ${theme.spacing(2)};
flex-basis: 100%;
flex-grow: 1;
`,
};
});
export const DashboardPage = withTheme(UnthemedDashboardPage);
export const DashboardPage = withTheme2(UnthemedDashboardPage);
DashboardPage.displayName = 'DashboardPage';
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage));

View File

@ -1,8 +1,19 @@
import React, { FormEvent, PureComponent } from 'react';
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { css } from '@emotion/css';
import { AppEvents, NavModel } from '@grafana/data';
import { Button, stylesFactory, Input, TextArea, Field, Form, Legend, FileUpload } from '@grafana/ui';
import { AppEvents, GrafanaThemeV2, NavModel } from '@grafana/data';
import {
Button,
stylesFactory,
withTheme2,
Input,
TextArea,
Field,
Form,
Legend,
FileUpload,
Themeable2,
} from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { connectWithCleanUp } from 'app/core/components/connectWithCleanUp';
import { ImportDashboardOverview } from './components/ImportDashboardOverview';
@ -12,7 +23,7 @@ import appEvents from 'app/core/app_events';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';
interface OwnProps {}
interface OwnProps extends Themeable2 {}
interface ConnectedProps {
navModel: NavModel;
@ -26,7 +37,7 @@ interface DispatchProps {
type Props = OwnProps & ConnectedProps & DispatchProps;
class DashboardImportUnConnected extends PureComponent<Props> {
class UnthemedDashboardImport extends PureComponent<Props> {
onFileUpload = (event: FormEvent<HTMLInputElement>) => {
const { importDashboardJson } = this.props;
const file = event.currentTarget.files && event.currentTarget.files.length > 0 && event.currentTarget.files[0];
@ -72,7 +83,7 @@ class DashboardImportUnConnected extends PureComponent<Props> {
};
renderImportForm() {
const styles = importStyles();
const styles = importStyles(this.props.theme);
return (
<>
@ -134,6 +145,8 @@ class DashboardImportUnConnected extends PureComponent<Props> {
}
}
const DashboardImportUnConnected = withTheme2(UnthemedDashboardImport);
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state: StoreState) => ({
navModel: getNavModel(state.navIndex, 'import', undefined, true),
isLoaded: state.importDashboard.isLoaded,
@ -154,10 +167,10 @@ export default DashboardImportPage;
DashboardImportPage.displayName = 'DashboardImport';
const importStyles = stylesFactory(() => {
const importStyles = stylesFactory((theme: GrafanaThemeV2) => {
return {
option: css`
margin-bottom: 32px;
margin-bottom: ${theme.spacing(4)};
`,
};
});

View File

@ -2,7 +2,7 @@ import React, { FC } from 'react';
import { connect, MapStateToProps } from 'react-redux';
import { NavModel } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { useStyles } from '@grafana/ui';
import { useStyles2 } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { StoreState } from 'app/types';
@ -25,7 +25,7 @@ export interface RouteParams {
interface Props extends ConnectedProps, GrafanaRouteComponentProps<RouteParams> {}
export const PlaylistEditPage: FC<Props> = ({ navModel, match }) => {
const styles = useStyles(getPlaylistStyles);
const styles = useStyles2(getPlaylistStyles);
const { playlist, loading } = usePlaylist(match.params.id);
const onSubmit = async (playlist: Playlist) => {
await updatePlaylist(match.params.id, playlist);

View File

@ -2,7 +2,7 @@ import React, { FC } from 'react';
import { connect, MapStateToProps } from 'react-redux';
import { NavModel } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { useStyles } from '@grafana/ui';
import { useStyles2 } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { StoreState } from 'app/types';
@ -21,7 +21,7 @@ interface ConnectedProps {
interface Props extends ConnectedProps, GrafanaRouteComponentProps {}
export const PlaylistNewPage: FC<Props> = ({ navModel }) => {
const styles = useStyles(getPlaylistStyles);
const styles = useStyles2(getPlaylistStyles);
const { playlist, loading } = usePlaylist();
const onSubmit = async (playlist: Playlist) => {
await createPlaylist(playlist);

View File

@ -1,7 +1,7 @@
import { GrafanaTheme } from '@grafana/data';
import { GrafanaThemeV2 } from '@grafana/data';
import { css } from '@emotion/css';
export function getPlaylistStyles(theme: GrafanaTheme) {
export function getPlaylistStyles(theme: GrafanaThemeV2) {
return {
description: css`
label: description;
@ -10,7 +10,7 @@ export function getPlaylistStyles(theme: GrafanaTheme) {
`,
subHeading: css`
label: sub-heading;
margin-bottom: ${theme.spacing.md};
margin-bottom: ${theme.spacing(2)};
`,
};
}

View File

@ -1,8 +1,7 @@
import React, { PureComponent } from 'react';
import Page from 'app/core/components/Page/Page';
import { hot } from 'react-hot-loader';
import { Button, LegacyForms } from '@grafana/ui';
const { FormField } = LegacyForms;
import { Button, Form, Field, Input, FieldSet, Label, Tooltip, Icon } from '@grafana/ui';
import { NavModel } from '@grafana/data';
import { getBackendSrv, locationService } from '@grafana/runtime';
import { connect } from 'react-redux';
@ -13,78 +12,50 @@ export interface Props {
navModel: NavModel;
}
interface State {
interface TeamDTO {
name: string;
email: string;
}
export class CreateTeam extends PureComponent<Props, State> {
state: State = {
name: '',
email: '',
};
create = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const { name, email } = this.state;
const result = await getBackendSrv().post('/api/teams', { name, email });
export class CreateTeam extends PureComponent<Props> {
create = async (formModel: TeamDTO) => {
const result = await getBackendSrv().post('/api/teams', formModel);
if (result.teamId) {
locationService.push(`/org/teams/edit/${result.teamId}`);
}
};
onEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
email: event.target.value,
});
};
onNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
name: event.target.value,
});
};
render() {
const { navModel } = this.props;
const { name, email } = this.state;
return (
<Page navModel={navModel}>
<Page.Contents>
<>
<h3 className="page-sub-heading">New Team</h3>
<form className="gf-form-group" onSubmit={this.create}>
<FormField
className="gf-form"
label="Name"
value={name}
onChange={this.onNameChange}
inputWidth={30}
labelWidth={10}
required
/>
<FormField
type="email"
className="gf-form"
label="Email"
value={email}
onChange={this.onEmailChange}
inputWidth={30}
labelWidth={10}
placeholder="email@test.com"
tooltip="This is optional and is primarily used for allowing custom team avatars."
/>
<div className="gf-form-button-row">
<Button type="submit" variant="primary">
Create
</Button>
</div>
</form>
</>
<Form onSubmit={this.create}>
{({ register }) => (
<FieldSet label="New Team">
<Field label="Name">
<Input name="name" ref={register({ required: true })} width={60} />
</Field>
<Field
label={
<Label>
<span>Email</span>
<Tooltip content="This is optional and is primarily used for allowing custom team avatars.">
<Icon name="info-circle" style={{ marginLeft: 6 }} />
</Tooltip>
</Label>
}
>
<Input type="email" name="email" ref={register()} placeholder="email@test.com" width={60} />
</Field>
<div className="gf-form-button-row">
<Button type="submit" variant="primary">
Create
</Button>
</div>
</FieldSet>
)}
</Form>
</Page.Contents>
</Page>
);

View File

@ -5,46 +5,11 @@ exports[`Render should render component 1`] = `
navModel={Object {}}
>
<PageContents>
<h3
className="page-sub-heading"
>
New Team
</h3>
<form
className="gf-form-group"
<Form
onSubmit={[Function]}
>
<FormField
className="gf-form"
inputWidth={30}
label="Name"
labelWidth={10}
onChange={[Function]}
required={true}
value=""
/>
<FormField
className="gf-form"
inputWidth={30}
label="Email"
labelWidth={10}
onChange={[Function]}
placeholder="email@test.com"
tooltip="This is optional and is primarily used for allowing custom team avatars."
type="email"
value=""
/>
<div
className="gf-form-button-row"
>
<Button
type="submit"
variant="primary"
>
Create
</Button>
</div>
</form>
<Component />
</Form>
</PageContents>
</Page>
`;