Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add options for Query Based Parameter for a query with parameters #4648

Draft
wants to merge 31 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5e3613d
Start experiements with a 'search' parameter
gabrieldutra Feb 13, 2020
b70f0fa
Iterate over backend verification
gabrieldutra Feb 16, 2020
9cf3965
Parameter Mapping UI (1/2)
gabrieldutra Feb 18, 2020
137aa22
Parameter Mapping UI (2/2)
gabrieldutra Feb 18, 2020
bb0d783
Fixes + temp remove validation for Query param
gabrieldutra Feb 20, 2020
b7478de
Don't validade query params with params
gabrieldutra Feb 20, 2020
bdd7b14
Change stored mapping attributes
gabrieldutra Feb 21, 2020
e555642
Add is_safe check for parameterized query based
gabrieldutra Feb 21, 2020
df755fb
Add try except for NoResultFound
gabrieldutra Feb 21, 2020
90023ac
Make sure Table updates correctly
gabrieldutra Feb 21, 2020
264fb57
Merge branch 'master' into query-based-dropdown--parameters
gabrieldutra Feb 21, 2020
f128b4b
Only allow search for Text Parameters
gabrieldutra Feb 21, 2020
d40edb8
Fix backend tests
gabrieldutra Feb 21, 2020
6eeb3b3
Separate UI components
gabrieldutra Feb 21, 2020
6c27619
Make Parameter Mapping required in UI
gabrieldutra Feb 22, 2020
6aa0ea7
Invert tooltip messages order
gabrieldutra Feb 22, 2020
cc34e78
Small updates
gabrieldutra Feb 22, 2020
a37e7f9
Add is_safe test for queries with params
gabrieldutra Feb 22, 2020
8a1640c
Separate InputPopover component
gabrieldutra Feb 24, 2020
8bfcbf2
Remove redundant import
gabrieldutra Feb 24, 2020
f396c96
Merge branch 'master' into query-based-dropdown--parameters
gabrieldutra Feb 25, 2020
53385fa
Merge branch 'master' into query-based-dropdown--parameters
gabrieldutra Nov 10, 2020
a741341
Oops
gabrieldutra Nov 10, 2020
d0a787c
Make NoResultFound invalid parameters
gabrieldutra Nov 11, 2020
aa2064b
Fix other dropdown_values usages to use query obj
gabrieldutra Nov 11, 2020
08bcdf7
Mock query instead of query_has_parameters
gabrieldutra Nov 12, 2020
0c0b62a
Remove searchTerm from structure
gabrieldutra Nov 20, 2020
bd9ce68
Don't filter out values when param has search
gabrieldutra Nov 20, 2020
b40070d
Use LabeledValues for parameterized queries
gabrieldutra Nov 23, 2020
c1ed884
Merge branch 'master' into query-based-dropdown--parameters
gabrieldutra Nov 23, 2020
19343a0
Clear QueryBasedParameterInput
gabrieldutra Nov 23, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { includes, words, capitalize, clone, isNull } from "lodash";
import React, { useState, useEffect } from "react";
import { includes, words, capitalize, clone, isNull, map, get, find } from "lodash";
import React, { useState, useEffect, useRef, useMemo } from "react";
import PropTypes from "prop-types";
import Checkbox from "antd/lib/checkbox";
import Modal from "antd/lib/modal";
Expand All @@ -11,6 +11,8 @@ import Divider from "antd/lib/divider";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import QuerySelector from "@/components/QuerySelector";
import { Query } from "@/services/query";
import { QueryBasedParameterMappingType } from "@/services/parameters/QueryBasedDropdownParameter";
import QueryBasedParameterMappingTable from "./query-based-parameter/QueryBasedParameterMappingTable";

