Skip to content
Snippets Groups Projects
Unverified Commit dc4bea57 authored by Tom Robinson's avatar Tom Robinson Committed by GitHub
Browse files

scratch (#10069)

parent 7860686d
No related branches found
No related tags found
No related merge requests found
......@@ -13,6 +13,7 @@
.*/node_modules/update-notifier/.*
.*/node_modules/boxen/.*
.*/node_modules/libnpx/.*
.*/node_modules/@babel/standalone/.*
[include]
.*/frontend/.*
......
import React from "react";
import Form from "metabase/containers/Form";
export const component = Form;
export const description = `A standard form component.`;
export const examples = {
normal: (
<Form
form={{
fields: [
{
name: "email",
placeholder: "bob@metabase.com",
validate: v => (!v ? "required" : null),
},
{
name: "password",
type: "password",
validate: v => (!v ? "required" : null),
},
],
}}
onSubmit={values => alert(JSON.stringify(values))}
/>
),
};
......@@ -44,7 +44,7 @@ export type FormDefinition = {
validate?: (values: FormValues) => FormErrors,
};
type Form = {
type FormObject = {
fields: (values: FormValues) => FormFieldDefinition[],
fieldNames: (values: FormValues) => FormFieldName[],
initial: () => FormValues,
......@@ -62,7 +62,7 @@ type Props = {
let FORM_ID = 0;
export default class Form_ extends React.Component {
export default class Form extends React.Component {
props: Props;
_formName: ?string;
......@@ -157,7 +157,7 @@ export default class Form_ extends React.Component {
// form.fields[0] is { name: "foo", initial: () => "bar" }
//
function makeFormMethod(
form: Form,
form: FormObject,
methodName: string,
defaultValues: any = {},
) {
......@@ -181,7 +181,7 @@ function makeFormMethod(
function getValue(fnOrValue, ...args): any {
return typeof fnOrValue === "function" ? fnOrValue(...args) : fnOrValue;
}
function makeForm(formDef: FormDefinition): Form {
function makeForm(formDef: FormDefinition): FormObject {
const form = {
...formDef,
fields: values => getValue(formDef.fields, values),
......
......@@ -4,6 +4,7 @@ import React, { Component } from "react";
import { Link, Route } from "react-router";
import { slugify } from "metabase/lib/formatting";
import cx from "classnames";
// $FlowFixMe: react-virtualized ignored
import reactElementToJSXString from "react-element-to-jsx-string";
......@@ -12,14 +13,22 @@ import COMPONENTS from "../lib/components-webpack";
import AceEditor from "metabase/components/TextEditor";
import CopyButton from "metabase/components/CopyButton";
import Icon from "metabase/components/Icon";
const Section = ({ title, children }) => (
<div className="mb2">
<h3 className="my2">{title}</h3>
{title && <h3 className="my2">{title}</h3>}
{children}
</div>
);
function getComponentName(component) {
return component.displayName || component.name || "[Unknown]";
}
function getComponentSlug(component) {
return slugify(getComponentName(component));
}
export default class ComponentsApp extends Component {
static routes: ?[React$Element<Route>];
render() {
......@@ -35,54 +44,61 @@ export default class ComponentsApp extends Component {
<ul className="py2">
{COMPONENTS.filter(
({ component, description, examples }) =>
!componentName || componentName === slugify(component.name),
component &&
(!componentName ||
componentName === getComponentSlug(component)),
).map(({ component, description, examples }) => (
<li>
<a
className="py1 block link h3 text-bold"
href={`/_internal/components#${component.name}`}
href={`/_internal/components#${getComponentSlug(component)}`}
>
{component.name}
{getComponentName(component)}
</a>
</li>
))}
</ul>
</nav>
<div className="bg-light flex-full bg-white" style={{ flex: "66.66%" }}>
<div className="p4">
<div className="py4">
{COMPONENTS.filter(
({ component, description, examples }) =>
!componentName || componentName === slugify(component.name),
!componentName || componentName === getComponentSlug(component),
).map(({ component, description, examples }, index) => (
<div id={component.name} key={index}>
<div
id={getComponentSlug(component)}
key={index}
className="border-bottom mb4 pb3 px4"
>
<h2>
<Link
to={`_internal/components/${slugify(component.name)}`}
to={`_internal/components/${getComponentSlug(component)}`}
className="no-decoration"
>
{component.name}
{getComponentName(component)}
</Link>
</h2>
{description && <p className="my2">{description}</p>}
{component.propTypes && (
<Section title="Props">
<div className="border-left border-right border-bottom text-code">
{Object.keys(component.propTypes).map(prop => (
<div>
{prop}{" "}
{component.defaultProps &&
component.defaultProps[prop] !== undefined
? "(default: " +
JSON.stringify(component.defaultProps[prop]) +
")"
: ""}
</div>
))}
</div>
</Section>
)}
{componentName === getComponentSlug(component) &&
component.propTypes && (
<Section title="Props">
<div className="border-left border-right border-bottom text-code">
{Object.keys(component.propTypes).map(prop => (
<div>
{prop}{" "}
{component.defaultProps &&
component.defaultProps[prop] !== undefined
? "(default: " +
JSON.stringify(component.defaultProps[prop]) +
")"
: ""}
</div>
))}
</div>
</Section>
)}
{examples && (
<Section title="Examples">
<Section>
{Object.entries(examples)
.filter(
([name, element]) =>
......@@ -92,8 +108,8 @@ export default class ComponentsApp extends Component {
<div className="my2">
<h4 className="my1">
<Link
to={`_internal/components/${slugify(
component.name,
to={`_internal/components/${getComponentSlug(
component,
)}/${slugify(name)}`}
className="no-decoration"
>
......@@ -104,20 +120,7 @@ export default class ComponentsApp extends Component {
<div className="p2 bordered flex align-center flex-full">
<div className="full">{element}</div>
</div>
<div className="relative">
<AceEditor
value={reactElementToJSXString(element)}
mode="ace/mode/jsx"
theme="ace/theme/metabase"
readOnly
/>
<div className="absolute top right text-brand-hover cursor-pointer z2">
<CopyButton
className="p1"
value={reactElementToJSXString(element)}
/>
</div>
</div>
<SourcePane element={element} />
</div>
</div>
))}
......@@ -132,6 +135,68 @@ export default class ComponentsApp extends Component {
}
}
class SourcePane extends React.Component {
state = {
isOpen: false,
};
render() {
const { element } = this.props;
const { isOpen } = this.state;
const source = reactElementToJSXString(element, {
showFunctions: true,
showDefaultProps: false,
});
const scratchUrl = "/_internal/scratch#" + btoa(source);
return (
<div
className={cx("relative", {
"border-left border-right border-bottom": isOpen,
})}
>
{isOpen && (
<AceEditor
value={source}
mode="ace/mode/jsx"
theme="ace/theme/metabase"
readOnly
/>
)}
{isOpen ? (
<div className="absolute top right z2 flex align-center p1 text-medium">
<CopyButton
className="ml1 text-brand-hover cursor-pointer"
value={source}
/>
<Link to={scratchUrl}>
<Icon
name="pencil"
className="ml1 text-brand-hover cursor-pointer"
/>
</Link>
<Icon
name="close"
className="ml1 text-brand-hover cursor-pointer"
onClick={() => this.setState({ isOpen: false })}
/>
</div>
) : (
<div className="p1 flex align-ceneter justify-end">
<Link className="link ml1" to={scratchUrl}>
Open in Scratch
</Link>
<Link
className="link ml1"
onClick={() => this.setState({ isOpen: true })}
>
View Source
</Link>
</div>
)}
</div>
);
}
}
ComponentsApp.routes = [
<Route path="components" component={ComponentsApp} />,
<Route path="components/:componentName" component={ComponentsApp} />,
......
import React from "react";
import ReactDOM from "react-dom";
import CheckBox from "metabase/components/CheckBox";
import cx from "classnames";
import * as babel from "@babel/standalone";
import reactPreset from "babel-preset-react";
const BABEL_CONFIG = {
presets: [reactPreset],
};
import AceEditor from "metabase/components/TextEditor";
import context from "../lib/scratch-context";
export default class ScratchApp extends React.Component {
constructor(props) {
super(props);
const hash = window.location.hash.replace(/^#/, "");
this.state = {
code: hash ? atob(hash) : `<Button>Hello World</Button>`,
error: null,
centered: true,
};
}
handleChange = code => {
this.setState({ code });
history.replaceState({}, null, "/_internal/scratch#" + btoa(code));
};
async _update() {
try {
// transpile using babel, for JSX etc
const { code } = await babel.transform(this.state.code, BABEL_CONFIG);
// compile
let fn;
try {
// if the module is an expression
fn = new Function(
"module",
"context",
`with(context) { \nreturn ${code}\n }`,
);
} catch (e) {
fn = new Function(
"module",
"context",
`with(context) { \n ${code}\n }`,
);
}
// execute the function with module and context
const mod = {};
const result = fn(mod, context);
// get an element/component from the return value or module.exports
const elementOrComponent = mod.exports || result;
// make sure it's an element
const element = React.isValidElement(elementOrComponent)
? elementOrComponent
: React.createElement(elementOrComponent);
// render!
ReactDOM.unstable_renderSubtreeIntoContainer(
this,
element,
this._container,
);
this.setState({ error: null });
} catch (e) {
console.error(e);
this.setState({ error: e });
}
}
componentDidMount() {
this._update();
}
componentDidUpdate(prevProps, prevState) {
if (prevState.code !== this.state.code) {
this._update();
}
}
render() {
const { centered } = this.state;
return (
<div className="flex-full flex flex-column">
<div
ref={r => (this._container = r)}
className={cx("flex-full relative", {
"flex layout-centered": centered,
})}
/>
<AceEditor
mode="ace/mode/jsx"
theme="ace/theme/metabase"
style={{
height: 100,
outline: this.state.error ? "2px solid red" : null,
}}
value={this.state.code}
onChange={this.handleChange}
/>
<div className="absolute bottom right flex align-center p1">
<span className="mr1">Centered:</span>
<CheckBox
checked={centered}
onChange={e => this.setState({ centered: !centered })}
/>
</div>
</div>
);
}
}
// import all modules in this directory (http://stackoverflow.com/a/31770875)
const req = require.context(
const componentsReq = require.context(
"metabase/components",
true,
/^(.*\.info\.(js$))[^.]*$/im,
);
export default req
.keys()
.map(key => Object.assign({}, req(key), { showExample: true }));
const containersReq = require.context(
"metabase/containers",
true,
/^(.*\.info\.(js$))[^.]*$/im,
);
function getComponents(req) {
return req
.keys()
.map(key => Object.assign({}, req(key), { showExample: true }));
}
const components = [
...getComponents(componentsReq),
...getComponents(containersReq),
];
export default components;
import React from "react";
import styled from "styled-components";
import * as systemExports from "styled-system";
import * as gridExports from "grid-styled";
import colors, * as colorsExports from "metabase/lib/colors";
import * as entities from "metabase/entities";
import COMPONENTS from "./components-webpack";
const context = {
...systemExports,
...gridExports,
...colorsExports,
React,
styled,
colors,
};
// components with .info.js files
for (const { component } of COMPONENTS) {
context[component.displayName || component.name] = component;
}
// Metabase's entities, capitalized
import { capitalize } from "metabase/lib/formatting";
for (const [name, entity] of Object.entries(entities)) {
context[capitalize(name)] = entity;
}
export default context;
/* @flow */
import React from "react";
import { Link, Route, IndexRoute } from "react-router";
import { Link, Route, IndexRedirect } from "react-router";
import {
Archived,
......@@ -10,6 +10,8 @@ import {
Unauthorized,
} from "metabase/containers/ErrorPages";
import fitViewport from "metabase/hoc/FitViewPort";
const ErrorWithDetails = () => <GenericError details="Example error message" />;
// $FlowFixMe: doesn't know about require.context
......@@ -38,9 +40,9 @@ const WelcomeApp = () => {
);
};
const InternalLayout = ({ children }) => {
const InternalLayout = fitViewport(({ children }) => {
return (
<div className="flex flex-column full-height">
<div className="flex flex-column flex-full">
<nav className="wrapper flex align-center py3 border-bottom">
<a className="text-brand-hover" href="/_internal">
<h4>Style Guide</h4>
......@@ -61,11 +63,12 @@ const InternalLayout = ({ children }) => {
<div className="flex flex-full">{children}</div>
</div>
);
};
});
export default (
<Route component={InternalLayout}>
<IndexRoute component={WelcomeApp} />
<IndexRedirect to="welcome" />
<Route path="welcome" component={WelcomeApp} />
{Object.entries(PAGES).map(
([name, Component]) =>
Component &&
......
......@@ -87,6 +87,7 @@
"z-index": "0.0.1"
},
"devDependencies": {
"@babel/standalone": "^7.4.5",
"@slack/client": "^3.5.4",
"babel-cli": "^6.11.4",
"babel-core": "^6.20.0",
......
......@@ -48,6 +48,11 @@
dependencies:
regenerator-runtime "^0.13.2"
 
"@babel/standalone@^7.4.5":
version "7.4.5"
resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.4.5.tgz#60ded549756cf749eb6db0a54c21b4c48c7e18e5"
integrity sha512-Ddjq6OS+NxpJdvMEMiKVU4BCg/2IOpbP+2JNM9ZY1HULxlZGTyNXKIqXHRqzV0N6e+51zmTwQg+c1Lfs44bBHg==
"@babel/template@^7.0.0":
version "7.4.0"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.0.tgz#12474e9c077bae585c5d835a95c0b0b790c25c8b"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment