Skip to content
Snippets Groups Projects
Commit 83d06bfe authored by Tom Robinson's avatar Tom Robinson
Browse files

Various pulse UI enhancements

parent 5458301c
Branches
Tags
No related merge requests found
Showing
with 199 additions and 64 deletions
import React, { Component, PropTypes } from 'react';
import Icon from 'metabase/components/Icon.jsx';
import cx from "classnames";
export default class CheckBox extends Component {
static propTypes = {
checked: PropTypes.bool,
......@@ -33,7 +35,7 @@ export default class CheckBox extends Component {
justifyContent: 'center',
};
return (
<div className={className} style={style} onClick={() => this.onClick()}>
<div className={cx("cursor-pointer", className)} style={style} onClick={() => this.onClick()}>
{ checked ? <Icon name='check' width={size - 4} height={size - 4} /> : null }
</div>
)
......
......@@ -35,7 +35,7 @@ export default class DeleteModalWithConfirm extends Component {
let confirmed = confirmItems.reduce((acc, item, index) => acc && checked[index], true);
return (
<ModalContent
title={"Delete " + objectName + "?"}
title={"Delete \"" + objectName + "\"?"}
closeFn={this.props.onClose}
>
<div className="px4">
......@@ -44,7 +44,7 @@ export default class DeleteModalWithConfirm extends Component {
<li className="pb2 mb2 border-row-divider flex align-center">
<span className="text-error">
<CheckBox
checkColor="currentColor" borderColor="currentColor" size={20}
checkColor="currentColor" borderColor={checked[index] ? "currentColor" : undefined} size={20}
checked={checked[index]}
onChange={(e) => this.setState({ checked: { ...checked, [index]: e.target.checked } })}
/>
......
import React, { Component, PropTypes } from "react";
import RetinaImage from "react-retina-image";
import { loadIcon } from 'metabase/icon_paths';
export default class Icon extends Component {
render() {
var icon = loadIcon(this.props.name);
if (icon.svg) {
return (<svg {... icon.attrs} {... this.props} dangerouslySetInnerHTML={{__html: icon.svg}}></svg>);
if (icon.img) {
return (<RetinaImage forceOriginalDimensions={false} {...icon.attrs} {...this.props} src={icon.img} />);
} else if (icon.svg) {
return (<svg {...icon.attrs} {...this.props} dangerouslySetInnerHTML={{__html: icon.svg}}></svg>);
} else {
return (<svg {... icon.attrs} {... this.props}><path d={icon.path} /></svg>);
return (<svg {...icon.attrs} {...this.props}><path d={icon.path} /></svg>);
}
}
}
......@@ -11,8 +11,17 @@
margin-left: 180px;
}
.PulseButton {
color: rgb(121,130,127);
font-weight: 700;
border-width: 2px;
border-color: rgb(222,228,226);
}
.PulseEdit .input,
.PulseEdit .bordered,
.PulseEdit .border-bottom,
.PulseEdit .border-row-divider,
.PulseEdit .AdminSelect {
border-width: 2px;
border-color: rgb(222,228,226);
......@@ -23,7 +32,43 @@
padding: 1em;
}
.PulseEdit .input:focus {
.PulseEdit .input:focus,
.PulseEdit .input--focus {
border-width: 2px;
border-color: rgb(97,167,229);
border-color: rgb(97,167,229) !important;
}
.bg-grey-0 {
background-color: rgb(252,252,253);
}
.PulseEditButton {
opacity: 0;
transition: opacity 0.3s linear;
}
.PulseListItem:hover .PulseEditButton {
opacity: 1;
}
.DangerZone:hover {
border-color: var(--error-color);
}
.DangerZone .Button--danger {
opacity: 0.4;
background: #FBFCFD;
border: 1px solid #ddd;
color: #444;
}
.DangerZone:hover .Button--danger {
opacity: 1;
background-color: var(--danger-button-bg-color);
border-color: var(--danger-button-bg-color);
color: #fff;
}
.Modal.WhatsAPulseModal {
width: auto;
}
......@@ -69,6 +69,10 @@ export var ICON_PATHS = {
attrs: { viewBox: '0 0 24 24' }
},
lock: 'M8.8125,13.2659641 L5.50307055,13.2659641 C4.93891776,13.2659641 4.5,13.7132101 4.5,14.2649158 L4.5,30.8472021 C4.5,31.4051918 4.94908998,31.8461538 5.50307055,31.8461538 L26.4969294,31.8461538 C27.0610822,31.8461538 27.5,31.3989079 27.5,30.8472021 L27.5,14.2649158 C27.5,13.7069262 27.05091,13.2659641 26.4969294,13.2659641 L23.1875,13.2659641 L23.1875,7.18200446 C23.1875,3.22368836 19.9695466,0 16,0 C12.0385306,0 8.8125,3.21549292 8.8125,7.18200446 L8.8125,13.2659641 Z M12.3509615,7.187641 C12.3509615,5.17225484 13.9813894,3.53846154 15.9955768,3.53846154 C18.0084423,3.53846154 19.6401921,5.17309313 19.6401921,7.187641 L19.6401921,13.0473232 L12.3509615,13.0473232 L12.3509615,7.187641 Z',
mail: {
path: 'M0 6 L16 16 L32 6 z M0 9 L0 26 L32 26 L32 9 L16 19 z',
attrs: { viewBox: '0 0 32 32' }
},
mine: 'M28.4907419,50 C25.5584999,53.6578499 21.0527692,56 16,56 C10.9472308,56 6.44150015,53.6578499 3.50925809,50 L28.4907419,50 Z M29.8594823,31.9999955 C27.0930063,27.217587 21.922257,24 16,24 C10.077743,24 4.9069937,27.217587 2.1405177,31.9999955 L29.8594849,32 Z M16,21 C19.8659932,21 23,17.1944204 23,12.5 C23,7.80557963 22,3 16,3 C10,3 9,7.80557963 9,12.5 C9,17.1944204 12.1340068,21 16,21 Z',
number: 'M8,8.4963932 C8,8.22224281 8.22618103,8 8.4963932,8 L23.5036068,8 C23.7777572,8 24,8.22618103 24,8.4963932 L24,23.5036068 C24,23.7777572 23.773819,24 23.5036068,24 L8.4963932,24 C8.22224281,24 8,23.773819 8,23.5036068 L8,8.4963932 Z M12.136,19 L12.136,13.4 L11.232,13.4 C11.1999998,13.6133344 11.1333338,13.7919993 11.032,13.936 C10.9306662,14.0800007 10.8066674,14.1959996 10.66,14.284 C10.5133326,14.3720004 10.3480009,14.4333332 10.164,14.468 C9.97999908,14.5026668 9.78933432,14.5173334 9.592,14.512 L9.592,15.368 L11,15.368 L11,19 L12.136,19 Z M13.616,16.176 C13.616,16.7360028 13.6706661,17.2039981 13.78,17.58 C13.8893339,17.9560019 14.0373324,18.2559989 14.224,18.48 C14.4106676,18.7040011 14.6279988,18.8639995 14.876,18.96 C15.1240012,19.0560005 15.3866653,19.104 15.664,19.104 C15.9466681,19.104 16.2119988,19.0560005 16.46,18.96 C16.7080012,18.8639995 16.9266657,18.7040011 17.116,18.48 C17.3053343,18.2559989 17.4546661,17.9560019 17.564,17.58 C17.6733339,17.2039981 17.728,16.7360028 17.728,16.176 C17.728,15.6319973 17.6733339,15.1746685 17.564,14.804 C17.4546661,14.4333315 17.3053343,14.1360011 17.116,13.912 C16.9266657,13.6879989 16.7080012,13.5280005 16.46,13.432 C16.2119988,13.3359995 15.9466681,13.288 15.664,13.288 C15.3866653,13.288 15.1240012,13.3359995 14.876,13.432 C14.6279988,13.5280005 14.4106676,13.6879989 14.224,13.912 C14.0373324,14.1360011 13.8893339,14.4333315 13.78,14.804 C13.6706661,15.1746685 13.616,15.6319973 13.616,16.176 Z M14.752,16.176 C14.752,16.0799995 14.7533333,15.9640007 14.756,15.828 C14.7586667,15.6919993 14.7679999,15.5520007 14.784,15.408 C14.8000001,15.2639993 14.8266665,15.121334 14.864,14.98 C14.9013335,14.838666 14.953333,14.7120006 15.02,14.6 C15.086667,14.4879994 15.1719995,14.3973337 15.276,14.328 C15.3800005,14.2586663 15.5093326,14.224 15.664,14.224 C15.8186674,14.224 15.9493328,14.2586663 16.056,14.328 C16.1626672,14.3973337 16.2506663,14.4879994 16.32,14.6 C16.3893337,14.7120006 16.4413332,14.838666 16.476,14.98 C16.5106668,15.121334 16.5373332,15.2639993 16.556,15.408 C16.5746668,15.5520007 16.5853333,15.6919993 16.588,15.828 C16.5906667,15.9640007 16.592,16.0799995 16.592,16.176 C16.592,16.3360008 16.5866667,16.5293322 16.576,16.756 C16.5653333,16.9826678 16.5320003,17.2013323 16.476,17.412 C16.4199997,17.6226677 16.329334,17.8026659 16.204,17.952 C16.078666,18.1013341 15.8986678,18.176 15.664,18.176 C15.4346655,18.176 15.2586673,18.1013341 15.136,17.952 C15.0133327,17.8026659 14.9240003,17.6226677 14.868,17.412 C14.8119997,17.2013323 14.7786667,16.9826678 14.768,16.756 C14.7573333,16.5293322 14.752,16.3360008 14.752,16.176 Z M18.064,16.176 C18.064,16.7360028 18.1186661,17.2039981 18.228,17.58 C18.3373339,17.9560019 18.4853324,18.2559989 18.672,18.48 C18.8586676,18.7040011 19.0759988,18.8639995 19.324,18.96 C19.5720012,19.0560005 19.8346653,19.104 20.112,19.104 C20.3946681,19.104 20.6599988,19.0560005 20.908,18.96 C21.1560012,18.8639995 21.3746657,18.7040011 21.564,18.48 C21.7533343,18.2559989 21.9026661,17.9560019 22.012,17.58 C22.1213339,17.2039981 22.176,16.7360028 22.176,16.176 C22.176,15.6319973 22.1213339,15.1746685 22.012,14.804 C21.9026661,14.4333315 21.7533343,14.1360011 21.564,13.912 C21.3746657,13.6879989 21.1560012,13.5280005 20.908,13.432 C20.6599988,13.3359995 20.3946681,13.288 20.112,13.288 C19.8346653,13.288 19.5720012,13.3359995 19.324,13.432 C19.0759988,13.5280005 18.8586676,13.6879989 18.672,13.912 C18.4853324,14.1360011 18.3373339,14.4333315 18.228,14.804 C18.1186661,15.1746685 18.064,15.6319973 18.064,16.176 Z M19.2,16.176 C19.2,16.0799995 19.2013333,15.9640007 19.204,15.828 C19.2066667,15.6919993 19.2159999,15.5520007 19.232,15.408 C19.2480001,15.2639993 19.2746665,15.121334 19.312,14.98 C19.3493335,14.838666 19.401333,14.7120006 19.468,14.6 C19.534667,14.4879994 19.6199995,14.3973337 19.724,14.328 C19.8280005,14.2586663 19.9573326,14.224 20.112,14.224 C20.2666674,14.224 20.3973328,14.2586663 20.504,14.328 C20.6106672,14.3973337 20.6986663,14.4879994 20.768,14.6 C20.8373337,14.7120006 20.8893332,14.838666 20.924,14.98 C20.9586668,15.121334 20.9853332,15.2639993 21.004,15.408 C21.0226668,15.5520007 21.0333333,15.6919993 21.036,15.828 C21.0386667,15.9640007 21.04,16.0799995 21.04,16.176 C21.04,16.3360008 21.0346667,16.5293322 21.024,16.756 C21.0133333,16.9826678 20.9800003,17.2013323 20.924,17.412 C20.8679997,17.6226677 20.777334,17.8026659 20.652,17.952 C20.526666,18.1013341 20.3466678,18.176 20.112,18.176 C19.8826655,18.176 19.7066673,18.1013341 19.584,17.952 C19.4613327,17.8026659 19.3720003,17.6226677 19.316,17.412 C19.2599997,17.2013323 19.2266667,16.9826678 19.216,16.756 C19.2053333,16.5293322 19.2,16.3360008 19.2,16.176 Z',
pencil: 'M4.7352182,19.1979208 L11.3429107,25.5873267 L24.069853,12.5293069 L17.4624587,6.1419802 L4.7352182,19.1979208 Z M9.63604523,27.3406931 L3.02805455,20.9509901 L0.238146568,29.9610891 L9.63604523,27.3406931 Z M23.4499066,0 L19.1734989,4.38653465 L25.7811914,10.7759406 L30.0575991,6.38732673 L23.4499066,0 Z',
......@@ -143,6 +147,9 @@ export var ICON_PATHS = {
svg: '<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><path d="M12.8678801,11.1665983 C13.5821802,10.122879 14,8.86023303 14,7.5 C14,3.91014913 11.0898509,1 7.5,1 C3.91014913,1 1,3.91014913 1,7.5 C1,9.28845014 1.72229748,10.9081987 2.89109309,12.0834462 C4.35646231,14.0019965 7.48681642,16.4470748 7.48681641,22.2058556 C7.48681641,22.2058558 7.67883301,22.2058561 7.67883301,22.2058556 C7.67883301,17.6712018 10.736804,14.1476306 12.313368,11.998698 C12.5464391,11.681011 12.7265514,11.4132341 12.8678801,11.1665983 Z" id="Oval-1" stroke="#4C9DE6" fill="#78B5EC" ></path><ellipse id="Oval-3" fill="#3875AC" cx="7.55925926" cy="7.82743961" rx="2.18641975" ry="2.10434783"></ellipse></g>',
attrs: { viewBox: '0 0 13 21' }
},
"slack": {
img: "/app/img/slack.png"
}
};
ICON_PATHS["illustration-line"] = ICON_PATHS['illustration-area'];
......@@ -153,6 +160,10 @@ export function loadIcon(name) {
console.warn('Icon "' + name + '" does not exist.');
}
if (def.img) {
return def;
}
var icon = {
attrs: {
className: 'Icon Icon-' + name,
......
......@@ -3,9 +3,11 @@ import React, { Component, PropTypes } from "react";
import PulseEditName from "./PulseEditName.jsx";
import PulseEditCards from "./PulseEditCards.jsx";
import PulseEditChannels from "./PulseEditChannels.jsx";
import WhatsAPulse from "./WhatsAPulse.jsx";
import ActionButton from "metabase/components/ActionButton.jsx";
import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
import ModalContent from "metabase/components/ModalContent.jsx";
import DeleteModalWithConfirm from "metabase/components/DeleteModalWithConfirm.jsx";
import {
......@@ -20,6 +22,7 @@ import {
import _ from "underscore";
import cx from "classnames";
import { inflect } from "inflection";
export default class PulseEdit extends Component {
constructor(props) {
......@@ -42,7 +45,7 @@ export default class PulseEdit extends Component {
async save() {
await this.props.dispatch(saveEditingPulse());
this.props.onChangeLocation("/pulse/"+this.props.pulse.id);
this.props.onChangeLocation("/pulse");
}
async delete() {
......@@ -87,10 +90,14 @@ export default class PulseEdit extends Component {
}
getConfirmItems() {
return [
"This pulse will no longer be emailed to 2 addresses Weekly on Mondays at 8:00 am",
"Slack channel #general will no longer get this pulse every day at 8:00 am."
];
return this.props.pulse.channels.map(c =>
c.channel_type === "email" ?
<span>This pulse will no longer be emailed to <strong>{c.recipients.length} {inflect("address", c.recipients.length)}</strong> <strong>{c.schedule_type}</strong>.</span>
: c.channel_type === "slack" ?
<span>Slack channel <strong>{c.details.channel}</strong> will no longer get this pulse <strong>{c.schedule_type}</strong>.</span>
:
<span>Channel <strong>{c.channel_type}</strong> will no longer receive this pulse <strong>{c.schedule_type}</strong>.</span>
);
}
render() {
......@@ -99,14 +106,29 @@ export default class PulseEdit extends Component {
<div className="PulseEdit">
<div className="PulseEdit-header flex align-center border-bottom py3">
<h1>{pulse && pulse.id != null ? "Edit" : "New"} pulse</h1>
<a className="text-brand text-bold flex-align-right">What's a pulse?</a>
<ModalWithTrigger
ref="pulseInfo"
className="Modal WhatsAPulseModal"
triggerElement="What's a Pulse?"
triggerClasses="text-brand text-bold flex-align-right"
>
<ModalContent
closeFn={() => this.refs.pulseInfo.close()}
>
<div className="mx4 mb4">
<WhatsAPulse
button={<button className="Button Button--primary" onClick={() => this.refs.pulseInfo.close()}>Got it</button>}
/>
</div>
</ModalContent>
</ModalWithTrigger>
</div>
<div className="PulseEdit-content pt2">
<PulseEditName {...this.props} setPulse={this.setPulse} />
<PulseEditCards {...this.props} setPulse={this.setPulse} />
<PulseEditChannels {...this.props} setPulse={this.setPulse} />
{ pulse && pulse.id != null &&
<div className="mb2 rounded bordered p2 border-error relative">
<div className="DangerZone mb2 p2 rounded bordered relative">
<h3 className="text-error absolute top bg-white px1" style={{ marginTop: "-12px" }}>Danger Zone</h3>
<div className="">
<h4 className="text-bold mb1">Delete this pulse</h4>
......
......@@ -43,7 +43,7 @@ export default class PulseEditCards extends Component {
<ol className="my3">
{cards && pulseCards.map((card, index) =>
<li key={index} className="my1 flex align-top" style={{ width: "400px" }}>
<span className="h3 text-bold mr1 mt1">{index + 1}.</span>
<span className="h3 text-bold mr1 mt2">{index + 1}.</span>
{ card ?
<PulseCardPreview
card={card}
......
......@@ -5,7 +5,12 @@ import SchedulePicker from "./SchedulePicker.jsx";
import Select from "metabase/components/Select.jsx";
import Toggle from "metabase/components/Toggle.jsx";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
import Icon from "metabase/components/Icon.jsx";
const CHANNEL_ICONS = {
email: "mail",
slack: "slack"
};
export default class PulseEditChannels extends Component {
constructor(props) {
......@@ -92,7 +97,7 @@ export default class PulseEditChannels extends Component {
renderChannel(channel, index, channelSpec) {
return (
<li key={index} className="py1">
<li key={index} className="py2">
{ channelSpec.recipients &&
<div>
<div className="h4 text-bold mb1">To:</div>
......@@ -107,47 +112,51 @@ export default class PulseEditChannels extends Component {
{ channelSpec.fields &&
this.renderFields(channel, index, channelSpec)
}
<SchedulePicker
channel={channel}
channelSpec={channelSpec}
onPropertyChange={this.onChannelPropertyChange.bind(this, index)}
/>
{ channelSpec.schedules &&
<SchedulePicker
channel={channel}
channelSpec={channelSpec}
onPropertyChange={this.onChannelPropertyChange.bind(this, index)}
/>
}
</li>
);
}
renderChannelSection(channelSpec) {
let { pulse } = this.props;
let channels = pulse.channels
.map((c, index) => c.channel_type === channelSpec.type ? this.renderChannel(c, index, channelSpec) : null)
.filter(e => !!e);
return (
<li key={channelSpec.type} className="py2 border-row-divider">
<div className="flex align-center mb1">
<li key={channelSpec.type} className="border-row-divider">
<div className="flex align-center p3 border-row-divider">
{CHANNEL_ICONS[channelSpec.type] && <Icon className="mr1 text-grey-2" name={CHANNEL_ICONS[channelSpec.type]} width={28} />}
<h2>{channelSpec.name}</h2>
<Toggle className="flex-align-right" value={pulse.channels.some(c => c.channel_type === channelSpec.type)} onChange={this.toggleChannel.bind(this, channelSpec.type)} />
</div>
<ul>
{pulse.channels.map((channel, index) =>
channel.channel_type === channelSpec.type &&
this.renderChannel(channel, index, channelSpec)
)}
</ul>
{channels.length > 0 &&
<ul className="bg-grey-0 px3">{channels}</ul>
}
</li>
)
}
render() {
let { formInput } = this.props;
// Default to show the default channels until full formInput is loaded
let channels = formInput.channels || {
email: { name: "Email", type: "email" },
slack: { name: "Slack", type: "slack" }
};
return (
<div className="py1">
<h2>Where should this data go?</h2>
<LoadingAndErrorWrapper loading={!formInput.channels}>
{() =>
<ul>
{Object.values(formInput.channels).map(channelSpec =>
this.renderChannelSection(channelSpec)
)}
</ul>
}
</LoadingAndErrorWrapper>
<div className="py1 mb4">
<h2 className="mb3">Where should this data go?</h2>
<ul className="bordered rounded">
{Object.values(channels).map(channelSpec =>
this.renderChannelSection(channelSpec)
)}
</ul>
</div>
);
}
......
import React, { Component, PropTypes } from "react";
import PulseListItem from "./PulseListItem.jsx";
import WhatsAPulse from "./WhatsAPulse.jsx";
import { fetchPulses, fetchPulseFormInput } from "../actions";
......@@ -20,24 +21,32 @@ export default class PulseList extends Component {
render() {
let { pulses } = this.props;
return (
<div className="pt3">
<div className="PulseList pt3">
<div className="border-bottom mb2">
<div className="wrapper wrapper--trim flex align-center mb2">
<h1>Pulses</h1>
<a href="/pulse/create" className="Button flex-align-right">Create a pulse</a>
<a href="/pulse/create" className="PulseButton Button flex-align-right">Create a pulse</a>
</div>
</div>
<ul className="wrapper wrapper--trim">
{pulses && pulses.map(pulse =>
<li key={pulse.id}>
<PulseListItem
pulse={pulse}
formInput={this.props.formInput}
dispatch={this.props.dispatch}
/>
</li>
)}
</ul>
{ pulses && pulses.length > 0 ?
<ul className="wrapper wrapper--trim">
{pulses.map(pulse =>
<li key={pulse.id}>
<PulseListItem
pulse={pulse}
formInput={this.props.formInput}
dispatch={this.props.dispatch}
/>
</li>
)}
</ul>
:
<div className="mt4">
<WhatsAPulse
button={<a href="/pulse/create" className="Button Button--primary">Create a pulse</a>}
/>
</div>
}
</div>
);
}
......
......@@ -54,10 +54,10 @@ export default class PulseListChannel extends Component {
let channelTarget = channel.recipients && (channel.recipients.length + " " + inflect("people", channel.recipients.length));
if (channel.channel_type === "email") {
channelIcon = "close";
channelIcon = "mail";
channelVerb = "Emailed";
} else if (channel.channel_type === "slack") {
channelIcon = "close";
channelIcon = "slack";
channelVerb = "Slack'd";
channelTarget = channel.details.channel;
}
......
......@@ -12,14 +12,15 @@ export default class PulseListItem extends Component {
let { pulse, formInput } = this.props;
return (
<div className="bordered rounded mb2 pt3">
<div className="PulseListItem bordered rounded mb2 pt3">
<div className="flex px4 mb2">
<div>
<h2 className="mb1">
<a className="no-decoration" href={"/pulse/" + pulse.id}>{pulse.name}</a>
</h2>
<h2 className="mb1">{pulse.name}</h2>
<span>Pulse by <span className="text-bold">{pulse.creator && pulse.creator.common_name}</span></span>
</div>
<div className="flex-align-right">
<a className="PulseEditButton PulseButton Button no-decoration text-bold" href={"/pulse/" + pulse.id}>Edit</a>
</div>
</div>
<ol className="mb2 px4 flex">
{ pulse.cards.map((card, index) =>
......@@ -30,7 +31,7 @@ export default class PulseListItem extends Component {
</li>
)}
</ol>
<ul className="border-top px4" style={{backgroundColor: "rgb(252,252,253)"}}>
<ul className="border-top px4 bg-grey-0">
{pulse.channels.map(channel =>
<li className="border-row-divider">
<PulseListChannel
......
......@@ -16,7 +16,8 @@ export default class RecipientPicker extends Component {
this.state = {
inputValue: "",
filteredUsers: [],
selectedUser: null
selectedUser: null,
focused: props.recipients.length === 0
};
_.bindAll(this, "onMouseDownCapture", "onInputChange", "onInputKeyDown", "onInputFocus", "onInputBlur");
......@@ -152,9 +153,10 @@ export default class RecipientPicker extends Component {
ref="input"
type="text"
className="full h4 text-bold text-default no-focus borderless"
style={{"backgroundColor": "transparent"}}
placeholder={recipients.length === 0 ? "Enter email addresses you'd like this data to go to" : null}
autoFocus
value={this.state.inputValue}
autoFocus={this.state.focused}
onKeyDown={this.onInputKeyDown}
onChange={this.onInputChange}
onFocus={this.onInputFocus}
......
import React, { Component, PropTypes } from "react";
import RetinaImage from "react-retina-image";
export default class WhatsAPulse extends Component {
static propTypes = {
button: PropTypes.object
};
render() {
return (
<div className="flex flex-column align-center px4">
<div className="h2 mb4 text-centered text-brand text-bold">
Help everyone on your team stay in sync with your data.
</div>
<div className="mx4">
<RetinaImage
width={574}
src="/app/img/pulse_empty_illustration.png"
forceOriginalDimensions={false}
/>
</div>
<div className="h3 my3 text-centered text-grey-2 text-bold" style={{maxWidth: "500px"}}>
Pulses let you send data from Metabase to email or Slack on the schedule of your choice.
</div>
{this.props.button}
</div>
);
}
}
resources/frontend_client/app/img/pulse_empty_illustration.png

29 KiB

resources/frontend_client/app/img/pulse_empty_illustration@2x.png

72.9 KiB

resources/frontend_client/app/img/slack.png

1.58 KiB

resources/frontend_client/app/img/slack@2x.png

3.47 KiB

0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment