Skip to content

Commit

Permalink
Merge branch 'master' into updated-docker-build
Browse files Browse the repository at this point in the history
  • Loading branch information
gaecoli committed Apr 24, 2024
2 parents 0dc00fa + 5ac5d86 commit ccc6e95
Show file tree
Hide file tree
Showing 13 changed files with 99 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import PlainButton from "@/components/PlainButton";
import ExpandedWidgetDialog from "@/components/dashboards/ExpandedWidgetDialog";
import EditParameterMappingsDialog from "@/components/dashboards/EditParameterMappingsDialog";
import VisualizationRenderer from "@/components/visualizations/VisualizationRenderer";
import { ExecutionStatus } from "@/services/query-result";

import Widget from "./Widget";

Expand Down Expand Up @@ -278,7 +279,7 @@ class VisualizationWidget extends React.Component {
const widgetQueryResult = widget.getQueryResult();
const widgetStatus = widgetQueryResult && widgetQueryResult.getStatus();
switch (widgetStatus) {
case "failed":
case ExecutionStatus.FAILED:
return (
<div className="body-row-auto scrollbox">
{widgetQueryResult.getError() && (
Expand All @@ -288,7 +289,7 @@ class VisualizationWidget extends React.Component {
)}
</div>
);
case "done":
case ExecutionStatus.FINISHED:
return (
<div className="body-row-auto scrollbox">
<VisualizationRenderer
Expand Down
4 changes: 3 additions & 1 deletion client/app/pages/queries/QuerySource.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,9 @@ function QuerySource(props) {
<QueryVisualizationTabs
queryResult={queryResult}
visualizations={query.visualizations}
showNewVisualizationButton={queryFlags.canEdit && queryResultData.status === ExecutionStatus.DONE}
showNewVisualizationButton={
queryFlags.canEdit && queryResultData.status === ExecutionStatus.FINISHED
}
canDeleteVisualizations={queryFlags.canEdit}
selectedTab={selectedVisualization}
onChangeTab={setSelectedVisualization}
Expand Down
2 changes: 1 addition & 1 deletion client/app/pages/queries/QueryView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ function QueryView(props) {
<QueryVisualizationTabs
queryResult={queryResult}
visualizations={query.visualizations}
showNewVisualizationButton={queryFlags.canEdit && queryResultData.status === ExecutionStatus.DONE}
showNewVisualizationButton={queryFlags.canEdit && queryResultData.status === ExecutionStatus.FINISHED}
canDeleteVisualizations={queryFlags.canEdit}
selectedTab={selectedVisualization}
onChangeTab={setSelectedVisualization}
Expand Down
26 changes: 17 additions & 9 deletions client/app/pages/queries/components/QueryExecutionStatus.jsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,45 @@
import { includes } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Alert from "antd/lib/alert";
import Button from "antd/lib/button";
import Timer from "@/components/Timer";
import { ExecutionStatus } from "@/services/query-result";

export default function QueryExecutionStatus({ status, updatedAt, error, isCancelling, onCancel }) {
const alertType = status === "failed" ? "error" : "info";
const showTimer = status !== "failed" && updatedAt;
const isCancelButtonAvailable = includes(["waiting", "processing"], status);
const alertType = status === ExecutionStatus.FAILED ? "error" : "info";
const showTimer = status !== ExecutionStatus.FAILED && updatedAt;
const isCancelButtonAvailable = [
ExecutionStatus.SCHEDULED,
ExecutionStatus.QUEUED,
ExecutionStatus.STARTED,
ExecutionStatus.DEFERRED,
].includes(status);
let message = isCancelling ? <React.Fragment>Cancelling&hellip;</React.Fragment> : null;

switch (status) {
case "waiting":
case ExecutionStatus.QUEUED:
if (!isCancelling) {
message = <React.Fragment>Query in queue&hellip;</React.Fragment>;
}
break;
case "processing":
case ExecutionStatus.STARTED:
if (!isCancelling) {
message = <React.Fragment>Executing query&hellip;</React.Fragment>;
}
break;
case "loading-result":
case ExecutionStatus.LOADING_RESULT:
message = <React.Fragment>Loading results&hellip;</React.Fragment>;
break;
case "failed":
case ExecutionStatus.FAILED:
message = (
<React.Fragment>
Error running query: <strong>{error}</strong>
</React.Fragment>
);
break;
case ExecutionStatus.CANCELED:
message = <React.Fragment>Query was canceled</React.Fragment>;
break;
// no default
}

Expand Down Expand Up @@ -66,7 +74,7 @@ QueryExecutionStatus.propTypes = {
};

QueryExecutionStatus.defaultProps = {
status: "waiting",
status: ExecutionStatus.QUEUED,
updatedAt: null,
error: null,
isCancelling: true,
Expand Down
64 changes: 34 additions & 30 deletions client/app/services/query-result.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,15 @@ const QueryResultResource = {
};

export const ExecutionStatus = {
WAITING: "waiting",
PROCESSING: "processing",
DONE: "done",
QUEUED: "queued",
STARTED: "started",
FINISHED: "finished",
FAILED: "failed",
LOADING_RESULT: "loading-result",
};

const statuses = {
1: ExecutionStatus.WAITING,
2: ExecutionStatus.PROCESSING,
3: ExecutionStatus.DONE,
4: ExecutionStatus.FAILED,
CANCELED: "canceled",
DEFERRED: "deferred",
SCHEDULED: "scheduled",
STOPPED: "stopped",
};

function handleErrorResponse(queryResult, error) {
Expand All @@ -80,7 +77,7 @@ function handleErrorResponse(queryResult, error) {
queryResult.update({
job: {
error: "cached query result unavailable, please execute again.",
status: 4,
status: ExecutionStatus.FAILED,
},
});
return;
Expand All @@ -91,7 +88,7 @@ function handleErrorResponse(queryResult, error) {
queryResult.update({
job: {
error: get(error, "response.data.message", "Unknown error occurred. Please try again later."),
status: 4,
status: ExecutionStatus.FAILED,
},
});
}
Expand All @@ -102,11 +99,19 @@ function sleep(ms) {

export function fetchDataFromJob(jobId, interval = 1000) {
return axios.get(`api/jobs/${jobId}`).then(data => {
const status = statuses[data.job.status];
if (status === ExecutionStatus.WAITING || status === ExecutionStatus.PROCESSING) {
const status = data.job.status;
if (
[ExecutionStatus.QUEUED, ExecutionStatus.STARTED, ExecutionStatus.SCHEDULED, ExecutionStatus.DEFERRED].includes(
status
)
) {
return sleep(interval).then(() => fetchDataFromJob(data.job.id));
} else if (status === ExecutionStatus.DONE) {
return data.job.result;
} else if (status === ExecutionStatus.FINISHED) {
return data.job.result_id;
} else if (status === ExecutionStatus.CANCELED) {
return Promise.reject("Job was canceled");
} else if (status === ExecutionStatus.STOPPED) {
return Promise.reject("Job was stopped");
} else if (status === ExecutionStatus.FAILED) {
return Promise.reject(data.job.error);
}
Expand All @@ -122,7 +127,7 @@ class QueryResult {
this.deferred = defer();
this.job = {};
this.query_result = {};
this.status = "waiting";
this.status = ExecutionStatus.QUEUED;

this.updatedAt = moment();

Expand All @@ -138,8 +143,8 @@ class QueryResult {
extend(this, props);

if ("query_result" in props) {
this.status = ExecutionStatus.DONE;
this.deferred.onStatusChange(ExecutionStatus.DONE);
this.status = ExecutionStatus.FINISHED;
this.deferred.onStatusChange(ExecutionStatus.FINISHED);

const columnTypes = {};

Expand Down Expand Up @@ -183,11 +188,10 @@ class QueryResult {
});

this.deferred.resolve(this);
} else if (this.job.status === 3 || this.job.status === 2) {
this.deferred.onStatusChange(ExecutionStatus.PROCESSING);
this.status = "processing";
} else if (this.job.status === 4) {
this.status = statuses[this.job.status];
} else if (this.job.status === ExecutionStatus.STARTED || this.job.status === ExecutionStatus.FINISHED) {
this.status = ExecutionStatus.STARTED;
} else if (this.job.status === ExecutionStatus.FAILED) {
this.status = this.job.status;
this.deferred.reject(new QueryResultError(this.job.error));
} else {
this.deferred.onStatusChange(undefined);
Expand All @@ -211,7 +215,7 @@ class QueryResult {
if (this.isLoadingResult) {
return ExecutionStatus.LOADING_RESULT;
}
return this.status || statuses[this.job.status];
return this.status || this.job.status;
}

getError() {
Expand Down Expand Up @@ -374,7 +378,7 @@ class QueryResult {
this.isLoadingResult = true;
this.deferred.onStatusChange(ExecutionStatus.LOADING_RESULT);

QueryResultResource.get({ id: this.job.query_result_id })
QueryResultResource.get({ id: this.job.result_id })
.then(response => {
this.update(response);
this.isLoadingResult = false;
Expand All @@ -389,7 +393,7 @@ class QueryResult {
this.update({
job: {
error: "failed communicating with server. Please check your Internet connection and try again.",
status: 4,
status: ExecutionStatus.FAILED,
},
});
this.isLoadingResult = false;
Expand All @@ -413,9 +417,9 @@ class QueryResult {
.then(jobResponse => {
this.update(jobResponse);

if (this.getStatus() === "processing" && this.job.query_result_id && this.job.query_result_id !== "None") {
if (this.getStatus() === ExecutionStatus.STARTED && this.job.result_id && this.job.result_id !== "None") {
loadResult();
} else if (this.getStatus() !== "failed") {
} else if (this.getStatus() !== ExecutionStatus.FAILED) {
const waitTime = tryNumber > 10 ? 3000 : 500;
setTimeout(() => {
this.refreshStatus(query, parameters, tryNumber + 1);
Expand All @@ -428,7 +432,7 @@ class QueryResult {
this.update({
job: {
error: "failed communicating with server. Please check your Internet connection and try again.",
status: 4,
status: ExecutionStatus.FAILED,
},
});
});
Expand Down
5 changes: 3 additions & 2 deletions client/app/services/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import moment from "moment";
import debug from "debug";
import Mustache from "mustache";
import { axios } from "@/services/axios";
import { ExecutionStatus } from "@/services/query-result";
import {
zipObject,
isEmpty,
Expand Down Expand Up @@ -103,7 +104,7 @@ export class Query {
return new QueryResult({
job: {
error: `missing ${valuesWord} for ${missingParams.join(", ")} ${paramsWord}.`,
status: 4,
status: ExecutionStatus.FAILED,
},
});
}
Expand Down Expand Up @@ -360,7 +361,7 @@ export class QueryResultError {

// eslint-disable-next-line class-methods-use-this
getStatus() {
return "failed";
return ExecutionStatus.FAILED;
}

// eslint-disable-next-line class-methods-use-this
Expand Down
6 changes: 3 additions & 3 deletions redash/handlers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,11 @@ def json_representation(data, code, headers=None):
)
api.add_org_resource(
QueryResultResource,
"/api/query_results/<query_result_id>.<filetype>",
"/api/query_results/<query_result_id>",
"/api/query_results/<result_id>.<filetype>",
"/api/query_results/<result_id>",
"/api/queries/<query_id>/results",
"/api/queries/<query_id>/results.<filetype>",
"/api/queries/<query_id>/results/<query_result_id>.<filetype>",
"/api/queries/<query_id>/results/<result_id>.<filetype>",
endpoint="query_result",
)
api.add_org_resource(
Expand Down
17 changes: 9 additions & 8 deletions redash/handlers/query_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from flask import make_response, request
from flask_login import current_user
from flask_restful import abort
from rq.job import JobStatus

from redash import models, settings
from redash.handlers.base import BaseResource, get_object_or_404, record_event
Expand Down Expand Up @@ -38,7 +39,7 @@


def error_response(message, http_status=400):
return {"job": {"status": 4, "error": message}}, http_status
return {"job": {"status": JobStatus.FAILED, "error": message}}, http_status


error_messages = {
Expand Down Expand Up @@ -225,7 +226,7 @@ def add_cors_headers(headers):
headers["Access-Control-Allow-Credentials"] = str(settings.ACCESS_CONTROL_ALLOW_CREDENTIALS).lower()

@require_any_of_permission(("view_query", "execute_query"))
def options(self, query_id=None, query_result_id=None, filetype="json"):
def options(self, query_id=None, result_id=None, filetype="json"):
headers = {}
self.add_cors_headers(headers)

Expand Down Expand Up @@ -285,12 +286,12 @@ def post(self, query_id):
return error_messages["no_permission"]

@require_any_of_permission(("view_query", "execute_query"))
def get(self, query_id=None, query_result_id=None, filetype="json"):
def get(self, query_id=None, result_id=None, filetype="json"):
"""
Retrieve query results.
:param number query_id: The ID of the query whose results should be fetched
:param number query_result_id: the ID of the query result to fetch
:param number result_id: the ID of the query result to fetch
:param string filetype: Format to return. One of 'json', 'xlsx', or 'csv'. Defaults to 'json'.
:<json number id: Query result ID
Expand All @@ -305,13 +306,13 @@ def get(self, query_id=None, query_result_id=None, filetype="json"):
# This method handles two cases: retrieving result by id & retrieving result by query id.
# They need to be split, as they have different logic (for example, retrieving by query id
# should check for query parameters and shouldn't cache the result).
should_cache = query_result_id is not None
should_cache = result_id is not None

query_result = None
query = None

if query_result_id:
query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query_result_id, self.current_org)
if result_id:
query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, result_id, self.current_org)

if query_id is not None:
query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)
Expand Down Expand Up @@ -346,7 +347,7 @@ def get(self, query_id=None, query_result_id=None, filetype="json"):
event["object_id"] = query_id
else:
event["object_type"] = "query_result"
event["object_id"] = query_result_id
event["object_id"] = result_id

self.record_event(event)

Expand Down

0 comments on commit ccc6e95

Please sign in to comment.