diff --git a/packages/grafana-ui/src/components/Forms/Form.tsx b/packages/grafana-ui/src/components/Forms/Form.tsx index 94fb26f848b..0e31eec5e08 100644 --- a/packages/grafana-ui/src/components/Forms/Form.tsx +++ b/packages/grafana-ui/src/components/Forms/Form.tsx @@ -38,6 +38,7 @@ export function Form({
diff --git a/packages/grafana-ui/src/themes/ThemeContext.tsx b/packages/grafana-ui/src/themes/ThemeContext.tsx index ab89fb3b285..5e308ec73e4 100644 --- a/packages/grafana-ui/src/themes/ThemeContext.tsx +++ b/packages/grafana-ui/src/themes/ThemeContext.tsx @@ -4,6 +4,7 @@ import hoistNonReactStatics from 'hoist-non-react-statics'; import { getTheme } from './getTheme'; import { Themeable } from '../types/theme'; import { GrafanaTheme, GrafanaThemeType } from '@grafana/data'; +import { stylesFactory } from './stylesFactory'; type Omit = Pick>; type Subtract = Omit; @@ -37,6 +38,12 @@ export const withTheme =

(Component: Rea export function useTheme() { return useContext(ThemeContextMock || ThemeContext); } +/** Hook for using memoized styles with access to the theme. */ +export const useStyles = (getStyles: (theme?: GrafanaTheme) => any) => { + const currentTheme = useTheme(); + const callback = stylesFactory(stylesTheme => getStyles(stylesTheme)); + return callback(currentTheme); +}; /** * Enables theme context mocking diff --git a/packages/grafana-ui/src/themes/index.ts b/packages/grafana-ui/src/themes/index.ts index d1e0ea0295f..77f86a763fe 100644 --- a/packages/grafana-ui/src/themes/index.ts +++ b/packages/grafana-ui/src/themes/index.ts @@ -1,8 +1,8 @@ -import { ThemeContext, withTheme, useTheme, mockThemeContext } from './ThemeContext'; +import { ThemeContext, withTheme, useTheme, useStyles, mockThemeContext } from './ThemeContext'; import { getTheme, mockTheme } from './getTheme'; import { selectThemeVariant } from './selectThemeVariant'; export { stylesFactory } from './stylesFactory'; -export { ThemeContext, withTheme, mockTheme, getTheme, selectThemeVariant, useTheme, mockThemeContext }; +export { ThemeContext, withTheme, mockTheme, getTheme, selectThemeVariant, useTheme, mockThemeContext, useStyles }; import * as styleMixins from './mixins'; export { styleMixins }; diff --git a/public/app/core/components/Branding/Branding.tsx b/public/app/core/components/Branding/Branding.tsx index aa41fbc5025..deb119b2398 100644 --- a/public/app/core/components/Branding/Branding.tsx +++ b/public/app/core/components/Branding/Branding.tsx @@ -1,42 +1,53 @@ import React, { FC } from 'react'; import { css, cx } from 'emotion'; +import { useTheme } from '@grafana/ui'; export interface BrandComponentProps { className?: string; children?: JSX.Element | JSX.Element[]; } -export const LoginLogo: FC = ({ className }) => { - const maxSize = css` - max-width: 150px; - `; - - return ( - <> - Grafana -

- - ); +const LoginLogo: FC = ({ className }) => { + return Grafana; }; -export const LoginBackground: FC = ({ className, children }) => { +const LoginBackground: FC = ({ className, children }) => { + const theme = useTheme(); const background = css` - background: url(public/img/heatmap_bg_test.svg); + background: url(public/img/login_background_${theme.isDark ? 'dark' : 'light'}.svg); background-size: cover; `; return
{children}
; }; -export const MenuLogo: FC = ({ className }) => { +const MenuLogo: FC = ({ className }) => { return Grafana; }; -export const AppTitle = 'Grafana'; +const LoginBoxBackground = () => { + const theme = useTheme(); + return css` + background: ${theme.isLight ? 'rgba(6, 30, 200, 0.1 )' : 'rgba(18, 28, 41, 0.65)'}; + background-size: cover; + `; +}; export class Branding { static LoginLogo = LoginLogo; static LoginBackground = LoginBackground; static MenuLogo = MenuLogo; - static AppTitle = AppTitle; + static LoginBoxBackground = LoginBoxBackground; + static AppTitle = 'Grafana'; + static LoginTitle = 'Welcome to Grafana'; + static GetLoginSubTitle = () => { + const slogans = [ + "Don't get in the way of the data", + 'Your single pane of glass', + 'Built better together', + 'Democratising data', + ]; + const count = slogans.length; + return slogans[Math.floor(Math.random() * count)]; + }; } diff --git a/public/app/core/components/Login/ChangePassword.tsx b/public/app/core/components/Login/ChangePassword.tsx index d1d5acd5589..47cac53f63a 100644 --- a/public/app/core/components/Login/ChangePassword.tsx +++ b/public/app/core/components/Login/ChangePassword.tsx @@ -1,138 +1,60 @@ -import React, { ChangeEvent, PureComponent, SyntheticEvent } from 'react'; -import { Tooltip } from '@grafana/ui'; -import { AppEvents } from '@grafana/data'; - -import appEvents from 'app/core/app_events'; +import React, { FC, SyntheticEvent } from 'react'; +import { Tooltip, Form, Field, Input, VerticalGroup, Button, LinkButton } from '@grafana/ui'; import { selectors } from '@grafana/e2e-selectors'; - +import { submitButton } from './LoginForm'; interface Props { onSubmit: (pw: string) => void; - onSkip: Function; - focus?: boolean; + onSkip: (event?: SyntheticEvent) => void; } -interface State { +interface PasswordDTO { newPassword: string; confirmNew: string; - valid: boolean; } -export class ChangePassword extends PureComponent { - private userInput: HTMLInputElement; - constructor(props: Props) { - super(props); - this.state = { - newPassword: '', - confirmNew: '', - valid: false, - }; - } - - componentDidUpdate(prevProps: Props) { - if (!prevProps.focus && this.props.focus) { - this.focus(); - } - } - - focus() { - this.userInput.focus(); - } - - onSubmit = (e: SyntheticEvent) => { - e.preventDefault(); - - const { newPassword, valid } = this.state; - if (valid) { - this.props.onSubmit(newPassword); - } else { - appEvents.emit(AppEvents.alertWarning, ['New passwords do not match']); - } - }; - - onNewPasswordChange = (e: ChangeEvent) => { - this.setState({ - newPassword: e.target.value, - valid: this.validate('newPassword', e.target.value), - }); - }; - - onConfirmPasswordChange = (e: ChangeEvent) => { - this.setState({ - confirmNew: e.target.value, - valid: this.validate('confirmNew', e.target.value), - }); +export const ChangePassword: FC = ({ onSubmit, onSkip }) => { + const submit = (passwords: PasswordDTO) => { + onSubmit(passwords.newPassword); }; - - onSkip = (e: SyntheticEvent) => { - this.props.onSkip(); - }; - - validate(changed: string, pw: string) { - if (changed === 'newPassword') { - return this.state.confirmNew === pw; - } else if (changed === 'confirmNew') { - return this.state.newPassword === pw; - } - return false; - } - - render() { - return ( -
-
-
Change Password
- Before you can get started with awesome dashboards we need you to make your account more secure by changing - your password. -
- You can change your password again later. -
- -
- + {({ errors, register, getValues }) => ( + <> + + { - this.userInput = input; - }} + ref={register({ + required: 'New password required', + })} /> -
-
- + + v === getValues().newPassword || 'Passwords must match!', + })} /> -
-
+ + + - + Skip - + - - -
- -
- ); - } -} + + + )} + + ); +}; diff --git a/public/app/core/components/Login/LoginForm.tsx b/public/app/core/components/Login/LoginForm.tsx index 1ef5cde8ba4..c21c0a1c93f 100644 --- a/public/app/core/components/Login/LoginForm.tsx +++ b/public/app/core/components/Login/LoginForm.tsx @@ -1,122 +1,69 @@ -import React, { ChangeEvent, PureComponent, SyntheticEvent } from 'react'; +import React, { FC } from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { FormModel } from './LoginCtrl'; +import { Button, Form, Input, Field } from '@grafana/ui'; +import { css } from 'emotion'; interface Props { displayForgotPassword: boolean; - onChange?: (valid: boolean) => void; onSubmit: (data: FormModel) => void; isLoggingIn: boolean; passwordHint: string; loginHint: string; } -interface State { - user: string; - password: string; - email: string; - valid: boolean; -} - -export class LoginForm extends PureComponent { - private userInput: HTMLInputElement; - constructor(props: Props) { - super(props); - this.state = { - user: '', - password: '', - email: '', - valid: false, - }; - } - - componentDidMount() { - this.userInput.focus(); - } - onSubmit = (e: SyntheticEvent) => { - e.preventDefault(); - - const { user, password, email } = this.state; - if (this.state.valid) { - this.props.onSubmit({ user, password, email }); - } - }; +const forgottenPasswordStyles = css` + display: inline-block; + margin-top: 16px; + float: right; +`; - onChangePassword = (e: ChangeEvent) => { - this.setState({ - password: e.target.value, - valid: this.validate(this.state.user, e.target.value), - }); - }; +const wrapperStyles = css` + width: 100%; + padding-bottom: 16px; +`; - onChangeUsername = (e: ChangeEvent) => { - this.setState({ - user: e.target.value, - valid: this.validate(e.target.value, this.state.password), - }); - }; +export const submitButton = css` + justify-content: center; + width: 100%; +`; - validate(user: string, password: string) { - return user.length > 0 && password.length > 0; - } - - render() { - return ( -
-
- { - this.userInput = input; - }} - type="text" - name="user" - className="gf-form-input login-form-input" - required - placeholder={this.props.loginHint} - aria-label={selectors.pages.Login.username} - onChange={this.onChangeUsername} - /> -
-
- -
-
- {!this.props.isLoggingIn ? ( - - ) : ( - - )} - - {this.props.displayForgotPassword ? ( - - ) : null} -
-
- ); - } -} +export const LoginForm: FC = ({ displayForgotPassword, onSubmit, isLoggingIn, passwordHint, loginHint }) => { + return ( +
+
+ {({ register, errors }) => ( + <> + + + + + + + + {displayForgotPassword && ( + + Forgot your password? + + )} + + )} +
+
+ ); +}; diff --git a/public/app/core/components/Login/LoginPage.tsx b/public/app/core/components/Login/LoginPage.tsx index f051825ef45..cafc12f96fd 100644 --- a/public/app/core/components/Login/LoginPage.tsx +++ b/public/app/core/components/Login/LoginPage.tsx @@ -1,6 +1,6 @@ // Libraries import React, { FC } from 'react'; -import { CSSTransition } from 'react-transition-group'; +import { cx, keyframes, css } from 'emotion'; // Components import { UserSignup } from './UserSignup'; @@ -9,20 +9,25 @@ import LoginCtrl from './LoginCtrl'; import { LoginForm } from './LoginForm'; import { ChangePassword } from './ChangePassword'; import { Branding } from 'app/core/components/Branding/Branding'; -import { Footer } from 'app/core/components/Footer/Footer'; +import { useStyles } from '@grafana/ui'; +import { GrafanaTheme } from '@grafana/data'; export const LoginPage: FC = () => { + const loginStyles = useStyles(getLoginStyles); return ( - -
-
- + +
+
+ +
+

{Branding.LoginTitle}

+

{Branding.GetLoginSubTitle()}

+
{({ loginHint, passwordHint, - isOauthEnabled, ldapEnabled, authProxyEnabled, disableLoginForm, @@ -33,37 +38,123 @@ export const LoginPage: FC = () => { skipPasswordChange, isChangingPassword, }) => ( -
-
- {!disableLoginForm ? ( - - ) : null} +
+ {!isChangingPassword && ( +
+ {!disableLoginForm && ( + + )} - - {!disableUserSignUp ? : null} -
- - - + + {!disableUserSignUp && } +
+ )} + + {isChangingPassword && ( +
+ +
+ )}
)}
-