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

Poll for results in parameterized embeds #3752

Merged
merged 23 commits into from
May 6, 2019
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ae33080
add an endpoint for fetching job using a query's api_key
May 1, 2019
92d3c31
when unauthenticated, use api_key to get job, and fetch the latest query
May 1, 2019
c16263c
add 'refresh dataset' button to parameters directive
May 1, 2019
2d4b5fa
Merge branch 'master' into poll-for-results-in-embeds
May 1, 2019
41b0d81
fix scope error introduced by earlier commit
May 1, 2019
5653bd4
show timer when refreshing results
May 2, 2019
bbc54e3
Show input for missing parameters in embedded visualizations (#3741)
May 2, 2019
1968b49
Merge branch 'master' into poll-for-results-in-embeds
May 2, 2019
740472b
don't render the execute button for each parameter
May 2, 2019
d8bf19d
Merge branch 'poll-for-results-in-embeds' of github.com:getredash/red…
May 2, 2019
7389725
show 'missing parameter value' error
May 2, 2019
7d7414f
Don't reload the whole page when parameter value changes.
arikfr May 5, 2019
6651edf
Set API key and load config before rendering.
arikfr May 5, 2019
9915d88
Add Query#hasParameters method.
arikfr May 5, 2019
43e3abf
Don't show download controls for parameterized queries (they won't wo…
arikfr May 5, 2019
70da22d
Use getUrl to construct a correct query link.
arikfr May 5, 2019
7aa89dc
WIP: have a single way to load results
arikfr May 5, 2019
9841373
Show persistent errors and finish loading logic.
arikfr May 5, 2019
b6620cb
Check if query is safe and show message otherwise.
arikfr May 5, 2019
7ffd7eb
Fix test for unsafe parameters embed.
arikfr May 5, 2019
dadbe73
Merge branch 'master' into poll-for-results-in-embeds
May 5, 2019
13c4d45
Merge branch 'poll-for-results-in-embeds' of github.com:getredash/red…
May 5, 2019
55be3c6
wait for query results to return before taking snapshot
May 5, 2019
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
3 changes: 3 additions & 0 deletions client/app/components/parameters.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@
</button>
<parameter-value-input param="param"></parameter-value-input>
</div>
<button class="m-t-20 btn btn-primary" ng-if="onRefresh" ng-click="onRefresh()" title="Refresh Dataset">
<span class="zmdi zmdi-play"></span>
</button>
</div>
1 change: 1 addition & 0 deletions client/app/components/parameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function ParametersDirective($location) {
editable: '=?',
changed: '&onChange',
onUpdated: '=',
onRefresh: '=',
},
template,
link(scope) {
Expand Down
15 changes: 9 additions & 6 deletions client/app/components/queries/visualization-embed.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ <h3>
</div>

<div class="col-md-12 query__vis">
<div class="p-t-15 p-b-5" ng-if="$ctrl.query.getParametersDefs().length > 0">
<parameters parameters="$ctrl.query.getParametersDefs()"></parameters>
<div class="p-t-15 p-b-5" ng-if="$ctrl.query.hasParameters()">
<parameters parameters="$ctrl.query.getParametersDefs()" on-refresh="$ctrl.refreshQueryResults"></parameters>
</div>

<visualization-renderer visualization="$ctrl.visualization" query-result="$ctrl.queryResult" class="t-body">
Expand All @@ -23,15 +23,18 @@ <h3>
<div class="clearfix tile__bottom-control">
<div class="row">
<div class="col-xs-6">
<span class="small hidden-print"><i class="zmdi zmdi-time-restore"></i> <span am-time-ago="$ctrl.queryResult.getUpdatedAt()"></span></span>
<span class="small visible-print"><i class="zmdi zmdi-time-restore"></i> {{$ctrl.queryResult.getUpdatedAt() | dateTime}} UTC</span>
<a class="small hidden-print" ng-click="$ctrl.refreshQueryResults()">
<i ng-class='{"zmdi-hc-spin": $ctrl.loading}' class="zmdi zmdi-refresh"></i>
<span am-time-ago="$ctrl.queryResult.getUpdatedAt()" ng-if="!$ctrl.loading"></span>
<rd-timer from="$ctrl.refreshStartedAt" ng-if="$ctrl.loading"></rd-timer>
</a>
</div>
<div class="col-xs-6 text-right hidden-print">
<a class="btn btn-default btn-sm" ng-href="queries/{{$ctrl.query.id}}#{{$ctrl.visualization.id}}" target="_blank" tooltip="Open in Redash">
<a class="btn btn-default btn-sm" ng-href="{{$ctrl.query.getUrl()}}" target="_blank" tooltip="Open in Redash">
<span class="zmdi zmdi-link"></span>
</a>

<div class="btn-group dropup" uib-dropdown>
<div class="btn-group dropup" uib-dropdown ng-if="!$ctrl.query.hasParameters()">
<button type="button" class="btn btn-default btn-sm dropdown-toggle" aria-haspopup="true" uib-dropdown-toggle
aria-expanded="false">
Download Dataset <span class="caret"></span>
Expand Down
89 changes: 68 additions & 21 deletions client/app/components/queries/visualization-embed.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,101 @@
import { find } from 'lodash';
import queryStringParameters from '@/services/query-string';
import moment from 'moment';
// import queryStringParameters from '@/services/query-string';
import logoUrl from '@/assets/images/redash_icon_small.png';
import template from './visualization-embed.html';
// import PromiseRejectionError from '@/lib/promise-rejection-error';
// import notification from '@/services/notification';

const VisualizationEmbed = {
template,
bindings: {
data: '<',
query: '<',
},
controller($routeParams, Query, QueryResult) {
// controller($http, $q, $routeParams, QueryResult, $exceptionHandler) {
controller($routeParams) {
'ngInject';

document.querySelector('body').classList.add('headless');
this.refreshQueryResults = () => {
this.loading = true;
this.refreshStartedAt = moment();
this.query
.getQueryResultPromise()
.then((result) => {
this.loading = false;
this.queryResult = result;
})
.catch((error) => {
this.loading = false;
this.queryResult = error;
});
};

const visualizationId = parseInt($routeParams.visualizationId, 10);
this.visualization = find(this.query.visualizations, visualization => visualization.id === visualizationId);
this.showQueryDescription = $routeParams.showDescription;
this.apiKey = $routeParams.api_key;
this.logoUrl = logoUrl;
this.query = new Query(this.data[0]);
this.queryResult = new QueryResult(this.data[1]);
this.visualization =
find(this.query.visualizations, visualization => visualization.id === visualizationId);

document.querySelector('body').classList.add('headless');

this.refreshQueryResults();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


// const queryId = $routeParams.queryId;

// const query = $http.get(`api/queries/${queryId}`).then(response => response.data);
// const queryResult = $http
// .post(`api/queries/${queryId}/results`, {
// parameters: queryStringParameters(),
// })
// .then(
// response => response.data,
// (error) => {
// if (error.status === 400) {
// if (error.data.job) {
// notification.error(error.data.job.error);
// }

// return {};
// }

// // ANGULAR_REMOVE_ME This code is related to Angular's HTTP services
// if (error.status && error.data) {
// error = new PromiseRejectionError(error);
// }

// $exceptionHandler(error);
// },
// );

// $q.all([queryResult]).then((data) => {
// // this.query = new Query(data[0]);
// this.queryResult = new QueryResult(data[1]);
// });
},
};

export default function init(ngModule) {
ngModule.component('visualizationEmbed', VisualizationEmbed);

function session($http, $route, Auth) {
'ngInject';

function loadSession($route, Auth) {
const apiKey = $route.current.params.api_key;
Auth.setApiKey(apiKey);
return Auth.loadConfig();
}

function loadData($http, $route, $q, Auth) {
return session($http, $route, Auth).then(() => {
const queryId = $route.current.params.queryId;
const query = $http.get(`api/queries/${queryId}`).then(response => response.data);
const queryResult = $http.post(`api/queries/${queryId}/results`, { parameters: queryStringParameters() }).then(response => response.data);
return $q.all([query, queryResult]);
});
function loadQuery($route, Auth, Query) {
'ngInject';

return loadSession($route, Auth).then(() => Query.get({ id: $route.current.params.queryId }).$promise);
}

ngModule.config(($routeProvider) => {
$routeProvider.when('/embed/query/:queryId/visualization/:visualizationId', {
template: '<visualization-embed data="$resolve.data"></visualization-embed>',
resolve: {
data: loadData,
// session: loadSession,
query: loadQuery,
},
reloadOnSearch: false,
template: '<visualization-embed query="$resolve.query"></visualization-embed>',
});
});
}
Expand Down
2 changes: 1 addition & 1 deletion client/app/pages/queries/query.html
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ <h3>

<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">
<div class="p-t-15 p-b-5" ng-if="query.hasParameters()">
<parameters parameters="query.getParametersDefs()" sync-values="!query.isNew()" editable="sourceMode && canEdit" on-updated="onParametersUpdated"></parameters>
</div>
<!-- Query Execution Status -->
Expand Down
29 changes: 21 additions & 8 deletions client/app/services/query-result.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ function getColumnFriendlyName(column) {
return getColumnNameWithoutType(column).replace(/(?:^|\s)\S/g, a => a.toUpperCase());
}

function QueryResultService($resource, $timeout, $q, QueryResultError) {
function QueryResultService($resource, $timeout, $q, QueryResultError, Auth) {
const QueryResultResource = $resource('api/query_results/:id', { id: '@id' }, { post: { method: 'POST' } });
const Job = $resource('api/jobs/:id', { id: '@id' });
const JobWithApiKey = $resource('api/queries/:queryId/jobs/:id', { queryId: '@queryId', id: '@id' });
const statuses = {
1: 'waiting',
2: 'processing',
Expand Down Expand Up @@ -293,6 +294,13 @@ function QueryResultService($resource, $timeout, $q, QueryResultError) {
return queryResult;
}

loadLatestCachedResult(queryId, parameters) {
$resource('api/queries/:id/results', { id: '@queryId' }, { post: { method: 'POST' } })
.post({ queryId, parameters },
(response) => { this.update(response); },
(error) => { handleErrorResponse(this, error); });
}

loadResult(tryCount) {
this.isLoadingResult = true;
QueryResultResource.get(
Expand Down Expand Up @@ -324,18 +332,23 @@ function QueryResultService($resource, $timeout, $q, QueryResultError) {
);
}

refreshStatus(query, tryNumber = 1) {
Job.get(
{ id: this.job.id },
refreshStatus(query, parameters, tryNumber = 1) {
const resource = Auth.isAuthenticated() ? Job : JobWithApiKey;
const loadResult = () => (Auth.isAuthenticated()
? this.loadResult()
: this.loadLatestCachedResult(query, parameters));

resource.get(
{ queryId: query, id: this.job.id },
(jobResponse) => {
this.update(jobResponse);

if (this.getStatus() === 'processing' && this.job.query_result_id && this.job.query_result_id !== 'None') {
this.loadResult();
loadResult();
} else if (this.getStatus() !== 'failed') {
const waitTime = tryNumber > 10 ? 3000 : 500;
$timeout(() => {
this.refreshStatus(query, tryNumber + 1);
this.refreshStatus(query, parameters, tryNumber + 1);
}, waitTime);
}
},
Expand Down Expand Up @@ -377,7 +390,7 @@ function QueryResultService($resource, $timeout, $q, QueryResultError) {
queryResult.update(response);

if ('job' in response) {
queryResult.refreshStatus(id);
queryResult.refreshStatus(id, parameters);
}
},
(error) => {
Expand Down Expand Up @@ -408,7 +421,7 @@ function QueryResultService($resource, $timeout, $q, QueryResultError) {
queryResult.update(response);

if ('job' in response) {
queryResult.refreshStatus(query);
queryResult.refreshStatus(query, parameters);
}
},
(error) => {
Expand Down
4 changes: 4 additions & 0 deletions client/app/services/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,10 @@ function QueryResource(
return this.getParameters().isRequired();
};

QueryService.prototype.hasParameters = function hasParameters() {
return this.getParametersDefs().length > 0;
};

QueryService.prototype.prepareQueryResultExecution = function prepareQueryResultExecution(execute, maxAge) {
if (!this.query) {
return new QueryResultError("Can't execute empty query.");
Expand Down
5 changes: 4 additions & 1 deletion redash/handlers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ def json_representation(data, code, headers=None):
'/api/queries/<query_id>/results.<filetype>',
'/api/queries/<query_id>/results/<query_result_id>.<filetype>',
endpoint='query_result')
api.add_org_resource(JobResource, '/api/jobs/<job_id>', endpoint='job')
api.add_org_resource(JobResource,
'/api/jobs/<job_id>',
'/api/queries/<query_id>/jobs/<job_id>',
endpoint='job')

api.add_org_resource(UserListResource, '/api/users', endpoint='users')
api.add_org_resource(UserResource, '/api/users/<user_id>', endpoint='user')
Expand Down
2 changes: 1 addition & 1 deletion redash/handlers/query_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ def make_excel_response(query_result):


class JobResource(BaseResource):
def get(self, job_id):
def get(self, job_id, query_id=None):
"""
Retrieve info about a running query job.
"""
Expand Down