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>
pull/24254/head
Torkel Ödegaard 5 years ago committed by GitHub
parent 3487e518ab
commit 726009870b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      packages/grafana-ui/src/components/Forms/Form.tsx
  2. 7
      packages/grafana-ui/src/themes/ThemeContext.tsx
  3. 4
      packages/grafana-ui/src/themes/index.ts
  4. 43
      public/app/core/components/Branding/Branding.tsx
  5. 156
      public/app/core/components/Login/ChangePassword.tsx
  6. 161
      public/app/core/components/Login/LoginForm.tsx
  7. 153
      public/app/core/components/Login/LoginPage.tsx
  8. 47
      public/app/core/components/Login/LoginServiceButtons.tsx
  9. 11
      public/app/core/components/Login/UserSignup.tsx
  10. 2
      public/app/core/components/sidemenu/__snapshots__/SideMenu.test.tsx.snap
  11. 6465
      public/img/login_background_dark.svg
  12. 6774
      public/img/login_background_light.svg
  13. 395
      public/sass/pages/_login.scss

@ -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']);
}
};
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),
});
export const ChangePassword: FC<Props> = ({ 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 (
<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;
}
export class LoginForm extends PureComponent<Props, State> {
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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 (
<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"

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 483 KiB

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;
}

Loading…
Cancel
Save