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

Mostly working conversion of database edit/create to React. Also incorporate...

Mostly working conversion of database edit/create to React. Also incorporate some of the UX improvements (delete confirm, animating FormMessage)
parent 04b34df4
No related branches found
No related tags found
No related merge requests found
import React, { Component, PropTypes } from "react";
import DeleteDatabaseModal from "./DeleteDatabaseModal.jsx";
import DatabaseEditForms from "./DatabaseEditForms.jsx";
import ActionButton from "metabase/components/ActionButton.jsx";
import Icon from "metabase/components/Icon.jsx";
import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
// import cx from "classnames";
export default class DatabaseEdit extends Component {
constructor(props) {
super(props);
this.state = {};
}
static propTypes = {};
static defaultProps = {};
render() {
let { database } = this.props;
return (
<div className="wrapper">
<section className="Breadcrumbs">
<a className="Breadcrumb Breadcrumb--path" href="/admin/databases/">Databases</a>
<Icon name="chevronright" className="Breadcrumb-divider" width={12} height={12} />
{ database && database.id ?
<h2 className="Breadcrumb Breadcrumb--page" ng-if="database.id">{database.name}</h2>
:
<h2 className="Breadcrumb Breadcrumb--page">Add Database</h2>
}
</section>
<section className="Grid Grid--gutters Grid--2-of-3">
<div className="Grid-cell">
<div className="Form-new bordered rounded shadowed">
<DatabaseEditForms {...this.props} />
</div>
</div>
{ /* Sidebar Actions */ }
{ database && database.id != null &&
<div className="Grid-cell Cell--1of3" ng-if="database.id">
<div className="Actions bordered rounded shadowed">
<h3>Actions</h3>
<div className="Actions-group">
<ActionButton
actionFn={() => this.props.sync()}
className="Button"
normalText="Sync"
activeText="Starting…"
failedText="Failed to sync"
successText="Sync triggered!"
/>
</div>
<div className="Actions-group Actions--dangerZone">
<label className="Actions-groupLabel block">Danger Zone:</label>
<ModalWithTrigger
ref="deleteDatabaseModal"
triggerClasses="Button Button--danger"
triggerElement="Remove this database"
>
<DeleteDatabaseModal
database={database}
onClose={() => this.refs.deleteDatabaseModal.toggle()}
onDelete={() => this.props.delete(database.id)}
/>
</ModalWithTrigger>
</div>
</div>
</div>
}
</section>
</div>
);
}
}
import React, { Component, PropTypes } from "react";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
import DatabaseDetailsForm from "metabase/components/database/DatabaseDetailsForm.jsx";
import cx from "classnames";
export default class DatabaseEditForms extends Component {
constructor(props) {
super(props);
this.state = {
formError: null
};
}
static propTypes = {};
static defaultProps = {};
async detailsCaptured(database) {
this.setState({ formError: null })
try {
await this.props.save(database, database.details);
} catch (error) {
this.setState({ formError: error })
}
}
render() {
let { database, details, hiddenFields, ENGINES } = this.props;
let { formError } = this.state;
let errors = {};
return (
<LoadingAndErrorWrapper loading={!database} error={null}>
{() =>
<div>
<div className={cx("Form-field", { "Form--fieldError": errors["engine"] })}>
<label className="Form-label Form-offset">Database type: <span>{errors["engine"]}</span></label>
<label className="Select Form-offset mt1">
<select className="Select" value={database.engine} onChange={(e) => this.props.selectEngine(e.target.value)}>
<option value="" disabled>Select a database type</option>
{Object.keys(ENGINES).map(engine =>
<option value={engine}>{ENGINES[engine].name}</option>
)}
</select>
</label>
</div>
{ database.engine ?
<DatabaseDetailsForm
details={{ ...details, name: database.name }}
engine={database.engine}
formError={formError}
hiddenFields={hiddenFields}
submitFn={this.detailsCaptured.bind(this)}
submitButtonText={'Save'}>
</DatabaseDetailsForm>
: null }
</div>
}
</LoadingAndErrorWrapper>
);
}
}
import React, { Component, PropTypes } from "react";
import DeleteDatabaseModal from "./DeleteDatabaseModal.jsx";
import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
import cx from "classnames";
......@@ -45,7 +48,17 @@ export default class DatabaseList extends Component {
{ENGINES[database.engine].name}
</td>
<td className="Table-actions">
<button className="Button Button--danger" onClick={() => confirm("Are you sure?") && this.props.delete(database.id)}>Delete</button>
<ModalWithTrigger
ref="deleteDatabaseModal"
triggerClasses="Button Button--danger"
triggerElement="Delete"
>
<DeleteDatabaseModal
database={database}
onClose={() => this.refs.deleteDatabaseModal.toggle()}
onDelete={() => this.props.delete(database.id)}
/>
</ModalWithTrigger>
</td>
</tr>
)
......
import React, { Component, PropTypes } from "react";
import ModalContent from "metabase/components/ModalContent.jsx";
import cx from "classnames";
export default class DeleteDatabaseModal extends Component {
constructor(props, context) {
super(props, context);
this.state = {
confirmValue: "",
error: null
};
}
static propTypes = {
database: PropTypes.object.isRequired,
onClose: PropTypes.func,
onDelete: PropTypes.func
};
async deleteDatabase() {
try {
this.props.onDelete(this.props.database);
} catch (error) {
this.setState({ error });
}
}
render() {
var formError;
if (this.state.error) {
var errorMessage = "Server error encountered";
if (this.state.error.data &&
this.state.error.data.message) {
errorMessage = this.state.error.data.message;
} else {
errorMessage = this.state.error.message;
}
// TODO: timeout display?
formError = (
<span className="text-error px2">{errorMessage}</span>
);
}
let confirmed = this.state.confirmValue.toUpperCase() === "DELETE";
return (
<ModalContent
title="Delete Database"
closeFn={this.props.onClose}
>
<div className="Form-inputs mb4">
<p>
Are you sure you want to delete this database? All saved questions that rely on this database will be lost. <strong>This cannot be undone</strong>. If you're sure, please type <strong>DELETE</strong> in this box:
</p>
<input className="Form-input" type="text" onChange={(e) => this.setState({ confirmValue: e.target.value })}/>
</div>
<div className="Form-actions">
<button className={cx("Button Button--danger", { "disabled": !confirmed })} onClick={() => this.deleteDatabase()}>Delete</button>
<button className="Button Button--primary ml1" onClick={this.props.onClose}>Cancel</button>
{formError}
</div>
</ModalContent>
);
}
}
import DatabaseList from "./components/DatabaseList.jsx";
import DatabaseEdit from "./components/DatabaseEdit.jsx";
import _ from "underscore";
......@@ -59,6 +60,8 @@ DatabasesControllers.controller('DatabaseList', ['$scope', 'Metabase', 'Metabase
DatabasesControllers.controller('DatabaseEdit', ['$scope', '$routeParams', '$location', 'Metabase', 'MetabaseCore',
function($scope, $routeParams, $location, Metabase, MetabaseCore) {
$scope.DatabaseEdit = DatabaseEdit;
$scope.ENGINES = MetabaseCore.ENGINES;
// if we're adding a new database then hide the SSL field; we'll determine it automatically <3
......@@ -66,6 +69,10 @@ DatabasesControllers.controller('DatabaseEdit', ['$scope', '$routeParams', '$loc
ssl: true
};
$scope.selectEngine = function(engine) {
$scope.details.engine = $scope.database.engine = engine;
}
// update an existing Database
var update = function(database, details) {
$scope.$broadcast("form:reset");
......@@ -183,7 +190,7 @@ DatabasesControllers.controller('DatabaseEdit', ['$scope', '$routeParams', '$loc
// prepare an empty database for creation
$scope.database = {
name: '',
engine: null,
engine: Object.keys(MetabaseCore.ENGINES)[0],
details: {},
created: false
};
......
......@@ -8,11 +8,11 @@ AdminDatabases.config(['$routeProvider', function ($routeProvider) {
controller: 'DatabaseList'
});
$routeProvider.when('/admin/databases/create', {
templateUrl: '/app/admin/databases/partials/database_edit.html',
template: '<div class="flex flex-column flex-full" mb-react-component="DatabaseEdit"></div>',
controller: 'DatabaseEdit'
});
$routeProvider.when('/admin/databases/:databaseId', {
templateUrl: '/app/admin/databases/partials/database_edit.html',
template: '<div class="flex flex-column flex-full" mb-react-component="DatabaseEdit"></div>',
controller: 'DatabaseEdit'
});
}]);
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.jsx";
......@@ -59,6 +58,14 @@ export default class DatabaseDetailsForm extends Component {
}
}
componentDidMount() {
this.validateForm();
}
componentDidUpdate() {
this.validateForm();
}
onChange(fieldName) {
this.validateForm();
}
......@@ -106,12 +113,10 @@ export default class DatabaseDetailsForm extends Component {
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">
<div className={cx("Button", details[field.fieldName] == choice.value ? "Button--" + choice.selectionAccent : null)}
onClick={(e) => { details[field.fieldName] = choice.value; this.onChange(field.fieldName, e)}}>
{choice.name}
</button>
</div>
)}
</div>
);
......@@ -132,19 +137,19 @@ export default class DatabaseDetailsForm extends Component {
let { details, engine, formError, hiddenFields, submitButtonText } = this.props;
let { valid } = this.state;
hiddenFields = hiddenFields || [];
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 />
<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 onChange={this.onChange.bind(this, "name")} />
<span className="Form-charm"></span>
</FormField>
<div className="FormInputGroup">
{ MetabaseCore.ENGINES[engine].fields.filter(field => !_.contains(hiddenFields, field.fieldName)).map(field =>
{ MetabaseCore.ENGINES[engine].fields.filter(field => !hiddenFields[field.fieldName]).map(field =>
<FormField fieldName={field.fieldName}>
<FormLabel title={field.displayName} fieldName={field.fieldName}></FormLabel>
......
......@@ -7,11 +7,6 @@ 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;
......@@ -23,12 +18,14 @@ export default class FormMessage extends Component {
}
}
if (message) {
return (
<span className={classes}>{message}</span>
);
} else {
return null;
}
const classes = cx('Form-message', 'px2', className, {
'Form-message--visible': !!message,
'text-success': formSuccess !== undefined,
'text-error': formError !== undefined
});
return (
<span className={classes}>{message}</span>
);
}
}
......@@ -50,6 +50,16 @@
transition: color .3s linear;
}
.Form-message {
opacity: 0;
transition: none;
}
.Form-message.Form-message--visible {
opacity: 1;
transition: opacity 500ms linear;
}
/* form-input font sizes */
.Form-input { font-size: var(--form-input-size); }
......
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