mirror of
https://github.com/getredash/redash.git
synced 2025-12-25 01:03:20 -05:00
Migrated query edit/add param dialog to React/AntD/Hooks (#3488)
This commit is contained in:
@@ -953,3 +953,7 @@ text.slicetext {
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.select-option-divider {
|
||||
margin: 10px 0 !important;
|
||||
}
|
||||
|
||||
189
client/app/components/EditParameterSettingsDialog.jsx
Normal file
189
client/app/components/EditParameterSettingsDialog.jsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import { includes, words, capitalize, clone, isNull } from 'lodash';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import Form from 'antd/lib/form';
|
||||
import Checkbox from 'antd/lib/checkbox';
|
||||
import Select from 'antd/lib/select';
|
||||
import Input from 'antd/lib/input';
|
||||
import Divider from 'antd/lib/divider';
|
||||
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
|
||||
import { QuerySelector } from '@/components/QuerySelector';
|
||||
import { Query } from '@/services/query';
|
||||
|
||||
const { Option } = Select;
|
||||
const formItemProps = { labelCol: { span: 6 }, wrapperCol: { span: 16 } };
|
||||
|
||||
function getDefaultTitle(text) {
|
||||
return capitalize(words(text).join(' ')); // humanize
|
||||
}
|
||||
|
||||
function NameInput({ name, onChange, existingNames, setValidation }) {
|
||||
let helpText = `This is what will be added to your query editor {{ ${name} }}`;
|
||||
let validateStatus = '';
|
||||
|
||||
if (!name) {
|
||||
helpText = 'Choose a keyword for this parameter';
|
||||
setValidation(false);
|
||||
} else if (includes(existingNames, name)) {
|
||||
helpText = 'Parameter with this name already exists';
|
||||
setValidation(false);
|
||||
validateStatus = 'error';
|
||||
} else {
|
||||
setValidation(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
required
|
||||
label="Keyword"
|
||||
help={helpText}
|
||||
validateStatus={validateStatus}
|
||||
{...formItemProps}
|
||||
>
|
||||
<Input onChange={e => onChange(e.target.value)} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
NameInput.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
existingNames: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
setValidation: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function EditParameterSettingsDialog(props) {
|
||||
const [param, setParam] = useState(clone(props.parameter));
|
||||
const [isNameValid, setIsNameValid] = useState(true);
|
||||
const [initialQuery, setInitialQuery] = useState();
|
||||
|
||||
const isNew = !props.parameter.name;
|
||||
|
||||
// fetch query by id
|
||||
useEffect(() => {
|
||||
const { queryId } = props.parameter;
|
||||
if (queryId) {
|
||||
Query.get({ id: queryId }, (query) => {
|
||||
setInitialQuery(query);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
function isFulfilled() {
|
||||
// name
|
||||
if (!isNameValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// title
|
||||
if (param.title === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// query
|
||||
if (param.type === 'query' && !param.queryId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
// update title to default
|
||||
if (!param.title) {
|
||||
// forced to do this cause param won't update in time for save
|
||||
param.title = getDefaultTitle(param.name);
|
||||
setParam(param);
|
||||
}
|
||||
|
||||
props.dialog.close(param);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
{...props.dialog.props}
|
||||
title={isNew ? 'Add Parameter' : param.name}
|
||||
onOk={onConfirm}
|
||||
okText={isNew ? 'Add Parameter' : null}
|
||||
okButtonProps={{ disabled: !isFulfilled() }}
|
||||
>
|
||||
<Form layout="horizontal">
|
||||
{isNew && (
|
||||
<NameInput
|
||||
name={param.name}
|
||||
onChange={name => setParam({ ...param, name })}
|
||||
setValidation={setIsNameValid}
|
||||
existingNames={props.existingParams}
|
||||
/>
|
||||
)}
|
||||
<Form.Item label="Title" {...formItemProps}>
|
||||
<Input
|
||||
value={isNull(param.title) ? getDefaultTitle(param.name) : param.title}
|
||||
onChange={e => setParam({ ...param, title: e.target.value })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Type" {...formItemProps}>
|
||||
<Select value={param.type} onChange={type => setParam({ ...param, type })}>
|
||||
<Option value="text">Text</Option>
|
||||
<Option value="number">Number</Option>
|
||||
<Option value="enum">Dropdown List</Option>
|
||||
<Option value="query">Query Based Dropdown List</Option>
|
||||
<Option disabled key="dv1">
|
||||
<Divider className="select-option-divider" />
|
||||
</Option>
|
||||
<Option value="date">Date</Option>
|
||||
<Option value="datetime-local">Date and Time</Option>
|
||||
<Option value="datetime-with-seconds">Date and Time (with seconds)</Option>
|
||||
<Option disabled key="dv2">
|
||||
<Divider className="select-option-divider" />
|
||||
</Option>
|
||||
<Option value="date-range">Date Range</Option>
|
||||
<Option value="datetime-range">Date and Time Range</Option>
|
||||
<Option value="datetime-range-with-seconds">Date and Time Range (with seconds)</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{includes(['date', 'datetime-local', 'datetime-with-seconds'], param.type) && (
|
||||
<Form.Item label=" " colon={false} {...formItemProps}>
|
||||
<Checkbox
|
||||
defaultChecked={param.useCurrentDateTime}
|
||||
onChange={e => setParam({ ...param, useCurrentDateTime: e.target.checked })}
|
||||
>
|
||||
Default to Today/Now if no other value is set
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
)}
|
||||
{param.type === 'enum' && (
|
||||
<Form.Item label="Values" help="Dropdown list values (newline delimeted)" {...formItemProps}>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
value={param.enumOptions}
|
||||
onChange={e => setParam({ ...param, enumOptions: e.target.value })}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{param.type === 'query' && (
|
||||
<Form.Item label="Query" help="Select query to load dropdown values from" {...formItemProps}>
|
||||
<QuerySelector
|
||||
selectedQuery={initialQuery}
|
||||
onChange={q => setParam({ ...param, queryId: q && q.id })}
|
||||
type="select"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditParameterSettingsDialog.propTypes = {
|
||||
parameter: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
dialog: DialogPropType.isRequired,
|
||||
existingParams: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
EditParameterSettingsDialog.defaultProps = {
|
||||
existingParams: [],
|
||||
};
|
||||
|
||||
export default wrapDialog(EditParameterSettingsDialog);
|
||||
@@ -1,65 +0,0 @@
|
||||
<form novalidate ng-submit="$ctrl.close({ $value: $ctrl.parameter })">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" aria-label="Close" ng-click="$ctrl.dismiss()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">{{ $ctrl.isNewParameter ? 'Add Parameter' : $ctrl.parameter.name }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form">
|
||||
<div class="form-group" ng-if="$ctrl.isNewParameter" ng-class="{'has-error': $ctrl.parameterAlreadyExists($ctrl.parameter.name)}">
|
||||
<label>Keyword</label>
|
||||
<input type="text" class="form-control" ng-model="$ctrl.parameter.name" autofocus ng-change="$ctrl.updateTitle()">
|
||||
<div class="help-block" ng-if="!$ctrl.parameterAlreadyExists($ctrl.parameter.name)">
|
||||
This is what will be added to your query editor<span ng-if="$ctrl.parameter.name != ''">: \{\{ {{ $ctrl.parameter.name }} \}\}</span>
|
||||
</div>
|
||||
<div class="help-block" ng-if="$ctrl.parameterAlreadyExists($ctrl.parameter.name)">
|
||||
Parameter with this name already exists.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Title</label>
|
||||
<input type="text" class="form-control" ng-model="$ctrl.parameter.title" ng-change="$ctrl.shouldGenerateTitle = false">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Type</label>
|
||||
<select ng-model="$ctrl.parameter.type" class="form-control">
|
||||
<option value="text">Text</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="enum">Dropdown List</option>
|
||||
<option value="query">Query Based Dropdown List</option>
|
||||
<option disabled>--------------------------------------------------</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="datetime-local">Date and Time</option>
|
||||
<option value="datetime-with-seconds">Date and Time (with seconds)</option>
|
||||
<option disabled>--------------------------------------------------</option>
|
||||
<option value="date-range">Date Range</option>
|
||||
<option value="datetime-range">Date and Time Range</option>
|
||||
<option value="datetime-range-with-seconds">Date and Time Range (with seconds)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" ng-if="['date', 'datetime-local', 'datetime-with-seconds'].indexOf($ctrl.parameter.type) >= 0">
|
||||
<label>
|
||||
<input type="checkbox" class="form-inline" ng-model="$ctrl.parameter.useCurrentDateTime"> Use Today/Now as default value if no other value is set
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group" ng-if="$ctrl.parameter.type === 'enum'">
|
||||
<label>Dropdown List Values (newline delimited)</label>
|
||||
<textarea class="form-control" rows="3" ng-model="$ctrl.parameter.enumOptions"></textarea>
|
||||
</div>
|
||||
<div class="form-group" ng-if="$ctrl.parameter.type === 'query'">
|
||||
<label>Query to load dropdown values from:</label>
|
||||
<ui-select ng-model="$ctrl.parameter.queryId" reset-search-input="false">
|
||||
<ui-select-match placeholder="Search a query by name">{{$select.selected.name}}</ui-select-match>
|
||||
<ui-select-choices repeat="q.id as q in $ctrl.queries" refresh="$ctrl.searchQueries($select.search)" refresh-delay="0">
|
||||
<div class="form-group" ng-bind-html="$ctrl.trustAsHtml(q.name | highlight: $select.search)"></div>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer p-t-0" ng-if="$ctrl.isNewParameter">
|
||||
<button type="button" class="btn btn-default pull-left" ng-click="$ctrl.dismiss()">Close</button>
|
||||
<button type="submit" class="btn btn-primary pull-right" ng-disabled="($ctrl.parameter.name == '') || $ctrl.parameterAlreadyExists($ctrl.parameter.name)">+ Add Parameter</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -5,7 +5,7 @@
|
||||
>
|
||||
<div class="form-group m-r-10" ng-repeat="param in parameters">
|
||||
<label class="parameter-label">{{param.title}}</label>
|
||||
<button class="btn btn-default btn-xs" ng-if="editable" ng-click="showParameterSettings(param)">
|
||||
<button class="btn btn-default btn-xs" ng-if="editable" ng-click="showParameterSettings(param, $index)">
|
||||
<i class="zmdi zmdi-settings"></i>
|
||||
</button>
|
||||
<parameter-value-input param="param"></parameter-value-input>
|
||||
|
||||
@@ -1,53 +1,8 @@
|
||||
import { includes, words, capitalize, extend } from 'lodash';
|
||||
import { extend } from 'lodash';
|
||||
import template from './parameters.html';
|
||||
import parameterSettingsTemplate from './parameter-settings.html';
|
||||
import EditParameterSettingsDialog from './EditParameterSettingsDialog';
|
||||
|
||||
function humanize(str) {
|
||||
return capitalize(words(str).join(' '));
|
||||
}
|
||||
|
||||
const ParameterSettingsComponent = {
|
||||
template: parameterSettingsTemplate,
|
||||
bindings: {
|
||||
resolve: '<',
|
||||
close: '&',
|
||||
dismiss: '&',
|
||||
},
|
||||
controller($sce, Query) {
|
||||
'ngInject';
|
||||
|
||||
this.trustAsHtml = html => $sce.trustAsHtml(html);
|
||||
this.parameter = this.resolve.parameter;
|
||||
this.isNewParameter = this.parameter.name === '';
|
||||
this.shouldGenerateTitle = this.isNewParameter && this.parameter.title === '';
|
||||
|
||||
this.parameterAlreadyExists = name => includes(this.resolve.existingParameters, name);
|
||||
|
||||
if (this.parameter.queryId) {
|
||||
Query.get({ id: this.parameter.queryId }, (query) => {
|
||||
this.queries = [query];
|
||||
});
|
||||
}
|
||||
|
||||
this.searchQueries = (term) => {
|
||||
if (!term || term.length < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
Query.query({ q: term }, (results) => {
|
||||
this.queries = results.results;
|
||||
});
|
||||
};
|
||||
|
||||
this.updateTitle = () => {
|
||||
if (this.shouldGenerateTitle) {
|
||||
this.parameter.title = humanize(this.parameter.name);
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function ParametersDirective($location, $uibModal) {
|
||||
function ParametersDirective($location) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
@@ -56,6 +11,7 @@ function ParametersDirective($location, $uibModal) {
|
||||
syncValues: '=?',
|
||||
editable: '=?',
|
||||
changed: '&onChange',
|
||||
onUpdated: '=',
|
||||
},
|
||||
template,
|
||||
link(scope) {
|
||||
@@ -77,13 +33,13 @@ function ParametersDirective($location, $uibModal) {
|
||||
);
|
||||
}
|
||||
|
||||
scope.showParameterSettings = (param) => {
|
||||
$uibModal.open({
|
||||
component: 'parameterSettings',
|
||||
resolve: {
|
||||
parameter: param,
|
||||
},
|
||||
});
|
||||
scope.showParameterSettings = (parameter, index) => {
|
||||
EditParameterSettingsDialog
|
||||
.showModal({ parameter })
|
||||
.result.then((updated) => {
|
||||
scope.parameters[index] = extend(parameter, updated);
|
||||
scope.onUpdated();
|
||||
});
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -91,7 +47,6 @@ function ParametersDirective($location, $uibModal) {
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('parameters', ParametersDirective);
|
||||
ngModule.component('parameterSettings', ParameterSettingsComponent);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -191,7 +191,7 @@
|
||||
<section class="flex-fill p-relative t-body query-visualizations-wrapper">
|
||||
<div class="d-flex flex-column p-b-15 p-absolute static-position__mobile" style="left: 0; top: 0; right: 0; bottom: 0;">
|
||||
<div class="p-t-15 p-b-5" ng-if="query.getParametersDefs().length > 0">
|
||||
<parameters parameters="query.getParametersDefs()" sync-values="!query.isNew()" editable="sourceMode && canEdit"></parameters>
|
||||
<parameters parameters="query.getParametersDefs()" sync-values="!query.isNew()" editable="sourceMode && canEdit" on-updated="onParametersUpdated"></parameters>
|
||||
</div>
|
||||
<!-- Query Execution Status -->
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { map, defer } from 'lodash';
|
||||
import template from './query.html';
|
||||
import EditParameterSettingsDialog from '@/components/EditParameterSettingsDialog';
|
||||
|
||||
function QuerySourceCtrl(
|
||||
Events,
|
||||
@@ -73,19 +74,15 @@ function QuerySourceCtrl(
|
||||
};
|
||||
|
||||
$scope.addNewParameter = () => {
|
||||
$uibModal
|
||||
.open({
|
||||
component: 'parameterSettings',
|
||||
resolve: {
|
||||
parameter: {
|
||||
title: '',
|
||||
name: '',
|
||||
type: 'text',
|
||||
value: null,
|
||||
global: false,
|
||||
},
|
||||
existingParameters: () => map($scope.query.getParameters().get(), p => p.name),
|
||||
EditParameterSettingsDialog
|
||||
.showModal({
|
||||
parameter: {
|
||||
title: null,
|
||||
name: '',
|
||||
type: 'text',
|
||||
value: null,
|
||||
},
|
||||
existingParams: map($scope.query.getParameters().get(), p => p.name),
|
||||
})
|
||||
.result.then((param) => {
|
||||
param = $scope.query.getParameters().add(param);
|
||||
@@ -94,6 +91,14 @@ function QuerySourceCtrl(
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onParametersUpdated = () => {
|
||||
// save if query clean
|
||||
// https://discuss.redash.io/t/query-unsaved-changes-indication/3302/5
|
||||
if (!$scope.isDirty) {
|
||||
$scope.saveQuery();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.listenForEditorCommand = f => $scope.$on('query-editor.command', f);
|
||||
$scope.listenForResize = f => $scope.$parent.$on('angular-resizable.resizing', f);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user