const { Option } = Select;
const formItemProps = { labelCol: { span: 6 }, wrapperCol: { span: 16 } };
Expand Down Expand Up @@ -69,17 +71,27 @@ NameInput.propTypes = {
function EditParameterSettingsDialog(props) {
const [param, setParam] = useState(clone(props.parameter));
const [isNameValid, setIsNameValid] = useState(true);
const [initialQuery, setInitialQuery] = useState();
const [paramQuery, setParamQuery] = useState();
const mappingParameters = useMemo(
() =>
map(paramQuery && paramQuery.getParametersDefs(), mappingParam => ({
mappingParam,
existingMapping: get(param.parameterMapping, mappingParam.name, {
mappingType: QueryBasedParameterMappingType.UNDEFINED,
}),
})),
[param.parameterMapping, paramQuery]
);

const isNew = !props.parameter.name;

// fetch query by id
const initialQueryId = useRef(props.parameter.queryId);
useEffect(() => {
const queryId = props.parameter.queryId;
if (queryId) {
Query.get({ id: queryId }).then(setInitialQuery);
if (initialQueryId.current) {
Query.get({ id: initialQueryId.current }).then(setParamQuery);
}
}, [props.parameter.queryId]);
}, []);

function isFulfilled() {
// name
Expand All @@ -93,8 +105,14 @@ function EditParameterSettingsDialog(props) {
}

// query
if (param.type === "query" && !param.queryId) {
return false;
if (param.type === "query") {
if (!param.queryId) {
return false;
}

if (find(mappingParameters, { existingMapping: { mappingType: QueryBasedParameterMappingType.UNDEFINED } })) {
return false;
}
}

return true;
Expand Down Expand Up @@ -187,14 +205,28 @@ function EditParameterSettingsDialog(props) {
</Form.Item>
)}
{param.type === "query" && (
<Form.Item label="Query" help="Select query to load dropdown values from" {...formItemProps}>
<Form.Item label="Query" help="Select query to load dropdown values from" required {...formItemProps}>
<QuerySelector
selectedQuery={initialQuery}
onChange={q => setParam({ ...param, queryId: q && q.id })}
selectedQuery={paramQuery}
onChange={q => {
if (q) {
setParamQuery(q);
setParam({ ...param, queryId: q.id, parameterMapping: {} });
}
}}
type="select"
/>
</Form.Item>
)}
{param.type === "query" && paramQuery && paramQuery.hasParameters() && (
<Form.Item className="m-t-15 m-b-5" label="Parameters" required {...formItemProps}>
<QueryBasedParameterMappingTable
param={param}
mappingParameters={mappingParameters}
onChangeParam={setParam}
/>
</Form.Item>
)}
{(param.type === "enum" || param.type === "query") && (
<Form.Item className="m-b-0" label=" " colon={false} {...formItemProps}>
<Checkbox
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React, { useState, useEffect, useRef, useReducer } from "react";
import PropTypes from "prop-types";
import { values } from "lodash";
import Button from "antd/lib/button";
import Tooltip from "antd/lib/tooltip";
import Radio from "antd/lib/radio";
import Typography from "antd/lib/typography/Typography";
import ParameterValueInput from "@/components/ParameterValueInput";
import InputPopover from "@/components/InputPopover";
import Form from "antd/lib/form";
import { QueryBasedParameterMappingType } from "@/services/parameters/QueryBasedDropdownParameter";

import QuestionCircleFilledIcon from "@ant-design/icons/QuestionCircleFilled";
import EditOutlinedIcon from "@ant-design/icons/EditOutlined";

const { Text } = Typography;

const formItemProps = { labelCol: { span: 6 }, wrapperCol: { span: 16 } };
export default function QueryBasedParameterMappingEditor({ parameter, mapping, searchAvailable, onChange }) {
const [showPopover, setShowPopover] = useState(false);
const [newMapping, setNewMapping] = useReducer((prevState, updates) => ({ ...prevState, ...updates }), mapping);

const newMappingRef = useRef(newMapping);
useEffect(() => {
if (
mapping.mappingType !== newMappingRef.current.mappingType ||
mapping.staticValue !== newMappingRef.current.staticValue
) {
setNewMapping(mapping);
}
}, [mapping]);

const parameterRef = useRef(parameter);
useEffect(() => {
parameterRef.current.setValue(mapping.staticValue);
}, [mapping.staticValue]);

const onCancel = () => {
setNewMapping(mapping);
setShowPopover(false);
};

const onOk = () => {
onChange(newMapping);
setShowPopover(false);
};

let currentState = <Text type="secondary">Pick a type</Text>;
if (mapping.mappingType === QueryBasedParameterMappingType.DROPDOWN_SEARCH) {
currentState = "Dropdown Search";
} else if (mapping.mappingType === QueryBasedParameterMappingType.STATIC) {
currentState = `Value: ${mapping.staticValue}`;
}
return (
<>
{currentState}
<InputPopover
placement="left"
trigger="click"
header="Edit Parameter Source"
okButtonProps={{
disabled: newMapping.mappingType === QueryBasedParameterMappingType.STATIC && parameter.isEmpty,
}}
onOk={onOk}
onCancel={onCancel}
content={
<Form>
<Form.Item className="m-b-15" label="Source" {...formItemProps}>
<Radio.Group
value={newMapping.mappingType}
onChange={({ target }) => setNewMapping({ mappingType: target.value })}>
<Radio
className="radio"
value={QueryBasedParameterMappingType.DROPDOWN_SEARCH}
disabled={!searchAvailable || parameter.type !== "text"}>
Dropdown Search{" "}
{(!searchAvailable || parameter.type !== "text") && (
<Tooltip
title={
parameter.type !== "text"
? "Dropdown Search is only available for Text Parameters"
: "There is already a parameter mapped with the Dropdown Search type."
}>
<QuestionCircleFilledIcon />
</Tooltip>
)}
</Radio>
<Radio className="radio" value={QueryBasedParameterMappingType.STATIC}>
Static Value
</Radio>
</Radio.Group>
</Form.Item>
{newMapping.mappingType === QueryBasedParameterMappingType.STATIC && (
<Form.Item label="Value" required {...formItemProps}>
<ParameterValueInput
type={parameter.type}
value={parameter.normalizedValue}
enumOptions={parameter.enumOptions}
queryId={parameter.queryId}
parameter={parameter}
onSelect={value => {
parameter.setValue(value);
setNewMapping({ staticValue: parameter.getExecutionValue({ joinListValues: true }) });
}}
/>
</Form.Item>
)}
</Form>
}
visible={showPopover}
onVisibleChange={setShowPopover}>
<Button className="m-l-5" size="small" type="dashed">
<EditOutlinedIcon />
</Button>
</InputPopover>
</>
);
}

QueryBasedParameterMappingEditor.propTypes = {
parameter: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
mapping: PropTypes.shape({
mappingType: PropTypes.oneOf(values(QueryBasedParameterMappingType)),
staticValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}),
searchAvailable: PropTypes.bool,
onChange: PropTypes.func,
};

QueryBasedParameterMappingEditor.defaultProps = {
mapping: { mappingType: QueryBasedParameterMappingType.UNDEFINED, staticValue: undefined },
searchAvailable: false,
onChange: () => {},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from "react";
import { findKey } from "lodash";
import PropTypes from "prop-types";
import Table from "antd/lib/table";
import { QueryBasedParameterMappingType } from "@/services/parameters/QueryBasedDropdownParameter";
import QueryBasedParameterMappingEditor from "./QueryBasedParameterMappingEditor";

export default function QueryBasedParameterMappingTable({ param, mappingParameters, onChangeParam }) {
return (
<Table
dataSource={mappingParameters}
size="middle"
pagination={false}
rowKey={({ mappingParam }) => `param${mappingParam.name}`}>
<Table.Column title="Title" key="title" render={({ mappingParam }) => mappingParam.getTitle()} />
<Table.Column
title="Keyword"
key="keyword"
className="keyword"
render={({ mappingParam }) => <code>{`{{ ${mappingParam.name} }}`}</code>}
/>
<Table.Column
title="Value Source"
key="source"
render={({ mappingParam, existingMapping }) => (
<QueryBasedParameterMappingEditor
parameter={mappingParam.setValue(existingMapping.staticValue)}
mapping={existingMapping}
searchAvailable={
!findKey(param.parameterMapping, {
mappingType: QueryBasedParameterMappingType.DROPDOWN_SEARCH,
}) || existingMapping.mappingType === QueryBasedParameterMappingType.DROPDOWN_SEARCH
}
onChange={mapping =>
onChangeParam({
...param,
parameterMapping: { ...param.parameterMapping, [mappingParam.name]: mapping },
})
}
/>
)}
/>
</Table>
);
}

QueryBasedParameterMappingTable.propTypes = {
param: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
mappingParameters: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types
onChangeParam: PropTypes.func,
};

QueryBasedParameterMappingTable.defaultProps = {
mappingParameters: [],
onChangeParam: () => {},
};
57 changes: 57 additions & 0 deletions client/app/components/InputPopover/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from "react";
import PropTypes from "prop-types";
import Button from "antd/lib/button";
import Popover from "antd/lib/popover";

import "./index.less";

export default function InputPopover({
header,
content,
children,
okButtonProps,
cancelButtonProps,
onCancel,
onOk,
...props
}) {
return (
<Popover
{...props}
content={
<div className="input-popover-content" data-test="InputPopoverContent">
{header && <header>{header}</header>}
{content}
<footer>
<Button onClick={onCancel} {...cancelButtonProps}>
Cancel
</Button>
<Button onClick={onOk} type="primary" {...okButtonProps}>
OK
</Button>
</footer>
</div>
}>
{children}
</Popover>
);
}

InputPopover.propTypes = {
header: PropTypes.node,
content: PropTypes.node,
children: PropTypes.node,
okButtonProps: PropTypes.object,
cancelButtonProps: PropTypes.object,
onOk: PropTypes.func,
onCancel: PropTypes.func,
};

InputPopover.defaultProps = {
header: null,
children: null,
okButtonProps: null,
cancelButtonProps: null,
onOk: () => {},
onCancel: () => {},
};
37 changes: 37 additions & 0 deletions client/app/components/InputPopover/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
@import "~antd/lib/modal/style/index"; // for ant @vars

.input-popover-content {
width: 390px;

.radio {
display: block;
height: 30px;
line-height: 30px;
}

.form-item {
margin-bottom: 10px;
}

header {
padding: 0 16px 10px;
margin: 0 -16px 20px;
border-bottom: @border-width-base @border-style-base @border-color-split;
font-size: @font-size-lg;
font-weight: 500;
color: @heading-color;
display: flex;
justify-content: space-between;
}

footer {
border-top: @border-width-base @border-style-base @border-color-split;
padding: 10px 16px 0;
margin: 0 -16px;
text-align: right;

button {
margin-left: 8px;
}
}
}
Loading