From 7a9409fc3f2c7a0b320c1c8ee83a4c7e5e7d0ae1 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Sun, 29 Dec 2019 15:56:37 -0300 Subject: [PATCH] Query service --- .../EditParameterSettingsDialog.jsx | 2 +- client/app/components/QuerySelector.jsx | 4 +- .../components/FavoritesDropdown.jsx | 7 +- .../components/dashboards/AddWidgetDialog.jsx | 2 +- .../components/queries/VisualizationEmbed.jsx | 5 +- client/app/pages/home/Home.jsx | 8 +- client/app/pages/queries/source-view.js | 7 +- client/app/pages/queries/view.js | 54 +- .../parameters/QueryBasedDropdownParameter.js | 4 +- client/app/services/query-result.js | 9 +- client/app/services/query.js | 483 ++++++++---------- client/app/services/widget.js | 30 +- 12 files changed, 285 insertions(+), 330 deletions(-) diff --git a/client/app/components/EditParameterSettingsDialog.jsx b/client/app/components/EditParameterSettingsDialog.jsx index d57c1fec67..8ad25e742e 100644 --- a/client/app/components/EditParameterSettingsDialog.jsx +++ b/client/app/components/EditParameterSettingsDialog.jsx @@ -77,7 +77,7 @@ function EditParameterSettingsDialog(props) { useEffect(() => { const queryId = props.parameter.queryId; if (queryId) { - Query.get({ id: queryId }, query => { + Query.get({ id: queryId }).then(query => { setInitialQuery(query); }); } diff --git a/client/app/components/QuerySelector.jsx b/client/app/components/QuerySelector.jsx index 8f3a7bf301..58f70e787a 100644 --- a/client/app/components/QuerySelector.jsx +++ b/client/app/components/QuerySelector.jsx @@ -14,14 +14,14 @@ const { Option } = Select; function search(term) { // get recent if (!term) { - return Query.recent().$promise.then(results => { + return Query.recent().then(results => { const filteredResults = results.filter(item => !item.is_draft); // filter out draft return Promise.resolve(filteredResults); }); } // search by query - return Query.query({ q: term }).$promise.then(({ results }) => Promise.resolve(results)); + return Query.query({ q: term }).then(({ results }) => Promise.resolve(results)); } export function QuerySelector(props) { diff --git a/client/app/components/app-header/components/FavoritesDropdown.jsx b/client/app/components/app-header/components/FavoritesDropdown.jsx index 1d3c1a71fa..444abd1a03 100644 --- a/client/app/components/app-header/components/FavoritesDropdown.jsx +++ b/client/app/components/app-header/components/FavoritesDropdown.jsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useCallback, useEffect } from "react"; import PropTypes from "prop-types"; -import { isEmpty, template } from "lodash"; +import { isEmpty, template, get } from "lodash"; import Dropdown from "antd/lib/dropdown"; import Icon from "antd/lib/icon"; @@ -18,8 +18,9 @@ export default function FavoritesDropdown({ fetch, urlTemplate }) { const fetchItems = useCallback( (showLoadingState = true) => { setLoading(showLoadingState); - fetch() - .$promise.then(({ results }) => { + const request = fetch(); + get(request, "$promise", request) + .then(({ results }) => { setItems(results); }) .finally(() => { diff --git a/client/app/components/dashboards/AddWidgetDialog.jsx b/client/app/components/dashboards/AddWidgetDialog.jsx index b612850fe5..447eba0b95 100644 --- a/client/app/components/dashboards/AddWidgetDialog.jsx +++ b/client/app/components/dashboards/AddWidgetDialog.jsx @@ -36,7 +36,7 @@ class AddWidgetDialog extends React.Component { }); if (selectedQuery) { - Query.get({ id: selectedQuery.id }, query => { + Query.get({ id: selectedQuery.id }).then(query => { if (query) { const existingParamNames = map(this.props.dashboard.getParametersDefs(), param => param.name); this.setState({ diff --git a/client/app/components/queries/VisualizationEmbed.jsx b/client/app/components/queries/VisualizationEmbed.jsx index 7ef854561c..4e072e5b2b 100644 --- a/client/app/components/queries/VisualizationEmbed.jsx +++ b/client/app/components/queries/VisualizationEmbed.jsx @@ -20,6 +20,7 @@ import QueryResultsLink from "@/components/EditVisualizationButton/QueryResultsL import VisualizationName from "@/visualizations/VisualizationName"; import { VisualizationRenderer } from "@/visualizations/VisualizationRenderer"; import { VisualizationType } from "@/visualizations"; +import { Query } from "@/services/query"; import logoUrl from "@/assets/images/redash_icon_small.png"; @@ -201,10 +202,10 @@ export default function init(ngModule) { return Auth.loadConfig(); } - function loadQuery($route, Auth, Query) { + function loadQuery($route, Auth) { "ngInject"; - return loadSession($route, Auth).then(() => Query.get({ id: $route.current.params.queryId }).$promise); + return loadSession($route, Auth).then(() => Query.get({ id: $route.current.params.queryId })); } ngModule.config($routeProvider => { diff --git a/client/app/pages/home/Home.jsx b/client/app/pages/home/Home.jsx index d2f9ea58c7..22c8b04401 100644 --- a/client/app/pages/home/Home.jsx +++ b/client/app/pages/home/Home.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import axios from "@/services/axios"; import PropTypes from "prop-types"; -import { includes, isEmpty } from "lodash"; +import { includes, isEmpty, get } from "lodash"; import { react2angular } from "react2angular"; import Alert from "antd/lib/alert"; import Icon from "antd/lib/icon"; @@ -67,9 +67,9 @@ function FavoriteList({ title, resource, itemUrl, emptyState }) { useEffect(() => { setLoading(true); - resource - .favorites() - .$promise.then(({ results }) => setItems(results)) + const request = resource.favorites(); + get(request, "$promise", request) + .then(({ results }) => setItems(results)) .finally(() => setLoading(false)); }, [resource]); diff --git a/client/app/pages/queries/source-view.js b/client/app/pages/queries/source-view.js index f2569cbc26..8d4db4dc84 100644 --- a/client/app/pages/queries/source-view.js +++ b/client/app/pages/queries/source-view.js @@ -2,6 +2,7 @@ import { map, debounce } from "lodash"; import template from "./query.html"; import EditParameterSettingsDialog from "@/components/EditParameterSettingsDialog"; import DataSource from "@/services/data-source"; +import { Query } from "@/services/query"; function QuerySourceCtrl( Events, @@ -119,7 +120,7 @@ export default function init(ngModule) { controller: "QuerySourceCtrl", reloadOnSearch: false, resolve: { - query: function newQuery(Query) { + query: function newQuery() { "ngInject"; return Query.newQuery(); @@ -137,10 +138,10 @@ export default function init(ngModule) { controller: "QuerySourceCtrl", reloadOnSearch: false, resolve: { - query: (Query, $route) => { + query: $route => { "ngInject"; - return Query.get({ id: $route.current.params.queryId }).$promise; + return Query.get({ id: $route.current.params.queryId }); }, }, }, diff --git a/client/app/pages/queries/view.js b/client/app/pages/queries/view.js index d8c4911a7a..627506a3d8 100644 --- a/client/app/pages/queries/view.js +++ b/client/app/pages/queries/view.js @@ -12,6 +12,7 @@ import AddToDashboardDialog from "@/components/queries/AddToDashboardDialog"; import PermissionsEditorDialog from "@/components/permissions-editor/PermissionsEditorDialog"; import notification from "@/services/notification"; import template from "./query.html"; +import { Query } from "@/services/query"; function QueryViewCtrl( $scope, @@ -26,8 +27,7 @@ function QueryViewCtrl( AlertDialog, clientConfig, $uibModal, - currentUser, - Query + currentUser ) { // Should create it here since visualization registry might not be fulfilled when this file is loaded const DEFAULT_VISUALIZATION = newVisualization("TABLE", { itemsPerPage: 50 }); @@ -208,7 +208,7 @@ function QueryViewCtrl( const tabName = "duplicatedQueryTab" + Math.random().toString(); $window.open("", tabName); - Query.fork({ id: $scope.query.id }, newQuery => { + Query.fork({ id: $scope.query.id }).then(newQuery => { const queryUrl = newQuery.getUrl(true); $window.open(queryUrl, tabName); }); @@ -231,7 +231,7 @@ function QueryViewCtrl( if (request) { // Don't save new query with partial data if ($scope.query.isNew()) { - return $q.reject(); + return Promise.reject(); } request.id = $scope.query.id; request.version = $scope.query.version; @@ -276,13 +276,13 @@ function QueryViewCtrl( $scope.saveQuery(options, data); } - return Query.save( - request, - updatedQuery => { + return Query.save(request) + .then(updatedQuery => { notification.success(options.successMessage); $scope.query.version = updatedQuery.version; - }, - error => { + return updatedQuery; + }) + .catch(error => { if (error.status === 409) { const errorMessage = "It seems like the query has been modified by another user."; @@ -302,8 +302,7 @@ function QueryViewCtrl( } else { notification.error(options.errorMessage); } - } - ).$promise; + }); }; $scope.togglePublished = () => { @@ -341,16 +340,14 @@ function QueryViewCtrl( $scope.archiveQuery = () => { function archive() { - Query.delete( - { id: $scope.query.id }, - () => { + Query.delete({ id: $scope.query.id }) + .then(() => { $scope.query.is_archived = true; $scope.query.schedule = null; - }, - () => { + }) + .catch(() => { notification.error("Query could not be archived."); - } - ); + }); } const title = "Archive Query"; @@ -374,16 +371,13 @@ function QueryViewCtrl( $scope.query.latest_query_data_id = null; if ($scope.query.id) { - Query.save( - { - id: $scope.query.id, - data_source_id: $scope.query.data_source_id, - latest_query_data_id: null, - }, - updatedQuery => { - $scope.query.version = updatedQuery.version; - } - ); + Query.save({ + id: $scope.query.id, + data_source_id: $scope.query.data_source_id, + latest_query_data_id: null, + }).then(updatedQuery => { + $scope.query.version = updatedQuery.version; + }); } $scope.dataSource = find($scope.dataSources, ds => ds.id === $scope.query.data_source_id); @@ -552,10 +546,10 @@ export default function init(ngModule) { controller: "QueryViewCtrl", reloadOnSearch: false, resolve: { - query: (Query, $route) => { + query: $route => { "ngInject"; - return Query.get({ id: $route.current.params.queryId }).$promise; + return Query.get({ id: $route.current.params.queryId }); }, }, }, diff --git a/client/app/services/parameters/QueryBasedDropdownParameter.js b/client/app/services/parameters/QueryBasedDropdownParameter.js index 1419bb05fe..32ab7c43e3 100644 --- a/client/app/services/parameters/QueryBasedDropdownParameter.js +++ b/client/app/services/parameters/QueryBasedDropdownParameter.js @@ -67,10 +67,10 @@ class QueryBasedDropdownParameter extends Parameter { loadDropdownValues() { if (this.parentQueryId) { - return Query.associatedDropdown({ queryId: this.parentQueryId, dropdownQueryId: this.queryId }).$promise; + return Query.associatedDropdown({ queryId: this.parentQueryId, dropdownQueryId: this.queryId }); } - return Query.asDropdown({ id: this.queryId }).$promise; + return Query.asDropdown({ id: this.queryId }); } } diff --git a/client/app/services/query-result.js b/client/app/services/query-result.js index 89b8d1753d..c0ff65b175 100644 --- a/client/app/services/query-result.js +++ b/client/app/services/query-result.js @@ -1,10 +1,13 @@ import debug from "debug"; import moment from "moment"; import { uniqBy, each, isNumber, isString, includes, extend, forOwn } from "lodash"; +import { QueryResultError } from "./query"; const logger = debug("redash:services:QueryResult"); const filterTypes = ["filter", "multi-filter", "multiFilter"]; +export let QueryResult = null; // eslint-disable-line import/no-mutable-exports + function getColumnNameWithoutType(column) { let typeSplit; if (column.indexOf("::") !== -1) { @@ -35,7 +38,7 @@ function getColumnFriendlyName(column) { return getColumnNameWithoutType(column).replace(/(?:^|\s)\S/g, a => a.toUpperCase()); } -function QueryResultService($resource, $timeout, $q, QueryResultError, Auth) { +function QueryResultService($resource, $timeout, $q, Auth) { const QueryResultResource = $resource("api/query_results/:id", { id: "@id" }, { post: { method: "POST" } }); const QueryResultByQueryIdResource = $resource("api/queries/:queryId/results/:id.json", { queryId: "@queryId", @@ -447,6 +450,10 @@ function QueryResultService($resource, $timeout, $q, QueryResultError, Auth) { export default function init(ngModule) { ngModule.factory("QueryResult", QueryResultService); + + ngModule.run($injector => { + QueryResult = $injector.get("QueryResult"); + }); } init.init = true; diff --git a/client/app/services/query.js b/client/app/services/query.js index 2c4a878527..169f15d937 100644 --- a/client/app/services/query.js +++ b/client/app/services/query.js @@ -1,14 +1,16 @@ import moment from "moment"; import debug from "debug"; import Mustache from "mustache"; +import axios from "@/services/axios"; import { zipObject, isEmpty, map, filter, includes, union, uniq, has, identity, extend, each, some } from "lodash"; import { Parameter } from "./parameters"; +import { currentUser } from "./auth"; +import { $location } from "./ng"; +import { QueryResult } from "./query-result"; Mustache.escape = identity; // do not html-escape values -export let Query = null; // eslint-disable-line import/no-mutable-exports - const logger = debug("redash:services:query"); function collectParams(parts) { @@ -25,6 +27,148 @@ function collectParams(parts) { return parameters; } +export class Query { + constructor(query) { + extend(this, query); + } + + getSourceLink() { + return `/queries/${this.id}/source`; + } + + isNew() { + return this.id === undefined; + } + + hasDailySchedule() { + return this.schedule && this.schedule.match(/\d\d:\d\d/) !== null; + } + + scheduleInLocalTime() { + const parts = this.schedule.split(":"); + return moment + .utc() + .hour(parts[0]) + .minute(parts[1]) + .local() + .format("HH:mm"); + } + + hasResult() { + return !!(this.latest_query_data || this.latest_query_data_id); + } + + paramsRequired() { + return this.getParameters().isRequired(); + } + + hasParameters() { + return this.getParametersDefs().length > 0; + } + + prepareQueryResultExecution(execute, maxAge) { + const parameters = this.getParameters(); + const missingParams = parameters.getMissing(); + + if (missingParams.length > 0) { + let paramsWord = "parameter"; + let valuesWord = "value"; + if (missingParams.length > 1) { + paramsWord = "parameters"; + valuesWord = "values"; + } + + return new QueryResult({ + job: { + error: `missing ${valuesWord} for ${missingParams.join(", ")} ${paramsWord}.`, + status: 4, + }, + }); + } + + if (parameters.isRequired()) { + // Need to clear latest results, to make sure we don't use results for different params. + this.latest_query_data = null; + this.latest_query_data_id = null; + } + + if (this.latest_query_data && maxAge !== 0) { + if (!this.queryResult) { + this.queryResult = new QueryResult({ + query_result: this.latest_query_data, + }); + } + } else if (this.latest_query_data_id && maxAge !== 0) { + if (!this.queryResult) { + this.queryResult = QueryResult.getById(this.id, this.latest_query_data_id); + } + } else { + this.queryResult = execute(); + } + + return this.queryResult; + } + + getQueryResult(maxAge) { + const execute = () => QueryResult.getByQueryId(this.id, this.getParameters().getExecutionValues(), maxAge); + return this.prepareQueryResultExecution(execute, maxAge); + } + + getQueryResultByText(maxAge, selectedQueryText) { + const queryText = selectedQueryText || this.query; + if (!queryText) { + return new QueryResultError("Can't execute empty query."); + } + + const parameters = this.getParameters().getExecutionValues({ joinListValues: true }); + const execute = () => QueryResult.get(this.data_source_id, queryText, parameters, maxAge, this.id); + return this.prepareQueryResultExecution(execute, maxAge); + } + + getUrl(source, hash) { + let url = `queries/${this.id}`; + + if (source) { + url += "/source"; + } + + let params = {}; + if (this.getParameters().isRequired()) { + this.getParametersDefs().forEach(param => { + extend(params, param.toUrlParams()); + }); + } + Object.keys(params).forEach(key => params[key] == null && delete params[key]); + params = map(params, (value, name) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`).join("&"); + + if (params !== "") { + url += `?${params}`; + } + + if (hash) { + url += `#${hash}`; + } + + return url; + } + + getQueryResultPromise() { + return this.getQueryResult().toPromise(); + } + + getParameters() { + if (!this.$parameters) { + this.$parameters = new Parameters(this, $location.search()); + } + + return this.$parameters; + } + + getParametersDefs(update = true) { + return this.getParameters().get(update); + } +} + class Parameters { constructor(query, queryString) { this.query = query; @@ -145,280 +289,83 @@ class Parameters { .join("&"); } } - -function QueryResultErrorFactory($q) { - class QueryResultError { - constructor(errorMessage) { - this.errorMessage = errorMessage; - this.updatedAt = moment.utc(); - } - - getUpdatedAt() { - return this.updatedAt; - } - - getError() { - return this.errorMessage; - } - - toPromise() { - return $q.reject(this); - } - - // eslint-disable-next-line class-methods-use-this - getStatus() { - return "failed"; - } - - // eslint-disable-next-line class-methods-use-this - getData() { - return null; - } - - // eslint-disable-next-line class-methods-use-this - getLog() { - return null; - } +export class QueryResultError { + constructor(errorMessage) { + this.errorMessage = errorMessage; + this.updatedAt = moment.utc(); } - return QueryResultError; -} - -function QueryResource($resource, $http, $location, $q, currentUser, QueryResultError, QueryResult) { - const QueryService = $resource( - "api/queries/:id", - { id: "@id" }, - { - recent: { - method: "get", - isArray: true, - url: "api/queries/recent", - }, - archive: { - method: "get", - isArray: false, - url: "api/queries/archive", - }, - query: { - isArray: false, - }, - myQueries: { - method: "get", - isArray: false, - url: "api/queries/my", - }, - fork: { - method: "post", - isArray: false, - url: "api/queries/:id/fork", - params: { id: "@id" }, - }, - resultById: { - method: "get", - isArray: false, - url: "api/queries/:id/results.json", - }, - asDropdown: { - method: "get", - isArray: true, - url: "api/queries/:id/dropdown", - }, - associatedDropdown: { - method: "get", - isArray: true, - url: "api/queries/:queryId/dropdowns/:dropdownQueryId", - }, - favorites: { - method: "get", - isArray: false, - url: "api/queries/favorites", - }, - favorite: { - method: "post", - isArray: false, - url: "api/queries/:id/favorite", - transformRequest: [() => ""], // body not needed - }, - unfavorite: { - method: "delete", - isArray: false, - url: "api/queries/:id/favorite", - transformRequest: [() => ""], // body not needed - }, - } - ); - - QueryService.newQuery = function newQuery() { - return new QueryService({ - query: "", - name: "New Query", - schedule: null, - user: currentUser, - options: {}, - }); - }; - - QueryService.format = function formatQuery(syntax, query) { - if (syntax === "json") { - try { - const formatted = JSON.stringify(JSON.parse(query), " ", 4); - return $q.resolve(formatted); - } catch (err) { - return $q.reject(String(err)); - } - } else if (syntax === "sql") { - return $http.post("api/queries/format", { query }).then(response => response.data.query); - } else { - return $q.reject("Query formatting is not supported for your data source syntax."); - } - }; - - QueryService.prototype.getSourceLink = function getSourceLink() { - return `/queries/${this.id}/source`; - }; - - QueryService.prototype.isNew = function isNew() { - return this.id === undefined; - }; - - QueryService.prototype.hasDailySchedule = function hasDailySchedule() { - return this.schedule && this.schedule.match(/\d\d:\d\d/) !== null; - }; - - QueryService.prototype.scheduleInLocalTime = function scheduleInLocalTime() { - const parts = this.schedule.split(":"); - return moment - .utc() - .hour(parts[0]) - .minute(parts[1]) - .local() - .format("HH:mm"); - }; - - QueryService.prototype.hasResult = function hasResult() { - return !!(this.latest_query_data || this.latest_query_data_id); - }; - - QueryService.prototype.paramsRequired = function paramsRequired() { - return this.getParameters().isRequired(); - }; - - QueryService.prototype.hasParameters = function hasParameters() { - return this.getParametersDefs().length > 0; - }; - - QueryService.prototype.prepareQueryResultExecution = function prepareQueryResultExecution(execute, maxAge) { - const parameters = this.getParameters(); - const missingParams = parameters.getMissing(); - - if (missingParams.length > 0) { - let paramsWord = "parameter"; - let valuesWord = "value"; - if (missingParams.length > 1) { - paramsWord = "parameters"; - valuesWord = "values"; - } - - return new QueryResult({ - job: { - error: `missing ${valuesWord} for ${missingParams.join(", ")} ${paramsWord}.`, - status: 4, - }, - }); - } - - if (parameters.isRequired()) { - // Need to clear latest results, to make sure we don't use results for different params. - this.latest_query_data = null; - this.latest_query_data_id = null; - } - - if (this.latest_query_data && maxAge !== 0) { - if (!this.queryResult) { - this.queryResult = new QueryResult({ - query_result: this.latest_query_data, - }); - } - } else if (this.latest_query_data_id && maxAge !== 0) { - if (!this.queryResult) { - this.queryResult = QueryResult.getById(this.id, this.latest_query_data_id); - } - } else { - this.queryResult = execute(); - } - - return this.queryResult; - }; - - QueryService.prototype.getQueryResult = function getQueryResult(maxAge) { - const execute = () => QueryResult.getByQueryId(this.id, this.getParameters().getExecutionValues(), maxAge); - return this.prepareQueryResultExecution(execute, maxAge); - }; - - QueryService.prototype.getQueryResultByText = function getQueryResultByText(maxAge, selectedQueryText) { - const queryText = selectedQueryText || this.query; - if (!queryText) { - return new QueryResultError("Can't execute empty query."); - } - - const parameters = this.getParameters().getExecutionValues({ joinListValues: true }); - const execute = () => QueryResult.get(this.data_source_id, queryText, parameters, maxAge, this.id); - return this.prepareQueryResultExecution(execute, maxAge); - }; - - QueryService.prototype.getUrl = function getUrl(source, hash) { - let url = `queries/${this.id}`; - - if (source) { - url += "/source"; - } - - let params = {}; - if (this.getParameters().isRequired()) { - this.getParametersDefs().forEach(param => { - extend(params, param.toUrlParams()); - }); - } - Object.keys(params).forEach(key => params[key] == null && delete params[key]); - params = map(params, (value, name) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`).join("&"); - - if (params !== "") { - url += `?${params}`; - } - - if (hash) { - url += `#${hash}`; - } - - return url; - }; + getUpdatedAt() { + return this.updatedAt; + } - QueryService.prototype.getQueryResultPromise = function getQueryResultPromise() { - return this.getQueryResult().toPromise(); - }; + getError() { + return this.errorMessage; + } - QueryService.prototype.getParameters = function getParameters() { - if (!this.$parameters) { - this.$parameters = new Parameters(this, $location.search()); - } + toPromise() { + return Promise.reject(this); + } - return this.$parameters; - }; + // eslint-disable-next-line class-methods-use-this + getStatus() { + return "failed"; + } - QueryService.prototype.getParametersDefs = function getParametersDefs(update = true) { - return this.getParameters().get(update); - }; + // eslint-disable-next-line class-methods-use-this + getData() { + return null; + } - return QueryService; + // eslint-disable-next-line class-methods-use-this + getLog() { + return null; + } } -export default function init(ngModule) { - ngModule.factory("QueryResultError", QueryResultErrorFactory); - ngModule.factory("Query", QueryResource); - - ngModule.run($injector => { - Query = $injector.get("Query"); +const getQuery = query => new Query(query); +const saveOrCreateUrl = data => (data.id ? `api/queries/${data.id}` : "api/queries"); + +const QueryService = { + query: params => axios.get("api/queries", { params }), + get: data => axios.get(`api/queries/${data.id}`, data).then(getQuery), + save: data => axios.post(saveOrCreateUrl(data), data).then(getQuery), + delete: data => axios.delete(`api/queries/${data.id}`), + recent: params => axios.get(`api/queries/recent`, { params }), + archive: params => axios.get(`api/queries/archive`, { params }), + myQueries: params => axios.get("api/queries/my", { params }), + fork: ({ id }) => axios.post(`api/queries/${id}/fork`, { id }).then(getQuery), + resultById: data => axios.get(`api/queries/${data.id}/results.json`), + asDropdown: data => axios.get(`api/queries/${data.id}/dropdown`), + associatedDropdown: data => axios.get(`api/queries/${data.id}/dropdowns/${data.dropdownQueryId}`), + favorites: params => axios.get("api/queries/favorites", { params }), + favorite: data => axios.post(`api/queries/${data.id}/favorite`), + unfavorite: data => axios.delete(`api/queries/${data.id}/unfavorite`), +}; + +QueryService.newQuery = function newQuery() { + return new Query({ + query: "", + name: "New Query", + schedule: null, + user: currentUser, + options: {}, }); -} +}; + +QueryService.format = function formatQuery(syntax, query) { + if (syntax === "json") { + try { + const formatted = JSON.stringify(JSON.parse(query), " ", 4); + return Promise.resolve(formatted); + } catch (err) { + return Promise.reject(String(err)); + } + } else if (syntax === "sql") { + return axios.post("api/queries/format", { query }).then(data => data.query); + } else { + return Promise.reject("Query formatting is not supported for your data source syntax."); + } +}; -init.init = true; +extend(Query, QueryService); diff --git a/client/app/services/widget.js b/client/app/services/widget.js index ead77c51ec..1df5abde06 100644 --- a/client/app/services/widget.js +++ b/client/app/services/widget.js @@ -2,6 +2,7 @@ import moment from "moment"; import { each, pick, extend, isObject, truncate, keys, difference, filter, map, merge } from "lodash"; import dashboardGridOptions from "@/config/dashboard-grid-options"; import { registeredVisualizations } from "@/visualizations"; +import { Query } from "./query"; export let Widget = null; // eslint-disable-line import/no-mutable-exports @@ -75,7 +76,7 @@ export const ParameterMappingType = { StaticValue: "static-value", }; -function WidgetFactory($http, $location, Query) { +function WidgetFactory($http, $location) { class WidgetService { static MappingType = ParameterMappingType; @@ -202,19 +203,22 @@ function WidgetFactory($http, $location, Query) { const queryParams = $location.search(); const localTypes = [WidgetService.MappingType.WidgetLevel, WidgetService.MappingType.StaticValue]; - return map(filter(params, param => localTypes.indexOf(mappings[param.name].type) >= 0), param => { - const mapping = mappings[param.name]; - const result = param.clone(); - result.title = mapping.title || param.title; - result.locals = [param]; - result.urlPrefix = `p_w${this.id}_`; - if (mapping.type === WidgetService.MappingType.StaticValue) { - result.setValue(mapping.value); - } else { - result.fromUrlParams(queryParams); + return map( + filter(params, param => localTypes.indexOf(mappings[param.name].type) >= 0), + param => { + const mapping = mappings[param.name]; + const result = param.clone(); + result.title = mapping.title || param.title; + result.locals = [param]; + result.urlPrefix = `p_w${this.id}_`; + if (mapping.type === WidgetService.MappingType.StaticValue) { + result.setValue(mapping.value); + } else { + result.fromUrlParams(queryParams); + } + return result; } - return result; - }); + ); } getParameterMappings() {