Skip to content

Commit

Permalink
basic frontend for handling parameterization of native based cards.
Browse files Browse the repository at this point in the history
  • Loading branch information
agilliland committed Jun 16, 2016
1 parent 5e1e24d commit 8116326
Show file tree
Hide file tree
Showing 13 changed files with 415 additions and 12 deletions.
9 changes: 9 additions & 0 deletions frontend/src/metabase/css/core/bordered.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
:root {
--border-size: 1px;
--border-size-med: 2px;
--border-style: solid;
--border-color: #F0F0F0;
}
Expand Down Expand Up @@ -58,6 +59,10 @@
border-color: rgba(0,0,0,0.2) !important;
}

.border-grey-1 {
border-color: var(--grey-1) !important;
}

.border-green {
border-color: var(--green-color) !important;
}
Expand Down Expand Up @@ -100,3 +105,7 @@
.border-dashed {
border-style: dashed;
}

.border-med {
border-width: var(--border-size-med);
}
24 changes: 22 additions & 2 deletions frontend/src/metabase/css/query_builder.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
transition: margin-right 0.35s;
}

.QueryBuilder--showDataReference {
.QueryBuilder--showSideDrawer {
margin-right: 300px;
}

Expand Down Expand Up @@ -535,7 +535,7 @@

/* DATA REFERENCE */

.DataReference {
.SideDrawer {
z-index: -1;
position: absolute;
top: 0;
Expand Down Expand Up @@ -773,3 +773,23 @@
border: 1px solid #D5DBE3;
border-radius: 4px;
}

.ParameterValuePickerNoPopover input {
font-size: 16px;
color: var(--default-font-color);
border: none;
}

.ParameterValuePickerNoPopover--selected input {
font-weight: bold;
color: var(--brand-color);
}

.ParameterValuePickerNoPopover input:focus {
outline: none;
color: var(--default-font-color);
}

.ParameterValuePickerNoPopover input::-webkit-input-placeholder {
color: var(--grey-4);
}
1 change: 1 addition & 0 deletions frontend/src/metabase/lib/card.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export function serializeCardForUrl(card) {
description: card.description,
dataset_query: dataset_query,
display: card.display,
parameters: card.parameters,
visualization_settings: card.visualization_settings
};
return utf8_to_b64url(JSON.stringify(cardCopy));
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/metabase/meta/Dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { slugify, stripId } from "metabase/lib/formatting";

import _ from "underscore";

const PARAMETER_OPTIONS: Array<ParameterOption> = [
export const PARAMETER_OPTIONS: Array<ParameterOption> = [
{
type: "date/month-year",
name: "Month and Year",
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/metabase/query_builder/NativeQueryEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import _ from "underscore";

import DataSelector from './DataSelector.jsx';
import Icon from "metabase/components/Icon.jsx";
import ParameterValuePicker from "./parameters/ParameterValuePicker.jsx";

// This should return an object with information about the mode the ACE Editor should use to edit the query.
// This object should have 2 properties:
Expand Down Expand Up @@ -39,6 +40,7 @@ export default class NativeQueryEditor extends Component {
}

static propTypes = {
card: PropTypes.object.isRequired,
databases: PropTypes.array.isRequired,
query: PropTypes.object.isRequired,
setQueryFn: PropTypes.func.isRequired,
Expand Down Expand Up @@ -243,6 +245,16 @@ export default class NativeQueryEditor extends Component {
<div className="NativeQueryEditor bordered rounded shadowed">
<div className="flex">
{dataSelectors}
{ this.props.card && this.props.card.parameters && this.props.card.parameters.map(parameter =>
<div key={parameter.name} className="pl2 GuiBuilder-section GuiBuilder-data flex align-center">
<span className="GuiBuilder-section-label Query-label">{parameter.label}</span>
<ParameterValuePicker
parameter={parameter}
value={this.props.parameterValues[parameter.id]}
setValue={(v) => this.props.setParameterValue(parameter.id, v)}
/>
</div>
)}
<a className="Query-label no-decoration flex-align-right flex align-center px2" onClick={this.toggleEditor}>
<span className="mx2">{toggleEditorText}</span>
<Icon name={toggleEditorIcon} width="20" height="20"/>
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/metabase/query_builder/QueryHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,21 @@ export default class QueryHeader extends Component {
}
}

// parameters
if (Query.isNative(this.props.query) && this.props.card.parameters && this.props.card.parameters.length > 0) {
const parametersButtonClasses = cx('transition-color', {
'text-brand': this.props.uiControls.isShowingParametersEditor,
'text-brand-hover': !this.props.uiControls.isShowingParametersEditor
});
buttonSections.push([
<Tooltip key="parameterEdititor" tooltip="Parameters">
<a className={parametersButtonClasses}>
<Icon name="gear" width="16px" height="16px" onClick={this.props.toggleParametersEditor}></Icon>
</a>
</Tooltip>
]);
}

// add to dashboard
if (!this.props.cardIsNewFn() && !this.props.isEditing) {
// simply adding an existing saved card to a dashboard, so show the modal to do so
Expand Down
116 changes: 113 additions & 3 deletions frontend/src/metabase/query_builder/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { formatSQL } from "metabase/lib/formatting";
import Query from "metabase/lib/query";
import { createQuery } from "metabase/lib/query";
import { loadTable } from "metabase/lib/table";
import Utils from "metabase/lib/utils";

const Metabase = new AngularResourceProxy("Metabase", ["db_list_with_tables", "db_tables", "dataset", "table_query_metadata"]);
const User = new AngularResourceProxy("User", ["update_qbnewb"]);
Expand Down Expand Up @@ -134,6 +135,9 @@ export const initializeQB = createThunkAction(INITIALIZE_QB, () => {
export const TOGGLE_DATA_REFERENCE = "TOGGLE_DATA_REFERENCE";
export const toggleDataReference = createAction(TOGGLE_DATA_REFERENCE);

export const TOGGLE_PARAMETERS_EDITOR = "TOGGLE_PARAMETERS_EDITOR";
export const toggleParametersEditor = createAction(TOGGLE_PARAMETERS_EDITOR);

export const CLOSE_QB_TUTORIAL = "CLOSE_QB_TUTORIAL";
export const closeQbTutorial = createAction(CLOSE_QB_TUTORIAL, () => {
MetabaseAnalytics.trackEvent("QueryBuilder", "Tutorial Close");
Expand Down Expand Up @@ -248,6 +252,46 @@ export const setCardVisualizationSettings = createThunkAction(SET_CARD_VISUALIZA
};
});

export const UPDATE_PARAMETER = "UPDATE_PARAMETER";
export const updateParameter = createThunkAction(UPDATE_PARAMETER, (parameter) => {
return (dispatch, getState) => {
const { card, uiControls } = getState();

let updatedCard = JSON.parse(JSON.stringify(card));

// when the query changes on saved card we change this into a new query w/ a known starting point
if (!uiControls.isEditing && updatedCard.id) {
delete updatedCard.id;
delete updatedCard.name;
delete updatedCard.description;
}

let updateIdx = _.findIndex(updatedCard.parameters, (p) => p.id === parameter.id);
updatedCard.parameters[updateIdx] = parameter;

return updatedCard;
};
});

export const SET_PARAMETER_VALUE = "SET_PARAMETER_VALUE";
export const setParameterValue = createThunkAction(SET_PARAMETER_VALUE, (parameterId, value) => {
return (dispatch, getState) => {
let { parameterValues } = getState();

// always clone before modifying
parameterValues = JSON.parse(JSON.stringify(parameterValues));

// apply this specific value
parameterValues = { ...parameterValues, [parameterId]: value};

// whenever a parameter value is set run the query
dispatch(runQuery(null, null, parameterValues));

// the return value from our action is still just the id/value of the parameter set
return {id: parameterId, value};
};
});


export const NOTIFY_CARD_CREATED = "NOTIFY_CARD_CREATED";
export const notifyCardCreatedFn = createThunkAction(NOTIFY_CARD_CREATED, (card) => {
Expand Down Expand Up @@ -328,7 +372,8 @@ export const setQuery = createThunkAction(SET_QUERY, (dataset_query) => {
return (dispatch, getState) => {
const { card, uiControls } = getState();

let updatedCard = JSON.parse(JSON.stringify(card));
let updatedCard = JSON.parse(JSON.stringify(card)),
openParametersEditor = uiControls.isShowingParametersEditor;

// when the query changes on saved card we change this into a new query w/ a known starting point
if (!uiControls.isEditing && updatedCard.id) {
Expand All @@ -339,7 +384,59 @@ export const setQuery = createThunkAction(SET_QUERY, (dataset_query) => {

updatedCard.dataset_query = JSON.parse(JSON.stringify(dataset_query));

return updatedCard;
// special handling for NATIVE cards to automatically detect parameters ... {{varname}}
if (Query.isNative(dataset_query) && !_.isEmpty(dataset_query.native.query)) {
let variables = [];

// look for variable usage in the query (like '{{varname}}'). we only allow alphanumeric characters for the variable name
// a variable name can optionally end with :start or :end which is not considered part of the actual variable name
// expected pattern is like mustache templates, so we are looking for something like {{category}} or {{date:start}}
// anything that doesn't match our rule is ignored, so {{&foo!}} would simply be ignored
let match, re = /\{\{([A-Za-z0-9]*?)(?:\:start|\:end)*\}\}/g;
while((match = re.exec(dataset_query.native.query)) != null) {
variables.push(match[1]);
}

// eliminate any duplicates since it's allowed for a user to reference the same variable multiple times
variables = _.uniq(variables);

// if we ended up with any variables in the query then update the card parameters list accordingly
if (variables.length > 0 || (updatedCard.parameters && updatedCard.parameters.length > 0)) {
let existingVariables = updatedCard.parameters ? updatedCard.parameters.map(p => p.name) : [];

let newVariables = _.difference(variables, existingVariables);
let oldVariables = _.difference(existingVariables, variables);

let parameters = updatedCard.parameters;
if (oldVariables.length === 1 && newVariables.length === 1) {
// renaming
let param = _.find(parameters, p => p.name === oldVariables[0]);
param.name = newVariables[0];
} else {
// remove old vars
parameters = _.reject(parameters, p => _.contains(oldVariables, p.name));

// create new vars
newVariables.forEach(function (paramName) {
parameters.push({id: Utils.uuid(), target: ["VAR", paramName], label: paramName, type: null});
});
}

updatedCard.parameters = parameters;

if (newVariables.length > 0) {
openParametersEditor = true;
} else if (parameters.length === 0) {
openParametersEditor = false;
}
}
}

return {
card: updatedCard,
openParametersEditor

};
};
});

Expand Down Expand Up @@ -534,7 +631,7 @@ export const setQuerySort = createThunkAction(SET_QUERY_SORT, (column) => {

// runQuery
export const RUN_QUERY = "RUN_QUERY";
export const runQuery = createThunkAction(RUN_QUERY, (card, updateUrl=true) => {
export const runQuery = createThunkAction(RUN_QUERY, (card, updateUrl=true, paramValues) => {
return async (dispatch, getState) => {
const state = getState();

Expand All @@ -549,6 +646,19 @@ export const runQuery = createThunkAction(RUN_QUERY, (card, updateUrl=true) => {
dataset_query.query = Query.cleanQuery(dataset_query.query);
}

// apply any parameters, if specified
if (card.parameters && card.parameters.length > 0) {
let parameterValues = paramValues || state.parameterValues || {};
let queryParameters = [];
card.parameters.forEach(param => {
let parameter = JSON.parse(JSON.stringify(param));
parameter.value = parameterValues[param.id];
queryParameters.push(parameter);
});

dataset_query.parameters = queryParameters;
}

if (updateUrl) {
state.updateUrl(card, cardIsDirty);
}
Expand Down
19 changes: 15 additions & 4 deletions frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import GuiQueryEditor from "../GuiQueryEditor.jsx";
import NativeQueryEditor from "../NativeQueryEditor.jsx";
import QueryVisualization from "../QueryVisualization.jsx";
import DataReference from "../dataref/DataReference.jsx";
import ParameterEditorSidebar from "../parameters/ParameterEditorSidebar.jsx";
import QueryBuilderTutorial from "../../tutorial/QueryBuilderTutorial.jsx";
import SavedQuestionIntroModal from "../SavedQuestionIntroModal.jsx";

Expand All @@ -21,6 +22,7 @@ import {
originalCard,
databases,
queryResult,
parameterValues,
isDirty,
isObjectDetail,
tables,
Expand Down Expand Up @@ -67,6 +69,7 @@ const mapStateToProps = (state, props) => {
card: card(state),
originalCard: originalCard(state),
query: state.card && state.card.dataset_query, // TODO: EOL, redundant
parameterValues: parameterValues(state),
databases: databases(state),
tables: tables(state),
tableMetadata: tableMetadata(state),
Expand Down Expand Up @@ -119,7 +122,8 @@ export default class QueryBuilder extends Component {
}

componentWillReceiveProps(nextProps) {
if (nextProps.uiControls.isShowingDataReference !== this.props.uiControls.isShowingDataReference) {
if (nextProps.uiControls.isShowingDataReference !== this.props.uiControls.isShowingDataReference ||
nextProps.uiControls.isShowingParametersEditor !== this.props.uiControls.isShowingParametersEditor) {
// when the data reference is toggled we need to trigger a rerender after a short delay in order to
// ensure that some components are updated after the animation completes (e.g. card visualization)
window.setTimeout(this.forceUpdateDebounced, 300);
Expand Down Expand Up @@ -164,9 +168,10 @@ export default class QueryBuilder extends Component {
);
}

const showDrawer = uiControls.isShowingDataReference || uiControls.isShowingParametersEditor;
return (
<div>
<div className={cx("QueryBuilder flex flex-column bg-white spread", {"QueryBuilder--showDataReference": uiControls.isShowingDataReference})}>
<div className={cx("QueryBuilder flex flex-column bg-white spread", {"QueryBuilder--showSideDrawer": showDrawer})}>
<div id="react_qb_header">
<QueryHeader {...this.props}/>
</div>
Expand All @@ -184,8 +189,14 @@ export default class QueryBuilder extends Component {
</div>
</div>

<div className="DataReference" id="react_data_reference">
<DataReference {...this.props} closeFn={() => this.props.toggleDataReference()} />
<div className="SideDrawer">
{ uiControls.isShowingDataReference &&
<DataReference {...this.props} closeFn={() => this.props.toggleDataReference()} />
}

{ uiControls.isShowingParametersEditor &&
<ParameterEditorSidebar {...this.props} onClose={() => this.props.toggleParametersEditor()} />
}
</div>

{ uiControls.isShowingTutorial &&
Expand Down
Loading

0 comments on commit 8116326

Please sign in to comment.