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
15 changed files with 108 additions and 148 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
export { createTheme } from './createTheme'; export { createTheme } from './createTheme';
export { ThemeRichColor, GrafanaThemeV2 } from './types'; export { ThemeRichColor, GrafanaThemeV2 } from './types';
export { ThemeColors } from './createColors'; export { ThemeColors } from './createColors';
export { ThemeBreakpoints } from './breakpoints'; export { ThemeBreakpoints, ThemeBreakpointsKey } from './breakpoints';
export { ThemeShadows } from './createShadows'; export { ThemeShadows } from './createShadows';
export { ThemeShape } from './createShape'; export { ThemeShape } from './createShape';
export { ThemeTypography, ThemeTypographyVariant } from './createTypography'; export { ThemeTypography, ThemeTypographyVariant } from './createTypography';
@@ -1,7 +1,7 @@
import { createTheme, GrafanaTheme, GrafanaThemeV2 } from '@grafana/data'; import { createTheme, GrafanaTheme, GrafanaThemeV2 } from '@grafana/data';
import hoistNonReactStatics from 'hoist-non-react-statics'; import hoistNonReactStatics from 'hoist-non-react-statics';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { Themeable } from '../types/theme'; import { Themeable, Themeable2 } from '../types/theme';
import { stylesFactory } from './stylesFactory'; import { stylesFactory } from './stylesFactory';
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>; 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 */ /** @alpha */
export const withTheme2 = <P extends Themeable, S extends {} = {}>(Component: React.ComponentType<P>) => { export const withTheme2 = <P extends Themeable2, S extends {} = {}>(Component: React.ComponentType<P>) => {
const WithTheme: React.FunctionComponent<Subtract<P, Themeable>> = (props) => { const WithTheme: React.FunctionComponent<Subtract<P, Themeable2>> = (props) => {
/** /**
* If theme context is mocked, let's use it instead of the original context * 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 * This is used in tests when mocking theme using mockThemeContext function defined below
+10 -1
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 { getTheme, mockTheme } from './getTheme';
export { stylesFactory } from './stylesFactory'; export { stylesFactory } from './stylesFactory';
export { GlobalStyles } from './GlobalStyles/GlobalStyles'; export { GlobalStyles } from './GlobalStyles/GlobalStyles';
+9 -9
View File
@@ -6,15 +6,15 @@ import { getTitleFromNavModel } from 'app/core/selectors/navModel';
import PageHeader from '../PageHeader/PageHeader'; import PageHeader from '../PageHeader/PageHeader';
import { Footer } from '../Footer/Footer'; import { Footer } from '../Footer/Footer';
import { PageContents } from './PageContents'; import { PageContents } from './PageContents';
import { CustomScrollbar, useStyles } from '@grafana/ui'; import { CustomScrollbar, useStyles2 } from '@grafana/ui';
import { GrafanaTheme, NavModel } from '@grafana/data'; import { GrafanaThemeV2, NavModel, ThemeBreakpointsKey } from '@grafana/data';
import { Branding } from '../Branding/Branding'; import { Branding } from '../Branding/Branding';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
interface Props extends HTMLAttributes<HTMLDivElement> { interface Props extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode; children: React.ReactNode;
navModel: NavModel; navModel: NavModel;
contentWidth?: keyof GrafanaTheme['breakpoints']; contentWidth?: ThemeBreakpointsKey;
} }
export interface PageType extends FC<Props> { 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 }) => { export const Page: PageType = ({ navModel, children, className, contentWidth, ...otherProps }) => {
const styles = useStyles(getStyles); const styles = useStyles2(getStyles);
useEffect(() => { useEffect(() => {
const title = getTitleFromNavModel(navModel); const title = getTitleFromNavModel(navModel);
@@ -51,17 +51,17 @@ Page.Contents = PageContents;
export default Page; export default Page;
const getStyles = (theme: GrafanaTheme) => ({ const getStyles = (theme: GrafanaThemeV2) => ({
wrapper: css` wrapper: css`
background: ${theme.colors.background.primary};
bottom: 0;
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0;
width: 100%; width: 100%;
background: ${theme.colors.bg1};
`, `,
contentWidth: (size: keyof GrafanaTheme['breakpoints']) => css` contentWidth: (size: ThemeBreakpointsKey) => css`
.page-container { .page-container {
max-width: ${theme.breakpoints[size]}; max-width: ${theme.breakpoints.values[size]}px;
} }
`, `,
}); });
@@ -2,9 +2,9 @@ import React, { FormEvent, PureComponent } from 'react';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
import { connect, ConnectedProps } from 'react-redux'; import { connect, ConnectedProps } from 'react-redux';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { GrafanaThemeV2, SelectableValue } from '@grafana/data';
import { locationService } from '@grafana/runtime'; 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 { config } from 'app/core/config';
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper'; import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper';
import { AlertingQueryEditor } from './components/AlertingQueryEditor'; import { AlertingQueryEditor } from './components/AlertingQueryEditor';
@@ -48,13 +48,13 @@ const connector = connect(mapStateToProps, mapDispatchToProps);
interface RouteProps extends GrafanaRouteComponentProps<{ id: string }> {} interface RouteProps extends GrafanaRouteComponentProps<{ id: string }> {}
interface OwnProps { interface OwnProps extends Themeable2 {
saveDefinition: typeof createAlertDefinition | typeof updateAlertDefinition; saveDefinition: typeof createAlertDefinition | typeof updateAlertDefinition;
} }
type Props = OwnProps & ConnectedProps<typeof connector>; type Props = OwnProps & ConnectedProps<typeof connector>;
class NextGenAlertingPageUnconnected extends PureComponent<Props> { class UnthemedNextGenAlertingPage extends PureComponent<Props> {
componentDidMount() { componentDidMount() {
const { getAlertDefinition, pageId } = this.props; const { getAlertDefinition, pageId } = this.props;
@@ -122,9 +122,9 @@ class NextGenAlertingPageUnconnected extends PureComponent<Props> {
} }
render() { 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 ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
@@ -154,16 +154,18 @@ class NextGenAlertingPageUnconnected extends PureComponent<Props> {
} }
} }
const NextGenAlertingPageUnconnected = withTheme2(UnthemedNextGenAlertingPage);
export default hot(module)(connector(NextGenAlertingPageUnconnected)); export default hot(module)(connector(NextGenAlertingPageUnconnected));
const getStyles = stylesFactory((theme: GrafanaTheme) => ({ const getStyles = stylesFactory((theme: GrafanaThemeV2) => ({
wrapper: css` wrapper: css`
width: calc(100% - 55px); width: calc(100% - 55px);
height: 100%; height: 100%;
position: fixed; position: fixed;
top: 0; top: 0;
bottom: 0; bottom: 0;
background: ${theme.colors.dashboardBg}; background: ${theme.colors.background.canvas};
display: flex; display: flex;
flex-direction: column; flex-direction: column;
`, `,
@@ -187,8 +187,8 @@ describe('ApiKeysPage', () => {
}); });
function toggleShowExpired() { function toggleShowExpired() {
expect(screen.getByText(/show expired/i)).toBeInTheDocument(); expect(screen.queryByLabelText(/show expired/i)).toBeInTheDocument();
userEvent.click(screen.getByText(/show expired/i)); userEvent.click(screen.getByLabelText(/show expired/i));
} }
async function addAndVerifyApiKey(addApiKeyMock: jest.Mock, includeExpired: boolean) { async function addAndVerifyApiKey(addApiKeyMock: jest.Mock, includeExpired: boolean) {
+4 -4
View File
@@ -11,7 +11,7 @@ import { ApiKeysAddedModal } from './ApiKeysAddedModal';
import config from 'app/core/config'; import config from 'app/core/config';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; 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 { rangeUtil } from '@grafana/data';
import { getTimeZone } from 'app/features/profile/state/selectors'; import { getTimeZone } from 'app/features/profile/state/selectors';
import { setSearchQuery } from './state/reducers'; import { setSearchQuery } from './state/reducers';
@@ -21,8 +21,6 @@ import { ApiKeysTable } from './ApiKeysTable';
import { ApiKeysController } from './ApiKeysController'; import { ApiKeysController } from './ApiKeysController';
import { ShowModalReactEvent } from 'app/types/events'; import { ShowModalReactEvent } from 'app/types/events';
const { Switch } = LegacyForms;
function mapStateToProps(state: StoreState) { function mapStateToProps(state: StoreState) {
return { return {
navModel: getNavModel(state.navIndex, 'apikeys'), navModel: getNavModel(state.navIndex, 'apikeys'),
@@ -154,7 +152,9 @@ export class ApiKeysPageUnconnected extends PureComponent<Props, State> {
{showTable ? ( {showTable ? (
<> <>
<h3 className="page-heading">Existing API keys</h3> <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} /> <ApiKeysTable apiKeys={apiKeys} timeZone={timeZone} onDelete={this.onDeleteApiKey} />
</> </>
) : null} ) : null}
@@ -8,7 +8,7 @@ import { notifyApp } from 'app/core/actions';
import { cleanUpDashboardAndVariables } from '../state/actions'; import { cleanUpDashboardAndVariables } from '../state/actions';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; 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', () => ({})); jest.mock('app/features/dashboard/components/DashboardSettings/GeneralSettings', () => ({}));
@@ -68,7 +68,7 @@ function dashboardPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
cancelVariables: jest.fn(), cancelVariables: jest.fn(),
templateVarsChangedInUrl: jest.fn(), templateVarsChangedInUrl: jest.fn(),
dashboard: null, dashboard: null,
theme: getTheme(), theme: createTheme(),
}; };
Object.assign(props, propOverrides); Object.assign(props, propOverrides);
@@ -5,7 +5,7 @@ import { hot } from 'react-hot-loader';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { locationService } from '@grafana/runtime'; import { locationService } from '@grafana/runtime';
import { selectors } from '@grafana/e2e-selectors'; 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 { createErrorNotification } from 'app/core/copy/appNotification';
import { Branding } from 'app/core/components/Branding/Branding'; 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 { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getTimeSrv } from '../services/TimeSrv'; import { getTimeSrv } from '../services/TimeSrv';
import { getKioskMode } from 'app/core/navigation/kiosk'; 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 { DashboardLoading } from '../components/DashboardLoading/DashboardLoading';
import { DashboardFailed } from '../components/DashboardLoading/DashboardFailed'; import { DashboardFailed } from '../components/DashboardLoading/DashboardFailed';
@@ -50,7 +50,7 @@ type DashboardPageRouteSearchParams = {
}; };
export interface Props export interface Props
extends Themeable, extends Themeable2,
GrafanaRouteComponentProps<DashboardPageRouteParams, DashboardPageRouteSearchParams> { GrafanaRouteComponentProps<DashboardPageRouteParams, DashboardPageRouteSearchParams> {
initPhase: DashboardInitPhase; initPhase: DashboardInitPhase;
isInitSlow: boolean; isInitSlow: boolean;
@@ -373,7 +373,7 @@ const mapDispatchToProps = {
/* /*
* Styles * Styles
*/ */
export const getStyles = stylesFactory((theme: GrafanaTheme) => { export const getStyles = stylesFactory((theme: GrafanaThemeV2) => {
return { return {
dashboardContainer: css` dashboardContainer: css`
position: absolute; position: absolute;
@@ -392,13 +392,13 @@ export const getStyles = stylesFactory((theme: GrafanaTheme) => {
display: flex; display: flex;
`, `,
dashboardContent: css` dashboardContent: css`
padding: ${theme.spacing.md}; padding: ${theme.spacing(2)};
flex-basis: 100%; flex-basis: 100%;
flex-grow: 1; flex-grow: 1;
`, `,
}; };
}); });
export const DashboardPage = withTheme(UnthemedDashboardPage); export const DashboardPage = withTheme2(UnthemedDashboardPage);
DashboardPage.displayName = 'DashboardPage'; DashboardPage.displayName = 'DashboardPage';
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage)); export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage));
@@ -1,8 +1,19 @@
import React, { FormEvent, PureComponent } from 'react'; import React, { FormEvent, PureComponent } from 'react';
import { MapDispatchToProps, MapStateToProps } from 'react-redux'; import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { AppEvents, NavModel } from '@grafana/data'; import { AppEvents, GrafanaThemeV2, NavModel } from '@grafana/data';
import { Button, stylesFactory, Input, TextArea, Field, Form, Legend, FileUpload } from '@grafana/ui'; import {
Button,
stylesFactory,
withTheme2,
Input,
TextArea,
Field,
Form,
Legend,
FileUpload,
Themeable2,
} from '@grafana/ui';
import Page from 'app/core/components/Page/Page'; import Page from 'app/core/components/Page/Page';
import { connectWithCleanUp } from 'app/core/components/connectWithCleanUp'; import { connectWithCleanUp } from 'app/core/components/connectWithCleanUp';
import { ImportDashboardOverview } from './components/ImportDashboardOverview'; import { ImportDashboardOverview } from './components/ImportDashboardOverview';
@@ -12,7 +23,7 @@ import appEvents from 'app/core/app_events';
import { getNavModel } from 'app/core/selectors/navModel'; import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
interface OwnProps {} interface OwnProps extends Themeable2 {}
interface ConnectedProps { interface ConnectedProps {
navModel: NavModel; navModel: NavModel;
@@ -26,7 +37,7 @@ interface DispatchProps {
type Props = OwnProps & ConnectedProps & DispatchProps; type Props = OwnProps & ConnectedProps & DispatchProps;
class DashboardImportUnConnected extends PureComponent<Props> { class UnthemedDashboardImport extends PureComponent<Props> {
onFileUpload = (event: FormEvent<HTMLInputElement>) => { onFileUpload = (event: FormEvent<HTMLInputElement>) => {
const { importDashboardJson } = this.props; const { importDashboardJson } = this.props;
const file = event.currentTarget.files && event.currentTarget.files.length > 0 && event.currentTarget.files[0]; const file = event.currentTarget.files && event.currentTarget.files.length > 0 && event.currentTarget.files[0];
@@ -72,7 +83,7 @@ class DashboardImportUnConnected extends PureComponent<Props> {
}; };
renderImportForm() { renderImportForm() {
const styles = importStyles(); const styles = importStyles(this.props.theme);
return ( return (
<> <>
@@ -134,6 +145,8 @@ class DashboardImportUnConnected extends PureComponent<Props> {
} }
} }
const DashboardImportUnConnected = withTheme2(UnthemedDashboardImport);
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state: StoreState) => ({ const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state: StoreState) => ({
navModel: getNavModel(state.navIndex, 'import', undefined, true), navModel: getNavModel(state.navIndex, 'import', undefined, true),
isLoaded: state.importDashboard.isLoaded, isLoaded: state.importDashboard.isLoaded,
@@ -154,10 +167,10 @@ export default DashboardImportPage;
DashboardImportPage.displayName = 'DashboardImport'; DashboardImportPage.displayName = 'DashboardImport';
const importStyles = stylesFactory(() => { const importStyles = stylesFactory((theme: GrafanaThemeV2) => {
return { return {
option: css` option: css`
margin-bottom: 32px; margin-bottom: ${theme.spacing(4)};
`, `,
}; };
}); });
@@ -2,7 +2,7 @@ import React, { FC } from 'react';
import { connect, MapStateToProps } from 'react-redux'; import { connect, MapStateToProps } from 'react-redux';
import { NavModel } from '@grafana/data'; import { NavModel } from '@grafana/data';
import { locationService } from '@grafana/runtime'; import { locationService } from '@grafana/runtime';
import { useStyles } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import Page from 'app/core/components/Page/Page'; import Page from 'app/core/components/Page/Page';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
@@ -25,7 +25,7 @@ export interface RouteParams {
interface Props extends ConnectedProps, GrafanaRouteComponentProps<RouteParams> {} interface Props extends ConnectedProps, GrafanaRouteComponentProps<RouteParams> {}
export const PlaylistEditPage: FC<Props> = ({ navModel, match }) => { export const PlaylistEditPage: FC<Props> = ({ navModel, match }) => {
const styles = useStyles(getPlaylistStyles); const styles = useStyles2(getPlaylistStyles);
const { playlist, loading } = usePlaylist(match.params.id); const { playlist, loading } = usePlaylist(match.params.id);
const onSubmit = async (playlist: Playlist) => { const onSubmit = async (playlist: Playlist) => {
await updatePlaylist(match.params.id, playlist); await updatePlaylist(match.params.id, playlist);
@@ -2,7 +2,7 @@ import React, { FC } from 'react';
import { connect, MapStateToProps } from 'react-redux'; import { connect, MapStateToProps } from 'react-redux';
import { NavModel } from '@grafana/data'; import { NavModel } from '@grafana/data';
import { locationService } from '@grafana/runtime'; import { locationService } from '@grafana/runtime';
import { useStyles } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import Page from 'app/core/components/Page/Page'; import Page from 'app/core/components/Page/Page';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
@@ -21,7 +21,7 @@ interface ConnectedProps {
interface Props extends ConnectedProps, GrafanaRouteComponentProps {} interface Props extends ConnectedProps, GrafanaRouteComponentProps {}
export const PlaylistNewPage: FC<Props> = ({ navModel }) => { export const PlaylistNewPage: FC<Props> = ({ navModel }) => {
const styles = useStyles(getPlaylistStyles); const styles = useStyles2(getPlaylistStyles);
const { playlist, loading } = usePlaylist(); const { playlist, loading } = usePlaylist();
const onSubmit = async (playlist: Playlist) => { const onSubmit = async (playlist: Playlist) => {
await createPlaylist(playlist); await createPlaylist(playlist);
+3 -3
View File
@@ -1,7 +1,7 @@
import { GrafanaTheme } from '@grafana/data'; import { GrafanaThemeV2 } from '@grafana/data';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
export function getPlaylistStyles(theme: GrafanaTheme) { export function getPlaylistStyles(theme: GrafanaThemeV2) {
return { return {
description: css` description: css`
label: description; label: description;
@@ -10,7 +10,7 @@ export function getPlaylistStyles(theme: GrafanaTheme) {
`, `,
subHeading: css` subHeading: css`
label: sub-heading; label: sub-heading;
margin-bottom: ${theme.spacing.md}; margin-bottom: ${theme.spacing(2)};
`, `,
}; };
} }
+26 -55
View File
@@ -1,8 +1,7 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import Page from 'app/core/components/Page/Page'; import Page from 'app/core/components/Page/Page';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
import { Button, LegacyForms } from '@grafana/ui'; import { Button, Form, Field, Input, FieldSet, Label, Tooltip, Icon } from '@grafana/ui';
const { FormField } = LegacyForms;
import { NavModel } from '@grafana/data'; import { NavModel } from '@grafana/data';
import { getBackendSrv, locationService } from '@grafana/runtime'; import { getBackendSrv, locationService } from '@grafana/runtime';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@@ -13,78 +12,50 @@ export interface Props {
navModel: NavModel; navModel: NavModel;
} }
interface State { interface TeamDTO {
name: string; name: string;
email: string; email: string;
} }
export class CreateTeam extends PureComponent<Props, State> { export class CreateTeam extends PureComponent<Props> {
state: State = { create = async (formModel: TeamDTO) => {
name: '', const result = await getBackendSrv().post('/api/teams', formModel);
email: '',
};
create = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const { name, email } = this.state;
const result = await getBackendSrv().post('/api/teams', { name, email });
if (result.teamId) { if (result.teamId) {
locationService.push(`/org/teams/edit/${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() { render() {
const { navModel } = this.props; const { navModel } = this.props;
const { name, email } = this.state;
return ( return (
<Page navModel={navModel}> <Page navModel={navModel}>
<Page.Contents> <Page.Contents>
<> <Form onSubmit={this.create}>
<h3 className="page-sub-heading">New Team</h3> {({ register }) => (
<FieldSet label="New Team">
<form className="gf-form-group" onSubmit={this.create}> <Field label="Name">
<FormField <Input name="name" ref={register({ required: true })} width={60} />
className="gf-form" </Field>
label="Name" <Field
value={name} label={
onChange={this.onNameChange} <Label>
inputWidth={30} <span>Email</span>
labelWidth={10} <Tooltip content="This is optional and is primarily used for allowing custom team avatars.">
required <Icon name="info-circle" style={{ marginLeft: 6 }} />
/> </Tooltip>
<FormField </Label>
type="email" }
className="gf-form" >
label="Email" <Input type="email" name="email" ref={register()} placeholder="email@test.com" width={60} />
value={email} </Field>
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"> <div className="gf-form-button-row">
<Button type="submit" variant="primary"> <Button type="submit" variant="primary">
Create Create
</Button> </Button>
</div> </div>
</form> </FieldSet>
</> )}
</Form>
</Page.Contents> </Page.Contents>
</Page> </Page>
); );
@@ -5,46 +5,11 @@ exports[`Render should render component 1`] = `
navModel={Object {}} navModel={Object {}}
> >
<PageContents> <PageContents>
<h3 <Form
className="page-sub-heading"
>
New Team
</h3>
<form
className="gf-form-group"
onSubmit={[Function]} onSubmit={[Function]}
> >
<FormField <Component />
className="gf-form" </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>
</PageContents> </PageContents>
</Page> </Page>
`; `;