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

A lot more of the Metadata editing functionality

parent 681186a8
No related branches found
No related tags found
No related merge requests found
Showing
with 729 additions and 443 deletions
......@@ -13,7 +13,7 @@ export default React.createClass({
databases: React.PropTypes.array.isRequired,
selectDatabase: React.PropTypes.func.isRequired,
tableId: React.PropTypes.number,
tables: React.PropTypes.array.isRequired,
tables: React.PropTypes.object.isRequired,
selectTable: React.PropTypes.func.isRequired,
updateTable: React.PropTypes.func.isRequired,
updateField: React.PropTypes.func.isRequired
......@@ -21,8 +21,6 @@ export default React.createClass({
getInitialState: function() {
return {
saving: false,
error: null,
isShowingSchema: false
}
},
......@@ -31,32 +29,30 @@ export default React.createClass({
this.setState({ isShowingSchema: !this.state.isShowingSchema });
},
updateTable: function(table) {
this.setState({ saving: true });
this.props.updateTable(table).then(() => {
this.setState({ saving: false });
handleSaveResult: function(promise) {
this.refs.header.setSaving();
promise.then(() => {
this.refs.header.setSaved();
}, (error) => {
this.setState({ saving: false, error: error });
this.refs.header.setSaveError(error.data);
});
},
updateTable: function(table) {
this.handleSaveResult(this.props.updateTable(table));
},
updateField: function(field) {
this.setState({ saving: true });
this.props.updateField(field).then(() => {
this.setState({ saving: false });
}, (error) => {
this.setState({ saving: false, error: error });
});
this.handleSaveResult(this.props.updateField(field));
},
render: function() {
var table = _.find(this.props.tables, (t) => t.id === this.props.tableId);
var table = this.props.tables[this.props.tableId];
var content;
if (this.state.isShowingSchema) {
content = (
<MetadataSchema
table={table}
metadata={table && this.props.tablesMetadata[table.id]}
updateTable={this.updateTable}
updateField={this.updateField}
/>
......@@ -65,7 +61,6 @@ export default React.createClass({
content = (
<MetadataTable
table={table}
metadata={table && this.props.tablesMetadata[table.id]}
updateTable={this.updateTable}
updateField={this.updateField}
/>
......@@ -74,10 +69,10 @@ export default React.createClass({
return (
<div className="MetadataEditor flex flex-column flex-full p3">
<MetadataHeader
ref="header"
databaseId={this.props.databaseId}
databases={this.props.databases}
selectDatabase={this.props.selectDatabase}
saving={this.state.saving}
isShowingSchema={this.state.isShowingSchema}
toggleShowSchema={this.toggleShowSchema}
/>
......
"use strict";
import Input from "./Input.react";
import Select from "./Select.react";
import Icon from '../../../query_builder/icon.react';
import MetabaseCore from 'metabase/lib/core';
export default React.createClass({
displayName: "MetadataField",
......@@ -22,19 +26,49 @@ export default React.createClass({
this.updateProperty("description", event.target.value);
},
onTypeChange: function(type) {
this.updateProperty("field_type", type.id);
},
onSpecialTypeChange: function(special_type) {
this.updateProperty("special_type", special_type.id);
},
render: function() {
var typeSelect = (
<Select
className="TableEditor-field-type"
value={_.find(MetabaseCore.field_field_types, (type) => type.id === this.props.field.field_type)}
options={MetabaseCore.field_field_types}
onChange={this.onTypeChange}
/>
);
var specialTypeSelect = (
<Select
className="TableEditor-field-special-type"
value={_.find(MetabaseCore.field_special_types, (type) => type.id === this.props.field.special_type)}
options={MetabaseCore.field_special_types}
onChange={this.onSpecialTypeChange}
/>
);
return (
<li className="my1 flex">
<div className="MetadataTable-title flex flex-column flex-full bordered rounded mr1">
<Input className="AdminInput TableEditor-field-name text-bold border-bottom rounded-top" type="text" value={this.props.field.display_name} onBlurChange={this.onNameChange}/>
<Input className="AdminInput TableEditor-field-description rounded-bottom" type="text" value={this.props.field.description} onBlurChange={this.onDescriptionChange} placeholder="No table description yet" />
</div>
<div className="flex-half">
<label className="Select Select--small Select--blue">
<select></select>
</label>
<div className="flex-half px1">
{typeSelect}
</div>
<div className="flex-half flex flex-column justify-between px1">
{specialTypeSelect}
<div className="AdminSelect flex align-center">
<span>To User Login</span>
<Icon className="flex-align-right" name="chevrondown" width="10" height="10"/>
</div>
</div>
<div className="flex-half">Details</div>
</li>
)
}
......
......@@ -10,11 +10,33 @@ export default React.createClass({
databaseId: React.PropTypes.number,
databases: React.PropTypes.array.isRequired,
selectDatabase: React.PropTypes.func.isRequired,
// metabaseApi: React.PropTypes.func.isRequired,
// isShowingSchema: React.PropTypes.bool.isRequired,
isShowingSchema: React.PropTypes.bool.isRequired,
toggleShowSchema: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
saving: false,
recentlySavedTimeout: null,
error: null
}
},
setSaving: function() {
clearTimeout(this.state.recentlySavedTimeout);
this.setState({ saving: true, recentlySavedTimeout: null, error: null });
},
setSaved: function() {
clearTimeout(this.state.recentlySavedTimeout);
var recentlySavedTimeout = setTimeout(() => this.setState({ recentlySavedTimeout: null }), 5000);
this.setState({ saving: false, recentlySavedTimeout: recentlySavedTimeout, error: null });
},
setSaveError: function(error) {
this.setState({ saving: false, recentlySavedTimeout: null, error: error });
},
renderDbSelector: function() {
var database = this.props.databases.filter((db) => db.id === this.props.databaseId)[0];
if (database) {
......@@ -51,18 +73,24 @@ export default React.createClass({
}
},
render: function() {
var spinner;
if (this.props.saving || true) {
spinner = (<div className="mx2 px2 border-right"><div className="spinner"></div></div>);
renderSaveWidget: function() {
if (this.state.saving) {
return (<div className="mx2 px2 border-right"><div className="Spinner"></div></div>);
} else if (this.state.error) {
return (<div className="mx2 px2 border-right text-error">Error: {this.state.error}</div>)
} else if (this.state.recentlySavedTimeout != null) {
return (<div className="mx2 px2 border-right"><Icon name="check" width="12" height="12" /> Saved</div>)
}
},
render: function() {
return (
<div className="MetadataEditor-header flex align-center">
<div className="MetadataEditor-header-section h2">
<span className="text-grey-4">Edit Metadata for</span> {this.renderDbSelector()}
</div>
<div className="MetadataEditor-header-section flex-align-right flex align-center">
{spinner}
{this.renderSaveWidget()}
<span>Show original schema</span>
<span className={"Toggle " + (this.props.isShowingSchema ? "selected" : "")} onClick={this.props.toggleShowSchema}></span>
</div>
......
......@@ -9,8 +9,7 @@ import cx from "classnames";
export default React.createClass({
displayName: "MetadataSchema",
propTypes: {
table: React.PropTypes.object,
metadata: React.PropTypes.object
table: React.PropTypes.object
},
render: function() {
......@@ -19,23 +18,20 @@ export default React.createClass({
return false;
}
var fields;
if (this.props.metadata) {
fields = this.props.metadata.fields.map((field) => {
return (
<li key={field.id} className="px1 py2 flex border-bottom">
<div className="TableEditor-column-column flex flex-column mr1">
<span className="TableEditor-field-name text-bold">{field.name}</span>
</div>
<div className="TableEditor-column-type">
<span className="text-bold">{field.base_type}</span>
</div>
<div className="TableEditor-column-details">
</div>
</li>
);
});
}
var fields = this.props.table.fields.map((field) => {
return (
<li key={field.id} className="px1 py2 flex border-bottom">
<div className="flex-full flex flex-column mr1">
<span className="TableEditor-field-name text-bold">{field.name}</span>
</div>
<div className="flex-half">
<span className="text-bold">{field.base_type}</span>
</div>
<div className="flex-half">
</div>
</li>
);
});
return (
<div className="MetadataTable px2 flex-full">
......@@ -44,9 +40,9 @@ export default React.createClass({
</div>
<div className="mt2 ">
<div className="text-uppercase text-grey-3 py1 flex">
<div className="TableEditor-column-column px1">Column</div>
<div className="TableEditor-column-type px1">Data Type</div>
<div className="TableEditor-column-details px1">Additional Info</div>
<div className="flex-full px1">Column</div>
<div className="flex-half px1">Data Type</div>
<div className="flex-half px1">Additional Info</div>
</div>
<ol className="border-top border-bottom scroll-y">
{fields}
......
......@@ -3,6 +3,7 @@
import Input from "./Input.react";
import MetadataField from "./MetadataField.react";
import ProgressBar from "./ProgressBar.react";
import cx from "classnames";
......@@ -10,7 +11,6 @@ export default React.createClass({
displayName: "MetadataTable",
propTypes: {
table: React.PropTypes.object,
metadata: React.PropTypes.object,
updateTable: React.PropTypes.func.isRequired,
updateField: React.PropTypes.func.isRequired
},
......@@ -66,12 +66,9 @@ export default React.createClass({
return false;
}
var fields;
if (this.props.metadata) {
fields = this.props.metadata.fields.map((field) => {
return <MetadataField key={field.id} field={field} updateField={this.props.updateField} />
});
}
var fields = this.props.table.fields.map((field) => {
return <MetadataField key={field.id} field={field} updateField={this.props.updateField} />
});
return (
<div className="MetadataTable px2 flex-full">
......@@ -82,7 +79,10 @@ export default React.createClass({
<div className="MetadataTable-header flex align-center py2 text-grey-3">
<span className="mx1 text-uppercase">Visibility</span>
{this.renderVisibilityWidget()}
<span className="text-uppercase flex-align-right">Metadata Strength</span>
<span className="flex-align-right flex align-center">
<span className="text-uppercase mr1">Metadata Strength</span>
<ProgressBar percentage={table.metadataStrength} />
</span>
</div>
<div className={"mt2 " + (this.isHidden() ? "disabled" : "")}>
<div className="text-uppercase text-grey-3 py1 flex">
......
'use strict';
import ProgressBar from './ProgressBar.react';
import cx from 'classnames';
import Humanize from 'humanize';
......@@ -7,7 +9,7 @@ export default React.createClass({
displayName: "MetadataTableList",
propTypes: {
tableId: React.PropTypes.number,
tables: React.PropTypes.array.isRequired,
tables: React.PropTypes.object.isRequired,
selectTable: React.PropTypes.func.isRequired
},
......@@ -31,13 +33,16 @@ export default React.createClass({
var hiddenTables = [];
if (this.props.tables) {
this.props.tables.forEach((table, index) => {
_.each(this.props.tables, (table) => {
var classes = cx("AdminList-item", {
"selected": this.props.tableId === table.id
"selected": this.props.tableId === table.id,
"flex": true,
"align-center": true
});
var row = (
<li key={table.id} className={classes} onClick={this.props.selectTable.bind(null, table)}>
{table.display_name}
<ProgressBar className="ProgressBar ProgressBar--mini flex-align-right" percentage={table.metadataStrength} />
</li>
)
var regex = this.state.searchRegex;
......
'use strict';
export default React.createClass({
displayName: "ProgressBar",
propTypes: {
percentage: React.PropTypes.number.isRequired
},
getDefaultProps: function() {
return {
className: "ProgressBar"
};
},
render: function() {
return (
<div className={this.props.className}>
<div className="ProgressBar-progress" style={{"width": (this.props.percentage * 100) + "%"}}></div>
</div>
);
}
});
"use strict";
import ColumnarSelector from '../../../query_builder/columnar_selector.react';
import Icon from '../../../query_builder/icon.react';
import PopoverWithTrigger from '../../../query_builder/popover_with_trigger.react';
export default React.createClass({
displayName: "Select",
propTypes: {
value: React.PropTypes.object,
options: React.PropTypes.array.isRequired,
onChange: React.PropTypes.func
},
getDefaultProps: function() {
return {
isInitiallyOpen: false,
placeholder: ""
};
},
toggleModal: function() {
this.refs.popover.toggleModal();
},
render: function() {
var selectedName = this.props.value ? this.props.value.name : this.props.placeholder;
var triggerElement = (
<div className="flex flex-full align-center">
<span>{selectedName}</span>
<Icon className="flex-align-right" name="chevrondown" width="10" height="10"/>
</div>
);
var columns = [
{
selectedItem: this.props.value,
items: this.props.options,
itemTitleFn: (item) => item.name,
itemDescriptionFn: (item) => item.description,
itemSelectFn: (item) => {
this.props.onChange(item)
this.toggleModal();
}
}
];
var tetherOptions = {
attachment: 'top center',
targetAttachment: 'bottom center',
targetOffset: '10px 0'
};
return (
<div className="AdminSelect flex align-center">
<PopoverWithTrigger ref="popover"
className={"PopoverBody PopoverBody--withArrow " + (this.props.className || "")}
tetherOptions={tetherOptions}
triggerElement={triggerElement}
triggerClasses={this.props.className + " flex flex-full align-center" }>
<ColumnarSelector columns={columns}/>
</PopoverWithTrigger>
</div>
);
}
});
......@@ -20,8 +20,7 @@ function($scope, $route, $routeParams, $location, $q, $timeout, databases, Metab
$scope.databases = databases;
$scope.tableId = null;
$scope.tables = [];
$scope.tablesMetadata = {};
$scope.tables = {};
// mildly hacky way to prevent reloading controllers as the URL changes
var lastRoute = $route.current;
......@@ -48,15 +47,16 @@ function($scope, $route, $routeParams, $location, $q, $timeout, databases, Metab
}, true);
$scope.$watch('databaseId', function() {
$scope.tables = {};
Metabase.db_tables({ 'dbId': $scope.databaseId }).$promise
.then(function(tables) {
$scope.tables = tables;
return $q.all($scope.tables.map((table) => {
return $q.all(tables.map((table) => {
return Metabase.table_query_metadata({
'tableId': table.id,
'include_sensitive_fields': true
}).$promise.then(function(result) {
$scope.tablesMetadata[table.id] = result;
$scope.tables[table.id] = result;
computeMetadataStrength($scope.tables[table.id]);
});
})).then(function() {
$timeout(() => $scope.$digest());
......@@ -75,10 +75,38 @@ function($scope, $route, $routeParams, $location, $q, $timeout, databases, Metab
};
$scope.updateTable = function(table) {
return Metabase.table_update(table).$promise;
return Metabase.table_update(table).$promise.then(function(result) {
_.each(result, (value, key) => { if (key.charAt(0) !== "$") { table[key] = value } });
computeMetadataStrength($scope.tables[table.id]);
$timeout(() => $scope.$digest());
});
};
$scope.updateField = function(field) {
return Metabase.field_update(field).$promise;
return Metabase.field_update(field).$promise.then(function(result) {
_.each(result, (value, key) => { if (key.charAt(0) !== "$") { field[key] = value } });
computeMetadataStrength($scope.tables[field.table_id]);
$timeout(() => $scope.$digest());
});
};
function computeMetadataStrength(table) {
var total = 0;
var completed = 0;
function score(value) {
total++;
if (value) { completed++; }
}
score(table.description);
table.fields.forEach(function(field) {
score(field.description);
score(field.special_type);
if (field.special_type === "fk") {
score(field.target);
}
});
table.metadataStrength = completed / total;
}
}]);
......@@ -28,6 +28,12 @@
padding-left: var(--padding-3);
}
.ColumnarSelector-description {
margin-top: 0.5em;
color: color(var(--base-grey) shade(30%));
max-width: 270px;
}
.ColumnarSelector-row {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
......@@ -42,7 +48,12 @@
color: white !important;
}
.ColumnarSelector-row:hover .ColumnarSelector-description {
color: rgba(255,255,255,0.25);
}
.ColumnarSelector-row--selected {
color: inherit !important;
background: white;
border-top: var(--border-size) var(--border-style) var(--border-color);
border-bottom: var(--border-size) var(--border-style) var(--border-color);
......
......@@ -240,8 +240,10 @@
.AdminList-item:hover {
background-color: white;
border-color: var(--border-color);
margin-left: -10px;
margin-right: -10px;
margin-left: -0.5em;
margin-right: -0.5em;
padding-left: 1.5em;
padding-right: 1.5em;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
......@@ -254,6 +256,14 @@
font-size: smaller;
}
.AdminList-item .ProgressBar {
opacity: 0.2;
}
.AdminList-item.selected .ProgressBar {
opacity: 1.0;
}
.AdminList-search {
padding: 0.5em;
font-size: 18px;
......@@ -275,6 +285,16 @@
outline: 0;
}
.AdminSelect {
padding: 0.6em;
border: 1px solid var(--border-color);
border-radius: var(--default-border-radius);
font-size: 14px;
font-weight: 700;
margin-bottom: 3px;
}
.MetadataTable-title {
background-color: #FCFCFC;
}
......@@ -292,9 +312,29 @@
font-size: 14px;
}
.spinner {
width: 18px;
height: 18px;
.TableEditor-field-type {
color: var(--purple-color);
}
.TableEditor-field-type .ColumnarSelector-row:hover {
background-color: var(--purple-color) !important;
color: white !important;
}
.TableEditor-field-special-type,
.TableEditor-field-target {
color: var(--green-color);
}
.TableEditor-field-special-type .ColumnarSelector-row:hover,
.TableEditor-field-target .ColumnarSelector-row:hover {
background-color: var(--green-color) !important;
color: white !important;
}
.Spinner {
width: 24px;
height: 24px;
border-radius: 99px;
border: 3px solid rgba(111,122,139,0.25);
border-left: 3px solid rgba(111,122,139,1.0);
......@@ -348,3 +388,27 @@
background-color: white;
}
.ProgressBar {
position: relative;
border: 1px solid #6F7A8B;
width: 55px;
height: 10px;
border-radius: 99px;
}
.ProgressBar--mini {
width: 17px;
height: 8px;
border-radius: 2px;
}
.ProgressBar-progress {
background-color: #6F7A8B;
position: absolute;
height: 100%;
top: 0px;
left: 0px;
border-radius: inherit;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
......@@ -42,6 +42,10 @@ button {
outline: none;
}
a {
color: inherit;
}
.disabled {
pointer-events: none;
opacity: 0.4;
......
......@@ -20,6 +20,10 @@
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.align-start {
align-items: flex-start;
}
......
......@@ -142,19 +142,22 @@ CorvusDirectives.directive('mbReactComponent', ['$timeout', function ($timeout)
function render() {
var props = {};
angular.forEach(scope, function(value, key) {
for (var key in scope) {
var value = scope[key];
if (typeof value === "function") {
props[key] = function() {
try {
return value.apply(this, arguments);
} finally {
$timeout(() => scope.$digest());
(function(value) {
props[key] = function() {
try {
return value.apply(this, arguments);
} finally {
$timeout(() => scope.$digest());
}
}
}
})(value);
} else {
props[key] = value;
}
});
}
React.render(<Component {...props}/>, element[0]);
}
......
'use strict';
/*global _, exports*/
(function() {
this.perms = [{
'id': 0,
'name': 'Private'
}, {
'id': 1,
'name': 'Public (others can read)'
}];
this.permName = function(permId) {
if (permId >= 0 && permId <= (this.perms.length - 1)) {
return this.perms[permId].name;
}
return null;
};
this.charts = [{
'id': 'scalar',
'name': 'Scalar'
}, {
'id': 'table',
'name': 'Table'
}, {
'id': 'pie',
'name': 'Pie Chart'
}, {
'id': 'bar',
'name': 'Bar Chart'
}, {
'id': 'line',
'name': 'Line Chart'
}, {
'id': 'area',
'name': 'Area Chart'
}, {
'id': 'timeseries',
'name': 'Time Series'
}, {
'id': 'pin_map',
'name': 'Pin Map'
}, {
'id': 'country',
'name': 'World Heatmap'
}, {
'id': 'state',
'name': 'State Heatmap'
}];
this.chartName = function(chartId) {
for (var i = 0; i < this.charts.length; i++) {
if (this.charts[i].id == chartId) {
return this.charts[i].name;
}
}
return null;
};
this.table_entity_types = [{
'id': null,
'name': 'None'
}, {
'id': 'person',
'name': 'Person'
}, {
'id': 'event',
'name': 'Event'
}, {
'id': 'photo',
'name': 'Photo'
}, {
'id': 'place',
'name': 'Place'
}, {
'id': 'evt-cohort',
'name': 'Cohorts-compatible Event'
}];
this.tableEntityType = function(typeId) {
for (var i = 0; i < this.table_entity_types.length; i++) {
if (this.table_entity_types[i].id == typeId) {
return this.table_entity_types[i].name;
}
}
return null;
};
this.field_special_types = [{
'id': null,
'name': 'None'
}, {
'id': 'avatar',
'name': 'Avatar Image URL'
}, {
'id': 'category',
'name': 'Category'
}, {
'id': 'city',
'name': 'City'
}, {
'id': 'country',
'name': 'Country'
}, {
'id': 'desc',
'name': 'Description'
}, {
'id': 'fk',
'name': 'Foreign Key'
}, {
'id': 'id',
'name': 'Entity Key'
}, {
'id': 'image',
'name': 'Image URL'
}, {
'id': 'json',
'name': 'Field containing JSON'
}, {
'id': 'latitude',
'name': 'Latitude'
}, {
'id': 'longitude',
'name': 'Longitude'
}, {
'id': 'name',
'name': 'Entity Name'
}, {
'id': 'number',
'name': 'Number'
}, {
'id': 'state',
'name': 'State'
}, {
id: 'timestamp_seconds',
name: 'UNIX Timestamp (Seconds)'
}, {
id: 'timestamp_milliseconds',
name: 'UNIX Timestamp (Milliseconds)'
}, {
'id': 'url',
'name': 'URL'
}, {
'id': 'zip_code',
'name': 'Zip Code'
}];
this.field_field_types = [{
'id': 'info',
'name': 'Information',
'description': 'Non-numerical value that is not meant to be used'
}, {
'id': 'metric',
'name': 'Metric',
'description': 'A number that can be added, graphed, etc.'
}, {
'id': 'dimension',
'name': 'Dimension',
'description': 'A high or low-cardinality numerical string value that is meant to be used as a grouping'
}, {
'id': 'sensitive',
'name': 'Sensitive Information',
'description': 'A Fields that should never be shown anywhere'
}];
this.boolean_types = [{
'id': true,
'name': 'Yes'
}, {
'id': false,
'name': 'No'
}, ];
this.fieldSpecialType = function(typeId) {
for (var i = 0; i < this.field_special_types.length; i++) {
if (this.field_special_types[i].id == typeId) {
return this.field_special_types[i].name;
}
}
return null;
};
this.builtinToChart = {
'latlong_heatmap': 'll_heatmap'
};
this.getTitleForBuiltin = function(viewtype, field1Name, field2Name) {
var builtinToTitleMap = {
'state': 'State Heatmap',
'country': 'Country Heatmap',
'pin_map': 'Pin Map',
'heatmap': 'Heatmap',
'cohorts': 'Cohorts',
'latlong_heatmap': 'Lat/Lon Heatmap'
};
var title = builtinToTitleMap[viewtype];
if (field1Name) {
title = title.replace("{0}", field1Name);
}
if (field2Name) {
title = title.replace("{1}", field2Name);
}
return title;
};
this.createLookupTables = function(table) {
// Create lookup tables (ported from ExploreTableDetailData)
table.fields_lookup = {};
_.each(table.fields, function(field) {
table.fields_lookup[field.id] = field;
field.operators_lookup = {};
_.each(field.valid_operators, function(operator) {
field.operators_lookup[operator.name] = operator;
});
});
};
// The various DB engines we support <3
// TODO - this should probably come back from the API, no?
//
// NOTE:
// A database's connection details is stored in a JSON map in the field database.details.
//
// ENGINE DICT FORMAT:
// * name - human-facing name to use for this DB engine
// * fields - array of available fields to display when a user adds/edits a DB of this type. Each field should be a dict of the format below:
//
// FIELD DICT FORMAT:
// * displayName - user-facing name for the Field
// * fieldName - name used for the field in a database details dict
// * transform - function to apply to this value before passing to the API, such as 'parseInt'. (default: none)
// * placeholder - placeholder value that should be used in text input for this field (default: none)
// * placeholderIsDefault - if true, use the value of 'placeholder' as the default value of this field if none is specified (default: false)
// (if you set this, don't set 'required', or user will still have to add a value for the field)
// * required - require the user to enter a value for this field? (default: false)
// * choices - array of possible values for this field. If provided, display a button toggle instead of a text input.
// Each choice should be a dict of the format below: (optional)
//
// CHOICE DICT FORMAT:
// * name - User-facing name for the choice.
// * value - Value to use for the choice in the database connection details dict.
// * selectionAccent - What accent type should be applied to the field when its value is chosen? Either 'active' (currently green), or 'danger' (currently red).
this.ENGINES = {
postgres: {
name: 'Postgres',
fields: [{
displayName: "Host",
fieldName: "host",
type: "text",
placeholder: "localhost",
placeholderIsDefault: true
}, {
displayName: "Port",
fieldName: "port",
type: "text",
transform: parseInt,
placeholder: "5432",
placeholderIsDefault: true
}, {
displayName: "Database name",
fieldName: "dbname",
type: "text",
placeholder: "birds_of_the_world",
required: true
}, {
displayName: "Database username",
fieldName: "user",
type: "text",
placeholder: "What username do you use to login to the database?",
required: true
}, {
displayName: "Database password",
fieldName: "password",
type: "password",
placeholder: "*******"
}, {
displayName: "Use a secure connection (SSL)?",
fieldName: "ssl",
type: "select",
choices: [{
name: 'Yes',
value: true,
selectionAccent: 'active'
}, {
name: 'No',
value: false,
selectionAccent: 'danger'
}]
}]
},
h2: {
name: 'H2',
fields: [{
displayName: "Connection String",
fieldName: "db",
type: "text",
placeholder: "file:/Users/camsaul/bird_sightings/toucans;AUTO_SERVER=TRUE"
}]
},
mongo: {
name: 'MongoDB',
fields: [{
displayName: "Host",
fieldName: "host",
type: "text",
placeholder: "localhost",
placeholderIsDefault: true
}, {
displayName: "Port",
fieldName: "port",
type: "text",
transform: parseInt,
placeholder: "27017"
}, {
displayName: "Database name",
fieldName: "dbname",
type: "text",
placeholder: "carrierPigeonDeliveries",
required: true
}, {
displayName: "Database username",
fieldName: "user",
type: "text",
placeholder: "What username do you use to login to the database?"
}, {
displayName: "Database password",
fieldName: "pass",
type: "password",
placeholder: "******"
}]
}
};
// Prepare database details before being sent to the API.
// This includes applying 'transform' functions and adding default values where applicable.
this.prepareDatabaseDetails = function(details) {
if (!details.engine) throw "Missing key 'engine' in database request details; please add this as API expects it in the request body.";
// iterate over each field definition
this.ENGINES[details.engine].fields.forEach(function(field) {
var fieldName = field.fieldName;
// set default value if applicable
if (!details[fieldName] && field.placeholderIsDefault) {
details[fieldName] = field.placeholder;
}
// apply transformation function if applicable
if (details[fieldName] && field.transform) {
details[fieldName] = field.transform(details[fieldName]);
}
});
return details;
};
}).apply(exports);
......@@ -6,6 +6,9 @@ var cx = React.addons.classSet;
export default React.createClass({
displayName: "ColumnarSelector",
propTypes: {
columns: React.PropTypes.array.isRequired
},
render: function() {
var columns = this.props.columns.map((column, columnIndex) => {
......@@ -19,13 +22,22 @@ export default React.createClass({
var itemClasses = cx({
'cursor-pointer': true,
'ColumnarSelector-row': true,
'ColumnarSelector-row--selected': item === column.selectedItem
'ColumnarSelector-row--selected': item === column.selectedItem,
'flex': true
});
var checkIcon = lastColumn ? <Icon name="check" width="14" height="14"/> : null;
var descriptionElement;
var description = column.itemDescriptionFn && column.itemDescriptionFn(item);
if (description) {
descriptionElement = <div className="ColumnarSelector-description">{description}</div>
}
return (
<li key={rowIndex} className={itemClasses} onClick={column.itemSelectFn.bind(null, item)}>
{checkIcon}
{column.itemTitleFn(item)}
<div className="flex flex-column">
{column.itemTitleFn(item)}
{descriptionElement}
</div>
</li>
);
});
......
......@@ -111,11 +111,9 @@ export default React.createClass({
classes += " " + this.props.triggerClasses;
}
return (
<span>
<a className={classes} href="#" onClick={this.toggleModal}>
{this.props.triggerElement}
</a>
</span>
<a className={classes} href="#" onClick={this.toggleModal}>
{this.props.triggerElement}
</a>
);
}
});
......@@ -3,7 +3,8 @@
/*global _*/
/* Services */
import MetabaseAnalytics from './lib/metabase_analytics';
import MetabaseAnalytics from 'metabase/lib/metabase_analytics';
import MetabaseCore from 'metabase/lib/core';
var CorvusServices = angular.module('corvus.services', ['http-auth-interceptor', 'ipCookie', 'corvus.core.services']);
......@@ -212,361 +213,12 @@ CorvusServices.factory('AppState', ['$rootScope', '$q', '$location', '$timeout',
}
]);
CorvusServices.service('CorvusCore', ['$resource', 'User', function($resource, User) {
this.perms = [{
'id': 0,
'name': 'Private'
}, {
'id': 1,
'name': 'Public (others can read)'
}];
this.permName = function(permId) {
if (permId >= 0 && permId <= (this.perms.length - 1)) {
return this.perms[permId].name;
}
return null;
};
this.charts = [{
'id': 'scalar',
'name': 'Scalar'
}, {
'id': 'table',
'name': 'Table'
}, {
'id': 'pie',
'name': 'Pie Chart'
}, {
'id': 'bar',
'name': 'Bar Chart'
}, {
'id': 'line',
'name': 'Line Chart'
}, {
'id': 'area',
'name': 'Area Chart'
}, {
'id': 'timeseries',
'name': 'Time Series'
}, {
'id': 'pin_map',
'name': 'Pin Map'
}, {
'id': 'country',
'name': 'World Heatmap'
}, {
'id': 'state',
'name': 'State Heatmap'
}];
this.chartName = function(chartId) {
for (var i = 0; i < this.charts.length; i++) {
if (this.charts[i].id == chartId) {
return this.charts[i].name;
}
}
return null;
};
this.table_entity_types = [{
'id': null,
'name': 'None'
}, {
'id': 'person',
'name': 'Person'
}, {
'id': 'event',
'name': 'Event'
}, {
'id': 'photo',
'name': 'Photo'
}, {
'id': 'place',
'name': 'Place'
}, {
'id': 'evt-cohort',
'name': 'Cohorts-compatible Event'
}];
this.tableEntityType = function(typeId) {
for (var i = 0; i < this.table_entity_types.length; i++) {
if (this.table_entity_types[i].id == typeId) {
return this.table_entity_types[i].name;
}
}
return null;
};
this.field_special_types = [{
'id': null,
'name': 'None'
}, {
'id': 'avatar',
'name': 'Avatar Image URL'
}, {
'id': 'category',
'name': 'Category'
}, {
'id': 'city',
'name': 'City'
}, {
'id': 'country',
'name': 'Country'
}, {
'id': 'desc',
'name': 'Description'
}, {
'id': 'fk',
'name': 'Foreign Key'
}, {
'id': 'id',
'name': 'Entity Key'
}, {
'id': 'image',
'name': 'Image URL'
}, {
'id': 'json',
'name': 'Field containing JSON'
}, {
'id': 'latitude',
'name': 'Latitude'
}, {
'id': 'longitude',
'name': 'Longitude'
}, {
'id': 'name',
'name': 'Entity Name'
}, {
'id': 'number',
'name': 'Number'
}, {
'id': 'state',
'name': 'State'
}, {
id: 'timestamp_seconds',
name: 'UNIX Timestamp (Seconds)'
}, {
id: 'timestamp_milliseconds',
name: 'UNIX Timestamp (Milliseconds)'
}, {
'id': 'url',
'name': 'URL'
}, {
'id': 'zip_code',
'name': 'Zip Code'
}];
this.field_field_types = [{
'id': 'info',
'name': 'Information'
}, {
'id': 'metric',
'name': 'Metric'
}, {
'id': 'dimension',
'name': 'Dimension'
}, {
'id': 'sensitive',
'name': 'Sensitive Information'
}];
this.boolean_types = [{
'id': true,
'name': 'Yes'
}, {
'id': false,
'name': 'No'
}, ];
this.fieldSpecialType = function(typeId) {
for (var i = 0; i < this.field_special_types.length; i++) {
if (this.field_special_types[i].id == typeId) {
return this.field_special_types[i].name;
}
}
return null;
};
this.builtinToChart = {
'latlong_heatmap': 'll_heatmap'
};
this.getTitleForBuiltin = function(viewtype, field1Name, field2Name) {
var builtinToTitleMap = {
'state': 'State Heatmap',
'country': 'Country Heatmap',
'pin_map': 'Pin Map',
'heatmap': 'Heatmap',
'cohorts': 'Cohorts',
'latlong_heatmap': 'Lat/Lon Heatmap'
};
var title = builtinToTitleMap[viewtype];
if (field1Name) {
title = title.replace("{0}", field1Name);
}
if (field2Name) {
title = title.replace("{1}", field2Name);
}
return title;
};
this.createLookupTables = function(table) {
// Create lookup tables (ported from ExploreTableDetailData)
table.fields_lookup = {};
_.each(table.fields, function(field) {
table.fields_lookup[field.id] = field;
field.operators_lookup = {};
_.each(field.valid_operators, function(operator) {
field.operators_lookup[operator.name] = operator;
});
});
};
CorvusServices.service('CorvusCore', ['User', function(User) {
// this just makes it easier to access the current user
this.currentUser = User.current;
// The various DB engines we support <3
// TODO - this should probably come back from the API, no?
//
// NOTE:
// A database's connection details is stored in a JSON map in the field database.details.
//
// ENGINE DICT FORMAT:
// * name - human-facing name to use for this DB engine
// * fields - array of available fields to display when a user adds/edits a DB of this type. Each field should be a dict of the format below:
//
// FIELD DICT FORMAT:
// * displayName - user-facing name for the Field
// * fieldName - name used for the field in a database details dict
// * transform - function to apply to this value before passing to the API, such as 'parseInt'. (default: none)
// * placeholder - placeholder value that should be used in text input for this field (default: none)
// * placeholderIsDefault - if true, use the value of 'placeholder' as the default value of this field if none is specified (default: false)
// (if you set this, don't set 'required', or user will still have to add a value for the field)
// * required - require the user to enter a value for this field? (default: false)
// * choices - array of possible values for this field. If provided, display a button toggle instead of a text input.
// Each choice should be a dict of the format below: (optional)
//
// CHOICE DICT FORMAT:
// * name - User-facing name for the choice.
// * value - Value to use for the choice in the database connection details dict.
// * selectionAccent - What accent type should be applied to the field when its value is chosen? Either 'active' (currently green), or 'danger' (currently red).
this.ENGINES = {
postgres: {
name: 'Postgres',
fields: [{
displayName: "Host",
fieldName: "host",
type: "text",
placeholder: "localhost",
placeholderIsDefault: true
}, {
displayName: "Port",
fieldName: "port",
type: "text",
transform: parseInt,
placeholder: "5432",
placeholderIsDefault: true
}, {
displayName: "Database name",
fieldName: "dbname",
type: "text",
placeholder: "birds_of_the_world",
required: true
}, {
displayName: "Database username",
fieldName: "user",
type: "text",
placeholder: "What username do you use to login to the database?",
required: true
}, {
displayName: "Database password",
fieldName: "password",
type: "password",
placeholder: "*******"
}, {
displayName: "Use a secure connection (SSL)?",
fieldName: "ssl",
type: "select",
choices: [{
name: 'Yes',
value: true,
selectionAccent: 'active'
}, {
name: 'No',
value: false,
selectionAccent: 'danger'
}]
}]
},
h2: {
name: 'H2',
fields: [{
displayName: "Connection String",
fieldName: "db",
type: "text",
placeholder: "file:/Users/camsaul/bird_sightings/toucans;AUTO_SERVER=TRUE"
}]
},
mongo: {
name: 'MongoDB',
fields: [{
displayName: "Host",
fieldName: "host",
type: "text",
placeholder: "localhost",
placeholderIsDefault: true
}, {
displayName: "Port",
fieldName: "port",
type: "text",
transform: parseInt,
placeholder: "27017"
}, {
displayName: "Database name",
fieldName: "dbname",
type: "text",
placeholder: "carrierPigeonDeliveries",
required: true
}, {
displayName: "Database username",
fieldName: "user",
type: "text",
placeholder: "What username do you use to login to the database?"
}, {
displayName: "Database password",
fieldName: "pass",
type: "password",
placeholder: "******"
}]
}
};
// Prepare database details before being sent to the API.
// This includes applying 'transform' functions and adding default values where applicable.
this.prepareDatabaseDetails = function(details) {
if (!details.engine) throw "Missing key 'engine' in database request details; please add this as API expects it in the request body.";
// iterate over each field definition
this.ENGINES[details.engine].fields.forEach(function(field) {
var fieldName = field.fieldName;
// set default value if applicable
if (!details[fieldName] && field.placeholderIsDefault) {
details[fieldName] = field.placeholder;
}
// apply transformation function if applicable
if (details[fieldName] && field.transform) {
details[fieldName] = field.transform(details[fieldName]);
}
});
return details;
};
// copy over MetabaseCore properties and functions
angular.forEach(MetabaseCore, (value, key) => this[key] = value);
}]);
......
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