mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
LoginPage: New design (#23892)
* LoginPage: initial poc * wIP * Prgress * Start Forms migration * Fix layout and change password animation * Migrate style to emotion * Fix small things * Remove classes * Fix logo and title * Disable disabled button * Add custom fields and fix layout * Update flyin animation * Change animation timing * Update comment * Same styles for submit button * Update snapshot * Minor tweaks and made slogan random Co-authored-by: Tobias Skarhed <tobias.skarhed@gmail.com>
This commit is contained in:
@@ -38,6 +38,7 @@ export function Form<T>({
|
||||
<form
|
||||
className={css`
|
||||
max-width: ${maxWidth}px;
|
||||
width: 100%;
|
||||
`}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
|
||||
@@ -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<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||
type Subtract<T, K> = Omit<T, keyof K>;
|
||||
@@ -37,6 +38,12 @@ export const withTheme = <P extends Themeable, S extends {} = {}>(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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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<BrandComponentProps> = ({ className }) => {
|
||||
const maxSize = css`
|
||||
max-width: 150px;
|
||||
`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<img className={cx(className, maxSize)} src="public/img/grafana_icon.svg" alt="Grafana" />
|
||||
<div className="logo-wordmark" />
|
||||
</>
|
||||
);
|
||||
const LoginLogo: FC<BrandComponentProps> = ({ className }) => {
|
||||
return <img className={className} src="public/img/grafana_icon.svg" alt="Grafana" />;
|
||||
};
|
||||
|
||||
export const LoginBackground: FC<BrandComponentProps> = ({ className, children }) => {
|
||||
const LoginBackground: FC<BrandComponentProps> = ({ 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 <div className={cx(background, className)}>{children}</div>;
|
||||
};
|
||||
|
||||
export const MenuLogo: FC<BrandComponentProps> = ({ className }) => {
|
||||
const MenuLogo: FC<BrandComponentProps> = ({ className }) => {
|
||||
return <img className={className} src="public/img/grafana_icon.svg" alt="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)];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<Props, State> {
|
||||
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']);
|
||||
}
|
||||
export const ChangePassword: FC<Props> = ({ onSubmit, onSkip }) => {
|
||||
const submit = (passwords: PasswordDTO) => {
|
||||
onSubmit(passwords.newPassword);
|
||||
};
|
||||
|
||||
onNewPasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
newPassword: e.target.value,
|
||||
valid: this.validate('newPassword', e.target.value),
|
||||
});
|
||||
};
|
||||
|
||||
onConfirmPasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
confirmNew: e.target.value,
|
||||
valid: this.validate('confirmNew', e.target.value),
|
||||
});
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="login-inner-box" id="change-password-view">
|
||||
<div className="text-left login-change-password-info">
|
||||
<h5>Change Password</h5>
|
||||
Before you can get started with awesome dashboards we need you to make your account more secure by changing
|
||||
your password.
|
||||
<br />
|
||||
You can change your password again later.
|
||||
</div>
|
||||
<form className="login-form-group gf-form-group">
|
||||
<div className="login-form">
|
||||
<input
|
||||
return (
|
||||
<Form onSubmit={submit}>
|
||||
{({ errors, register, getValues }) => (
|
||||
<>
|
||||
<Field label="New password" invalid={!!errors.newPassword} error={errors?.newPassword?.message}>
|
||||
<Input
|
||||
autoFocus
|
||||
type="password"
|
||||
id="newPassword"
|
||||
name="newPassword"
|
||||
className="gf-form-input login-form-input"
|
||||
required
|
||||
placeholder="New password"
|
||||
onChange={this.onNewPasswordChange}
|
||||
ref={input => {
|
||||
this.userInput = input;
|
||||
}}
|
||||
ref={register({
|
||||
required: 'New password required',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="login-form">
|
||||
<input
|
||||
</Field>
|
||||
<Field label="Confirm new password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}>
|
||||
<Input
|
||||
type="password"
|
||||
name="confirmNew"
|
||||
className="gf-form-input login-form-input"
|
||||
required
|
||||
ng-model="command.confirmNew"
|
||||
placeholder="Confirm new password"
|
||||
onChange={this.onConfirmPasswordChange}
|
||||
ref={register({
|
||||
required: 'Confirmed password is required',
|
||||
validate: v => v === getValues().newPassword || 'Passwords must match!',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="login-button-group login-button-group--right text-right">
|
||||
</Field>
|
||||
<VerticalGroup>
|
||||
<Button type="submit" className={submitButton}>
|
||||
Submit
|
||||
</Button>
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
content="If you skip you will be prompted to change password next time you login."
|
||||
placement="bottom"
|
||||
>
|
||||
<a className="btn btn-link" onClick={this.onSkip} aria-label={selectors.pages.Login.skip}>
|
||||
<LinkButton variant="link" onClick={onSkip} aria-label={selectors.pages.Login.skip}>
|
||||
Skip
|
||||
</a>
|
||||
</LinkButton>
|
||||
</Tooltip>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className={`btn btn-large p-x-2 ${this.state.valid ? 'btn-primary' : 'btn-inverse'}`}
|
||||
onClick={this.onSubmit}
|
||||
disabled={!this.state.valid}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</VerticalGroup>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
const forgottenPasswordStyles = css`
|
||||
display: inline-block;
|
||||
margin-top: 16px;
|
||||
float: right;
|
||||
`;
|
||||
|
||||
export class LoginForm extends PureComponent<Props, State> {
|
||||
private userInput: HTMLInputElement;
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
user: '',
|
||||
password: '',
|
||||
email: '',
|
||||
valid: false,
|
||||
};
|
||||
}
|
||||
const wrapperStyles = css`
|
||||
width: 100%;
|
||||
padding-bottom: 16px;
|
||||
`;
|
||||
|
||||
componentDidMount() {
|
||||
this.userInput.focus();
|
||||
}
|
||||
onSubmit = (e: SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
export const submitButton = css`
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const { user, password, email } = this.state;
|
||||
if (this.state.valid) {
|
||||
this.props.onSubmit({ user, password, email });
|
||||
}
|
||||
};
|
||||
|
||||
onChangePassword = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
password: e.target.value,
|
||||
valid: this.validate(this.state.user, e.target.value),
|
||||
});
|
||||
};
|
||||
|
||||
onChangeUsername = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
user: e.target.value,
|
||||
valid: this.validate(e.target.value, this.state.password),
|
||||
});
|
||||
};
|
||||
|
||||
validate(user: string, password: string) {
|
||||
return user.length > 0 && password.length > 0;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<form name="loginForm" className="login-form-group gf-form-group">
|
||||
<div className="login-form">
|
||||
<input
|
||||
ref={input => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
<div className="login-form">
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
className="gf-form-input login-form-input"
|
||||
required
|
||||
ng-model="formModel.password"
|
||||
id="inputPassword"
|
||||
placeholder={this.props.passwordHint}
|
||||
aria-label={selectors.pages.Login.password}
|
||||
onChange={this.onChangePassword}
|
||||
/>
|
||||
</div>
|
||||
<div className="login-button-group">
|
||||
{!this.props.isLoggingIn ? (
|
||||
<button
|
||||
type="submit"
|
||||
aria-label={selectors.pages.Login.submit}
|
||||
className={`btn btn-large p-x-2 ${this.state.valid ? 'btn-primary' : 'btn-inverse'}`}
|
||||
onClick={this.onSubmit}
|
||||
disabled={!this.state.valid}
|
||||
>
|
||||
Log In
|
||||
</button>
|
||||
) : (
|
||||
<button type="submit" disabled className="btn btn-large p-x-2 btn-inverse btn-loading">
|
||||
Logging In<span>.</span>
|
||||
<span>.</span>
|
||||
<span>.</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{this.props.displayForgotPassword ? (
|
||||
<div className="small login-button-forgot-password">
|
||||
<a href="user/password/send-reset-email">Forgot your password?</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
export const LoginForm: FC<Props> = ({ displayForgotPassword, onSubmit, isLoggingIn, passwordHint, loginHint }) => {
|
||||
return (
|
||||
<div className={wrapperStyles}>
|
||||
<Form onSubmit={onSubmit} validateOn="onChange">
|
||||
{({ register, errors }) => (
|
||||
<>
|
||||
<Field label="Email or username" invalid={!!errors.user} error={errors.user?.message}>
|
||||
<Input
|
||||
autoFocus
|
||||
name="user"
|
||||
ref={register({ required: 'Email or username is required' })}
|
||||
placeholder={loginHint}
|
||||
aria-label={selectors.pages.Login.username}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Password" invalid={!!errors.password} error={errors.password?.message}>
|
||||
<Input
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder={passwordHint}
|
||||
ref={register({ required: 'Password is requireed' })}
|
||||
aria-label={selectors.pages.Login.password}
|
||||
/>
|
||||
</Field>
|
||||
<Button aria-label={selectors.pages.Login.submit} className={submitButton} disabled={isLoggingIn}>
|
||||
{isLoggingIn ? 'Logging in...' : 'Log in'}
|
||||
</Button>
|
||||
{displayForgotPassword && (
|
||||
<a className={forgottenPasswordStyles} href="user/password/send-reset-email">
|
||||
Forgot your password?
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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.LoginBackground className="login container">
|
||||
<div className="login-content">
|
||||
<div className="login-branding">
|
||||
<Branding.LoginLogo className="login-logo" />
|
||||
<Branding.LoginBackground className={loginStyles.container}>
|
||||
<div className={cx(loginStyles.loginContent, Branding.LoginBoxBackground())}>
|
||||
<div className={loginStyles.loginLogoWrapper}>
|
||||
<Branding.LoginLogo className={loginStyles.loginLogo} />
|
||||
<div className={loginStyles.titleWrapper}>
|
||||
<h1 className={loginStyles.mainTitle}>{Branding.LoginTitle}</h1>
|
||||
<h3 className={loginStyles.subTitle}>{Branding.GetLoginSubTitle()}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<LoginCtrl>
|
||||
{({
|
||||
loginHint,
|
||||
passwordHint,
|
||||
isOauthEnabled,
|
||||
ldapEnabled,
|
||||
authProxyEnabled,
|
||||
disableLoginForm,
|
||||
@@ -33,37 +38,123 @@ export const LoginPage: FC = () => {
|
||||
skipPasswordChange,
|
||||
isChangingPassword,
|
||||
}) => (
|
||||
<div className="login-outer-box">
|
||||
<div className={`login-inner-box ${isChangingPassword ? 'hidden' : ''}`} id="login-view">
|
||||
{!disableLoginForm ? (
|
||||
<LoginForm
|
||||
displayForgotPassword={!(ldapEnabled || authProxyEnabled)}
|
||||
onSubmit={login}
|
||||
loginHint={loginHint}
|
||||
passwordHint={passwordHint}
|
||||
isLoggingIn={isLoggingIn}
|
||||
/>
|
||||
) : null}
|
||||
<div className={loginStyles.loginOuterBox}>
|
||||
{!isChangingPassword && (
|
||||
<div className={`${loginStyles.loginInnerBox} ${isChangingPassword ? 'hidden' : ''}`} id="login-view">
|
||||
{!disableLoginForm && (
|
||||
<LoginForm
|
||||
displayForgotPassword={!(ldapEnabled || authProxyEnabled)}
|
||||
onSubmit={login}
|
||||
loginHint={loginHint}
|
||||
passwordHint={passwordHint}
|
||||
isLoggingIn={isLoggingIn}
|
||||
/>
|
||||
)}
|
||||
|
||||
<LoginServiceButtons />
|
||||
{!disableUserSignUp ? <UserSignup /> : null}
|
||||
</div>
|
||||
<CSSTransition
|
||||
appear={true}
|
||||
mountOnEnter={true}
|
||||
in={isChangingPassword}
|
||||
timeout={250}
|
||||
classNames="login-inner-box"
|
||||
>
|
||||
<ChangePassword onSubmit={changePassword} onSkip={skipPasswordChange} focus={isChangingPassword} />
|
||||
</CSSTransition>
|
||||
<LoginServiceButtons />
|
||||
{!disableUserSignUp && <UserSignup />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isChangingPassword && (
|
||||
<div className={cx(loginStyles.loginInnerBox, loginStyles.enterAnimation)}>
|
||||
<ChangePassword onSubmit={changePassword} onSkip={skipPasswordChange as any} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</LoginCtrl>
|
||||
|
||||
<div className="clearfix" />
|
||||
</div>
|
||||
<Footer />
|
||||
</Branding.LoginBackground>
|
||||
);
|
||||
};
|
||||
|
||||
const flyInAnimation = keyframes`
|
||||
from{
|
||||
transform: translate(-400px, 0px);
|
||||
}
|
||||
|
||||
to{
|
||||
transform: translate(0px, 0px);
|
||||
}`;
|
||||
|
||||
export const getLoginStyles = (theme: GrafanaTheme) => {
|
||||
return {
|
||||
container: css`
|
||||
min-height: 100vh;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
min-width: 100%;
|
||||
margin-left: 0;
|
||||
background-color: $black;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`,
|
||||
submitButton: css`
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`,
|
||||
loginLogo: css`
|
||||
width: 100%;
|
||||
max-width: 100px;
|
||||
margin-bottom: 15px;
|
||||
`,
|
||||
loginLogoWrapper: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: ${theme.spacing.lg};
|
||||
`,
|
||||
titleWrapper: css`
|
||||
text-align: center;
|
||||
`,
|
||||
mainTitle: css`
|
||||
font-size: '32px';
|
||||
`,
|
||||
subTitle: css`
|
||||
font-size: ${theme.typography.size.md};
|
||||
color: ${theme.colors.textSemiWeak};
|
||||
`,
|
||||
loginContent: css`
|
||||
max-width: 550px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
min-height: 320px;
|
||||
border-radius: 3px;
|
||||
padding: 20px 0;
|
||||
`,
|
||||
loginOuterBox: css`
|
||||
display: flex;
|
||||
overflow-y: hidden;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`,
|
||||
loginInnerBox: css`
|
||||
padding: ${theme.spacing.xl};
|
||||
@media (max-width: 320px) {
|
||||
padding: ${theme.spacing.lg};
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
max-width: 415px;
|
||||
width: 100%;
|
||||
transform: translate(0px, 0px);
|
||||
transition: 0.25s ease;
|
||||
`,
|
||||
enterAnimation: css`
|
||||
animation: ${flyInAnimation} ease-out 0.2s;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React from 'react';
|
||||
import config from 'app/core/config';
|
||||
import { css } from 'emotion';
|
||||
import { useStyles } from '@grafana/ui';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
|
||||
const loginServices: () => LoginServices = () => {
|
||||
const oauthEnabled = !!config.oauth;
|
||||
@@ -58,18 +61,49 @@ export interface LoginServices {
|
||||
[key: string]: LoginService;
|
||||
}
|
||||
|
||||
const getServiceStyles = (theme: GrafanaTheme) => {
|
||||
return {
|
||||
container: css`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
`,
|
||||
button: css`
|
||||
color: #d8d9da;
|
||||
margin: 0 0 ${theme.spacing.md};
|
||||
width: 100%;
|
||||
`,
|
||||
divider: {
|
||||
base: css`
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin: 0 25% ${theme.spacing.md} 25%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
text-align: center;
|
||||
color: ${theme.colors.text};
|
||||
`,
|
||||
line: css`
|
||||
width: 100px;
|
||||
height: 10px;
|
||||
border-bottom: 1px solid ${theme.colors.text};
|
||||
`,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const LoginDivider = () => {
|
||||
const styles = useStyles(getServiceStyles);
|
||||
return (
|
||||
<>
|
||||
<div className="text-center login-divider">
|
||||
<div className={styles.divider.base}>
|
||||
<div>
|
||||
<div className="login-divider-line" />
|
||||
<div className={styles.divider.line} />
|
||||
</div>
|
||||
<div>
|
||||
<span className="login-divider-text">{config.disableLoginForm ? null : <span>or</span>}</span>
|
||||
<span>{!config.disableLoginForm && <span>or</span>}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="login-divider-line" />
|
||||
<div className={styles.divider.line} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="clearfix" />
|
||||
@@ -78,6 +112,7 @@ const LoginDivider = () => {
|
||||
};
|
||||
|
||||
export const LoginServiceButtons = () => {
|
||||
const styles = useStyles(getServiceStyles);
|
||||
const keyNames = Object.keys(loginServices());
|
||||
const serviceElementsEnabled = keyNames.filter(key => {
|
||||
const service: LoginService = loginServices()[key];
|
||||
@@ -93,7 +128,7 @@ export const LoginServiceButtons = () => {
|
||||
return (
|
||||
<a
|
||||
key={key}
|
||||
className={`btn btn-medium btn-service btn-service--${service.className || key} login-btn`}
|
||||
className={`${styles.button} btn btn-medium btn-service btn-service--${service.className || key}`}
|
||||
href={`login/${service.hrefName ? service.hrefName : key}`}
|
||||
target="_self"
|
||||
>
|
||||
@@ -107,7 +142,7 @@ export const LoginServiceButtons = () => {
|
||||
return (
|
||||
<>
|
||||
{divider}
|
||||
<div className="login-oauth text-center">{serviceElements}</div>
|
||||
<div className={styles.container}>{serviceElements}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { FC } from 'react';
|
||||
import { LinkButton, HorizontalGroup } from '@grafana/ui';
|
||||
|
||||
export const UserSignup: FC<{}> = () => {
|
||||
return (
|
||||
<div className="login-signup-box">
|
||||
<div className="login-signup-title p-r-1">New to Grafana?</div>
|
||||
<a href="signup" className="btn btn-medium btn-signup btn-p-x-2">
|
||||
<HorizontalGroup justify="flex-start">
|
||||
<LinkButton href="signup" variant="secondary">
|
||||
Sign Up
|
||||
</a>
|
||||
</div>
|
||||
</LinkButton>
|
||||
<span>New to Grafana?</span>
|
||||
</HorizontalGroup>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ Array [
|
||||
href="/"
|
||||
key="logo"
|
||||
>
|
||||
<Component />
|
||||
<MenuLogo />
|
||||
</a>,
|
||||
<div
|
||||
className="sidemenu__logo_small_breakpoint"
|
||||
|
||||
6465
public/img/login_background_dark.svg
Normal file
6465
public/img/login_background_dark.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 483 KiB |
6774
public/img/login_background_light.svg
Normal file
6774
public/img/login_background_light.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 502 KiB |
@@ -1,398 +1,5 @@
|
||||
$login-border: #8daac5;
|
||||
|
||||
.login {
|
||||
min-height: 100vh;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
min-width: 100%;
|
||||
margin-left: 0;
|
||||
background-color: $black;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #d8d9da;
|
||||
|
||||
& a {
|
||||
color: #d8d9da !important;
|
||||
}
|
||||
|
||||
& .btn-primary {
|
||||
@include buttonBackground(#ff6600, #bc3e06);
|
||||
}
|
||||
}
|
||||
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill,
|
||||
textarea:-webkit-autofill,
|
||||
textarea:-webkit-autofill:hover,
|
||||
textarea:-webkit-autofill:focus,
|
||||
select:-webkit-autofill,
|
||||
select:-webkit-autofill:hover,
|
||||
select:-webkit-autofill:focus {
|
||||
-webkit-box-shadow: 0 0 0px 1000px $input-bg inset !important;
|
||||
-webkit-text-fill-color: $input-color !important;
|
||||
box-shadow: 0 0 0px 1000px $input-bg inset;
|
||||
}
|
||||
|
||||
.login-form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
margin-bottom: $space-md;
|
||||
}
|
||||
|
||||
/* Is still used in some angular templates*/
|
||||
.login-form {
|
||||
margin-bottom: $space-md;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-form-input {
|
||||
border: 1px solid $login-border;
|
||||
border-radius: 4px;
|
||||
opacity: 0.6;
|
||||
background: $black;
|
||||
color: #fbfbfb;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid $login-border;
|
||||
}
|
||||
}
|
||||
|
||||
.login-button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: $space-sm;
|
||||
|
||||
&--right {
|
||||
justify-content: flex-end;
|
||||
|
||||
& .btn {
|
||||
margin-left: $space-md;
|
||||
}
|
||||
}
|
||||
& .btn-inverse {
|
||||
color: #e3e3e3;
|
||||
text-shadow: 0px 1px 0 rgba(0, 0, 0, 0.1);
|
||||
background-color: #2a2a2c;
|
||||
background-image: linear-gradient(to bottom, #262628, #303032);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #262628;
|
||||
box-shadow: -1px -1px 0 0 rgba(255, 255, 255, 0.1), 1px 1px 0 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.login-button-forgot-password {
|
||||
padding-top: $space-md;
|
||||
}
|
||||
|
||||
.login-text {
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
.login-content {
|
||||
max-width: 700px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.login-branding {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 0;
|
||||
padding: $space-xl;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.app-grafana {
|
||||
.logo-wordmark {
|
||||
background: url('../img/grafana_typelogo.svg') top center no-repeat;
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-enterprise {
|
||||
.logo-wordmark {
|
||||
background: url('../img/grafana_enterprise_typelogo.svg') top center no-repeat;
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-outer-box {
|
||||
display: flex;
|
||||
overflow-y: hidden;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-inner-box {
|
||||
text-align: center;
|
||||
padding: $space-xl;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
max-width: 415px;
|
||||
width: 100%;
|
||||
transform: tranlate(0px, 0px);
|
||||
transition: 0.25s ease;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-enter {
|
||||
transform: translate(0px, 320px);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&-enter-active {
|
||||
transform: translate(0px, 0px);
|
||||
}
|
||||
}
|
||||
|
||||
.login-change-password-info {
|
||||
padding-bottom: $space-lg;
|
||||
|
||||
& h5 {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
.btn-signup {
|
||||
color: $white;
|
||||
border: 1px solid $login-border;
|
||||
background-color: $btn-semi-transparent;
|
||||
}
|
||||
|
||||
.login-submit-button-row {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
button {
|
||||
padding: 14px 23px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
min-width: 150px;
|
||||
display: inline-block;
|
||||
border: 1px solid lighten($btn-inverse-bg, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.login-oauth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-divider {
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin: 0 25% $space-md 25%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.login-divider-line {
|
||||
width: 100px;
|
||||
height: 10px;
|
||||
border-bottom: 1px solid $login-border;
|
||||
|
||||
.login-divider-text {
|
||||
background-color: $panel-bg;
|
||||
color: $gray-2;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-signup-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
margin-top: $space-md;
|
||||
}
|
||||
|
||||
.login-signup-title {
|
||||
justify-self: flex-start;
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
margin: 0 0 $space-md;
|
||||
}
|
||||
|
||||
.signup-page-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-image: url(../img/background_tease.jpg);
|
||||
opacity: 0.3;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.invite-box {
|
||||
text-align: center;
|
||||
border: 1px solid $tight-form-func-bg;
|
||||
background-color: $panel-bg;
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
.tight-form {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
float: right;
|
||||
font-size: 140%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.modal-tagline {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
.login-branding {
|
||||
padding: $space-md;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
.login-content {
|
||||
flex-direction: row;
|
||||
flex: 1 0 100%;
|
||||
}
|
||||
|
||||
.login-divider {
|
||||
.login-divider-line {
|
||||
width: 110px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-branding {
|
||||
width: 45%;
|
||||
padding: $space-xl;
|
||||
flex-grow: 1;
|
||||
border-right: 1px solid $login-border;
|
||||
}
|
||||
|
||||
.login-button-group {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.login-inner-box {
|
||||
width: 55%;
|
||||
padding: $space-md 56px;
|
||||
}
|
||||
|
||||
.login-button-forgot-password {
|
||||
padding-top: 0;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.login-form-input {
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
perspective: 1000px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
z-index: 0;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
justify-items: stretch;
|
||||
height: 100%;
|
||||
|
||||
.login-bg__row {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
height: 10px;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.login-bg__item {
|
||||
width: 4%;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
// background: hotpink;
|
||||
// border:1px solid #0F1926;
|
||||
transition: 1s ease-in-out;
|
||||
z-index: 1;
|
||||
transform-style: preserve-3d;
|
||||
|
||||
&.login-bg-flip {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
backface-visibility: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
content: '';
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:after {
|
||||
transform: rotateY(180deg);
|
||||
background-color: rgb(25, 50, 80);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-bg-fx {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
background-image: -webkit-radial-gradient(
|
||||
center center,
|
||||
ellipse farthest-corner,
|
||||
transparent 0%,
|
||||
transparent 10%,
|
||||
rgba(18, 22, 29, 1) 100%
|
||||
);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user