Skip to content
Snippets Groups Projects
Unverified Commit 392ceddf authored by Kyle Doherty's avatar Kyle Doherty Committed by GitHub
Browse files

Standardize form presentation (#10673)

* clean up Form-input hover state

* standardize on one form style, remove old styles from use

* update snapshot test

* fix forgot password position

* fix form error css

* fix padding on setup flow to account for offset removal

* fix layout on admin database add/edit

* increase admin db form padding

* cleanup scheduler

* fix placeholder text

* tweak label and field weight and border contrast

* error message separator

* match select style in Form-field to other fields

* fix admin / database edit form width

* use updated input style on admin/settings

* clean up whitespace in database details form
parent 444ae3c2
Branches
Tags
No related merge requests found
Showing
with 100 additions and 279 deletions
......@@ -30,10 +30,10 @@ export default class DatabaseEditForms extends Component {
<div
className={cx("Form-field", { "Form--fieldError": errors["engine"] })}
>
<label className="Form-label Form-offset">
<label className="Form-label">
Database type: <span>{errors["engine"]}</span>
</label>
<label className="Select Form-offset mt1">
<label className="Select mt1">
<select
className="Select"
defaultValue={database.engine}
......
......@@ -37,7 +37,7 @@ export const SyncOption = ({ selected, name, children, select }) => (
/>
)}
</div>
<div className="Form-offset ml1">
<div className="ml4 pl2">
<div className={cx({ "text-brand": selected })}>
<h3>{name}</h3>
</div>
......@@ -116,7 +116,7 @@ export default class DatabaseSchedulingForm extends Component {
<LoadingAndErrorWrapper loading={!this.props.database} error={null}>
{() => (
<form onSubmit={this.onSubmitForm} noValidate>
<div className="Form-offset mr4 mt4">
<div className="mr4 mt4">
<div style={{ maxWidth: 600 }} className="border-bottom pb2">
<p className="text-paragraph text-measure">
{t`To do some of its magic, Metabase needs to scan your database. We will also rescan it periodically to keep the metadata up-to-date. You can control when the periodic rescans happen below.`}
......
......@@ -5,6 +5,7 @@ import PropTypes from "prop-types";
import { connect } from "react-redux";
import title from "metabase/hoc/Title";
import { t } from "ttag";
import { Box, Flex } from "grid-styled";
import MetabaseSettings from "metabase/lib/settings";
import DeleteDatabaseModal from "../components/DeleteDatabaseModal";
......@@ -132,11 +133,11 @@ export default class DatabaseEditApp extends Component {
[addingNewDatabase ? t`Add Database` : database.name],
]}
/>
<section className="Grid Grid--gutters Grid--2-of-3">
<div className="Grid-cell">
<div className="Form-new bordered rounded shadowed pt0">
<Flex pb={2}>
<Box>
<div className="pt0">
{showTabs && (
<div className="Form-offset border-bottom">
<div className="border-bottom">
<Radio
value={currentTab}
options={TABS}
......@@ -178,12 +179,12 @@ export default class DatabaseEditApp extends Component {
)}
</LoadingAndErrorWrapper>
</div>
</div>
</Box>
{/* Sidebar Actions */}
{editingExistingDatabase && (
<div className="Grid-cell Cell--1of3">
<div className="Actions bordered rounded shadowed">
<Box ml={[2, 3]} w={420}>
<div className="Actions bg-light rounded p3">
<div className="Actions-group">
<label className="Actions-groupLabel block text-bold">{t`Actions`}</label>
<ol>
......@@ -253,9 +254,9 @@ export default class DatabaseEditApp extends Component {
</ol>
</div>
</div>
</div>
</Box>
)}
</section>
</Flex>
</div>
);
}
......
......@@ -13,7 +13,7 @@ const SettingInput = ({
type = "text",
}) => (
<InputBlurChange
className={cx(" AdminInput bordered rounded h3", {
className={cx("Form-input", {
SettingsInput: type !== "password",
SettingsPassword: type === "password",
"border-error bg-error-input": errorMessage,
......
......@@ -62,11 +62,11 @@ export default class ForgotPasswordApp extends Component {
{!sentNotification ? (
<div>
<form
className="ForgotForm bg-white Form-new bordered rounded shadowed"
className="ForgotForm bg-white bordered rounded shadowed"
name="form"
noValidate
>
<h3 className="Login-header Form-offset mb3">{t`Forgot password`}</h3>
<h3 className="Login-header mb3">{t`Forgot password`}</h3>
<FormMessage
message={error && error.data && error.data.message}
......@@ -79,7 +79,7 @@ export default class ForgotPasswordApp extends Component {
formError={error}
/>
<input
className="Form-input Form-offset full"
className="Form-input full"
name="email"
placeholder={t`The email you use for your Metabase account`}
type="text"
......@@ -87,7 +87,6 @@ export default class ForgotPasswordApp extends Component {
defaultValue={this.state.email}
autoFocus
/>
<span className="Form-charm" />
</FormField>
<div className="Form-actions">
......
......@@ -126,14 +126,14 @@ export default class LoginApp extends Component {
</div>
<div className="Login-content Grid-cell">
<form
className="Form-new bg-white bordered rounded shadowed"
className="p4 bg-white bordered rounded shadowed"
name="form"
onSubmit={e => this.formSubmitted(e)}
>
<h3 className="Login-header Form-offset">{t`Sign in to Metabase`}</h3>
<h2 className="Login-header mb2">{t`Sign in to Metabase`}</h2>
{Settings.ssoEnabled() && !preferUsernameAndPassword && (
<div className="mx4 py3 relative my4">
<div className="py3 relative my4">
<div className="relative border-bottom pb4">
<SSOLoginButton provider="google" ref="ssoLoginButton" />
{/*<div className="g-signin2 ml1 relative z2" id="g-signin2"></div>*/}
......@@ -146,7 +146,7 @@ export default class LoginApp extends Component {
</div>
<div className="py3">
<Link to="/auth/login?useMBLogin=true">
<Button className="EmailSignIn full">
<Button className="EmailSignIn full py2">
{t`Sign in with email`}
</Button>
</Link>
......@@ -161,7 +161,6 @@ export default class LoginApp extends Component {
loginError && loginError.data.message ? loginError : null
}
/>
<FormField
key="username"
fieldName="username"
......@@ -177,7 +176,7 @@ export default class LoginApp extends Component {
formError={loginError}
/>
<input
className="Form-input Form-offset full py1"
className="Form-input full"
name="username"
placeholder="youlooknicetoday@email.com"
type={
......@@ -192,7 +191,6 @@ export default class LoginApp extends Component {
onChange={e => this.onChange("username", e.target.value)}
autoFocus
/>
<span className="Form-charm" />
</FormField>
<FormField
......@@ -206,31 +204,28 @@ export default class LoginApp extends Component {
formError={loginError}
/>
<input
className="Form-input Form-offset full py1"
className="Form-input full"
name="password"
placeholder="Shh..."
type="password"
onChange={e => this.onChange("password", e.target.value)}
/>
<span className="Form-charm" />
</FormField>
<div className="Form-field">
<div className="Form-offset flex align-center">
<div className="flex align-center">
<CheckBox
name="remember"
checked={this.state.rememberMe}
onChange={() =>
this.setState({
rememberMe: !this.state.rememberMe,
})
this.setState({ rememberMe: !this.state.rememberMe })
}
/>
<span className="ml1">{t`Remember Me`}</span>
</div>
</div>
<div className="Form-actions p4">
<div className="Form-actions flex align-center">
<Button
primary={this.state.valid}
disabled={!this.state.valid}
......@@ -244,7 +239,7 @@ export default class LoginApp extends Component {
? "?email=" + this.state.credentials.username
: "")
}
className="Grid-cell py2 sm-py0 md-text-right text-centered flex-full link"
className="text-right ml-auto link"
onClick={e => {
window.OSX ? window.OSX.resetPassword() : null;
}}
......
......@@ -120,8 +120,8 @@ export default class PasswordResetApp extends Component {
/>
</div>
<div className="Grid-cell bordered rounded shadowed">
<h3 className="Login-header Form-offset mt4">{t`Whoops, that's an expired link`}</h3>
<p className="Form-offset mb4 mr4">
<h3 className="Login-header mt4">{t`Whoops, that's an expired link`}</h3>
<p className="mb4 mr4">
{jt`For security reasons, password reset links expire after a little while. If you still need
to reset your password, you can ${requestLink}.`}
</p>
......@@ -147,9 +147,9 @@ export default class PasswordResetApp extends Component {
onSubmit={e => this.formSubmitted(e)}
noValidate
>
<h3 className="Login-header Form-offset">{t`New password`}</h3>
<h3 className="Login-header-offset">{t`New password`}</h3>
<p className="Form-offset text-medium mb4">{t`To keep your data secure, passwords ${passwordComplexity}`}</p>
<p className="text-medium mb4">{t`To keep your data secure, passwords ${passwordComplexity}`}</p>
<FormMessage
formError={
......@@ -168,14 +168,13 @@ export default class PasswordResetApp extends Component {
formError={resetError}
/>
<input
className="Form-input Form-offset full"
className="Form-input full"
name="password"
placeholder={t`Make sure its secure like the instructions above`}
type="password"
onChange={e => this.onChange("password", e.target.value)}
autoFocus
/>
<span className="Form-charm" />
</FormField>
<FormField
......@@ -189,13 +188,12 @@ export default class PasswordResetApp extends Component {
formError={resetError}
/>
<input
className="Form-input Form-offset full"
className="Form-input full"
name="password2"
placeholder={t`Make sure it matches the one you just entered`}
type="password"
onChange={e => this.onChange("password2", e.target.value)}
/>
<span className="Form-charm" />
</FormField>
<div className="Form-actions">
......
......@@ -153,7 +153,7 @@ export default class DatabaseDetailsForm extends Component {
switch (field.type) {
case "boolean":
return (
<div className="Form-input Form-offset full Button-group">
<div className="Form-input full Button-group">
<div
className={cx(
"Button",
......@@ -182,7 +182,7 @@ export default class DatabaseDetailsForm extends Component {
return (
<input
type={field.type === "password" ? "password" : "text"}
className="Form-input Form-offset full"
className="Form-input full"
ref={field.name}
name={field.name}
value={value}
......@@ -207,7 +207,7 @@ export default class DatabaseDetailsForm extends Component {
: details["tunnel-enabled"];
return (
<FormField key={field.name} fieldName={field.name}>
<div className="flex align-center Form-offset">
<div className="flex align-center">
<div className="Grid-cell--top">
<Toggle
value={on}
......@@ -235,7 +235,7 @@ export default class DatabaseDetailsForm extends Component {
: details["use-jvm-timezone"];
return (
<FormField key={field.name} fieldName={field.name}>
<div className="flex align-center Form-offset">
<div className="flex align-center">
<div className="Grid-cell--top">
<Toggle
value={on}
......@@ -256,7 +256,7 @@ export default class DatabaseDetailsForm extends Component {
const on = details["use-srv"] == null ? false : details["use-srv"];
return (
<FormField key={field.name} fieldName={field.name}>
<div className="flex align-center Form-offset">
<div className="flex align-center">
<div className="Grid-cell--top">
<Toggle
value={on}
......@@ -266,9 +266,7 @@ export default class DatabaseDetailsForm extends Component {
<div className="px2">
<h3>{t`Use DNS SRV when connecting`}</h3>
<div style={{ maxWidth: "40rem" }} className="pt1">
{t`Using this option requires that provided host is a FQDN. If connecting to
an Atlas cluster, you might need to enable this option. If you don't know what this means,
leave this disabled.`}
{t`Using this option requires that provided host is a FQDN. If connecting to an Atlas cluster, you might need to enable this option. If you don't know what this means, leave this disabled.`}
</div>
</div>
</div>
......@@ -281,7 +279,7 @@ export default class DatabaseDetailsForm extends Component {
: details["let-user-control-scheduling"];
return (
<FormField key={field.name} fieldName={field.name}>
<div className="flex align-center Form-offset">
<div className="flex align-center">
<div className="Grid-cell--top">
<Toggle
value={on}
......@@ -293,8 +291,7 @@ export default class DatabaseDetailsForm extends Component {
<div className="px2">
<h3>{t`This is a large database, so let me choose when Metabase syncs and scans`}</h3>
<div style={{ maxWidth: "40rem" }} className="pt1">
{t`By default, Metabase does a lightweight hourly sync and an intensive daily scan of field values.
If you have a large database, we recommend turning this on and reviewing when and how often the field value scans happen.`}
{t`By default, Metabase does a lightweight hourly sync and an intensive daily scan of field values. If you have a large database, we recommend turning this on and reviewing when and how often the field value scans happen.`}
</div>
</div>
</div>
......@@ -307,7 +304,7 @@ export default class DatabaseDetailsForm extends Component {
: details["auto_run_queries"];
return (
<FormField key={field.name} fieldName={field.name}>
<div className="flex align-center Form-offset">
<div className="flex align-center">
<div className="Grid-cell--top">
<Toggle
value={on}
......@@ -330,7 +327,7 @@ export default class DatabaseDetailsForm extends Component {
const credentialsURL =
CREDENTIALS_URL_PREFIXES[engine] + (projectID || "");
const credentialsURLLink = (
<div className="flex align-center Form-offset">
<div className="flex align-center">
<div className="Grid-cell--top">
{jt`${(
<a className="link" href={credentialsURL} target="_blank">
......@@ -357,7 +354,7 @@ export default class DatabaseDetailsForm extends Component {
if (clientID) {
const authURL = AUTH_URL_PREFIXES[engine] + clientID;
authURLLink = (
<div className="flex align-center Form-offset">
<div className="flex align-center">
<div className="Grid-cell--top">
{jt`${(
<a className="link" href={authURL} target="_blank">
......@@ -390,7 +387,7 @@ export default class DatabaseDetailsForm extends Component {
// URL looks like https://console.developers.google.com/apis/api/analytics.googleapis.com/overview?project=12343611585
const enableAPIURL = ENABLE_API_PREFIXES[engine] + projectID;
enableAPILink = (
<div className="flex align-center Form-offset">
<div className="flex align-center">
<div className="Grid-cell--top">
{t`To use Metabase with this data you must enable API access in the Google Developers Console.`}
</div>
......@@ -426,7 +423,6 @@ export default class DatabaseDetailsForm extends Component {
formError={formError}
/>
{this.renderFieldInput(field, fieldIndex)}
<span className="Form-charm" />
</FormField>
);
}
......
......@@ -33,7 +33,7 @@ export default class ModalContent extends Component {
<div
id={this.props.id}
className={cx(
"ModalContent NewForm flex-full flex flex-column relative",
"ModalContent flex-full flex flex-column relative",
className,
{ "full-height": fullPageModal && !formModal },
// add bottom padding if this is a standard "form modal" with no footer
......
......@@ -16,7 +16,7 @@ export default class QuestionSavedModal extends Component {
id="QuestionSavedModal"
title={t`Saved! Add this to a dashboard?`}
onClose={this.props.onClose}
className="Modal-content Modal-content--small NewForm"
className="Modal-content Modal-content--small"
>
<div>
<button
......
......@@ -25,7 +25,7 @@ export default class FormField extends Component {
};
render() {
const { displayName, offset, formError, children, hidden } = this.props;
const { displayName, formError, children, hidden } = this.props;
const name = this.props.name || this.props.fieldName;
let error = this.props.error || getIn(formError, ["data", "errors", name]);
......@@ -42,12 +42,9 @@ export default class FormField extends Component {
})}
>
{displayName && (
<label
className={cx("Form-label", { "Form-offset": offset })}
htmlFor={name}
>
<label className="Form-label" htmlFor={name}>
{displayName}{" "}
{error && <span className="text-error mx1">{error}</span>}
{error && <span className="text-error">: {error}</span>}
</label>
)}
{children}
......
import React, { Component } from "react";
import PropTypes from "prop-types";
import cx from "classnames";
export default class FormLabel extends Component {
static propTypes = {
......@@ -9,12 +8,8 @@ export default class FormLabel extends Component {
message: PropTypes.string,
};
static defaultProps = {
offset: true,
};
render() {
let { fieldName, formError, message, offset, title } = this.props;
let { fieldName, formError, message, title } = this.props;
if (!message) {
message =
......@@ -24,7 +19,7 @@ export default class FormLabel extends Component {
}
return (
<label className={cx("Form-label", { "Form-offset": offset })}>
<label className="Form-label">
{title} {message !== undefined ? <span>: {message}</span> : null}
</label>
);
......
......@@ -41,17 +41,15 @@ const StandardForm = ({
displayName={
formField.title || nameComponents[nameComponents.length - 1]
}
offset={!newForm}
{...field}
hidden={formField.type === "hidden"}
>
<FormWidget field={field} offset={!newForm} {...formField} />
{!newForm && <span className="Form-charm" />}
<FormWidget field={field} {...formField} />
</FormField>
);
})}
</div>
<div className={cx("flex", { "Form-offset": !newForm })}>
<div className="flex">
<div className="ml-auto flex align-center">
{error && <FormMessage message={error} formError />}
{onClose && (
......
import React from "react";
import ColorPicker from "metabase/components/ColorPicker";
import cx from "classnames";
const FormColorWidget = ({ field, offset, initial }) => (
<div className={cx({ "Form-offset": offset })}>
const FormColorWidget = ({ field, initial }) => (
<div>
<ColorPicker
{...field}
value={
......
import React from "react";
import cx from "classnames";
import { formDomOnlyProps } from "metabase/lib/redux";
const FormInputWidget = ({ type = "text", placeholder, field, offset }) => (
const FormInputWidget = ({ type = "text", placeholder, field }) => (
<input
className={cx("Form-input full", { "Form-offset": offset })}
className="Form-input full"
type={type}
placeholder={placeholder}
{...formDomOnlyProps(field)}
......
import React from "react";
import cx from "classnames";
import { formDomOnlyProps } from "metabase/lib/redux";
import NumericInput from "metabase/components/NumericInput";
const FormInputWidget = ({ placeholder, field, offset }) => (
const FormInputWidget = ({ placeholder, field }) => (
<NumericInput
className={cx("Form-input full", { "Form-offset": offset })}
className="Form-input full"
placeholder={placeholder}
{...formDomOnlyProps(field)}
/>
......
......@@ -2,7 +2,7 @@ import React from "react";
import Select, { Option } from "metabase/components/Select";
const FormSelectWidget = ({ placeholder, options = [], field, offset }) => (
const FormSelectWidget = ({ placeholder, options = [], field }) => (
<Select
placeholder={placeholder}
{...field}
......
import React from "react";
import cx from "classnames";
const FormTextAreaWidget = ({ placeholder, field, offset }) => (
<textarea
className={cx("Form-input full", { "Form-offset": offset })}
placeholder={placeholder}
{...field}
/>
const FormTextAreaWidget = ({ placeholder, field }) => (
<textarea className="Form-input full" placeholder={placeholder} {...field} />
);
export default FormTextAreaWidget;
......@@ -47,12 +47,6 @@
color: var(--color-text-white);
}
.Actions {
background-color: color(var(--color-bg-light) alpha(-54%));
border: 1px solid var(--color-border);
padding: 2em;
}
.Actions-group {
margin-bottom: 2em;
}
......
:root {
--form-padding: 1em;
--form-input-placeholder-color: var(--color-text-light);
--form-input-size: 1rem;
--form-input-size-medium: 1.25rem;
--form-input-size-large: 1.571rem;
--form-label-color: var(--color-text-medium);
--form-offset: 2.4rem;
--form-field-border-color: color(var(--color-border) blackness(+10%));
}
.Form-new {
padding-top: 2rem;
::-webkit-input-placeholder {
color: var(--color-text-light);
}
/* TODO: combine this and the scoped version */
.Form-label {
display: block;
color: var(--color-text-medium);
font-size: 1.2rem;
:-moz-placeholder {
color: var(--color-text-light);
}
:-ms-input-placeholder {
color: var(--color-text-light);
}
.Form-field {
position: relative;
margin-bottom: 1.5em;
color: var(--color-text-medium);
margin-bottom: 1.5rem;
}
/* TODO: remove this scoping once we've converted non admin forms */
/* form labels inherit the color of the parent, allowing for easy error changes */
.Form-field .Form-label {
display: block;
font-size: 0.85em;
font-weight: 700;
color: currentColor;
}
.Form-field.Form--fieldError {
color: var(--color-error);
}
.Form-label {
display: block;
font-weight: 900;
font-size: 0.88em;
color: inherit;
margin-bottom: 0.5em;
}
.Form-input {
padding-top: 0.6rem;
padding-bottom: 0.6rem;
font-family: var(--default-font-family);
font-weight: 700;
font-size: 16px;
color: var(--color-text-dark);
background-color: var(--color-bg-white);
border: 1px solid var(--form-field-border-color);
border-radius: 4px;
padding: 0.75em;
line-height: 1;
border: none;
background-color: transparent;
transition: color 0.3s linear;
outline: none;
}
.Form-field.Form--fieldError .Form-input {
border-color: var(--color-error);
}
.Form-message {
......@@ -60,75 +55,10 @@
transition: opacity 500ms linear;
}
/* form-input font sizes */
.Form-input {
font-size: var(--form-input-size);
}
@media screen and (--breakpoint-min-md) {
.Form-input {
font-size: var(--form-input-size-medium);
}
}
@media screen and (--breakpoint-min-lg) {
.Form-input {
font-size: var(--form-input-size-large);
}
}
.Form-input:focus {
outline: none;
}
.Form-offset {
padding-left: var(--form-offset);
}
.Form-charm {
position: absolute;
display: block;
bottom: 0;
left: 0;
width: 0.15em;
height: 3em;
background-color: var(--color-bg-medium);
box-sizing: border-box;
opacity: 0;
transition: background-color 0.3s linear;
transition: opacity 0.3s linear;
}
.Form-field.Form--fieldError .Form-charm {
background-color: var(--color-error);
opacity: 1;
}
.Form-input:focus + .Form-charm {
background-color: var(--color-brand);
opacity: 1;
}
.Form-input:focus,
.Form-field:hover .Form-input {
color: var(--color-text-light);
background: color(var(--color-bg-black) alpha(-98%));
}
/* ewww */
.Form-field:hover .Form-input.ng-dirty {
color: var(--color-text-dark);
background-color: var(--color-bg-white);
}
.Form-field:hover .Form-charm {
opacity: 1;
}
.Form-field:hover .Form-input:focus {
transition: color 0.3s linear;
color: var(--color-text-dark);
background-color: transparent;
transition: background 0.3s linear;
border-color: var(--color-brand);
transition: border 300ms ease-in-out;
}
.Form-radio {
......@@ -153,82 +83,11 @@
background-color: var(--color-brand);
}
/* TODO: replace instances of Form-group with Form-field */
.Form-group {
padding: var(--form-padding);
transition: opacity 0.3s linear;
}
.Form-groupDisabled {
opacity: 0.2;
pointer-events: none;
transition: opacity 0.3s linear;
.Form-field .AdminSelect {
border-color: var(--form-field-border-color);
}
.Form-actions {
padding-left: var(--form-offset);
padding-bottom: var(--form-offset);
display: flex;
align-items: center;
}
.FormTitleSeparator {
position: relative;
border-bottom: 1px solid var(--color-border);
}
::-webkit-input-placeholder {
/* WebKit browsers */
color: var(--color-text-light);
}
:-moz-placeholder {
/* Mozilla Firefox 4 to 18 */
color: var(--color-text-light);
opacity: 1;
}
::-moz-placeholder {
/* Mozilla Firefox 19+ */
color: var(--color-text-light);
opacity: 1;
}
:-ms-input-placeholder {
/* Internet Explorer 10+ */
color: var(--color-text-light);
}
.NewForm .Form-label {
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-medium);
margin-bottom: 0.5em;
}
.NewForm .Form-input {
font-size: 16px;
color: var(--color-text-dark);
padding: 0.5em;
background-color: var(--color-bg-white);
border: 1px solid var(--color-border);
border-radius: 4px;
}
.NewForm .Form-input:focus {
.Form-field .AdminSelect:hover {
border-color: var(--color-brand);
box-shadow: none;
outline: 0;
}
.NewForm .Form-header {
padding: var(--padding-4);
}
.NewForm .Form-inputs {
padding-left: var(--padding-4);
padding-right: var(--padding-4);
}
.NewForm .Form-actions {
padding-bottom: 1.2rem;
padding-top: 1.2rem;
padding-left: var(--padding-4);
padding-right: var(--padding-4);
transition: border 300ms ease-in-out;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment