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

Merge branch 'master' into dash_world_writable

parents d417115a e778d5eb
No related branches found
No related tags found
No related merge requests found
Showing
with 482 additions and 140 deletions
"use strict";
export default React.createClass({
displayName: "Input",
propTypes: {
type: React.PropTypes.string,
value: React.PropTypes.string,
placeholder: React.PropTypes.string,
onChange: React.PropTypes.func,
onBlurChange: React.PropTypes.func
},
getDefaultProps: function() {
return {
type: "text"
};
},
getInitialState: function() {
return { value: this.props.value };
},
......
......@@ -49,7 +49,7 @@ export default React.createClass({
if (this.props.field.special_type === "fk") {
targetSelect = (
<Select
className="TableEditor-field-target"
className="TableEditor-field-target block"
placeholder="Select a target"
value={this.props.field.target && _.find(this.props.idfields, (field) => field.id === this.props.field.target.id)}
options={this.props.idfields}
......@@ -67,7 +67,7 @@ export default React.createClass({
</div>
<div className="flex-half px1">
<Select
className="TableEditor-field-type"
className="TableEditor-field-type block"
placeholder="Select a field type"
value={_.find(MetabaseCore.field_field_types, (type) => type.id === this.props.field.field_type)}
options={MetabaseCore.field_field_types}
......@@ -76,7 +76,7 @@ export default React.createClass({
</div>
<div className="flex-half flex flex-column justify-between px1">
<Select
className="TableEditor-field-special-type"
className="TableEditor-field-special-type block"
placeholder="Select a special type"
value={_.find(MetabaseCore.field_special_types, (type) => type.id === this.props.field.special_type)}
options={MetabaseCore.field_special_types}
......
'use strict';
import SaveStatus from './SaveStatus.react';
import Toggle from './Toggle.react';
import PopoverWithTrigger from '../../../query_builder/popover_with_trigger.react';
import ColumnarSelector from '../../../query_builder/columnar_selector.react';
import Icon from '../../../query_builder/icon.react';
import LoadingIcon from '../../../components/icons/loading.react';
export default React.createClass({
displayName: "MetadataHeader",
......@@ -15,27 +17,16 @@ export default React.createClass({
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 });
this.refs.status.setSaving.apply(this, arguments);
},
setSaved: function() {
clearTimeout(this.state.recentlySavedTimeout);
var recentlySavedTimeout = setTimeout(() => this.setState({ recentlySavedTimeout: null }), 5000);
this.setState({ saving: false, recentlySavedTimeout: recentlySavedTimeout, error: null });
this.refs.status.setSaved.apply(this, arguments);
},
setSaveError: function(error) {
this.setState({ saving: false, recentlySavedTimeout: null, error: error });
setSaveError: function() {
this.refs.status.setSaveError.apply(this, arguments);
},
renderDbSelector: function() {
......@@ -74,26 +65,16 @@ export default React.createClass({
}
},
renderSaveWidget: function() {
if (this.state.saving) {
return (<div className="mx2 px2 border-right"><LoadingIcon width="24" height="24" /></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">
<div className="MetadataEditor-headerSection h2">
<span className="text-grey-4">Edit Metadata for</span> {this.renderDbSelector()}
</div>
<div className="MetadataEditor-header-section flex-align-right flex align-center">
{this.renderSaveWidget()}
<span>Show original schema</span>
<span className={"Toggle " + (this.props.isShowingSchema ? "selected" : "")} onClick={this.props.toggleShowSchema}></span>
<div className="MetadataEditor-headerSection flex-align-right flex align-center">
<SaveStatus ref="status" />
<span className="mr1">Show original schema</span>
<Toggle value={this.props.isShowingSchema} onChange={this.props.toggleShowSchema} />
</div>
</div>
);
......
......@@ -37,17 +37,17 @@ export default React.createClass({
if (this.props.tables) {
var tables = _.sortBy(this.props.tables, "display_name");
_.each(tables, (table) => {
var classes = cx("AdminList-item", {
"selected": this.props.tableId === table.id,
"flex": true,
"align-center": true
var classes = cx("AdminList-item", "flex", "align-center", "no-decoration", {
"selected": this.props.tableId === table.id
});
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 key={table.id}>
<a href="#" className={classes} onClick={this.props.selectTable.bind(null, table)}>
{table.display_name}
<ProgressBar className="ProgressBar ProgressBar--mini flex-align-right" percentage={table.metadataStrength} />
</a>
</li>
)
);
var regex = this.state.searchRegex;
if (!regex || regex.test(table.display_name) || regex.test(table.name)) {
if (table.visibility_type) {
......
'use strict';
import Icon from '../../../query_builder/icon.react';
import LoadingIcon from '../../../components/icons/loading.react';
export default React.createClass({
displayName: "SaveStatus",
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 });
},
render: function() {
if (this.state.saving) {
return (<div className="SaveStatus mx2 px2 border-right"><LoadingIcon width="24" height="24" /></div>);
} else if (this.state.error) {
return (<div className="SaveStatus mx2 px2 border-right text-error">Error: {this.state.error}</div>)
} else if (this.state.recentlySavedTimeout != null) {
return (
<div className="SaveStatus mx2 px2 border-right flex align-center text-success">
<Icon name="check" width="16" height="16" />
<div className="ml1 h3 text-bold">Saved</div>
</div>
)
} else {
return <span />;
}
}
});
......@@ -7,17 +7,20 @@ import PopoverWithTrigger from '../../../query_builder/popover_with_trigger.reac
export default React.createClass({
displayName: "Select",
propTypes: {
value: React.PropTypes.object,
value: React.PropTypes.any,
options: React.PropTypes.array.isRequired,
placeholder: React.PropTypes.string,
onChange: React.PropTypes.func,
optionNameFn: React.PropTypes.func
optionNameFn: React.PropTypes.func,
optionValueFn: React.PropTypes.func
},
getDefaultProps: function() {
return {
isInitiallyOpen: false,
placeholder: "",
optionNameFn: (field) => field.name
optionNameFn: (option) => option.name,
optionValueFn: (option) => option
};
},
......@@ -29,8 +32,8 @@ export default React.createClass({
var selectedName = this.props.value ? this.props.optionNameFn(this.props.value) : this.props.placeholder;
var triggerElement = (
<div className={"flex flex-full align-center" + (!this.props.value ? " text-grey-3" : "")}>
<span>{selectedName}</span>
<div className={"flex align-center " + (!this.props.value ? " text-grey-3" : "")}>
<span className="mr1">{selectedName}</span>
<Icon className="flex-align-right" name="chevrondown" width="10" height="10"/>
</div>
);
......@@ -50,7 +53,7 @@ export default React.createClass({
itemTitleFn: this.props.optionNameFn,
itemDescriptionFn: (item) => item.description,
itemSelectFn: (item) => {
this.props.onChange(item)
this.props.onChange(this.props.optionValueFn(item))
this.toggleModal();
}
}
......@@ -67,7 +70,7 @@ export default React.createClass({
className={"PopoverBody PopoverBody--withArrow " + (this.props.className || "")}
tetherOptions={tetherOptions}
triggerElement={triggerElement}
triggerClasses={this.props.className + " AdminSelect flex align-center" }>
triggerClasses={"AdminSelect " + (this.props.className || "")}>
<ColumnarSelector columns={columns}/>
</PopoverWithTrigger>
);
......
"use strict";
import cx from "classnames";
export default React.createClass({
displayName: "Toggle",
propTypes: {
value: React.PropTypes.bool.isRequired,
onChange: React.PropTypes.func
},
onClick: function() {
if (this.props.onChange) {
this.props.onChange(!this.props.value);
}
},
render: function() {
return (
<a href="#" className={cx("Toggle", "no-decoration", { selected: this.props.value })} onClick={this.onClick} />
);
}
});
'use strict';
/*global _*/
import SettingsHeader from "./SettingsHeader.react";
import SettingsSetting from "./SettingsSetting.react";
import cx from 'classnames';
export default React.createClass({
displayName: "SettingsEditor",
propTypes: {
sections: React.PropTypes.object.isRequired,
updateSetting: React.PropTypes.func.isRequired
},
getInitialState: function() {
return {
currentSection: Object.keys(this.props.sections)[0]
};
},
selectSection: function(section) {
this.setState({ currentSection: section });
},
updateSetting: function(setting, value) {
this.refs.header.refs.status.setSaving();
setting.value = value;
this.props.updateSetting(setting).then(() => {
this.refs.header.refs.status.setSaved();
}, (error) => {
this.refs.header.refs.status.setSaveError(error.data);
});
},
handleChangeEvent: function(setting, event) {
this.updateSetting(setting, event.target.value);
},
renderSettingsPane: function() {
var section = this.props.sections[this.state.currentSection];
var settings = section.map((setting, index) => {
return <SettingsSetting key={setting.key} setting={setting} updateSetting={this.updateSetting} handleChangeEvent={this.handleChangeEvent} autoFocus={index === 0}/>
});
return (
<div className="MetadataTable px2 flex-full">
<ul>{settings}</ul>
</div>
);
},
renderSettingsSections: function() {
var sections = _.map(this.props.sections, (section, sectionName, sectionIndex) => {
var classes = cx("AdminList-item", "flex", "align-center", "no-decoration", {
"selected": this.state.currentSection === sectionName
});
return (
<li key={sectionName}>
<a href="#" className={classes} onClick={this.selectSection.bind(null, sectionName)}>
{sectionName}
</a>
</li>
);
});
return (
<div className="MetadataEditor-table-list AdminList">
<ul className="AdminList-items pt1">
{sections}
</ul>
</div>
);
},
render: function() {
return (
<div className="MetadataEditor flex flex-column flex-full p4">
<SettingsHeader ref="header" />
<div className="MetadataEditor-main flex flex-row flex-full mt2">
{this.renderSettingsSections()}
{this.renderSettingsPane()}
</div>
</div>
);
}
});
"use strict";
import SaveStatus from "../../metadata/components/SaveStatus.react";
export default React.createClass({
displayName: "SettingsHeader",
render: function() {
return (
<div className="MetadataEditor-header flex align-center relative">
<div className="MetadataEditor-headerSection h2 text-grey-4">
Settings
</div>
<div className="MetadataEditor-headerSection absolute right top bottom flex layout-centered">
<SaveStatus ref="status" />
</div>
</div>
);
},
});
"use strict";
/*global _*/
import Input from "../../metadata/components/Input.react";
import Select from "../../metadata/components/Select.react";
import Toggle from "../../metadata/components/Toggle.react";
import cx from "classnames";
export default React.createClass({
displayName: "SettingsSetting",
propTypes: {
setting: React.PropTypes.object.isRequired,
updateSetting: React.PropTypes.func.isRequired,
handleChangeEvent: React.PropTypes.func.isRequired,
autoFocus: React.PropTypes.bool
},
renderStringInput: function(setting, type="text") {
var className = type === "password" ? "SettingsPassword" : "SettingsInput";
return (
<Input
className={className + " AdminInput bordered rounded h3"}
type={type}
value={setting.value}
placeholder={setting.placeholder}
onBlurChange={this.props.handleChangeEvent.bind(null, setting)}
autoFocus={this.props.autoFocus}
/>
);
},
renderRadioInput: function(setting) {
var options = _.map(setting.options, (name, value) => {
var classes = cx("h3", "text-bold", "text-brand-hover", "no-decoration", { "text-brand": setting.value === value });
return (
<li className="mr3" key={value}>
<a className={classes} href="#" onClick={this.props.updateSetting.bind(null, setting, value)}>{name}</a>
</li>
);
});
return <ul className="flex text-grey-4">{options}</ul>
},
renderSelectInput: function(setting) {
return (
<Select
className="full-width"
placeholder={setting.placeholder}
value={setting.value}
options={setting.options}
onChange={this.props.updateSetting.bind(null, setting)}
optionNameFn={option => option}
optionValueFn={option => option}
/>
);
},
renderToggleInput: function(setting) {
var on = (setting.value == null ? setting.default : setting.value) === "true";
return (
<div className="flex align-center pt1">
<Toggle value={on} onChange={this.props.updateSetting.bind(null, setting, on ? "false" : "true")}/>
<span className="text-bold mx1">{on ? "Enabled" : "Disabled"}</span>
</div>
);
},
render: function() {
var setting = this.props.setting;
var control;
switch (setting.type) {
case "string": control = this.renderStringInput(setting); break;
case "password": control = this.renderStringInput(setting, "password"); break;
case "select": control = this.renderSelectInput(setting); break;
case "radio": control = this.renderRadioInput(setting); break;
case "boolean": control = this.renderToggleInput(setting); break;
default:
console.warn("No render method for setting type " + setting.type + ", defaulting to string input.");
control = this.renderStringInput(setting);
}
return (
<li className="m2 mb4">
<div className="text-grey-4 text-bold text-uppercase">{setting.display_name}</div>
<div className="text-grey-4 my1">{setting.description}</div>
<div className="flex">{control}</div>
</li>
);
}
});
<div class="wrapper">
<section class="PageHeader">
<h2 class="PageTitle">Global Settings</h2>
</section>
<section>
<form class="Form-new bordered rounded shadowed" name="form" novalidate>
<!-- Form -->
<div class="FormInputGroup">
<div class="Form-field" ng-repeat="setting in settings" mb-form-field="{{setting.key}}">
<mb-form-label display-name="{{settingName(setting)}}" field-name="{{setting.key}}"></mb-form-label>
<input class="Form-input Form-offset full" name="{{setting.key}}" placeholder="{{settingPlaceholder(setting)}}" ng-model="setting.value" />
<span class="Form-charm"></span>
</div>
</div>
<!-- Bottom Actions -->
<div class="Form-actions">
<button class="Button" ng-class="{'Button--primary': form.$valid}" ng-click="save(database, details)" ng-disabled="!form.$valid">
Save
</button>
<mb-form-message></mb-form-message>
</div>
</form>
</section>
</div>
'use strict';
/*global _*/
import SettingsEditor from './components/SettingsEditor.react';
import Humanize from "humanize";
var SettingsAdminControllers = angular.module('corvusadmin.settings.controllers', ['corvusadmin.settings.services']);
SettingsAdminControllers.controller('SettingsAdminController', ['$scope', '$q', 'AppState', 'SettingsAdminServices',
function($scope, $q, AppState, SettingsAdminServices) {
$scope.settings = [];
SettingsAdminServices.list(function(results) {
$scope.settings = _.map(results, function(result) {
result.originalValue = result.value;
return result;
});
}, function(error) {
console.log("Error fetching settings list: ", error);
});
$scope.settingName = function(setting) {
return setting.description.replace(/\.$/, '');
}
$scope.settingPlaceholder = function(setting) {
return setting.default;
}
$scope.save = function() {
$scope.$broadcast("form:reset");
return $q.all($scope.settings.map(function(setting) {
if (setting.value !== setting.originalValue) {
return SettingsAdminServices.put({
key: setting.key
}, setting).$promise.then(function() {
setting.originalValue = setting.value;
});
}
})).then(function(results) {
$scope.$broadcast("form:api-success", "Successfully saved!");
// refresh the app-wide settings now as the user may have just changed some of them
AppState.refreshSiteSettings();
}, function(error) {
$scope.$broadcast("form:api-error", error);
throw error;
});
};
// from common.clj
var TIMEZONES = [
"GMT",
"UTC",
"US/Alaska",
"US/Arizona",
"US/Central",
"US/Eastern",
"US/Hawaii",
"US/Mountain",
"US/Pacific",
"America/Costa_Rica",
];
// temporary hardcoded metadata
var EXTRA_SETTINGS_METADATA = {
"site-name": { display_name: "Site Name", section: "General", index: 0, type: "string" },
"-site-url": { display_name: "Site URL", section: "General", index: 1, type: "string" },
"report-timezone": { display_name: "Report Timezone", section: "General", index: 2, type: "select", options: TIMEZONES, placeholder: "Select a timezone" },
"anon-tracking-enabled":{ display_name: "Anonymous Tracking", section: "General", index: 3, type: "boolean" },
"email-smtp-host": { display_name: "SMTP Host", section: "Email", index: 0, type: "string" },
"email-smtp-port": { display_name: "SMTP Port", section: "Email", index: 1, type: "string" },
"email-smtp-security": { display_name: "SMTP Security", section: "Email", index: 2, type: "radio", options: { none: "None", tls: "TLS", ssl: "SSL" } },
"email-smtp-username": { display_name: "SMTP Username", section: "Email", index: 3, type: "string" },
"email-smtp-password": { display_name: "SMTP Password", section: "Email", index: 4, type: "password" },
"email-from-address": { display_name: "From Address", section: "Email", index: 5, type: "string" },
};
SettingsAdminControllers.controller('SettingsEditor', ['$scope', 'SettingsAdminServices', 'AppState', 'settings', function($scope, SettingsAdminServices, AppState, settings) {
$scope.SettingsEditor = SettingsEditor;
$scope.updateSetting = async function(setting) {
await SettingsAdminServices.put({ key: setting.key }, setting).$promise;
AppState.refreshSiteSettings();
}
$scope.sections = {};
settings.forEach(function(setting) {
var defaults = { display_name: keyToDisplayName(setting.key), placeholder: setting.default };
setting = _.extend(defaults, EXTRA_SETTINGS_METADATA[setting.key], setting);
var sectionName = setting.section || "Other";
$scope.sections[sectionName] = $scope.sections[sectionName] || [];
$scope.sections[sectionName].push(setting);
});
_.each($scope.sections, (section) => section.sort((a, b) => a.index - b.index))
function keyToDisplayName(key) {
return Humanize.capitalizeAll(key.replace(/-/g, " ")).trim();
}
]);
}]);
......@@ -7,7 +7,16 @@ var SettingsAdmin = angular.module('corvusadmin.settings', [
SettingsAdmin.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/admin/settings/', {
templateUrl: '/app/admin/settings/partials/settings.html',
controller: 'SettingsAdminController'
template: '<div class="flex flex-column flex-full" mb-react-component="SettingsEditor"></div>',
controller: 'SettingsEditor',
resolve: {
settings: ['SettingsAdminServices', async function(SettingsAdminServices) {
var settings = await SettingsAdminServices.list().$promise
return settings.map(function(setting) {
setting.originalValue = setting.value;
return setting;
});
}]
}
});
}]);
......@@ -299,12 +299,14 @@
}
.AdminSelect {
display: inline-block;
padding: 0.6em;
border: 1px solid var(--border-color);
border-radius: var(--default-border-radius);
font-size: 14px;
font-weight: 700;
margin-bottom: 3px;
min-width: 90px;
}
......@@ -346,8 +348,7 @@
}
.Toggle {
margin: 0.5em;
box-sizing: border-box;
width: 48px;
height: 24px;
border-radius: 99px;
......@@ -408,3 +409,19 @@
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
.SaveStatus {
line-height: 1;
}
.SaveStatus:last-child {
border-right: none !important;
}
.SettingsInput {
width: 400px;
}
.SettingsPassword {
width: 200px;
}
......@@ -20,10 +20,10 @@ export default React.createClass({
var title = section.title;
var items = section.items.map((item, rowIndex) => {
var itemClasses = cx({
'cursor-pointer': true,
'ColumnarSelector-row': true,
'ColumnarSelector-row--selected': item === column.selectedItem,
'flex': true
'flex': true,
'no-decoration': true
});
var checkIcon = lastColumn ? <Icon name="check" width="14" height="14"/> : null;
var descriptionElement;
......@@ -32,12 +32,14 @@ export default React.createClass({
descriptionElement = <div className="ColumnarSelector-description">{description}</div>
}
return (
<li key={rowIndex} className={itemClasses} onClick={column.itemSelectFn.bind(null, item)}>
{checkIcon}
<div className="flex flex-column">
{column.itemTitleFn(item)}
{descriptionElement}
</div>
<li key={rowIndex}>
<a className={itemClasses} href="#" onClick={column.itemSelectFn.bind(null, item)}>
{checkIcon}
<div className="flex flex-column">
{column.itemTitleFn(item)}
{descriptionElement}
</div>
</a>
</li>
);
});
......
sorry, you are not authorized to view the specified page.
<h1 class="flex layout-centered flex-full text-grey-2">Sorry, you are not authorized to view the specified page.</h1>
databaseChangeLog:
- changeSet:
id: 9
author: cammsaul
changes:
- createTable:
tableName: revision
columns:
- column:
name: id
type: int
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: model
type: varchar(16)
constraints:
nullable: false
- column:
name: model_id
type: int
constraints:
nullable: false
- column:
name: user_id
type: int
constraints:
nullable: false
references: core_user(id)
foreignKeyName: fk_revision_ref_user_id
deferrable: false
initiallyDeferred: false
- column:
name: timestamp
type: DATETIME
constraints:
nullable: false
- column:
name: object
type: varchar
constraints:
nullable: false
- column:
name: is_reversion
type: boolean
defaultValueBoolean: false
constraints:
nullable: false
- createIndex:
tableName: revision
indexName: idx_revision_model_model_id
columns:
column:
name: model
column:
name: model_id
- modifySql:
dbms: postgresql
replace:
replace: WITHOUT
with: WITH
......@@ -8,6 +8,7 @@
{"include": {"file": "migrations/007_add_field_parent_id.json"}},
{"include": {"file": "migrations/008_add_display_name_columns.json"}},
{"include": {"file": "migrations/009_add_table_visibility_type_column.json"}},
{"include": {"file": "migrations/010_cleanup_dashboard_perms.yaml"}}
{"include": {"file": "migrations/010_add_revision_table.yaml"}},
{"include": {"file": "migrations/011_cleanup_dashboard_perms.yaml"}}
]
}
......@@ -8,6 +8,7 @@
[card :refer [Card] :as card]
[card-favorite :refer [CardFavorite]]
[common :as common]
[revision :refer [push-revision]]
[user :refer [User]])))
(defannotation CardFilterOption
......@@ -46,12 +47,12 @@
display [Required CardDisplayType]}
;; TODO - which other params are required?
(ins Card
:creator_id *current-user-id*
:dataset_query dataset_query
:description description
:display display
:name name
:public_perms public_perms
:creator_id *current-user-id*
:dataset_query dataset_query
:description description
:display display
:name name
:public_perms public_perms
:visualization_settings visualization_settings))
(defendpoint GET "/:id"
......@@ -75,7 +76,7 @@
:name name
:public_perms public_perms
:visualization_settings visualization_settings))
(Card id))
(push-revision :entity Card, :object (Card id)))
(defendpoint DELETE "/:id"
"Delete a `Card`."
......
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