Skip to content
Snippets Groups Projects
Commit dcb9f937 authored by Allen Gilliland's avatar Allen Gilliland
Browse files

Merge branch 'pulses' of github.com:metabase/metabase into pulses

parents 3770ac9c aeeda6c2
No related branches found
No related tags found
No related merge requests found
Showing
with 585 additions and 224 deletions
import { createAction } from "redux-actions";
import { AngularResourceProxy, createThunkAction } from "metabase/lib/redux";
import { normalize, Schema, arrayOf } from "normalizr";
import moment from "moment";
const card = new Schema('card');
const pulse = new Schema('pulse');
// pulse.define({
// cards: arrayOf(card)
// });
const Pulse = new AngularResourceProxy("Pulse", ["list", "get", "create", "update"]);
const Card = new AngularResourceProxy("Card", ["list"]);
export const FETCH_PULSES = 'FETCH_PULSES';
export const SET_EDITING_PULSE = 'SET_EDITING_PULSE';
export const UPDATE_EDITING_PULSE = 'UPDATE_EDITING_PULSE';
export const SAVE_EDITING_PULSE = 'SAVE_EDITING_PULSE';
export const FETCH_CARDS = 'FETCH_CARDS';
export const fetchPulses = createThunkAction(FETCH_PULSES, function() {
return async function(dispatch, getState) {
let pulses = await Pulse.list();
return normalize(pulses, arrayOf(pulse));
};
});
export const setEditingPulse = createThunkAction(SET_EDITING_PULSE, function(id) {
return async function(dispatch, getState) {
if (id != null) {
try {
return await Pulse.get({ pulseId: id });
} catch (e) {
}
}
return {
name: null,
cards: [],
channels: []
}
};
});
export const updateEditingPulse = createAction(UPDATE_EDITING_PULSE);
export const saveEditingPulse = createThunkAction(SAVE_EDITING_PULSE, function() {
return async function(dispatch, getState) {
let { editingPulse } = getState();
if (editingPulse.id != null) {
return await Pulse.update(editingPulse);
} else {
return await Pulse.create(editingPulse);
}
};
});
// NOTE: duplicated from dashboards/actions.js
export const fetchCards = createThunkAction(FETCH_CARDS, function(filterMode = "all") {
return async function(dispatch, getState) {
let cards = await Card.list({ filterMode });
for (var c of cards) {
c.updated_at = moment(c.updated_at);
}
return normalize(cards, arrayOf(card));
};
});
import React, { Component, PropTypes } from "react";
import Icon from "metabase/components/Icon.jsx";
import _ from "underscore";
export default class PulseCardPreview extends Component {
constructor(props, context) {
super(props, context);
this.state = {
height: 200
};
_.bindAll(this, "onFrameLoad");
}
static propTypes = {
card: PropTypes.object.isRequired,
onRemove: PropTypes.func.isRequired
};
onFrameLoad() {
// set the height based on the content
try {
let height = React.findDOMNode(this.refs.iframe).contentWindow.document.body.scrollHeight;
this.setState({ height });
} catch (e) {
}
}
render() {
let { card } = this.props;
return (
<div className="flex relative flex-full">
<a className="text-grey-2 absolute" style={{ top: "15px", right: "15px" }} onClick={this.props.onRemove}>
<Icon name="close" width={16} height={16} />
</a>
<iframe className="bordered rounded flex-full" height={this.state.height} ref="iframe" src={"/api/pulse/preview_card/" + card.id} onLoad={this.onFrameLoad} />
</div>
);
}
}
import React, { Component, PropTypes } from "react";
import PulseEditName from "./PulseEditName.jsx";
import PulseEditCard from "./PulseEditCard.jsx";
import PulseEditChannel from "./PulseEditChannel.jsx";
import ActionButton from "metabase/components/ActionButton.jsx";
import {
setEditingPulse,
updateEditingPulse,
saveEditingPulse,
fetchCards
} from "../actions";
import _ from "underscore";
export default class PulseEdit extends Component {
constructor(props) {
super(props);
_.bindAll(this, "save", "setPulse");
}
static propTypes = {
pulses: PropTypes.object,
pulseId: PropTypes.number
};
componentDidMount() {
this.props.dispatch(setEditingPulse(this.props.pulseId));
this.props.dispatch(fetchCards());
}
async save() {
await this.props.dispatch(saveEditingPulse());
}
setPulse(pulse) {
this.props.dispatch(updateEditingPulse(pulse));
}
render() {
let { pulse } = this.props;
return (
<div className="wrapper">
<div className="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>
</div>
<PulseEditName {...this.props} setPulse={this.setPulse} />
<PulseEditCard {...this.props} setPulse={this.setPulse} />
<PulseEditChannel {...this.props} setPulse={this.setPulse} />
<div className="flex align-center border-top py3">
<ActionButton
actionFn={this.save}
className="Button Button--primary"
normalText={pulse.id != null ? "Save pulse" : "Create pulse"}
activeText="Saving…"
failedText="Save failed"
successText="Saved"
/>
<a className="text-bold flex-align-right" href="/pulse">Cancel</a>
</div>
</div>
);
}
}
import React, { Component, PropTypes } from "react";
import PulseCardPreview from "./PulseCardPreview.jsx";
import Select from "metabase/components/Select.jsx";
export default class PulseEditCard extends Component {
constructor(props) {
super(props);
this.state = {};
}
static propTypes = {};
static defaultProps = {};
setCard(index, cardId) {
let { pulse } = this.props;
this.props.setPulse({
...pulse,
cards: [...pulse.cards.slice(0, index), { id: cardId }, ...pulse.cards.slice(index + 1)]
});
}
removeCard(index) {
let { pulse } = this.props;
this.props.setPulse({
...pulse,
cards: [...pulse.cards.slice(0, index), ...pulse.cards.slice(index + 1)]
});
}
render() {
let { pulse, cards, cardList } = this.props;
let pulseCards = pulse ? pulse.cards.slice() : [];
if (pulseCards.length < 5) {
pulseCards.push(null);
}
return (
<div className="py4">
<h2>Pick your data</h2>
<p>Pick up to five questions you'd like to send in this pulse</p>
<ol className="my3">
{cards && pulseCards.map((card, index) =>
<li className="my1 flex align-top" style={{ width: "400px" }}>
<span className="h3 text-bold mr1 mt1">{index + 1}.</span>
{ card ?
<PulseCardPreview card={card} onRemove={this.removeCard.bind(this, index)} />
:
<Select
className="flex-full"
placeholder="Pick a question to include in this pulse"
value={card && cards[card.id]}
options={cardList}
optionNameFn={o => o.name}
optionValueFn={o => o.id}
onChange={this.setCard.bind(this, index)}
/>
}
</li>
)}
</ol>
</div>
);
}
}
import React, { Component, PropTypes } from "react";
import Select from "metabase/components/Select.jsx";
import CheckBox from "metabase/components/CheckBox.jsx";
// import CheckBox from "metabase/components/CheckBox.jsx";
import cx from "classnames";
const SCHEDULE_NAMES = {
"hourly": (<span>Hour<br /></span>),
"daily": (<span>Day<br />(8 am every week day)</span>),
"weekly": (<span>Week<br />(8 am on Mondays)</span>)
};
// const SCHEDULE_NAMES = {
// "hourly": (<span>Hour<br /></span>),
// "daily": (<span>Day<br />(8 am every week day)</span>),
// "weekly": (<span>Week<br />(8 am on Mondays)</span>)
// };
//
// function getScheduleField(options) {
// return {
// name: "schedule",
// displayName: "Send every",
// type: "select-button",
// options: options.map(o => ({ name: SCHEDULE_NAMES[o], value: o })),
// required: true
// };
// }
function getScheduleField(options) {
return {
name: "schedule",
displayName: "Send every",
type: "select-button",
options: options.map(o => ({ name: SCHEDULE_NAMES[o], value: o })),
required: true
};
}
const CHANNELS = {
"email": {
displayName: "Email",
fields: [
{
name: "recipients",
displayName: "Send to",
multi: true,
type: "email",
placeholder: "Enter email address these questions should be sent to",
required: true
},
getScheduleField(["daily", "weekly"])
]
},
"slack": {
displayName: "Slack",
fields: [
{
name: "channel",
displayName: "Send to",
multi: false,
type: "select",
options: ["#general", "#random", "#ios"],
required: true
},
getScheduleField(["hourly", "daily"])
]
}
};
// const CHANNELS = {
// "email": {
// displayName: "Email",
// fields: [
// {
// name: "recipients",
// displayName: "Send to",
// multi: true,
// type: "email",
// placeholder: "Enter email address these questions should be sent to",
// required: true
// },
// getScheduleField(["daily", "weekly"])
// ]
// },
// "slack": {
// displayName: "Slack",
// fields: [
// {
// name: "channel",
// displayName: "Send to",
// multi: false,
// type: "select",
// options: ["#general", "#random", "#ios"],
// required: true
// },
// getScheduleField(["hourly", "daily"])
// ]
// }
// };
export default class PulseModalChannelPane extends Component {
export default class PulseEditChannel extends Component {
constructor(props) {
super(props);
this.state = {};
......@@ -120,53 +120,52 @@ export default class PulseModalChannelPane extends Component {
}
render() {
let { pulse } = this.props;
console.log("pulse", pulse);
// let { pulse } = this.props;
let indexesForChannel = {};
for (let [index, channel] of Object.entries(pulse.channels)) {
indexesForChannel[channel.type] = indexesForChannel[channel.type] || []
indexesForChannel[channel.type].push(index);
}
// let indexesForChannel = {};
// for (let [index, channel] of Object.entries(pulse.channels)) {
// indexesForChannel[channel.type] = indexesForChannel[channel.type] || []
// indexesForChannel[channel.type].push(index);
// }
let channels = [];
Object.entries(CHANNELS).map(([type, CHANNEL]) => {
if (indexesForChannel[type]) {
indexesForChannel[type].map(index => {
let channel = pulse.channels[index];
channels.push(
<div>
<div className="flex align-center">
<CheckBox checked={true} onChange={this.removeChannel.bind(this, index)} />
<h3 className="ml1">{CHANNEL.displayName}</h3>
</div>
<ul className="ml3" >
{CHANNEL.fields.map(field =>
<li className="py1" key={field.name}>
<h4 className="py1">{field.displayName}</h4>
<div>{this.renderField(field, channel, index)}</div>
</li>
)}
</ul>
</div>
)
});
} else {
channels.push(
<div className="flex align-center">
<CheckBox checked={false} onChange={this.addChannel.bind(this, type)} />
<h3 className="ml1">{CHANNEL.displayName}</h3>
</div>
)
}
});
// Object.entries(CHANNELS).map(([type, CHANNEL]) => {
// if (indexesForChannel[type]) {
// indexesForChannel[type].map(index => {
// let channel = pulse.channels[index];
// channels.push(
// <div>
// <div className="flex align-center">
// <CheckBox checked={true} onChange={this.removeChannel.bind(this, index)} />
// <h3 className="ml1">{CHANNEL.displayName}</h3>
// </div>
// <ul className="ml3" >
// {CHANNEL.fields.map(field =>
// <li className="py1" key={field.name}>
// <h4 className="py1">{field.displayName}</h4>
// <div>{this.renderField(field, channel, index)}</div>
// </li>
// )}
// </ul>
// </div>
// )
// });
// } else {
// channels.push(
// <div className="flex align-center">
// <CheckBox checked={false} onChange={this.addChannel.bind(this, type)} />
// <h3 className="ml1">{CHANNEL.displayName}</h3>
// </div>
// )
// }
// });
return (
<div className="py4 flex flex-column align-center">
<h3>Send via</h3>
<ul className="mt2 bordered rounded">
<div className="py4">
<h2>Where should this data go?</h2>
<ul>
{channels.map(channel =>
<li className="border-row-divider p2">{channel}</li>
<li className="my2">{channel}</li>
)}
</ul>
</div>
......
......@@ -2,7 +2,7 @@ import React, { Component, PropTypes } from "react";
import _ from "underscore";
export default class PulseModalNamePane extends Component {
export default class PulseEditName extends Component {
constructor(props) {
super(props);
this.state = {};
......@@ -21,12 +21,12 @@ export default class PulseModalNamePane extends Component {
render() {
let { pulse } = this.props;
return (
<div className="py4 flex flex-column align-center">
<h3>Name your pulse</h3>
<div className="py4">
<h2>Name your pulse</h2>
<p>Give your pulse a name to help others understand what it's about.</p>
<div className="my3">
<input className="input" value={pulse.name} onChange={this.setName} />
</div>
<p>A pulse is away for you to send answers to people outside Metabase on a schedule. Start by giving it a name so people will know what they're getting.</p>
</div>
);
}
......
import React, { Component, PropTypes } from "react";
import PulseListItem from "./PulseListItem.jsx";
import PulseModal from "./PulseModal.jsx";
import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
import { fetchPulses, savePulse, createPulse } from "../actions";
import _ from "underscore";
const DEMO_PULSES = [
{ id: 0,
name: "October Growth Sprint",
creator: "Jack Mullis",
cards: ["Saves per day", "Average daily saves with users with 10+ followees"],
channels: [
{ type: "email", schedule: "daily", subscribers: [] }
]
},
{ id: 1,
name: "3 things that matter",
creator: "Luke Groesbeck",
cards: ["DAU/MAU", "Spots", "Waitlist emails"],
channels: [
{ type: "email", schedule: "daily", subscribers: ["tom@metabase.com"] }
],
subscribed: true
},
{ id: 2,
name: "IOS pulse",
creator: "Jack Mullis",
cards: ["Bugsnag Reports", "App Exceptions / HR", "Bugsnag - Crashes"],
channels: [
{ type: "email", schedule: "hourly", subscribers: [] },
{ type: "slack", schedule: "hourly", channel: "#ios" }
]
}
];
// const DEMO_PULSES = [
// { id: 0,
// name: "October Growth Sprint",
// creator: "Jack Mullis",
// cards: ["Saves per day", "Average daily saves with users with 10+ followees"],
// channels: [
// { type: "email", schedule: "daily", subscribers: [] }
// ]
// },
// { id: 1,
// name: "3 things that matter",
// creator: "Luke Groesbeck",
// cards: ["DAU/MAU", "Spots", "Waitlist emails"],
// channels: [
// { type: "email", schedule: "daily", subscribers: ["tom@metabase.com"] }
// ],
// subscribed: true
// },
// { id: 2,
// name: "IOS pulse",
// creator: "Jack Mullis",
// cards: ["Bugsnag Reports", "App Exceptions / HR", "Bugsnag - Crashes"],
// channels: [
// { type: "email", schedule: "hourly", subscribers: [] },
// { type: "slack", schedule: "hourly", channel: "#ios" }
// ]
// }
// ];
export default class PulseList extends Component {
constructor(props) {
super(props);
this.state = {
pulses: [...DEMO_PULSES]
};
// this.state = {
// pulses: [...DEMO_PULSES]
// };
_.bindAll(this, "onSave");
}
......@@ -49,37 +48,28 @@ export default class PulseList extends Component {
static propTypes = {};
static defaultProps = {};
componentDidMount() {
this.props.dispatch(fetchPulses())
}
onSave(pulse) {
if (pulse.id != null) {
let pulses = [...this.state.pulses];
pulses[pulse.id] = pulse;
this.setState({ pulses });
this.props.dispatch(savePulse(pulse));
} else {
pulse.id = this.state.pulses.length;
let pulses = [...this.state.pulses, pulse];
this.setState({ pulses });
this.props.dispatch(createPulse(pulse));
}
}
render() {
let { pulses } = this.state;
let { pulses } = this.props;
return (
<div className="wrapper pt3">
<div className="flex align-center mb2">
<h1>Pulses</h1>
<ModalWithTrigger
ref="createPulseModal"
triggerClasses="Button flex-align-right"
triggerElement="Create a pulse"
>
<PulseModal
onClose={() => this.refs.createPulseModal.close()}
onSave={this.onSave}
/>
</ModalWithTrigger>
<a href="/pulse/create" className="Button flex-align-right">Create a pulse</a>
</div>
<ul>
{pulses.map(pulse =>
{pulses && pulses.map(pulse =>
<li key={pulse.id}>
<PulseListItem
pulse={pulse}
......
import React, { Component, PropTypes } from "react";
import PulseModal from "./PulseModal.jsx";
import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
import _ from "underscore";
function formatSchedule(channels) {
if (!channels) { return "nyi" };
let types = {};
for (let c of channels) {
types[c.type] = types[c.type] || [];
......@@ -41,13 +38,12 @@ export default class PulseListItem extends Component {
render() {
let { pulse } = this.props;
return (
<div className="bordered rounded mb3 px4 py3">
<div className="flex mb2">
<div>
<h2 className="mb1">{pulse.name}</h2>
<span>Pulse by <span className="text-bold">{pulse.creator}</span></span>
<span>Pulse by <span className="text-bold">{pulse.creator && pulse.creator.common_name}</span></span>
</div>
<div className="flex-align-right">
{ pulse.subscribed ?
......@@ -55,23 +51,13 @@ export default class PulseListItem extends Component {
:
<button className="Button">Get this pulse</button>
}
<ModalWithTrigger
ref="editPulseModal"
triggerClasses="Button ml1"
triggerElement="Edit"
>
<PulseModal
pulse={pulse}
onClose={() => this.refs.editPulseModal.close()}
onSave={this.props.onSave}
/>
</ModalWithTrigger>
<a href={"/pulse/" + pulse.id} className="Button ml1">Edit</a>
</div>
</div>
<ol className="mb2">
{ pulse.cards.map(card =>
<li className="Button mr1">
{card}
{card.name}
</li>
)}
</ol>
......
import React, { Component, PropTypes } from "react";
import PulseModalNamePane from "./PulseModalNamePane.jsx";
import PulseModalCardPane from "./PulseModalCardPane.jsx";
import PulseModalChannelPane from "./PulseModalChannelPane.jsx";
import ModalContent from "metabase/components/ModalContent.jsx";
import cx from "classnames";
import _ from "underscore";
export default class PulseModal extends Component {
constructor(props) {
super(props);
this.state = {
step: props.initialStep || 0,
pulse: props.pulse || {
id: null,
name: null,
creator: null,
cards: [],
channels: []
}
};
_.bindAll(this, "close", "back", "next", "save", "setPulse");
}
static propTypes = {
onClose: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
pulse: PropTypes.object,
initialStep: PropTypes.number
};
close() {
this.props.onClose();
}
save() {
this.props.onSave(this.state.pulse);
this.props.onClose();
}
back() {
this.setState({ step: this.state.step - 1 });
}
next() {
this.setState({ step: this.state.step + 1 });
}
setPulse(pulse) {
this.setState({ pulse });
}
stepIsValid(step) {
let { pulse } = this.state;
switch (step) {
case 0: return !!pulse.name;
case 1: return pulse.cards.length > 0;
case 2: return pulse.channels.length > 0;
}
}
render() {
let { step, pulse } = this.state;
return (
<ModalContent
title="New pulse"
closeFn={this.close}
>
<div className="flex bordered mx4">
<a className="p2 text-centered text-bold flex-full">Name your pulse</a>
<a className="p2 text-centered text-bold flex-full">What should we send?</a>
<a className="p2 text-centered text-bold flex-full">Where and when?</a>
</div>
<div className="mx4">
{ step === 0 ? <PulseModalNamePane pulse={pulse} setPulse={this.setPulse} />
: step === 1 ? <PulseModalCardPane pulse={pulse} setPulse={this.setPulse} />
: step === 2 ? <PulseModalChannelPane pulse={pulse} setPulse={this.setPulse} />
: <div className="text-error text-centered my4">Error</div> }
</div>
<div className="flex align-center m4">
{ step === 0 ?
<a className="text-bold" onClick={this.close}>Cancel</a>
:
<a className="text-bold" onClick={this.back}>Back</a>
}
<div className="flex-align-right">
{ step === 2 ?
<a className="Button Button--primary" onClick={this.save}>Done</a>
:
<a className={cx("Button Button--primary", { "disabled": !this.stepIsValid(step) })} onClick={this.next}>Next</a>
}
</div>
</div>
</ModalContent>
);
}
}
import React, { Component, PropTypes } from "react";
import Select from "metabase/components/Select.jsx";
export default class PulseModalCardPane extends Component {
constructor(props) {
super(props);
this.state = {};
}
static propTypes = {};
static defaultProps = {};
setCard(index, card) {
let { pulse } = this.props;
let cards = [...pulse.cards];
cards[index] = card;
this.props.setPulse({ ...pulse, cards: cards })
}
render() {
let { pulse } = this.props;
return (
<div className="py4 flex flex-column align-center">
<h3>What should we send?</h3>
<ol className="my3">
{[1,2,3,4,5].map((n, i) =>
<li className="my1 flex align-center">
<span className="h3 text-bold mr1">{n}.</span>
<Select
value={pulse.cards[i]}
options={["", "a", "b", "c"]}
optionNameFn={o => o}
optionValueFn={o => o}
onChange={this.setCard.bind(this, i)}
/>
</li>
)}
</ol>
</div>
);
}
}
import React, { Component, PropTypes } from "react";
import { connect } from "react-redux";
import PulseEdit from "../components/PulseEdit.jsx";
import { editPulseSelectors } from "../selectors";
@connect(editPulseSelectors)
export default class PulseEditApp extends Component {
render() {
return (
<PulseEdit { ...this.props } />
);
}
}
......@@ -2,10 +2,10 @@ import React, { Component, PropTypes } from "react";
import { connect } from "react-redux";
import PulseList from "../components/PulseList.jsx";
import { pulseSelectors } from "../selectors";
import { listPulseSelectors } from "../selectors";
@connect(pulseSelectors)
export default class PulseApp extends Component {
@connect(listPulseSelectors)
export default class PulseListApp extends Component {
render() {
return (
<PulseList { ...this.props } />
......
......@@ -2,7 +2,9 @@ import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import promiseMiddleware from 'redux-promise';
import thunkMidleware from "redux-thunk";
import PulseApp from './containers/PulseApp.jsx';
import PulseListApp from './containers/PulseListApp.jsx';
import PulseEditApp from './containers/PulseEditApp.jsx';
import * as reducers from './reducers';
const finalCreateStore = compose(
......@@ -22,10 +24,34 @@ Pulse.config(['$routeProvider', function ($routeProvider) {
template: '<div mb-redux-component class="flex flex-column flex-full" />',
controller: ['$scope', '$location', '$route', '$routeParams',
function($scope, $location, $route, $routeParams) {
$scope.Component = PulseApp;
$scope.Component = PulseListApp;
$scope.props = {};
$scope.store = finalCreateStore(reducer, {});
}
]
});
$routeProvider.when('/pulse/create', {
template: '<div mb-redux-component class="flex flex-column flex-full" />',
controller: ['$scope', '$location', '$route', '$routeParams',
function($scope, $location, $route, $routeParams) {
$scope.Component = PulseEditApp;
$scope.props = {};
$scope.store = finalCreateStore(reducer, {});
}
]
});
$routeProvider.when('/pulse/:pulseId', {
template: '<div mb-redux-component class="flex flex-column flex-full" />',
controller: ['$scope', '$location', '$route', '$routeParams',
function($scope, $location, $route, $routeParams) {
$scope.Component = PulseEditApp;
$scope.props = {
pulseId: $routeParams.pulseId
};
$scope.store = finalCreateStore(reducer, {});
}
]
});
}]);
import { handleActions } from 'redux-actions';
import {
FETCH_PULSES,
SET_EDITING_PULSE,
UPDATE_EDITING_PULSE,
SAVE_EDITING_PULSE,
FETCH_CARDS
} from "./actions";
export const pulses = handleActions({
[FETCH_PULSES]: { next: (state, { payload }) => ({ ...payload.entities.pulse }) },
[SAVE_EDITING_PULSE]: { next: (state, { payload }) => ({ ...state, [payload.id]: payload }) }
}, {});
export const pulseList = handleActions({
[FETCH_PULSES]: { next: (state, { payload }) => payload.result },
// [DELETE_PULSE]: { next: (state, { payload }) => state }
}, null);
export const editingPulse = handleActions({
[SET_EDITING_PULSE]: { next: (state, { payload }) => payload },
[UPDATE_EDITING_PULSE]: { next: (state, { payload }) => payload },
[SAVE_EDITING_PULSE]: { next: (state, { payload }) => payload }
}, { name: null, cards: [], channels: [] });
// NOTE: duplicated from dashboards/reducers.js
export const cards = handleActions({
[FETCH_CARDS]: { next: (state, { payload }) => ({ ...payload.entities.card }) }
}, {});
export const cardList = handleActions({
[FETCH_CARDS]: { next: (state, { payload }) => payload.result }
}, []);
import { createSelector } from 'reselect';
// LIST
const pulsesSelector = state => state.pulses;
const pulseIdListSelector = state => state.pulseList;
const pulseListSelector = createSelector(
[pulseIdListSelector, pulsesSelector],
(pulseIdList, pulses) => pulseIdList && pulseIdList.map(id => pulses[id])
);
export const listPulseSelectors = createSelector(
[pulseListSelector],
(pulses) => ({ pulses })
);
// EDIT
const editingPulseSelector = state => state.editingPulse;
const cardsSelector = state => state.cards
const cardIdListSelector = state => state.cardList
const cardListSelector = createSelector(
[cardIdListSelector, cardsSelector],
(cardIdList, cards) => cardIdList && cardIdList.map(id => cards[id])
);
export const editPulseSelectors = createSelector(
[editingPulseSelector, cardsSelector, cardListSelector],
(pulse, cards, cardList) => ({ pulse, cards, cardList })
);
......@@ -10,7 +10,7 @@
[:td {:style td-style} (first row)]
[:td {:style (str td-style " font-weight: bolder;")} (second row)]
[:td {:style td-style}
[:div {:style (str "background-color: rgb(135, 93, 175); height: 40px; width: " (float (* 100 (/ (second row) max-value))) "%")} "&nbsp;"]]])
[:div {:style (str "background-color: rgb(135, 93, 175); height: 20px; width: " (float (* 100 (/ (second row) max-value))) "%")} "&nbsp;"]]])
(defn bar-chart
[data]
......
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