FEATURE: Add a graph report to query results (#93)
This commit is contained in:
parent
3151fde1e7
commit
4f33c22344
|
@ -0,0 +1,97 @@
|
||||||
|
import loadScript from "discourse/lib/load-script";
|
||||||
|
import { default as computed } from "discourse-common/utils/decorators";
|
||||||
|
import themeColor from "../lib/themeColor";
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
barsColor: themeColor("--tertiary"),
|
||||||
|
barsHoverColor: themeColor("--tertiary-high"),
|
||||||
|
gridColor: themeColor("--primary-low"),
|
||||||
|
labelsColor: themeColor("--primary-medium"),
|
||||||
|
chart: null,
|
||||||
|
|
||||||
|
@computed("data", "options")
|
||||||
|
config(data, options) {
|
||||||
|
return {
|
||||||
|
type: "bar",
|
||||||
|
data,
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("labels.[]", "values.[]", "datasetName")
|
||||||
|
data(labels, values, datasetName) {
|
||||||
|
return {
|
||||||
|
labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: datasetName,
|
||||||
|
data: values,
|
||||||
|
backgroundColor: this.barsColor,
|
||||||
|
hoverBackgroundColor: this.barsHoverColor,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed
|
||||||
|
options() {
|
||||||
|
return {
|
||||||
|
scales: {
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
fontColor: this.labelsColor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
gridLines: {
|
||||||
|
color: this.gridColor,
|
||||||
|
zeroLineColor: this.gridColor,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
fontColor: this.labelsColor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
gridLines: {
|
||||||
|
color: this.gridColor,
|
||||||
|
zeroLineColor: this.gridColor,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
beginAtZero: true,
|
||||||
|
fontColor: this.labelsColor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this._initChart();
|
||||||
|
},
|
||||||
|
|
||||||
|
didUpdate() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.chart.data = this.data;
|
||||||
|
this.chart.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
willDestroyElement() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.chart.destroy();
|
||||||
|
},
|
||||||
|
|
||||||
|
_initChart() {
|
||||||
|
loadScript("/javascripts/Chart.min.js").then(() => {
|
||||||
|
const canvas = this.element.querySelector("canvas");
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
const config = this.config;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
this.chart = new Chart(context, config);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
|
@ -32,6 +32,9 @@ const QueryResultComponent = Ember.Component.extend({
|
||||||
params: Ember.computed.alias("content.params"),
|
params: Ember.computed.alias("content.params"),
|
||||||
explainText: Ember.computed.alias("content.explain"),
|
explainText: Ember.computed.alias("content.explain"),
|
||||||
hasExplain: Ember.computed.notEmpty("content.explain"),
|
hasExplain: Ember.computed.notEmpty("content.explain"),
|
||||||
|
chartDatasetName: Ember.computed.reads("columnDispNames.1"),
|
||||||
|
chartValues: Ember.computed.mapBy("content.rows", "1"),
|
||||||
|
showChart: false,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
@ -139,6 +142,51 @@ const QueryResultComponent = Ember.Component.extend({
|
||||||
return transformedRelTable(groups);
|
return transformedRelTable(groups);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@computed(
|
||||||
|
"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
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("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${relationName.capitalize()}`];
|
||||||
|
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) {
|
lookupUser(id) {
|
||||||
return this.transformedUserTable[id];
|
return this.transformedUserTable[id];
|
||||||
},
|
},
|
||||||
|
@ -211,6 +259,15 @@ const QueryResultComponent = Ember.Component.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_cutChartLabel(label) {
|
||||||
|
const labelString = label.toString();
|
||||||
|
if (labelString.length > 25) {
|
||||||
|
return `${labelString.substring(0, 25)}...`;
|
||||||
|
} else {
|
||||||
|
return labelString;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
downloadResultJson() {
|
downloadResultJson() {
|
||||||
this.downloadResult("json");
|
this.downloadResult("json");
|
||||||
|
@ -218,6 +275,12 @@ const QueryResultComponent = Ember.Component.extend({
|
||||||
downloadResultCsv() {
|
downloadResultCsv() {
|
||||||
this.downloadResult("csv");
|
this.downloadResult("csv");
|
||||||
},
|
},
|
||||||
|
showChart() {
|
||||||
|
this.set("showChart", true);
|
||||||
|
},
|
||||||
|
showTable() {
|
||||||
|
this.set("showChart", false);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@ const QueryRowContentComponent = Ember.Component.extend({
|
||||||
|
|
||||||
const lookupFunc = parentView[`lookup${t.name.capitalize()}`];
|
const lookupFunc = parentView[`lookup${t.name.capitalize()}`];
|
||||||
if (lookupFunc) {
|
if (lookupFunc) {
|
||||||
ctx[t.name] = parentView[`lookup${t.name.capitalize()}`](id);
|
ctx[t.name] = lookupFunc.call(parentView, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (t.name === "url") {
|
if (t.name === "url") {
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
// The binarySearch() function is licensed under the UNLICENSE
|
|
||||||
// https://github.com/Olical/binary-search
|
|
||||||
|
|
||||||
// Modified for use in Discourse
|
|
||||||
|
|
||||||
export default function binarySearch(list, target, keyProp) {
|
|
||||||
let min = 0;
|
|
||||||
let max = list.length - 1;
|
|
||||||
let guess;
|
|
||||||
const keyProperty = keyProp || "id";
|
|
||||||
|
|
||||||
while (min <= max) {
|
|
||||||
guess = Math.floor((min + max) / 2);
|
|
||||||
|
|
||||||
if (Ember.get(list[guess], keyProperty) === target) {
|
|
||||||
return guess;
|
|
||||||
} else {
|
|
||||||
if (Ember.get(list[guess], keyProperty) < target) {
|
|
||||||
min = guess + 1;
|
|
||||||
} else {
|
|
||||||
max = guess - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default function themeColor(name) {
|
||||||
|
const style = getComputedStyle(document.body);
|
||||||
|
return style.getPropertyValue(name);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
<canvas></canvas>
|
|
@ -3,6 +3,13 @@
|
||||||
<div class="result-info">
|
<div class="result-info">
|
||||||
{{d-button action=(action "downloadResultJson") icon="download" label="explorer.download_json" group=group}}
|
{{d-button action=(action "downloadResultJson") icon="download" label="explorer.download_json" group=group}}
|
||||||
{{d-button action=(action "downloadResultCsv") icon="download" label="explorer.download_csv" group=group}}
|
{{d-button action=(action "downloadResultCsv") icon="download" label="explorer.download_csv" group=group}}
|
||||||
|
{{#if canShowChart}}
|
||||||
|
{{#if showChart}}
|
||||||
|
{{d-button action=(action "showTable") icon="table" label="explorer.show_table" group=group}}
|
||||||
|
{{else}}
|
||||||
|
{{d-button action=(action "showChart") icon="chart-bar" label="explorer.show_graph" group=group}}
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="result-about">
|
<div class="result-about">
|
||||||
|
@ -22,22 +29,29 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<table>
|
{{#if showChart}}
|
||||||
<thead>
|
{{data-explorer-bar-chart
|
||||||
<tr class="headers">
|
labels=chartLabels
|
||||||
{{#each columnDispNames as |col|}}
|
values=chartValues
|
||||||
<th>{{col}}</th>
|
datasetName=chartDatasetName}}
|
||||||
|
{{else}}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="headers">
|
||||||
|
{{#each columnDispNames as |col|}}
|
||||||
|
<th>{{col}}</th>
|
||||||
|
{{/each}}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each rows as |row|}}
|
||||||
|
{{query-row-content
|
||||||
|
row=row
|
||||||
|
fallbackTemplate=fallbackTemplate
|
||||||
|
columnTemplates=columnTemplates}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</tr>
|
</tbody>
|
||||||
</thead>
|
</table>
|
||||||
<tbody>
|
{{/if}}
|
||||||
{{#each rows as |row|}}
|
|
||||||
{{query-row-content
|
|
||||||
row=row
|
|
||||||
fallbackTemplate=fallbackTemplate
|
|
||||||
columnTemplates=columnTemplates}}
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
|
@ -34,6 +34,8 @@ en:
|
||||||
recover: "Undelete Query"
|
recover: "Undelete Query"
|
||||||
download_json: "JSON"
|
download_json: "JSON"
|
||||||
download_csv: "CSV"
|
download_csv: "CSV"
|
||||||
|
show_table: "Table"
|
||||||
|
show_graph: "Graph"
|
||||||
others_dirty: "A query has unsaved changes that will be lost if you navigate away."
|
others_dirty: "A query has unsaved changes that will be lost if you navigate away."
|
||||||
run_time: "Query completed in %{value} ms."
|
run_time: "Query completed in %{value} ms."
|
||||||
result_count:
|
result_count:
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
import {
|
||||||
|
acceptance,
|
||||||
|
exists,
|
||||||
|
query,
|
||||||
|
queryAll,
|
||||||
|
} from "discourse/tests/helpers/qunit-helpers";
|
||||||
|
import { clearPopupMenuOptionsCallback } from "discourse/controllers/composer";
|
||||||
|
import I18n from "I18n";
|
||||||
|
|
||||||
|
acceptance("Data Explorer Plugin | Run Query", function (needs) {
|
||||||
|
needs.user();
|
||||||
|
needs.settings({ data_explorer_enabled: true });
|
||||||
|
needs.hooks.beforeEach(() => {
|
||||||
|
clearPopupMenuOptionsCallback();
|
||||||
|
});
|
||||||
|
|
||||||
|
needs.pretender((server, helper) => {
|
||||||
|
server.get("/admin/plugins/explorer/groups.json", () => {
|
||||||
|
return helper.response([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "admins",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "moderators",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "staff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: "everyone",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
name: "trust_level_0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
name: "trust_level_1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
name: "trust_level_2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
name: "trust_level_3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
name: "trust_level_4",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.get("/admin/plugins/explorer/schema.json", () => {
|
||||||
|
return helper.response({
|
||||||
|
anonymous_users: [
|
||||||
|
{
|
||||||
|
column_name: "id",
|
||||||
|
data_type: "serial",
|
||||||
|
primary: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column_name: "user_id",
|
||||||
|
data_type: "integer",
|
||||||
|
fkey_info: "users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column_name: "master_user_id",
|
||||||
|
data_type: "integer",
|
||||||
|
fkey_info: "users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column_name: "active",
|
||||||
|
data_type: "boolean",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column_name: "created_at",
|
||||||
|
data_type: "timestamp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column_name: "updated_at",
|
||||||
|
data_type: "timestamp",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.get("/admin/plugins/explorer/queries", () => {
|
||||||
|
return helper.response({
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
id: -6,
|
||||||
|
sql:
|
||||||
|
"-- [params]\n-- int :months_ago = 1\n\nWITH query_period AS (\n SELECT\n date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' as period_start,\n date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' + INTERVAL '1 month' - INTERVAL '1 second' as period_end\n )\n\n SELECT\n ua.user_id,\n count(1) AS like_count\n FROM user_actions ua\n INNER JOIN query_period qp\n ON ua.created_at >= qp.period_start\n AND ua.created_at <= qp.period_end\n WHERE ua.action_type = 1\n GROUP BY ua.user_id\n ORDER BY like_count DESC\n LIMIT 100\n",
|
||||||
|
name: "Top 100 Likers",
|
||||||
|
description:
|
||||||
|
"returns the top 100 likers for a given monthly period ordered by like_count. It accepts a ‘months_ago’ parameter, defaults to 1 to give results for the last calendar month.",
|
||||||
|
param_info: [
|
||||||
|
{
|
||||||
|
identifier: "months_ago",
|
||||||
|
type: "int",
|
||||||
|
default: "1",
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
created_at: "2021-02-02T12:21:11.449Z",
|
||||||
|
username: "system",
|
||||||
|
group_ids: [],
|
||||||
|
last_run_at: "2021-02-11T08:29:59.337Z",
|
||||||
|
hidden: false,
|
||||||
|
user_id: -1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.post("/admin/plugins/explorer/queries/-6/run", () => {
|
||||||
|
return helper.response({
|
||||||
|
success: true,
|
||||||
|
errors: [],
|
||||||
|
duration: 27.5,
|
||||||
|
result_count: 2,
|
||||||
|
params: { months_ago: "1" },
|
||||||
|
columns: ["user_id", "like_count"],
|
||||||
|
default_limit: 1000,
|
||||||
|
relations: {
|
||||||
|
user: [
|
||||||
|
{
|
||||||
|
id: -2,
|
||||||
|
username: "discobot",
|
||||||
|
name: null,
|
||||||
|
avatar_template: "/user_avatar/localhost/discobot/{size}/2_2.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
username: "andrey1",
|
||||||
|
name: null,
|
||||||
|
avatar_template:
|
||||||
|
"/letter_avatar_proxy/v4/letter/a/c0e974/{size}.png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
colrender: {
|
||||||
|
0: "user",
|
||||||
|
},
|
||||||
|
rows: [
|
||||||
|
[-2, 2],
|
||||||
|
[2, 2],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it runs query and renders data and a chart", async function (assert) {
|
||||||
|
await visit("admin/plugins/explorer?id=-6");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
query("div.name h1").innerText.trim() === "Top 100 Likers",
|
||||||
|
"the query name was rendered"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(exists("div.query-edit"), "the query code was rendered");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
query("form.query-run button span").innerText.trim() ===
|
||||||
|
I18n.t("explorer.run"),
|
||||||
|
"the run button was rendered"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click("form.query-run button");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
queryAll("div.query-results table tbody tr").length === 2,
|
||||||
|
"the table with query results was rendered"
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
query("div.result-info button:nth-child(3) span").innerText.trim() ===
|
||||||
|
I18n.t("explorer.show_graph"),
|
||||||
|
"the chart button was rendered"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click("div.result-info button:nth-child(3)");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
exists("canvas.chartjs-render-monitor"),
|
||||||
|
"the chart was rendered"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,30 @@
|
||||||
|
import componentTest, {
|
||||||
|
setupRenderingTest,
|
||||||
|
} from "discourse/tests/helpers/component-test";
|
||||||
|
import { discourseModule, exists } from "discourse/tests/helpers/qunit-helpers";
|
||||||
|
import hbs from "htmlbars-inline-precompile";
|
||||||
|
|
||||||
|
discourseModule(
|
||||||
|
"Data Explorer Plugin | Integration | Component | data-explorer-bar-chart",
|
||||||
|
function (hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
componentTest("it renders a chart", {
|
||||||
|
template: hbs`{{data-explorer-bar-chart}}`,
|
||||||
|
|
||||||
|
beforeEach() {
|
||||||
|
this.set("labels", ["label_1", "label_2"]);
|
||||||
|
this.set("values", [115, 1000]);
|
||||||
|
this.set("datasetName", "data");
|
||||||
|
},
|
||||||
|
|
||||||
|
async test(assert) {
|
||||||
|
assert.ok(exists("canvas"), "it renders a canvas");
|
||||||
|
assert.ok(
|
||||||
|
exists("canvas.chartjs-render-monitor"),
|
||||||
|
"it initializes chart.js "
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
|
@ -3,15 +3,72 @@ import componentTest, {
|
||||||
} from "discourse/tests/helpers/component-test";
|
} from "discourse/tests/helpers/component-test";
|
||||||
import {
|
import {
|
||||||
discourseModule,
|
discourseModule,
|
||||||
queryAll,
|
exists,
|
||||||
|
query,
|
||||||
} from "discourse/tests/helpers/qunit-helpers";
|
} from "discourse/tests/helpers/qunit-helpers";
|
||||||
|
import { click } from "@ember/test-helpers";
|
||||||
import hbs from "htmlbars-inline-precompile";
|
import hbs from "htmlbars-inline-precompile";
|
||||||
|
import I18n from "I18n";
|
||||||
|
|
||||||
discourseModule(
|
discourseModule(
|
||||||
"Data Explorer Plugin | Integration | Component | query-result",
|
"Data Explorer Plugin | Integration | Component | query-result",
|
||||||
function (hooks) {
|
function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
componentTest("it renders query results", {
|
||||||
|
template: hbs`{{query-result content=content}}`,
|
||||||
|
|
||||||
|
beforeEach() {
|
||||||
|
const results = {
|
||||||
|
colrender: [],
|
||||||
|
result_count: 2,
|
||||||
|
columns: ["user_name", "like_count"],
|
||||||
|
rows: [
|
||||||
|
["user1", 10],
|
||||||
|
["user2", 20],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
this.set("content", results);
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(
|
||||||
|
query("div.result-info button:nth-child(1) span").innerText ===
|
||||||
|
I18n.t("explorer.download_json"),
|
||||||
|
"it renders the JSON button"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
query("div.result-info button:nth-child(2) span").innerText ===
|
||||||
|
I18n.t("explorer.download_csv"),
|
||||||
|
"it renders the CSV button"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
query("div.result-info button:nth-child(3) span").innerText ===
|
||||||
|
I18n.t("explorer.show_graph"),
|
||||||
|
"it renders the chart button"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(exists("div.result-about"), "it renders a query summary");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
query("table thead tr th:nth-child(1)").innerText === "user_name" &&
|
||||||
|
query("table thead tr th:nth-child(2)").innerText ===
|
||||||
|
"like_count" &&
|
||||||
|
query("table tbody tr:nth-child(1) td:nth-child(1)").innerText ===
|
||||||
|
"user1" &&
|
||||||
|
query("table tbody tr:nth-child(1) td:nth-child(2)").innerText ===
|
||||||
|
"10" &&
|
||||||
|
query("table tbody tr:nth-child(2) td:nth-child(1)").innerText ===
|
||||||
|
"user2" &&
|
||||||
|
query("table tbody tr:nth-child(2) td:nth-child(2)").innerText ===
|
||||||
|
"20",
|
||||||
|
"it renders a table with data"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
componentTest("it renders badge names in query results", {
|
componentTest("it renders badge names in query results", {
|
||||||
template: hbs`{{query-result content=content}}`,
|
template: hbs`{{query-result content=content}}`,
|
||||||
|
|
||||||
|
@ -38,11 +95,165 @@ discourseModule(
|
||||||
|
|
||||||
test(assert) {
|
test(assert) {
|
||||||
assert.ok(
|
assert.ok(
|
||||||
queryAll(
|
query("table tbody tr:nth-child(1) td:nth-child(1) span")
|
||||||
"table tbody tr:nth-child(1) td:nth-child(1) span"
|
.innerText === "badge display name"
|
||||||
).text() === "badge display name"
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
discourseModule(
|
||||||
|
"Data Explorer Plugin | Integration | Component | query-result | chart",
|
||||||
|
function (hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
componentTest("navigation between a table and a chart works", {
|
||||||
|
template: hbs`{{query-result content=content}}`,
|
||||||
|
|
||||||
|
beforeEach() {
|
||||||
|
const results = {
|
||||||
|
colrender: [],
|
||||||
|
result_count: 2,
|
||||||
|
columns: ["user_name", "like_count"],
|
||||||
|
rows: [
|
||||||
|
["user1", 10],
|
||||||
|
["user2", 20],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
this.set("content", results);
|
||||||
|
},
|
||||||
|
|
||||||
|
async test(assert) {
|
||||||
|
assert.equal(
|
||||||
|
query("div.result-info button:nth-child(3) span").innerText,
|
||||||
|
I18n.t("explorer.show_graph"),
|
||||||
|
"the chart button was rendered"
|
||||||
|
);
|
||||||
|
assert.ok(exists("table"), "the table was rendered");
|
||||||
|
|
||||||
|
await click("div.result-info button:nth-child(3)");
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
query("div.result-info button:nth-child(3) span").innerText,
|
||||||
|
I18n.t("explorer.show_table"),
|
||||||
|
"the chart button was changed to the table button"
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
exists("canvas.chartjs-render-monitor"),
|
||||||
|
"the chart was rendered"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click("div.result-info button:nth-child(3)");
|
||||||
|
assert.equal(
|
||||||
|
query("div.result-info button:nth-child(3) span").innerText,
|
||||||
|
I18n.t("explorer.show_graph"),
|
||||||
|
"the table button was changed to the chart button"
|
||||||
|
);
|
||||||
|
assert.ok(exists("table"), "the table was rendered");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
componentTest(
|
||||||
|
"it renders a chart button when data has two columns and numbers in the second column",
|
||||||
|
{
|
||||||
|
template: hbs`{{query-result content=content}}`,
|
||||||
|
|
||||||
|
beforeEach() {
|
||||||
|
const results = {
|
||||||
|
colrender: [],
|
||||||
|
result_count: 2,
|
||||||
|
columns: ["user_name", "like_count"],
|
||||||
|
rows: [
|
||||||
|
["user1", 10],
|
||||||
|
["user2", 20],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
this.set("content", results);
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.equal(
|
||||||
|
query("div.result-info button:nth-child(3) span").innerText,
|
||||||
|
I18n.t("explorer.show_graph")
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
componentTest(
|
||||||
|
"it doesn't render a chart button when data contains identifiers in the second column",
|
||||||
|
{
|
||||||
|
template: hbs`{{query-result content=content}}`,
|
||||||
|
|
||||||
|
beforeEach() {
|
||||||
|
const results = {
|
||||||
|
colrender: { 1: "user" },
|
||||||
|
relations: {
|
||||||
|
user: [
|
||||||
|
{ id: 1, username: "user1" },
|
||||||
|
{ id: 2, username: "user2" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
result_count: 2,
|
||||||
|
columns: ["topic_id", "user_id"],
|
||||||
|
rows: [
|
||||||
|
[1, 10],
|
||||||
|
[2, 20],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
this.set("content", results);
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(!exists("div.result-info button:nth-child(3)"));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
componentTest(
|
||||||
|
"it doesn't render a chart button when data contains one column",
|
||||||
|
{
|
||||||
|
template: hbs`{{query-result content=content}}`,
|
||||||
|
|
||||||
|
beforeEach() {
|
||||||
|
const results = {
|
||||||
|
colrender: [],
|
||||||
|
result_count: 2,
|
||||||
|
columns: ["user_name"],
|
||||||
|
rows: [["user1"], ["user2"]],
|
||||||
|
};
|
||||||
|
this.set("content", results);
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(!exists("div.result-info button:nth-child(3)"));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
componentTest(
|
||||||
|
"it doesn't render a chart button when data contains more than two columns",
|
||||||
|
{
|
||||||
|
template: hbs`{{query-result content=content}}`,
|
||||||
|
|
||||||
|
beforeEach() {
|
||||||
|
const results = {
|
||||||
|
colrender: [],
|
||||||
|
result_count: 2,
|
||||||
|
columns: ["user_name", "like_count", "post_count"],
|
||||||
|
rows: [
|
||||||
|
["user1", 10, 1],
|
||||||
|
["user2", 20, 2],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
this.set("content", results);
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(!exists("div.result-info button:nth-child(3)"));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { moduleFor } from "ember-qunit";
|
||||||
|
import { test } from "qunit";
|
||||||
|
|
||||||
|
moduleFor("component:query-result");
|
||||||
|
|
||||||
|
test("it transforms data for a chart", function (assert) {
|
||||||
|
const results = {
|
||||||
|
colrender: [],
|
||||||
|
result_count: 2,
|
||||||
|
columns: ["user", "like_count"],
|
||||||
|
rows: [
|
||||||
|
["user1", 10],
|
||||||
|
["user2", 20],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
this.subject().setProperties({
|
||||||
|
content: results,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
this.subject().chartLabels,
|
||||||
|
["user1", "user2"],
|
||||||
|
"labels are correct"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(this.subject().chartValues, [10, 20], "values are correct");
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
this.subject().chartDatasetName,
|
||||||
|
"like_count",
|
||||||
|
"the dataset name is correct"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it uses descriptive chart labels instead of identifiers", function (assert) {
|
||||||
|
const results = {
|
||||||
|
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],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
this.subject().setProperties({
|
||||||
|
content: results,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(this.subject().chartLabels, ["user1", "user2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it uses an identifier as a chart label if labelSelector doesn't exist", function (assert) {
|
||||||
|
const results = {
|
||||||
|
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],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
this.subject().setProperties({
|
||||||
|
content: results,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(this.subject().chartLabels, ["1", "2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it cuts too long chart labels", function (assert) {
|
||||||
|
const results = {
|
||||||
|
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],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
this.subject().setProperties({
|
||||||
|
content: results,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(this.subject().chartLabels, [
|
||||||
|
"This string is too long t...",
|
||||||
|
"This string is too long t...",
|
||||||
|
]);
|
||||||
|
});
|
Loading…
Reference in New Issue