Upgrade `query-result` to Octane (#204)
* Upgrade query-result to Octane
This commit is contained in:
parent
cf365f7df2
commit
4c70cfa100
|
@ -1,76 +1,73 @@
|
||||||
<article>
|
<article>
|
||||||
<header class="result-header">
|
<header class="result-header">
|
||||||
<div class="result-info">
|
<div class="result-info">
|
||||||
{{d-button
|
<DButton
|
||||||
action=(action "downloadResultJson")
|
@action={{this.downloadResultJson}}
|
||||||
icon="download"
|
@icon="download"
|
||||||
label="explorer.download_json"
|
@label="explorer.download_json"
|
||||||
group=group
|
/>
|
||||||
}}
|
|
||||||
|
|
||||||
{{d-button
|
<DButton
|
||||||
action=(action "downloadResultCsv")
|
@action={{this.downloadResultCsv}}
|
||||||
icon="download"
|
@icon="download"
|
||||||
label="explorer.download_csv"
|
@label="explorer.download_csv"
|
||||||
group=group
|
/>
|
||||||
}}
|
|
||||||
|
|
||||||
{{#if canShowChart}}
|
{{#if this.canShowChart}}
|
||||||
{{#if showChart}}
|
{{#if this.chartDisplayed}}
|
||||||
{{d-button
|
<DButton
|
||||||
action=(action "showTable")
|
@action={{this.hideChart}}
|
||||||
icon="table"
|
@icon="table"
|
||||||
label="explorer.show_table"
|
@label="explorer.show_table"
|
||||||
group=group
|
/>
|
||||||
}}
|
|
||||||
{{else}}
|
{{else}}
|
||||||
{{d-button
|
<DButton
|
||||||
action=(action "showChart")
|
@action={{this.showChart}}
|
||||||
icon="chart-bar"
|
@icon="chart-bar"
|
||||||
label="explorer.show_graph"
|
@label="explorer.show_graph"
|
||||||
group=group
|
/>
|
||||||
}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="result-about">
|
<div class="result-about">
|
||||||
{{resultCount}}
|
{{this.resultCount}}
|
||||||
{{duration}}
|
{{this.duration}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
{{~#if hasExplain}}
|
{{~#if this.explainText}}
|
||||||
<pre class="result-explain"><code>
|
<pre class="result-explain">
|
||||||
{{~content.explain}}
|
<code>
|
||||||
</code></pre>
|
{{~this.explainText}}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
{{~/if}}
|
{{~/if}}
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
{{#if showChart}}
|
{{#if this.chartDisplayed}}
|
||||||
<DataExplorerBarChart
|
<DataExplorerBarChart
|
||||||
@labels={{chartLabels}}
|
@labels={{this.chartLabels}}
|
||||||
@values={{chartValues}}
|
@values={{this.chartValues}}
|
||||||
@datasetName={{chartDatasetName}}
|
@datasetName={{this.chartDatasetName}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="headers">
|
<tr class="headers">
|
||||||
{{#each columnDispNames as |col|}}
|
{{#each this.columnNames as |col|}}
|
||||||
<th>{{col}}</th>
|
<th>{{col}}</th>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{#each rows as |row|}}
|
{{#each this.rows as |row|}}
|
||||||
<QueryRowContent
|
<QueryRowContent
|
||||||
@row={{row}}
|
@row={{row}}
|
||||||
@fallbackTemplate={{this.fallbackTemplate}}
|
|
||||||
@columnTemplates={{this.columnTemplates}}
|
@columnTemplates={{this.columnTemplates}}
|
||||||
@lookupUser={{this.lookupUser}}
|
@lookupUser={{this.lookupUser}}
|
||||||
@lookupBadge={{this.lookupBadge}}
|
@lookupBadge={{this.lookupBadge}}
|
|
@ -1,13 +1,266 @@
|
||||||
import Component from "@ember/component";
|
import Component from "@glimmer/component";
|
||||||
import { findRawTemplate } from "discourse-common/lib/raw-templates";
|
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
import Badge from "discourse/models/badge";
|
import Badge from "discourse/models/badge";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
|
||||||
import { capitalize } from "@ember/string";
|
import { capitalize } from "@ember/string";
|
||||||
import { alias, mapBy, notEmpty, reads } from "@ember/object/computed";
|
|
||||||
import { schedule } from "@ember/runloop";
|
import { schedule } from "@ember/runloop";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { findRawTemplate } from "discourse-common/lib/raw-templates";
|
||||||
|
|
||||||
|
export default class QueryResult extends Component {
|
||||||
|
@service site;
|
||||||
|
@tracked chartDisplayed = false;
|
||||||
|
|
||||||
|
get colRender() {
|
||||||
|
return this.args.content.colrender || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
get rows() {
|
||||||
|
return this.args.content.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
get columns() {
|
||||||
|
return this.args.content.columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
get params() {
|
||||||
|
return this.args.content.params;
|
||||||
|
}
|
||||||
|
|
||||||
|
get explainText() {
|
||||||
|
return this.args.content.explain;
|
||||||
|
}
|
||||||
|
|
||||||
|
get chartDatasetName() {
|
||||||
|
return this.columnNames[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
get columnNames() {
|
||||||
|
if (!this.columns) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.columns.map((colName) => {
|
||||||
|
if (colName.endsWith("_id")) {
|
||||||
|
return colName.slice(0, -3);
|
||||||
|
}
|
||||||
|
const dIdx = colName.indexOf("$");
|
||||||
|
if (dIdx >= 0) {
|
||||||
|
return colName.substring(dIdx + 1);
|
||||||
|
}
|
||||||
|
return colName;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get columnTemplates() {
|
||||||
|
if (!this.columns) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.columns.map((_, idx) => {
|
||||||
|
let viewName = "text";
|
||||||
|
if (this.colRender[idx]) {
|
||||||
|
viewName = this.colRender[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = findRawTemplate(`javascripts/explorer/${viewName}`);
|
||||||
|
return { name: viewName, template };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get chartValues() {
|
||||||
|
// return an array with the second value of this.row
|
||||||
|
return this.rows.mapBy(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
get colCount() {
|
||||||
|
return this.columns.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get resultCount() {
|
||||||
|
const count = this.args.content.result_count;
|
||||||
|
if (count === this.args.content.default_limit) {
|
||||||
|
return I18n.t("explorer.max_result_count", { count });
|
||||||
|
} else {
|
||||||
|
return I18n.t("explorer.result_count", { count });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get duration() {
|
||||||
|
return I18n.t("explorer.run_time", {
|
||||||
|
value: I18n.toNumber(this.args.content.duration, { precision: 1 }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get parameterAry() {
|
||||||
|
let arr = [];
|
||||||
|
for (let key in this.params) {
|
||||||
|
if (this.params.hasOwnProperty(key)) {
|
||||||
|
arr.push({ key, value: this.params[key] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
get transformedUserTable() {
|
||||||
|
return transformedRelTable(this.args.content.relations.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
get transformedBadgeTable() {
|
||||||
|
return transformedRelTable(this.args.content.relations.badge, Badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
get transformedPostTable() {
|
||||||
|
return transformedRelTable(this.args.content.relations.post);
|
||||||
|
}
|
||||||
|
|
||||||
|
get transformedTopicTable() {
|
||||||
|
return transformedRelTable(this.args.content.relations.topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
get transformedGroupTable() {
|
||||||
|
return transformedRelTable(this.site.groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
get canShowChart() {
|
||||||
|
const hasTwoColumns = this.colCount === 2;
|
||||||
|
const secondColumnContainsNumber =
|
||||||
|
this.resultCount.length && typeof this.rows[0][1] === "number";
|
||||||
|
const secondColumnContainsId = this.colRender[1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
hasTwoColumns && secondColumnContainsNumber && !secondColumnContainsId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get chartLabels() {
|
||||||
|
const labelSelectors = {
|
||||||
|
user: (user) => user.username,
|
||||||
|
badge: (badge) => badge.name,
|
||||||
|
topic: (topic) => topic.title,
|
||||||
|
group: (group) => group.name,
|
||||||
|
category: (category) => category.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
const relationName = this.colRender[0];
|
||||||
|
if (relationName) {
|
||||||
|
const lookupFunc = this[`lookup${capitalize(relationName)}`];
|
||||||
|
const labelSelector = labelSelectors[relationName];
|
||||||
|
|
||||||
|
if (lookupFunc && labelSelector) {
|
||||||
|
return this.rows.map((r) => {
|
||||||
|
const relation = lookupFunc.call(this, r[0]);
|
||||||
|
const label = labelSelector(relation);
|
||||||
|
return this._cutChartLabel(label);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.rows.map((r) => this._cutChartLabel(r[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
lookupUser(id) {
|
||||||
|
return this.transformedUserTable[id];
|
||||||
|
}
|
||||||
|
lookupBadge(id) {
|
||||||
|
return this.transformedBadgeTable[id];
|
||||||
|
}
|
||||||
|
lookupPost(id) {
|
||||||
|
return this.transformedPostTable[id];
|
||||||
|
}
|
||||||
|
lookupTopic(id) {
|
||||||
|
return this.transformedTopicTable[id];
|
||||||
|
}
|
||||||
|
lookupGroup(id) {
|
||||||
|
return this.transformedGroupTable[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
lookupCategory(id) {
|
||||||
|
return this.site.categoriesById[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
_cutChartLabel(label) {
|
||||||
|
const labelString = label.toString();
|
||||||
|
if (labelString.length > 25) {
|
||||||
|
return `${labelString.substring(0, 25)}...`;
|
||||||
|
} else {
|
||||||
|
return labelString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
downloadResultJson() {
|
||||||
|
this._downloadResult("json");
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
downloadResultCsv() {
|
||||||
|
this._downloadResult("csv");
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
showChart() {
|
||||||
|
this.chartDisplayed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
hideChart() {
|
||||||
|
this.chartDisplayed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_download_url() {
|
||||||
|
return this.group
|
||||||
|
? `/g/${this.group.name}/reports/`
|
||||||
|
: "/admin/plugins/explorer/queries/";
|
||||||
|
}
|
||||||
|
|
||||||
|
_downloadResult(format) {
|
||||||
|
// Create a frame to submit the form in (?)
|
||||||
|
// to avoid leaving an about:blank behind
|
||||||
|
let windowName = randomIdShort();
|
||||||
|
const newWindowContents =
|
||||||
|
"<style>body{font-size:36px;display:flex;justify-content:center;align-items:center;}</style><body>Click anywhere to close this window once the download finishes.<script>window.onclick=function(){window.close()};</script>";
|
||||||
|
|
||||||
|
window.open("data:text/html;base64," + btoa(newWindowContents), windowName);
|
||||||
|
|
||||||
|
let form = document.createElement("form");
|
||||||
|
form.setAttribute("id", "query-download-result");
|
||||||
|
form.setAttribute("method", "post");
|
||||||
|
form.setAttribute(
|
||||||
|
"action",
|
||||||
|
getURL(
|
||||||
|
this._download_url() +
|
||||||
|
this.get("query.id") +
|
||||||
|
"/run." +
|
||||||
|
format +
|
||||||
|
"?download=1"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
form.setAttribute("target", windowName);
|
||||||
|
form.setAttribute("style", "display:none;");
|
||||||
|
|
||||||
|
function addInput(name, value) {
|
||||||
|
let field;
|
||||||
|
field = document.createElement("input");
|
||||||
|
field.setAttribute("name", name);
|
||||||
|
field.setAttribute("value", value);
|
||||||
|
form.appendChild(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
addInput("params", JSON.stringify(this.params));
|
||||||
|
addInput("explain", this.explainText);
|
||||||
|
addInput("limit", "1000000");
|
||||||
|
|
||||||
|
ajax("/session/csrf.json").then((csrf) => {
|
||||||
|
addInput("authenticity_token", csrf.csrf);
|
||||||
|
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
schedule("afterRender", () => document.body.removeChild(form));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function randomIdShort() {
|
function randomIdShort() {
|
||||||
return "xxxxxxxx".replace(/[xy]/g, () => {
|
return "xxxxxxxx".replace(/[xy]/g, () => {
|
||||||
|
@ -28,249 +281,3 @@ function transformedRelTable(table, modelClass) {
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QueryResultComponent = Component.extend({
|
|
||||||
layoutName: "explorer-query-result",
|
|
||||||
|
|
||||||
rows: alias("content.rows"),
|
|
||||||
columns: alias("content.columns"),
|
|
||||||
params: alias("content.params"),
|
|
||||||
explainText: alias("content.explain"),
|
|
||||||
hasExplain: notEmpty("content.explain"),
|
|
||||||
chartDatasetName: reads("columnDispNames.1"),
|
|
||||||
chartValues: mapBy("content.rows", "1"),
|
|
||||||
showChart: false,
|
|
||||||
|
|
||||||
@discourseComputed("content.result_count")
|
|
||||||
resultCount(count) {
|
|
||||||
if (count === this.get("content.default_limit")) {
|
|
||||||
return I18n.t("explorer.max_result_count", { count });
|
|
||||||
} else {
|
|
||||||
return I18n.t("explorer.result_count", { count });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
colCount: reads("content.columns.length"),
|
|
||||||
|
|
||||||
@discourseComputed("content.duration")
|
|
||||||
duration(contentDuration) {
|
|
||||||
return I18n.t("explorer.run_time", {
|
|
||||||
value: I18n.toNumber(contentDuration, { precision: 1 }),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("params.[]")
|
|
||||||
parameterAry(params) {
|
|
||||||
let arr = [];
|
|
||||||
for (let key in params) {
|
|
||||||
if (params.hasOwnProperty(key)) {
|
|
||||||
arr.push({ key, value: params[key] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("content", "columns.[]")
|
|
||||||
columnDispNames(content, columns) {
|
|
||||||
if (!columns) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return columns.map((colName) => {
|
|
||||||
if (colName.endsWith("_id")) {
|
|
||||||
return colName.slice(0, -3);
|
|
||||||
}
|
|
||||||
const dIdx = colName.indexOf("$");
|
|
||||||
if (dIdx >= 0) {
|
|
||||||
return colName.substring(dIdx + 1);
|
|
||||||
}
|
|
||||||
return colName;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed
|
|
||||||
fallbackTemplate() {
|
|
||||||
return findRawTemplate("javascripts/explorer/text");
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("content", "columns.[]")
|
|
||||||
columnTemplates(content, columns) {
|
|
||||||
if (!columns) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return columns.map((colName, idx) => {
|
|
||||||
let viewName = "text";
|
|
||||||
if (this.get("content.colrender")[idx]) {
|
|
||||||
viewName = this.get("content.colrender")[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = findRawTemplate(`javascripts/explorer/${viewName}`);
|
|
||||||
|
|
||||||
return { name: viewName, template };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("content.relations.user")
|
|
||||||
transformedUserTable(contentRelationsUser) {
|
|
||||||
return transformedRelTable(contentRelationsUser);
|
|
||||||
},
|
|
||||||
@discourseComputed("content.relations.badge")
|
|
||||||
transformedBadgeTable(contentRelationsBadge) {
|
|
||||||
return transformedRelTable(contentRelationsBadge, Badge);
|
|
||||||
},
|
|
||||||
@discourseComputed("content.relations.post")
|
|
||||||
transformedPostTable(contentRelationsPost) {
|
|
||||||
return transformedRelTable(contentRelationsPost);
|
|
||||||
},
|
|
||||||
@discourseComputed("content.relations.topic")
|
|
||||||
transformedTopicTable(contentRelationsTopic) {
|
|
||||||
return transformedRelTable(contentRelationsTopic);
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("site.groups")
|
|
||||||
transformedGroupTable(groups) {
|
|
||||||
return transformedRelTable(groups);
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed(
|
|
||||||
"rows.[]",
|
|
||||||
"content.colrender.[]",
|
|
||||||
"content.result_count",
|
|
||||||
"colCount"
|
|
||||||
)
|
|
||||||
canShowChart(rows, colRender, resultCount, colCount) {
|
|
||||||
const hasTwoColumns = colCount === 2;
|
|
||||||
const secondColumnContainsNumber =
|
|
||||||
resultCount > 0 && typeof rows[0][1] === "number";
|
|
||||||
const secondColumnContainsId = colRender[1];
|
|
||||||
|
|
||||||
return (
|
|
||||||
hasTwoColumns && secondColumnContainsNumber && !secondColumnContainsId
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("content.rows.[]", "content.colrender.[]")
|
|
||||||
chartLabels(rows, colRender) {
|
|
||||||
const labelSelectors = {
|
|
||||||
user: (user) => user.username,
|
|
||||||
badge: (badge) => badge.name,
|
|
||||||
topic: (topic) => topic.title,
|
|
||||||
group: (group) => group.name,
|
|
||||||
category: (category) => category.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
const relationName = colRender[0];
|
|
||||||
|
|
||||||
if (relationName) {
|
|
||||||
const lookupFunc = this[`lookup${capitalize(relationName)}`];
|
|
||||||
const labelSelector = labelSelectors[relationName];
|
|
||||||
|
|
||||||
if (lookupFunc && labelSelector) {
|
|
||||||
return rows.map((r) => {
|
|
||||||
const relation = lookupFunc.call(this, r[0]);
|
|
||||||
const label = labelSelector(relation);
|
|
||||||
return this._cutChartLabel(label);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows.map((r) => this._cutChartLabel(r[0]));
|
|
||||||
},
|
|
||||||
|
|
||||||
lookupUser(id) {
|
|
||||||
return this.transformedUserTable[id];
|
|
||||||
},
|
|
||||||
lookupBadge(id) {
|
|
||||||
return this.transformedBadgeTable[id];
|
|
||||||
},
|
|
||||||
lookupPost(id) {
|
|
||||||
return this.transformedPostTable[id];
|
|
||||||
},
|
|
||||||
lookupTopic(id) {
|
|
||||||
return this.transformedTopicTable[id];
|
|
||||||
},
|
|
||||||
lookupGroup(id) {
|
|
||||||
return this.transformedGroupTable[id];
|
|
||||||
},
|
|
||||||
|
|
||||||
lookupCategory(id) {
|
|
||||||
return this.site.categoriesById[id];
|
|
||||||
},
|
|
||||||
|
|
||||||
download_url() {
|
|
||||||
return this.group
|
|
||||||
? `/g/${this.group.name}/reports/`
|
|
||||||
: "/admin/plugins/explorer/queries/";
|
|
||||||
},
|
|
||||||
|
|
||||||
downloadResult(format) {
|
|
||||||
// Create a frame to submit the form in (?)
|
|
||||||
// to avoid leaving an about:blank behind
|
|
||||||
let windowName = randomIdShort();
|
|
||||||
const newWindowContents =
|
|
||||||
"<style>body{font-size:36px;display:flex;justify-content:center;align-items:center;}</style><body>Click anywhere to close this window once the download finishes.<script>window.onclick=function(){window.close()};</script>";
|
|
||||||
|
|
||||||
window.open("data:text/html;base64," + btoa(newWindowContents), windowName);
|
|
||||||
|
|
||||||
let form = document.createElement("form");
|
|
||||||
form.setAttribute("id", "query-download-result");
|
|
||||||
form.setAttribute("method", "post");
|
|
||||||
form.setAttribute(
|
|
||||||
"action",
|
|
||||||
getURL(
|
|
||||||
this.download_url() +
|
|
||||||
this.get("query.id") +
|
|
||||||
"/run." +
|
|
||||||
format +
|
|
||||||
"?download=1"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
form.setAttribute("target", windowName);
|
|
||||||
form.setAttribute("style", "display:none;");
|
|
||||||
|
|
||||||
function addInput(name, value) {
|
|
||||||
let field;
|
|
||||||
field = document.createElement("input");
|
|
||||||
field.setAttribute("name", name);
|
|
||||||
field.setAttribute("value", value);
|
|
||||||
form.appendChild(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
addInput("params", JSON.stringify(this.params));
|
|
||||||
addInput("explain", this.hasExplain);
|
|
||||||
addInput("limit", "1000000");
|
|
||||||
|
|
||||||
ajax("/session/csrf.json").then((csrf) => {
|
|
||||||
addInput("authenticity_token", csrf.csrf);
|
|
||||||
|
|
||||||
document.body.appendChild(form);
|
|
||||||
form.submit();
|
|
||||||
schedule("afterRender", () => document.body.removeChild(form));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_cutChartLabel(label) {
|
|
||||||
const labelString = label.toString();
|
|
||||||
if (labelString.length > 25) {
|
|
||||||
return `${labelString.substring(0, 25)}...`;
|
|
||||||
} else {
|
|
||||||
return labelString;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
downloadResultJson() {
|
|
||||||
this.downloadResult("json");
|
|
||||||
},
|
|
||||||
downloadResultCsv() {
|
|
||||||
this.downloadResult("csv");
|
|
||||||
},
|
|
||||||
showChart() {
|
|
||||||
this.set("showChart", true);
|
|
||||||
},
|
|
||||||
showTable() {
|
|
||||||
this.set("showChart", false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default QueryResultComponent;
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
{{#if @results}}
|
||||||
|
<div class="query-results">
|
||||||
|
{{#if @showResults}}
|
||||||
|
<QueryResult @query={{@selectedItem}} @content={{@results}} />
|
||||||
|
{{else}}
|
||||||
|
{{#each @results.errors as |err|}}
|
||||||
|
<pre class="query-error"><code>{{~err}}</code></pre>
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
|
@ -9,8 +9,19 @@ import { get } from "@ember/object";
|
||||||
import { isEmpty } from "@ember/utils";
|
import { isEmpty } from "@ember/utils";
|
||||||
import { escapeExpression } from "discourse/lib/utilities";
|
import { escapeExpression } from "discourse/lib/utilities";
|
||||||
import { cached } from "@glimmer/tracking";
|
import { cached } from "@glimmer/tracking";
|
||||||
|
import { findRawTemplate } from "discourse-common/lib/raw-templates";
|
||||||
|
|
||||||
export default class QueryRowContent extends Component {
|
export default class QueryRowContent extends Component {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
this.helpers = {
|
||||||
|
"icon-or-image": icon_or_image_replacement,
|
||||||
|
"category-link": category_badge_replacement,
|
||||||
|
reltime: bound_date_replacement,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
get results() {
|
get results() {
|
||||||
return this.args.columnTemplates.map((t, idx) => {
|
return this.args.columnTemplates.map((t, idx) => {
|
||||||
|
@ -46,23 +57,15 @@ export default class QueryRowContent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return htmlSafe(
|
return htmlSafe((t.template || this.fallbackTemplate)(ctx, params));
|
||||||
(t.template || this.args.fallbackTemplate)(ctx, params)
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return "error";
|
return "error";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
fallbackTemplate() {
|
||||||
super(...arguments);
|
return findRawTemplate("javascripts/explorer/text");
|
||||||
|
|
||||||
this.helpers = {
|
|
||||||
"icon-or-image": icon_or_image_replacement,
|
|
||||||
"category-link": category_badge_replacement,
|
|
||||||
reltime: bound_date_replacement,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -252,17 +252,12 @@
|
||||||
{{conditional-loading-spinner condition=loading}}
|
{{conditional-loading-spinner condition=loading}}
|
||||||
|
|
||||||
{{#unless selectedItem.fake}}
|
{{#unless selectedItem.fake}}
|
||||||
{{#if results}}
|
<QueryResultsWrapper
|
||||||
<div class="query-results">
|
@results={{results}}
|
||||||
{{#if showResults}}
|
@showResults={{showResults}}
|
||||||
{{query-result query=selectedItem content=results}}
|
@query={{selectedItem}}
|
||||||
{{else}}
|
@content={{results}}
|
||||||
{{#each results.errors as |err|}}
|
/>
|
||||||
<pre class="query-error"><code>{{~err}}</code></pre>
|
|
||||||
{{/each}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
{{#if showRecentQueries}}
|
{{#if showRecentQueries}}
|
||||||
|
|
|
@ -199,6 +199,7 @@ acceptance("Data Explorer Plugin | Run Query", function (needs) {
|
||||||
queryAll("div.query-results table tbody tr").length === 2,
|
queryAll("div.query-results table tbody tr").length === 2,
|
||||||
"the table with query results was rendered"
|
"the table with query results was rendered"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
query("div.result-info button:nth-child(3) span").innerText.trim() ===
|
query("div.result-info button:nth-child(3) span").innerText.trim() ===
|
||||||
I18n.t("explorer.show_graph"),
|
I18n.t("explorer.show_graph"),
|
||||||
|
|
|
@ -16,7 +16,7 @@ discourseModule(
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
componentTest("it renders query results", {
|
componentTest("it renders query results", {
|
||||||
template: hbs`{{query-result content=content}}`,
|
template: hbs`<QueryResult @content={{content}} />`,
|
||||||
|
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
const results = {
|
const results = {
|
||||||
|
@ -70,7 +70,7 @@ discourseModule(
|
||||||
});
|
});
|
||||||
|
|
||||||
componentTest("it renders badge names in query results", {
|
componentTest("it renders badge names in query results", {
|
||||||
template: hbs`{{query-result content=content}}`,
|
template: hbs`<QueryResult @content={{content}} />`,
|
||||||
|
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
const results = {
|
const results = {
|
||||||
|
@ -102,7 +102,7 @@ discourseModule(
|
||||||
});
|
});
|
||||||
|
|
||||||
componentTest("it renders a post in query results", {
|
componentTest("it renders a post in query results", {
|
||||||
template: hbs`{{query-result content=content}}`,
|
template: hbs`<QueryResult @content={{content}} />`,
|
||||||
|
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
const results = {
|
const results = {
|
||||||
|
@ -141,7 +141,7 @@ discourseModule(
|
||||||
});
|
});
|
||||||
|
|
||||||
componentTest("it renders a category_id in query results", {
|
componentTest("it renders a category_id in query results", {
|
||||||
template: hbs`{{query-result content=content}}`,
|
template: hbs`<QueryResult @content={{content}} />`,
|
||||||
|
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
const results = {
|
const results = {
|
||||||
|
@ -183,7 +183,7 @@ discourseModule(
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
componentTest("navigation between a table and a chart works", {
|
componentTest("navigation between a table and a chart works", {
|
||||||
template: hbs`{{query-result content=content}}`,
|
template: hbs`<QueryResult @content={{content}} />`,
|
||||||
|
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
const results = {
|
const results = {
|
||||||
|
@ -228,7 +228,7 @@ discourseModule(
|
||||||
componentTest(
|
componentTest(
|
||||||
"it renders a chart button when data has two columns and numbers in the second column",
|
"it renders a chart button when data has two columns and numbers in the second column",
|
||||||
{
|
{
|
||||||
template: hbs`{{query-result content=content}}`,
|
template: hbs`<QueryResult @content={{content}} />`,
|
||||||
|
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
const results = {
|
const results = {
|
||||||
|
@ -255,7 +255,7 @@ discourseModule(
|
||||||
componentTest(
|
componentTest(
|
||||||
"it doesn't render a chart button when data contains identifiers in the second column",
|
"it doesn't render a chart button when data contains identifiers in the second column",
|
||||||
{
|
{
|
||||||
template: hbs`{{query-result content=content}}`,
|
template: hbs`<QueryResult @content={{content}} />`,
|
||||||
|
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
const results = {
|
const results = {
|
||||||
|
@ -285,7 +285,7 @@ discourseModule(
|
||||||
componentTest(
|
componentTest(
|
||||||
"it doesn't render a chart button when data contains one column",
|
"it doesn't render a chart button when data contains one column",
|
||||||
{
|
{
|
||||||
template: hbs`{{query-result content=content}}`,
|
template: hbs`<QueryResult @content={{content}} />`,
|
||||||
|
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
const results = {
|
const results = {
|
||||||
|
@ -306,7 +306,7 @@ discourseModule(
|
||||||
componentTest(
|
componentTest(
|
||||||
"it doesn't render a chart button when data contains more than two columns",
|
"it doesn't render a chart button when data contains more than two columns",
|
||||||
{
|
{
|
||||||
template: hbs`{{query-result content=content}}`,
|
template: hbs`<QueryResult @content={{content}} />`,
|
||||||
|
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
const results = {
|
const results = {
|
||||||
|
|
|
@ -1,111 +0,0 @@
|
||||||
import { discourseModule } from "discourse/tests/helpers/qunit-helpers";
|
|
||||||
import { test } from "qunit";
|
|
||||||
|
|
||||||
discourseModule("Unit | Component | query-result", function () {
|
|
||||||
test("it transforms data for a chart", function (assert) {
|
|
||||||
const component = this.container
|
|
||||||
.factoryFor("component:query-result")
|
|
||||||
.create({ renderer: {} });
|
|
||||||
|
|
||||||
component.setProperties({
|
|
||||||
content: {
|
|
||||||
colrender: [],
|
|
||||||
result_count: 2,
|
|
||||||
columns: ["user", "like_count"],
|
|
||||||
rows: [
|
|
||||||
["user1", 10],
|
|
||||||
["user2", 20],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepEqual(
|
|
||||||
component.chartLabels,
|
|
||||||
["user1", "user2"],
|
|
||||||
"labels are correct"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.deepEqual(component.chartValues, [10, 20], "values are correct");
|
|
||||||
|
|
||||||
assert.deepEqual(
|
|
||||||
component.chartDatasetName,
|
|
||||||
"like_count",
|
|
||||||
"the dataset name is correct"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it uses descriptive chart labels instead of identifiers", function (assert) {
|
|
||||||
const component = this.container
|
|
||||||
.factoryFor("component:query-result")
|
|
||||||
.create({ renderer: {} });
|
|
||||||
|
|
||||||
component.setProperties({
|
|
||||||
content: {
|
|
||||||
colrender: { 0: "user" },
|
|
||||||
relations: {
|
|
||||||
user: [
|
|
||||||
{ id: 1, username: "user1" },
|
|
||||||
{ id: 2, username: "user2" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
result_count: 2,
|
|
||||||
columns: ["user", "like_count"],
|
|
||||||
rows: [
|
|
||||||
[1, 10],
|
|
||||||
[2, 20],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepEqual(component.chartLabels, ["user1", "user2"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it uses an identifier as a chart label if labelSelector doesn't exist", function (assert) {
|
|
||||||
const component = this.container
|
|
||||||
.factoryFor("component:query-result")
|
|
||||||
.create({ renderer: {} });
|
|
||||||
|
|
||||||
component.setProperties({
|
|
||||||
content: {
|
|
||||||
colrender: { 0: "unknown_entity" },
|
|
||||||
relations: {
|
|
||||||
unknown_entity: [
|
|
||||||
{ id: 1, username: "user1" },
|
|
||||||
{ id: 2, username: "user2" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
result_count: 2,
|
|
||||||
columns: ["user", "like_count"],
|
|
||||||
rows: [
|
|
||||||
[1, 10],
|
|
||||||
[2, 20],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepEqual(component.chartLabels, ["1", "2"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it cuts too long chart labels", function (assert) {
|
|
||||||
const component = this.container
|
|
||||||
.factoryFor("component:query-result")
|
|
||||||
.create({ renderer: {} });
|
|
||||||
|
|
||||||
component.setProperties({
|
|
||||||
content: {
|
|
||||||
colrender: [],
|
|
||||||
result_count: 2,
|
|
||||||
columns: ["user", "like_count"],
|
|
||||||
rows: [
|
|
||||||
["This string is too long to be used as a label on a chart", 10],
|
|
||||||
["This string is too long to be used as a label on a chart", 20],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepEqual(component.chartLabels, [
|
|
||||||
"This string is too long t...",
|
|
||||||
"This string is too long t...",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Reference in New Issue