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

Merge branch 'master' of github.com:metabase/metabase-init into new_filters

parents 8fa6c446 76c92a39
Branches
Tags
No related merge requests found
Showing
with 5566 additions and 2009 deletions
This diff is collapsed.
'use strict';
var CardDirectives = angular.module('metabase.card.directives', []);
CardDirectives.directive('mbLatlongHeatmap', ['CardRenderer', function(CardRenderer) {
function link(scope, element, attr) {
scope.$watch('mbLatlongHeatmap', function(value) {
if (value) {
CardRenderer.latlongHeatmap('map-canvas', 'whatever', value);
}
});
}
return {
restrict: 'A',
scope: {
mbLatlongHeatmap: '='
},
link: link
};
}]);
/*global setTimeout */
var CardDirectives = angular.module('metabase.card.directives', []);
CardDirectives.directive('mbCardFavoriteButton', ['Card', function(Card) {
......
'use strict';
import React, { Component, PropTypes } from "react";
export default class LogoIcon extends React.Component {
render() {
let { height, width } = this.props;
return (
<svg className={this.props.className} viewBox="0 0 66 85" width={width} height={height} fill="currentcolor">
<path d="M46.8253288,70.4935014 C49.5764899,70.4935014 51.8067467,68.1774705 51.8067467,65.3205017 C51.8067467,62.4635329 49.5764899,60.147502 46.8253288,60.147502 C44.0741676,60.147502 41.8439108,62.4635329 41.8439108,65.3205017 C41.8439108,68.1774705 44.0741676,70.4935014 46.8253288,70.4935014 Z M32.8773585,84.9779005 C35.6285197,84.9779005 37.8587764,82.6618697 37.8587764,79.8049008 C37.8587764,76.947932 35.6285197,74.6319011 32.8773585,74.6319011 C30.1261973,74.6319011 27.8959405,76.947932 27.8959405,79.8049008 C27.8959405,82.6618697 30.1261973,84.9779005 32.8773585,84.9779005 Z M32.8773585,70.4935014 C35.6285197,70.4935014 37.8587764,68.1774705 37.8587764,65.3205017 C37.8587764,62.4635329 35.6285197,60.147502 32.8773585,60.147502 C30.1261973,60.147502 27.8959405,62.4635329 27.8959405,65.3205017 C27.8959405,68.1774705 30.1261973,70.4935014 32.8773585,70.4935014 Z M18.9293882,70.4935014 C21.6805494,70.4935014 23.9108062,68.1774705 23.9108062,65.3205017 C23.9108062,62.4635329 21.6805494,60.147502 18.9293882,60.147502 C16.1782271,60.147502 13.9479703,62.4635329 13.9479703,65.3205017 C13.9479703,68.1774705 16.1782271,70.4935014 18.9293882,70.4935014 Z M46.8253288,56.0091023 C49.5764899,56.0091023 51.8067467,53.6930714 51.8067467,50.8361026 C51.8067467,47.9791337 49.5764899,45.6631029 46.8253288,45.6631029 C44.0741676,45.6631029 41.8439108,47.9791337 41.8439108,50.8361026 C41.8439108,53.6930714 44.0741676,56.0091023 46.8253288,56.0091023 Z M18.9293882,56.0091023 C21.6805494,56.0091023 23.9108062,53.6930714 23.9108062,50.8361026 C23.9108062,47.9791337 21.6805494,45.6631029 18.9293882,45.6631029 C16.1782271,45.6631029 13.9479703,47.9791337 13.9479703,50.8361026 C13.9479703,53.6930714 16.1782271,56.0091023 18.9293882,56.0091023 Z M46.8253288,26.8995984 C49.5764899,26.8995984 51.8067467,24.5835675 51.8067467,21.7265987 C51.8067467,18.8696299 49.5764899,16.553599 46.8253288,16.553599 C44.0741676,16.553599 41.8439108,18.8696299 41.8439108,21.7265987 C41.8439108,24.5835675 44.0741676,26.8995984 46.8253288,26.8995984 Z M32.8773585,41.5247031 C35.6285197,41.5247031 37.8587764,39.2086723 37.8587764,36.3517034 C37.8587764,33.4947346 35.6285197,31.1787037 32.8773585,31.1787037 C30.1261973,31.1787037 27.8959405,33.4947346 27.8959405,36.3517034 C27.8959405,39.2086723 30.1261973,41.5247031 32.8773585,41.5247031 Z M32.8773585,10.3459994 C35.6285197,10.3459994 37.8587764,8.02996853 37.8587764,5.17299969 C37.8587764,2.31603085 35.6285197,0 32.8773585,0 C30.1261973,0 27.8959405,2.31603085 27.8959405,5.17299969 C27.8959405,8.02996853 30.1261973,10.3459994 32.8773585,10.3459994 Z M32.8773585,26.8995984 C35.6285197,26.8995984 37.8587764,24.5835675 37.8587764,21.7265987 C37.8587764,18.8696299 35.6285197,16.553599 32.8773585,16.553599 C30.1261973,16.553599 27.8959405,18.8696299 27.8959405,21.7265987 C27.8959405,24.5835675 30.1261973,26.8995984 32.8773585,26.8995984 Z M18.9293882,26.8995984 C21.6805494,26.8995984 23.9108062,24.5835675 23.9108062,21.7265987 C23.9108062,18.8696299 21.6805494,16.553599 18.9293882,16.553599 C16.1782271,16.553599 13.9479703,18.8696299 13.9479703,21.7265987 C13.9479703,24.5835675 16.1782271,26.8995984 18.9293882,26.8995984 Z" opacity="0.2"></path>
<path d="M60.773299,70.4935014 C63.5244602,70.4935014 65.754717,68.1774705 65.754717,65.3205017 C65.754717,62.4635329 63.5244602,60.147502 60.773299,60.147502 C58.0221379,60.147502 55.7918811,62.4635329 55.7918811,65.3205017 C55.7918811,68.1774705 58.0221379,70.4935014 60.773299,70.4935014 Z M4.98141795,70.3527958 C7.73257912,70.3527958 9.96283591,68.0367649 9.96283591,65.1797961 C9.96283591,62.3228273 7.73257912,60.0067964 4.98141795,60.0067964 C2.23025679,60.0067964 0,62.3228273 0,65.1797961 C0,68.0367649 2.23025679,70.3527958 4.98141795,70.3527958 Z M60.773299,56.0091023 C63.5244602,56.0091023 65.754717,53.6930714 65.754717,50.8361026 C65.754717,47.9791337 63.5244602,45.6631029 60.773299,45.6631029 C58.0221379,45.6631029 55.7918811,47.9791337 55.7918811,50.8361026 C55.7918811,53.6930714 58.0221379,56.0091023 60.773299,56.0091023 Z M32.8773585,56.0091023 C35.6285197,56.0091023 37.8587764,53.6930714 37.8587764,50.8361026 C37.8587764,47.9791337 35.6285197,45.6631029 32.8773585,45.6631029 C30.1261973,45.6631029 27.8959405,47.9791337 27.8959405,50.8361026 C27.8959405,53.6930714 30.1261973,56.0091023 32.8773585,56.0091023 Z M4.98141795,55.8683967 C7.73257912,55.8683967 9.96283591,53.5523658 9.96283591,50.695397 C9.96283591,47.8384281 7.73257912,45.5223973 4.98141795,45.5223973 C2.23025679,45.5223973 0,47.8384281 0,50.695397 C0,53.5523658 2.23025679,55.8683967 4.98141795,55.8683967 Z M60.773299,41.5247031 C63.5244602,41.5247031 65.754717,39.2086723 65.754717,36.3517034 C65.754717,33.4947346 63.5244602,31.1787037 60.773299,31.1787037 C58.0221379,31.1787037 55.7918811,33.4947346 55.7918811,36.3517034 C55.7918811,39.2086723 58.0221379,41.5247031 60.773299,41.5247031 Z M46.8253288,41.5247031 C49.5764899,41.5247031 51.8067467,39.2086723 51.8067467,36.3517034 C51.8067467,33.4947346 49.5764899,31.1787037 46.8253288,31.1787037 C44.0741676,31.1787037 41.8439108,33.4947346 41.8439108,36.3517034 C41.8439108,39.2086723 44.0741676,41.5247031 46.8253288,41.5247031 Z M60.773299,26.8995984 C63.5244602,26.8995984 65.754717,24.5835675 65.754717,21.7265987 C65.754717,18.8696299 63.5244602,16.553599 60.773299,16.553599 C58.0221379,16.553599 55.7918811,18.8696299 55.7918811,21.7265987 C55.7918811,24.5835675 58.0221379,26.8995984 60.773299,26.8995984 Z M18.9293882,41.5247031 C21.6805494,41.5247031 23.9108062,39.2086723 23.9108062,36.3517034 C23.9108062,33.4947346 21.6805494,31.1787037 18.9293882,31.1787037 C16.1782271,31.1787037 13.9479703,33.4947346 13.9479703,36.3517034 C13.9479703,39.2086723 16.1782271,41.5247031 18.9293882,41.5247031 Z M4.98141795,41.3839975 C7.73257912,41.3839975 9.96283591,39.0679667 9.96283591,36.2109978 C9.96283591,33.354029 7.73257912,31.0379981 4.98141795,31.0379981 C2.23025679,31.0379981 0,33.354029 0,36.2109978 C0,39.0679667 2.23025679,41.3839975 4.98141795,41.3839975 Z M4.98141795,26.8995984 C7.73257912,26.8995984 9.96283591,24.5835675 9.96283591,21.7265987 C9.96283591,18.8696299 7.73257912,16.553599 4.98141795,16.553599 C2.23025679,16.553599 0,18.8696299 0,21.7265987 C0,24.5835675 2.23025679,26.8995984 4.98141795,26.8995984 Z"></path>
</svg>
);
}
}
LogoIcon.PropTypes = {
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired
}
\ No newline at end of file
"use strict";
import React, { Component, PropTypes } from "react";
import cx from "classnames";
import _ from "underscore";
import MetabaseCore from "metabase/lib/core";
import FormField from "metabase/components/form/FormField.react";
import FormLabel from "metabase/components/form/FormLabel.react";
import FormMessage from "metabase/components/form/FormMessage.react";
// TODO - this should be somewhere more centralized
function isEmpty(str) {
return (!str || 0 === str.length);
}
/**
* This is a form for capturing database details for a given `engine` supplied via props.
* The intention is to encapsulate the entire <form> with standard MB form styling and allow a callback
* function to receive the captured form input when the form is submitted.
*/
export default class DatabaseDetailsForm extends Component {
constructor(props) {
super(props);
this.state = { valid: false }
}
validateForm() {
let { engine } = this.props;
let { valid } = this.state;
let isValid = true;
// name is required
if (isEmpty(React.findDOMNode(this.refs.name).value)) isValid = false;
// go over individual fields
for (var fieldIdx in MetabaseCore.ENGINES[engine].fields) {
let field = MetabaseCore.ENGINES[engine].fields[fieldIdx],
ref = React.findDOMNode(this.refs[field.fieldName]);
if (ref && field.required && isEmpty(ref.value)) {
isValid = false;
}
}
if(isValid !== valid) {
this.setState({
'valid': isValid
});
}
}
onChange(fieldName) {
this.validateForm();
}
formSubmitted(e) {
e.preventDefault();
let { engine, submitFn } = this.props;
// collect data
let response = {
'name': React.findDOMNode(this.refs.name).value,
'engine': engine,
'details': {}
};
for (var fieldIdx in MetabaseCore.ENGINES[engine].fields) {
let field = MetabaseCore.ENGINES[engine].fields[fieldIdx],
ref = React.findDOMNode(this.refs[field.fieldName]);
if (ref) {
let val = (ref.value && ref.value !== "") ? ref.value : null;
if (val === null && field.placeholderIsDefault) {
val = field.placeholder;
}
if (field.transform) {
val = field.transform(val);
}
response.details[field.fieldName] = val;
}
}
// do callback
submitFn(response);
}
renderFieldInput(field) {
let { details } = this.props;
let defaultValue = (details && field.fieldName in details) ? details[field.fieldName] : "";
switch(field.type) {
case 'select':
return (
<div className="Form-input Form-offset full Button-group">
{field.choices.map(choice =>
<button className="Button"
ng-className="details[field.fieldName] === choice.value ? {active: 'Button--active',
danger: 'Button--danger'}[choice.selectionAccent] : null"
ng-click="details[field.fieldName] = choice.value">
{choice.name}
</button>
)}
</div>
);
case 'password':
return (
<input type="password" className="Form-input Form-offset full" ref={field.fieldName} name={field.fieldName} defaultValue={defaultValue} placeholder={field.placeholder} onChange={this.onChange.bind(this, field.fieldName)} />
);
case 'text':
return (
<input className="Form-input Form-offset full" ref={field.fieldName} name={field.fieldName} defaultValue={defaultValue} placeholder={field.placeholder} onChange={this.onChange.bind(this, field.fieldName)} />
);
}
}
render() {
let { details, engine, formError, hiddenFields, submitButtonText } = this.props;
let { valid } = this.state;
hiddenFields = hiddenFields || [];
let existingName = (details && 'name' in details) ? details.name : "";
return (
<form onSubmit={this.formSubmitted.bind(this)} noValidate>
<FormField fieldName="name">
<FormLabel title="Name" fieldName="name"></FormLabel>
<input className="Form-input Form-offset full" ref="name" name="name" defaultValue={existingName} placeholder="How would you like to refer to this database?" required autofocus />
<span className="Form-charm"></span>
</FormField>
<div className="FormInputGroup">
{ MetabaseCore.ENGINES[engine].fields.filter(field => !_.contains(hiddenFields, field.fieldName)).map(field =>
<FormField fieldName={field.fieldName}>
<FormLabel title={field.displayName} fieldName={field.fieldName}></FormLabel>
{this.renderFieldInput(field)}
<span className="Form-charm"></span>
</FormField>
)}
</div>
<div className="Form-actions">
<button className={cx("Button", {"Button--primary": valid})} disabled={!valid}>
{submitButtonText}
</button>
<FormMessage formError={formError}></FormMessage>
</div>
</form>
);
}
}
DatabaseDetailsForm.propTypes = {
details: PropTypes.object,
engine: PropTypes.string.isRequired,
formError: PropTypes.object,
hiddenFields: PropTypes.array,
submitButtonText: PropTypes.string.isRequired,
submitFn: PropTypes.func.isRequired
}
"use strict";
import React, { Component, PropTypes } from "react";
import cx from "classnames";
export default class FormField extends Component {
render() {
let { children, className, fieldName, formError, error } = this.props;
const classes = cx('Form-field', className, {
'Form--fieldError': (error === true || (formError && formError.data.errors && fieldName in formError.data.errors))
});
return (
<div className={classes}>
{children}
</div>
);
}
}
FormField.propTypes = {
fieldName: PropTypes.string.isRequired
}
"use strict";
import React, { Component, PropTypes } from "react";
export default class FormLabel extends Component {
render() {
let { fieldName, formError, message, title } = this.props;
if (!message) {
message = (formError && formError.data.errors && fieldName in formError.data.errors) ? formError.data.errors[fieldName] : undefined;
}
return (
<label className="Form-label Form-offset">{title}: { message !== undefined ? <span>{message}</span> : null }</label>
);
}
}
FormLabel.propTypes = {
fieldName: PropTypes.string.isRequired,
formError: PropTypes.object,
message: PropTypes.string,
title: PropTypes.string.isRequired
}
"use strict";
import React, { Component, PropTypes } from "react";
import cx from "classnames";
export default class FormMessage extends Component {
render() {
let { className, formError, formSuccess, message } = this.props;
const classes = cx('px2', className, {
'text-success': formSuccess !== undefined,
'text-error': formError !== undefined
});
if (!message) {
if (formError && formError.data.message) {
message = formError.data.message;
} else if (formError && formError.status === 500) {
// generic 500 without a specific message
message = "Server error encountered";
} else if (formSuccess && formSuccess.data.message) {
message = formSuccess.data.message;
}
}
if (message) {
return (
<span className={classes}>{message}</span>
);
} else {
return null;
}
}
}
......@@ -69,3 +69,9 @@
max-width: var(--md-width);
}
}
@media screen and (--breakpoint-min-md) {
.wrapper.wrapper--small {
max-width: var(--sm-width);
}
}
:root {
--indicator-size: 3.000em; /* ~ 42 px */
--indicator-border-radius: 99px;
--setup-border-color: #D7D7D7;
}
.WelcomeMessage {
margin-top: 4em;
margin-bottom: 4em;
......@@ -9,17 +11,17 @@
.WelcomeMessage-title {
font-weight: 100;
font-size: 2.286em;
line-height: 2.786em;
font-size: 2.286rem;
line-height: 2.786rem;
}
.WelcomeMessage-subTitle {
font-size: 1.2em;
line-height: 1.4em;
font-size: 1.2rem;
line-height: 1.4rem;
}
.SetupSteps {
margin-top: 5.000em;
margin-top: 4.000rem;
}
.SetupNav {
......@@ -27,27 +29,27 @@
}
.Setup-brandWordMark {
font-size: 1.688em;
font-size: 1.688rem;
}
.SetupStep {
color: var(--brand-color);
margin-bottom: 1.714em;
background-color: #EDF2F8;
margin-bottom: 1.714rem;
border: 1px solid var(--setup-border-color);
flex: 1;
border: 1px dashed #e8e8e8;
}
.SetupStep.SetupStep--active {
background-color: #fff;
border-style: solid;
color: var(--brand-color);
}
.SetupStep.SetupStep--completed {
color: var(--success-color);
background-color: #fff;
border-style: solid;
}
.SetupStep.SetupStep--todo {
color: var(--brand-color);
background-color: #EDF2F8;
border-style: dashed;
}
.SetupStep-indicator {
......@@ -55,9 +57,11 @@
width: var(--indicator-size);
height: var(--indicator-size);
border-radius: var(--indicator-border-radius);
border-color: color(var(--base-grey) shade(20%));
font-weight: bold;
line-height: 1;
background-color: #fff;
margin-top: -3px;
}
.SetupStep-check {
......@@ -91,8 +95,8 @@
}
.SetupCompleted .SetupStep-title {
font-size: 2.000em;
line-height: 2.000em;
font-size: 2rem;
line-height: 2rem;
}
.SetupHelp {
......
'use strict';
export const METABASE_SESSION_COOKIE = 'metabase.SESSION_ID';
var mb_cookies = {};
// Handles management of Metabase cookie work
var MetabaseCookies = {
// a little weird, but needed to keep us hooked in with Angular
bootstrap: function($rootScope, $location, ipCookie) {
mb_cookies.scope = $rootScope;
mb_cookies.location = $location;
mb_cookies.ipCookie = ipCookie;
},
// set the session cookie. if sessionId is null, clears the cookie
setSessionCookie: function(sessionId) {
if (sessionId) {
// set a session cookie
var isSecure = (mb_cookies.location.protocol() === "https") ? true : false;
mb_cookies.ipCookie(METABASE_SESSION_COOKIE, sessionId, {
path: '/',
expires: 14,
secure: isSecure
});
// send a login notification event
mb_cookies.scope.$broadcast('appstate:login', sessionId);
} else {
var sessionId = mb_cookies.ipCookie(METABASE_SESSION_COOKIE);
// delete the current session cookie
mb_cookies.ipCookie.remove(METABASE_SESSION_COOKIE);
// send a logout notification event
mb_cookies.scope.$broadcast('appstate:logout', sessionId);
}
}
}
export default MetabaseCookies;
'use strict';
import _ from "underscore";
import inflection from 'inflection';
import MetabaseUtils from "metabase/lib/utils";
const mb_settings = _.clone(window.MetabaseBootstrap);
// provides access to Metabase application settings
const MetabaseSettings = {
get: function(propName, defaultValue) {
return mb_settings[propName] || defaultValue || null;
},
setAll: function(settings) {
for (var attrname in settings) {
mb_settings[attrname] = settings[attrname];
}
},
// these are all special accessors which provide a lookup of a property plus some additional help
hasSetupToken: function() {
return (mb_settings.setup_token !== undefined && mb_settings.setup_token !== null);
},
passwordComplexity: function() {
const complexity = this.get('password_complexity');
const clauseDescription = function(clause) {
switch (clause) {
case "lower": return "lower case letter";
case "upper": return "upper case letter";
case "digit": return "number";
case "special": return "special character";
}
};
let description = "Must be "+complexity.total+" characters long",
clauses = [];
["lower", "upper", "digit", "special"].forEach(function(clause) {
if (clause in complexity) {
let desc = (complexity[clause] > 1) ? inflection.pluralize(clauseDescription(clause)) : clauseDescription(clause);
clauses.push(MetabaseUtils.numberToWord(complexity[clause])+" "+desc);
}
});
if (clauses.length > 0) {
return description+" and include "+clauses.join(", ");
} else {
return description;
}
return description;
}
}
export default MetabaseSettings;
'use strict';
// provides functions for building urls to things we care about
var MetabaseUtils = {
isEmpty: function(str) {
return (!str || 0 === str.length);
},
// pretty limited. just does 0-9 for right now.
numberToWord: function(num) {
var names = ["zero","one","two","three","four","five","six","seven","eight","nine"];
if (num >= 0 && num <= 9) {
return names[num];
} else {
return ""+num;
}
},
validEmail: function(email) {
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
}
export default MetabaseUtils;
......@@ -5,7 +5,9 @@ import _ from "underscore";
/* Services */
import MetabaseAnalytics from 'metabase/lib/analytics';
import MetabaseCookies from 'metabase/lib/cookies';
import MetabaseCore from 'metabase/lib/core';
import MetabaseSettings from 'metabase/lib/settings';
var MetabaseServices = angular.module('metabase.services', ['http-auth-interceptor', 'ipCookie', 'metabase.core.services']);
......@@ -30,6 +32,9 @@ MetabaseServices.factory('AppState', ['$rootScope', '$q', '$location', '$interva
init: function() {
if (!initPromise) {
// hackery to allow MetabaseCookies to tie into Angular
MetabaseCookies.bootstrap($rootScope, $location, ipCookie);
var deferred = $q.defer();
initPromise = deferred.promise;
......@@ -89,6 +94,7 @@ MetabaseServices.factory('AppState', ['$rootScope', '$q', '$location', '$interva
var settings = _.indexBy(result, 'key');
service.model.siteSettings = settings;
MetabaseSettings.setAll(settings);
$rootScope.$broadcast('appstate:site-settings', settings);
......@@ -145,6 +151,17 @@ MetabaseServices.factory('AppState', ['$rootScope', '$q', '$location', '$interva
routeChangedImpl: function(event) {
// whenever we have a route change (including initial page load) we need to establish some context
// handle routing protections for /setup/
if ($location.path().indexOf('/setup') === 0 && !MetabaseSettings.hasSetupToken()) {
// someone trying to access setup process without having a setup token, so block that.
$location.path('/');
return;
} else if ($location.path().indexOf('/setup') !== 0 && MetabaseSettings.hasSetupToken()) {
// someone who has a setup token but isn't routing to setup yet, so send them there!
$location.path('/setup/');
return;
}
// if we don't have a current user then the only sensible destination is the login page
if (!service.model.currentUser) {
// make sure we clear out any current state just to be safe
......@@ -406,3 +423,12 @@ CoreServices.factory('Revision', ['$resource', function($resource) {
}
});
}]);
CoreServices.factory('Util', ['$resource', '$cookies', function($resource, $cookies) {
return $resource('/api/util/', {}, {
password_check: {
url: '/api/util/password_check',
method: 'POST'
}
});
}]);
"use strict";
//import _ from "underscore";
import { createAction } from "redux-actions";
import MetabaseCookies from "metabase/lib/cookies";
import MetabaseSettings from "metabase/lib/settings";
// HACK: just use our Angular resources for now
function AngularResourceProxy(serviceName, methods) {
methods.forEach((methodName) => {
this[methodName] = function(...args) {
let service = angular.element(document.querySelector("body")).injector().get(serviceName);
return service[methodName](...args).$promise;
}
});
}
// similar to createAction but accepts a (redux-thunk style) thunk and dispatches based on whether
// the promise returned from the thunk resolves or rejects, similar to redux-promise
function createThunkAction(actionType, actionThunkCreator) {
return function(...actionArgs) {
var thunk = actionThunkCreator(...actionArgs);
return async function(dispatch, getState) {
try {
let payload = await thunk(dispatch, getState);
dispatch({ type: actionType, payload });
} catch (error) {
dispatch({ type: actionType, payload: error, error: true });
throw error;
}
}
}
}
// // resource wrappers
const SetupApi = new AngularResourceProxy("Setup", ["create", "validate_db"]);
const UtilApi = new AngularResourceProxy("Util", ["password_check"]);
// action constants
export const SET_ACTIVE_STEP = 'SET_ACTIVE_STEP';
export const SET_USER_DETAILS = 'SET_USER_DETAILS';
export const SET_DATABASE_DETAILS = 'SET_DATABASE_DETAILS';
export const SET_ALLOW_TRACKING = 'SET_ALLOW_TRACKING';
export const VALIDATE_DATABASE = 'VALIDATE_DATABASE';
export const VALIDATE_PASSWORD = 'VALIDATE_PASSWORD';
export const SUBMIT_SETUP = 'SUBMIT_SETUP';
export const COMPLETE_SETUP = 'COMPLETE_SETUP';
// action creators
export const setActiveStep = createAction(SET_ACTIVE_STEP);
export const setUserDetails = createAction(SET_USER_DETAILS);
export const setDatabaseDetails = createAction(SET_DATABASE_DETAILS);
export const setAllowTracking = createAction(SET_ALLOW_TRACKING);
export const validateDatabase = createThunkAction(VALIDATE_DATABASE, function(details) {
return async function(dispatch, getState) {
return await SetupApi.validate_db({
'token': MetabaseSettings.get('setup_token'),
'details': details
});
};
});
export const validatePassword = createThunkAction(VALIDATE_PASSWORD, function(password) {
return async function(dispatch, getState) {
return await UtilApi.password_check({
'password': password
});
};
});
export const submitSetup = createThunkAction(SUBMIT_SETUP, function() {
return async function(dispatch, getState) {
let { allowTracking, databaseDetails, userDetails} = getState();
try {
let response = await SetupApi.create({
'token': MetabaseSettings.get('setup_token'),
'prefs': {
'site_name': userDetails.site_name,
'allow_tracking': allowTracking.toString()
},
'database': databaseDetails,
'user': userDetails
});
// setup complete!
dispatch(completeSetup(response));
return null;
} catch (error) {
return error;
}
};
});
export const completeSetup = createAction(COMPLETE_SETUP, function(apiResponse) {
// setup user session
MetabaseCookies.setSessionCookie(apiResponse.id);
// clear setup token from settings
MetabaseSettings.setAll({'setup_token': null});
return true;
});
"use strict";
import React, { Component, PropTypes } from "react";
import cx from "classnames";
import Icon from "metabase/components/Icon.react";
import { setActiveStep } from "../actions";
export default class SetupCollapsedStep extends Component {
gotoStep() {
if (this.props.isCompleted) {
this.props.dispatch(setActiveStep(this.props.stepNumber));
}
}
render() {
let { isCompleted, stepNumber, stepText } = this.props;
const classes = cx({
'SetupStep': true,
'rounded': true,
'full': true,
'relative': true,
'SetupStep--completed shadowed': isCompleted,
'SetupStep--todo': !isCompleted
});
return (
<section className={classes}>
<div className="flex align-center py2">
<span className="SetupStep-indicator flex layout-centered absolute bordered">
<span className="SetupStep-number">{stepNumber}</span>
<Icon name={'check'} className="SetupStep-check" width={16} height={16}></Icon>
</span>
<h3 className="SetupStep-title ml4 my1" onClick={this.gotoStep.bind(this)}>{stepText}</h3>
</div>
</section>
);
}
}
SetupCollapsedStep.propTypes = {
stepNumber: PropTypes.number.isRequired,
stepText: PropTypes.string.isRequired,
isCompleted: PropTypes.bool.isRequired
}
"use strict";
import React, { Component, PropTypes } from "react";
import _ from "underscore";
import DatabaseDetailsForm from "metabase/components/database/DatabaseDetailsForm.react";
import FormField from "metabase/components/form/FormField.react";
import MetabaseCore from "metabase/lib/core";
import StepTitle from './StepTitle.react'
import CollapsedStep from "./CollapsedStep.react";
import { setDatabaseDetails, validateDatabase } from "../actions";
export default class DatabaseStep extends Component {
constructor(props) {
super(props);
this.state = { 'engine': "", 'formError': null };
}
chooseDatabaseEngine() {
let engine = React.findDOMNode(this.refs.engine).value;
this.setState({
'engine': engine
});
}
async detailsCaptured(details) {
this.setState({
'formError': null
});
try {
// validate them first
await this.props.dispatch(validateDatabase(details));
// now that they are good, store them
this.props.dispatch(setDatabaseDetails({
'nextStep': ++this.props.stepNumber,
'details': details
}));
} catch (error) {
this.setState({
'formError': error
});
}
}
skipDatabase() {
this.setState({
'engine': ""
});
this.props.dispatch(setDatabaseDetails({
'nextStep': ++this.props.stepNumber,
'details': null
}));
}
renderEngineSelect() {
let { engine } = this.state,
engines = _.keys(MetabaseCore.ENGINES).sort();
let options = [(<option value="">Select the type of Database you use</option>)];
engines.forEach(function(opt) {
options.push((<option key={opt} value={opt}>{MetabaseCore.ENGINES[opt].name}</option>))
});
return (
<label className="Select Form-offset mt1">
<select ref="engine" defaultValue={engine} onChange={this.chooseDatabaseEngine.bind(this)}>
{options}
</select>
</label>
);
}
render() {
let { activeStep, databaseDetails, dispatch, stepNumber } = this.props;
let { engine, formError } = this.state;
let stepText = 'Add your data';
if (activeStep > stepNumber) {
stepText = (databaseDetails === null) ? "I'll add my own data later" : 'Connecting to '+databaseDetails.name;
}
if (activeStep !== stepNumber) {
return (<CollapsedStep dispatch={dispatch} stepNumber={stepNumber} stepText={stepText} isCompleted={activeStep > stepNumber}></CollapsedStep>)
} else {
return (
<section className="SetupStep rounded full relative SetupStep--active">
<StepTitle title={stepText} number={stepNumber} />
<div className="mb4">
<div style={{maxWidth: 600}} className="Form-field Form-offset">
Youll need some info about your database, like the username and password. If you dont have that right now, Metabase also comes with an Sample dataset you can get started with.
</div>
<FormField fieldName="engine">
{this.renderEngineSelect()}
</FormField>
{ engine !== "" ?
<DatabaseDetailsForm
details={(databaseDetails && 'details' in databaseDetails) ? databaseDetails.details : null}
engine={engine}
formError={formError}
hiddenFields={['ssl']}
submitFn={this.detailsCaptured.bind(this)}
submitButtonText={'Next'}>
</DatabaseDetailsForm>
: null }
<div className="Form-field Form-offset">
<a className="text-brand-hover" style={{textDecoration: "none"}} href="#" onClick={this.skipDatabase.bind(this)}>I'll add my data later</a>
</div>
</div>
</section>
);
}
}
}
DatabaseStep.propTypes = {
dispatch: PropTypes.func.isRequired,
stepNumber: PropTypes.number.isRequired
}
"use strict";
import React, { Component, PropTypes } from "react";
import Toggle from "metabase/components/Toggle.react";
import StepTitle from './StepTitle.react';
import CollapsedStep from "./CollapsedStep.react";
import { setAllowTracking, submitSetup } from "../actions";
export default class PreferencesStep extends Component {
toggleTracking() {
let { allowTracking } = this.props;
this.props.dispatch(setAllowTracking(!allowTracking));
}
async formSubmitted(e) {
e.preventDefault();
// okay, this is the big one. we actually submit everything to the api now and complete the process.
this.props.dispatch(submitSetup());
}
render() {
let { activeStep, allowTracking, setupComplete, stepNumber } = this.props;
let stepText = 'Usage data preferences';
if (setupComplete) {
stepText = allowTracking ? "Thanks for helping us improve" : "We won't collect any usage events";
}
if (activeStep !== stepNumber || setupComplete) {
return (<CollapsedStep stepNumber={stepNumber} stepText={stepText} isCompleted={setupComplete}></CollapsedStep>)
} else {
return (
<section className="SetupStep rounded full relative SetupStep--active">
<StepTitle title={stepText} number={stepNumber} />
<form onSubmit={this.formSubmitted.bind(this)} novalidate>
<div className="Form-field Form-offset">
In order to help us improve Metabase, we'd like to collect certain data about usage through Google Analytics. <a className="link" href="http://www.metabase.com/docs/latest/information-collection.html" target="_blank">Here's a full list of everything we track and why.</a>
</div>
<div className="Form-field Form-offset mr4">
<div style={{borderWidth: "2px"}} className="flex align-center bordered rounded p2">
<Toggle value={allowTracking} onChange={this.toggleTracking.bind(this)} className="inline-block" />
<span className="ml1">Allow Metabase to anonymously collect usage events</span>
</div>
</div>
{ allowTracking ?
<div className="Form-field Form-offset">
<ul style={{listStyle: "disc inside", lineHeight: "200%"}}>
<li>Metabase <span style={{fontWeight: "bold"}}>never</span> collects anything about your data or question results.</li>
<li>All collection is completely anonymous.</li>
<li>Collection can be turned off at any point in your admin settings.</li>
</ul>
</div>
: null }
<div className="Form-actions">
<button className="Button Button--primary" ng-click="setUsagePreference()">
Next
</button>
<mb-form-message form="usageForm"></mb-form-message>
</div>
</form>
</section>
);
}
}
}
PreferencesStep.propTypes = {
dispatch: PropTypes.func.isRequired,
stepNumber: PropTypes.number.isRequired
}
"use strict";
import React, { Component, PropTypes } from "react";
import LogoIcon from 'metabase/components/LogoIcon.react';
import UserStep from './UserStep.react';
import DatabaseStep from './DatabaseStep.react';
import PreferencesStep from './PreferencesStep.react';
import { setActiveStep } from '../actions';
const WELCOME_STEP_NUMBER = 0;
const USER_STEP_NUMBER = 1;
const DATABASE_STEP_NUMBER = 2;
const PREFERENCES_STEP_NUMBER = 3;
export default class Setup extends Component {
completeWelcome() {
this.props.dispatch(setActiveStep(USER_STEP_NUMBER));
}
renderFooter() {
return (
<div className="SetupHelp bordered border-dashed p2 rounded mb4" >
If you feel stuck, <a className="link" href="http://www.metabase.com/docs/latest/getting-started">our getting started guide</a> is just a click away.
</div>
);
}
render() {
let { activeStep, setupComplete } = this.props;
if (activeStep === WELCOME_STEP_NUMBER) {
return (
<div className="flex flex-column flex-full">
<div className="wrapper flex flex-column layout-centered wrapper wrapper--trim">
<section className="wrapper wrapper--trim flex layout-centered full-height flex-column">
<LogoIcon className="text-brand" width={109} height={138}></LogoIcon>
<div className="WelcomeMessage text-centered">
<h1 className="WelcomeMessage-title text-brand">Welcome to Metabase</h1>
<p className="WelcomeMessage-subTitle text-body">Looks like everything is installed and working great. Well quickly get to know you, connect you to your data, and well have you on your way to your data.</p>
</div>
<button className="Button Button--primary" onClick={() => (this.completeWelcome())}>Lets get started</button>
</section>
{this.renderFooter()}
</div>
</div>
);
} else {
return (
<div className="flex flex-column flex-full">
<nav className="SetupNav text-brand py2 flex layout-centered">
<LogoIcon width={41} height={51}></LogoIcon>
</nav>
<div className="wrapper wrapper--small flex flex-column layout-centered">
<div className="SetupSteps full">
<UserStep {...this.props} stepNumber={USER_STEP_NUMBER} />
<DatabaseStep {...this.props} stepNumber={DATABASE_STEP_NUMBER} />
<PreferencesStep {...this.props} stepNumber={PREFERENCES_STEP_NUMBER} />
{ setupComplete ?
<section className="SetupStep rounded SetupStep--active flex flex-column layout-centered p4">
<h1 style={{fontSize: "xx-large"}} className="text-normal pt2">You're all set up!</h1>
<div className="pt4 pb2">
<a className="Button Button--primary" href="/">Take me to Metabase</a>
</div>
</section>
: null }
</div>
{this.renderFooter()}
</div>
</div>
);
}
}
}
Setup.propTypes = {
dispatch: PropTypes.func.isRequired
}
'use strict';
import React, { Component, PropTypes } from 'react'
import Icon from "metabase/components/Icon.react";
export default class StepTitle extends Component {
render() {
const { number, title } = this.props;
return (
<div className="flex align-center pt3 pb1">
<span className="SetupStep-indicator flex layout-centered absolute bordered">
<span className="SetupStep-number">{number}</span>
<Icon name={'check'} className="SetupStep-check" width={16} height={16}></Icon>
</span>
<h3 style={{marginTop: 10}} className="SetupStep-title Form-offset">{title}</h3>
</div>
);
}
}
StepTitle.propTypes = {
number: PropTypes.number.isRequired,
title: PropTypes.string.isRequired
};
"use strict";
import React, { Component, PropTypes } from "react";
import _ from "underscore";
import cx from "classnames";
import FormField from "metabase/components/form/FormField.react";
import FormLabel from "metabase/components/form/FormLabel.react";
import FormMessage from "metabase/components/form/FormMessage.react";
import MetabaseSettings from "metabase/lib/settings";
import MetabaseUtils from "metabase/lib/utils";
import StepTitle from './StepTitle.react'
import CollapsedStep from "./CollapsedStep.react";
import { setUserDetails, validatePassword } from "../actions";
export default class UserStep extends Component {
constructor(props) {
super(props);
this.state = { formError: null, passwordError: null, valid: false, validPassword: false }
}
validateForm() {
let { valid, validPassword } = this.state;
let isValid = true;
// required: first_name, last_name, email, password
for (var fieldName in this.refs) {
let node = React.findDOMNode(this.refs[fieldName]);
if (node.required && MetabaseUtils.isEmpty(node.value)) isValid = false;
};
if (!validPassword) {
isValid = false;
}
if(isValid !== valid) {
this.setState({
'valid': isValid
});
}
}
async onPasswordBlur() {
try {
await this.props.dispatch(validatePassword(React.findDOMNode(this.refs.password).value));
this.setState({
passwordError: null,
validPassword: true
});
} catch(error) {
this.setState({
passwordError: error.data.errors.password,
validPassword: false
});
}
}
onChange() {
this.validateForm();
}
formSubmitted(e) {
e.preventDefault();
this.setState({
formError: null
});
let formErrors = {data:{errors:{}}};
// validate email address
if (!MetabaseUtils.validEmail(React.findDOMNode(this.refs.email).value)) {
formErrors.data.errors.email = "Not a valid formatted email address";
}
// TODO - validate password complexity
// validate password match
if (React.findDOMNode(this.refs.password).value !== React.findDOMNode(this.refs.passwordConfirm).value) {
formErrors.data.errors.password_confirm = "Passwords do not match";
}
if (_.keys(formErrors.data.errors).length > 0) {
this.setState({
formError: formErrors
});
return;
}
this.props.dispatch(setUserDetails({
'nextStep': ++this.props.stepNumber,
'details': {
'first_name': React.findDOMNode(this.refs.firstName).value,
'last_name': React.findDOMNode(this.refs.lastName).value,
'email': React.findDOMNode(this.refs.email).value,
'password': React.findDOMNode(this.refs.password).value,
'site_name': React.findDOMNode(this.refs.siteName).value
}
}));
}
render() {
let { activeStep, dispatch, stepNumber, userDetails } = this.props;
let { formError, passwordError, valid } = this.state;
const passwordComplexityDesc = MetabaseSettings.passwordComplexity();
const stepText = (activeStep <= stepNumber) ? 'What should we call you?' : 'Hi, ' + userDetails.first_name + '. nice to meet you!';
if (activeStep !== stepNumber) {
return (<CollapsedStep dispatch={dispatch} stepNumber={stepNumber} stepText={stepText} isCompleted={activeStep > stepNumber}></CollapsedStep>)
} else {
return (
<section className="SetupStep SetupStep--active rounded full relative">
<StepTitle title={stepText} number={stepNumber} />
<form name="userForm" onSubmit={this.formSubmitted.bind(this)} noValidate className="mt2">
<FormField className="Grid" fieldName="first_name" formError={formError}>
<div>
<FormLabel title="First name" fieldName="first_name" formError={formError}></FormLabel>
<input ref="firstName" className="Form-input Form-offset full" name="name" defaultValue={(userDetails) ? userDetails.first_name : ""} placeholder="Johnny" onChange={this.onChange.bind(this)} />
<span className="Form-charm"></span>
</div>
<div>
<FormLabel title="Last name" fieldName="last_name" formError={formError}></FormLabel>
<input ref="lastName" className="Form-input Form-offset" name="name" defaultValue={(userDetails) ? userDetails.last_name : ""} placeholder="Appleseed" required onChange={this.onChange.bind(this)} />
<span className="Form-charm"></span>
</div>
</FormField>
<FormField fieldName="email" formError={formError}>
<FormLabel title="Email address" fieldName="email" formError={formError}></FormLabel>
<input ref="email" className="Form-input Form-offset full" name="email" defaultValue={(userDetails) ? userDetails.email : ""} placeholder="youlooknicetoday@email.com" required onChange={this.onChange.bind(this)} />
<span className="Form-charm"></span>
</FormField>
<FormField fieldName="password" formError={formError} error={(passwordError !== null)}>
<FormLabel title="Create a password" fieldName="password" formError={formError} message={passwordError}></FormLabel>
<span style={{fontWeight: "normal"}} className="Form-label Form-offset">{passwordComplexityDesc}</span>
<input ref="password" className="Form-input Form-offset full" name="password" type="password" defaultValue={(userDetails) ? userDetails.password : ""} placeholder="Shhh..." required onChange={this.onChange.bind(this)} onBlur={this.onPasswordBlur.bind(this)}/>
<span className="Form-charm"></span>
</FormField>
<FormField fieldName="password_confirm" formError={formError}>
<FormLabel title="Confirm password" fieldName="password_confirm" formError={formError}></FormLabel>
<input ref="passwordConfirm" className="Form-input Form-offset full" name="passwordConfirm" type="password" defaultValue={(userDetails) ? userDetails.password : ""} placeholder="Shhh... but one more time so we get it right" required onChange={this.onChange.bind(this)} />
<span className="Form-charm"></span>
</FormField>
<FormField fieldName="site_name" formError={formError}>
<FormLabel title="Your company or team name" fieldName="site_name" formError={formError}></FormLabel>
<input ref="siteName" className="Form-input Form-offset full" name="site_name" type="text" defaultValue={(userDetails) ? userDetails.site_name : ""} placeholder="Department of awesome" required onChange={this.onChange.bind(this)} />
<span className="Form-charm"></span>
</FormField>
<div className="Form-actions">
<button className={cx("Button", {"Button--primary": valid})} disabled={!valid}>
Next
</button>
<FormMessage></FormMessage>
</div>
</form>
</section>
);
}
}
}
UserStep.propTypes = {
dispatch: PropTypes.func.isRequired,
stepNumber: PropTypes.number.isRequired
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment