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"),
|
||||
explainText: Ember.computed.alias("content.explain"),
|
||||
hasExplain: Ember.computed.notEmpty("content.explain"),
|
||||
chartDatasetName: Ember.computed.reads("columnDispNames.1"),
|
||||
chartValues: Ember.computed.mapBy("content.rows", "1"),
|
||||
showChart: false,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
@ -139,6 +142,51 @@ const QueryResultComponent = Ember.Component.extend({
|
|||
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) {
|
||||
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: {
|
||||
downloadResultJson() {
|
||||
this.downloadResult("json");
|
||||
|
@ -218,6 +275,12 @@ const QueryResultComponent = Ember.Component.extend({
|
|||
downloadResultCsv() {
|
||||
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()}`];
|
||||
if (lookupFunc) {
|
||||
ctx[t.name] = parentView[`lookup${t.name.capitalize()}`](id);
|
||||
ctx[t.name] = lookupFunc.call(parentView, id);
|
||||
}
|
||||
|
||||
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">
|
||||
{{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}}
|
||||
{{#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 class="result-about">
|
||||
|
@ -22,22 +29,29 @@
|
|||
</header>
|
||||
|
||||
<section>
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="headers">
|
||||
{{#each columnDispNames as |col|}}
|
||||
<th>{{col}}</th>
|
||||
{{#if showChart}}
|
||||
{{data-explorer-bar-chart
|
||||
labels=chartLabels
|
||||
values=chartValues
|
||||
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}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each rows as |row|}}
|
||||
{{query-row-content
|
||||
row=row
|
||||
fallbackTemplate=fallbackTemplate
|
||||
columnTemplates=columnTemplates}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
</section>
|
||||
</article>
|
|
@ -34,6 +34,8 @@ en:
|
|||
recover: "Undelete Query"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "Table"
|
||||
show_graph: "Graph"
|
||||
others_dirty: "A query has unsaved changes that will be lost if you navigate away."
|
||||
run_time: "Query completed in %{value} ms."
|
||||
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";
|
||||
import {
|
||||
discourseModule,
|
||||
queryAll,
|
||||
exists,
|
||||
query,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { click } from "@ember/test-helpers";
|
||||
import hbs from "htmlbars-inline-precompile";
|
||||
import I18n from "I18n";
|
||||
|
||||
discourseModule(
|
||||
"Data Explorer Plugin | Integration | Component | query-result",
|
||||
function (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", {
|
||||
template: hbs`{{query-result content=content}}`,
|
||||
|
||||
|
@ -38,11 +95,165 @@ discourseModule(
|
|||
|
||||
test(assert) {
|
||||
assert.ok(
|
||||
queryAll(
|
||||
"table tbody tr:nth-child(1) td:nth-child(1) span"
|
||||
).text() === "badge display name"
|
||||
query("table tbody tr:nth-child(1) td:nth-child(1) span")
|
||||
.innerText === "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