Upgrade query-result to Octane (#204)

* Upgrade query-result to Octane
This commit is contained in:
Isaac Janzen
2022-12-20 12:09:37 -06:00
committed by GitHub
parent cf365f7df2
commit 4c70cfa100
8 changed files with 334 additions and 431 deletions
@@ -1,76 +1,73 @@
<article>
<header class="result-header">
<div class="result-info">
{{d-button
action=(action "downloadResultJson")
icon="download"
label="explorer.download_json"
group=group
}}
<DButton
@action={{this.downloadResultJson}}
@icon="download"
@label="explorer.download_json"
/>
{{d-button
action=(action "downloadResultCsv")
icon="download"
label="explorer.download_csv"
group=group
}}
<DButton
@action={{this.downloadResultCsv}}
@icon="download"
@label="explorer.download_csv"
/>
{{#if canShowChart}}
{{#if showChart}}
{{d-button
action=(action "showTable")
icon="table"
label="explorer.show_table"
group=group
}}
{{#if this.canShowChart}}
{{#if this.chartDisplayed}}
<DButton
@action={{this.hideChart}}
@icon="table"
@label="explorer.show_table"
/>
{{else}}
{{d-button
action=(action "showChart")
icon="chart-bar"
label="explorer.show_graph"
group=group
}}
<DButton
@action={{this.showChart}}
@icon="chart-bar"
@label="explorer.show_graph"
/>
{{/if}}
{{/if}}
</div>
<div class="result-about">
{{resultCount}}
{{duration}}
{{this.resultCount}}
{{this.duration}}
</div>
<br />
{{~#if hasExplain}}
<pre class="result-explain"><code>
{{~content.explain}}
</code></pre>
{{~#if this.explainText}}
<pre class="result-explain">
<code>
{{~this.explainText}}
</code>
</pre>
{{~/if}}
<br />
</header>
<section>
{{#if showChart}}
{{#if this.chartDisplayed}}
<DataExplorerBarChart
@labels={{chartLabels}}
@values={{chartValues}}
@datasetName={{chartDatasetName}}
@labels={{this.chartLabels}}
@values={{this.chartValues}}
@datasetName={{this.chartDatasetName}}
/>
{{else}}
<table>
<thead>
<tr class="headers">
{{#each columnDispNames as |col|}}
{{#each this.columnNames as |col|}}
<th>{{col}}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each rows as |row|}}
{{#each this.rows as |row|}}
<QueryRowContent
@row={{row}}
@fallbackTemplate={{this.fallbackTemplate}}
@columnTemplates={{this.columnTemplates}}
@lookupUser={{this.lookupUser}}
@lookupBadge={{this.lookupBadge}}
@@ -1,13 +1,266 @@
import Component from "@ember/component";
import { findRawTemplate } from "discourse-common/lib/raw-templates";
import Component from "@glimmer/component";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import getURL from "discourse-common/lib/get-url";
import Badge from "discourse/models/badge";
import discourseComputed from "discourse-common/utils/decorators";
import { capitalize } from "@ember/string";
import { alias, mapBy, notEmpty, reads } from "@ember/object/computed";
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() {
return "xxxxxxxx".replace(/[xy]/g, () => {
@@ -28,249 +281,3 @@ function transformedRelTable(table, modelClass) {
});
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 { escapeExpression } from "discourse/lib/utilities";
import { cached } from "@glimmer/tracking";
import { findRawTemplate } from "discourse-common/lib/raw-templates";
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
get results() {
return this.args.columnTemplates.map((t, idx) => {
@@ -46,23 +57,15 @@ export default class QueryRowContent extends Component {
}
try {
return htmlSafe(
(t.template || this.args.fallbackTemplate)(ctx, params)
);
return htmlSafe((t.template || this.fallbackTemplate)(ctx, params));
} catch (e) {
return "error";
}
});
}
constructor() {
super(...arguments);
this.helpers = {
"icon-or-image": icon_or_image_replacement,
"category-link": category_badge_replacement,
reltime: bound_date_replacement,
};
fallbackTemplate() {
return findRawTemplate("javascripts/explorer/text");
}
}
@@ -252,17 +252,12 @@
{{conditional-loading-spinner condition=loading}}
{{#unless selectedItem.fake}}
{{#if results}}
<div class="query-results">
{{#if showResults}}
{{query-result query=selectedItem content=results}}
{{else}}
{{#each results.errors as |err|}}
<pre class="query-error"><code>{{~err}}</code></pre>
{{/each}}
{{/if}}
</div>
{{/if}}
<QueryResultsWrapper
@results={{results}}
@showResults={{showResults}}
@query={{selectedItem}}
@content={{results}}
/>
{{/unless}}
{{#if showRecentQueries}}