DEV: Convert admin charts to glimmer/gjs (#28271)

This commit is contained in:
Jarek Radosz 2024-08-23 14:59:56 +02:00 committed by GitHub
parent fee8caf529
commit 5a8e7c5f29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 367 additions and 631 deletions

View File

@ -1,61 +0,0 @@
import Component from "@ember/component";
import { tagName } from "@ember-decorators/component";
import loadScript from "discourse/lib/load-script";
@tagName("canvas")
export default class AdminGraph extends Component {
type = "line";
refreshChart() {
const ctx = this.element.getContext("2d");
const model = this.model;
const rawData = this.get("model.data");
let data = {
labels: rawData.map((r) => r.x),
datasets: [
{
data: rawData.map((r) => r.y),
label: model.get("title"),
backgroundColor: `rgba(200,220,240,${this.type === "bar" ? 1 : 0.3})`,
borderColor: "#08C",
},
],
};
const config = {
type: this.type,
data,
options: {
responsive: true,
plugins: {
tooltip: {
callbacks: {
title: (context) =>
moment(context[0].label, "YYYY-MM-DD").format("LL"),
},
},
},
scales: {
y: [
{
display: true,
ticks: {
stepSize: 1,
},
},
],
},
},
};
this._chart = new window.Chart(ctx, config);
}
didInsertElement() {
super.didInsertElement(...arguments);
loadScript("/javascripts/Chart.min.js").then(() =>
this.refreshChart.apply(this)
);
}
}

View File

@ -1,63 +1,19 @@
import Component from "@ember/component";
import { schedule } from "@ember/runloop";
import { classNames } from "@ember-decorators/component";
import Component from "@glimmer/component";
import { number } from "discourse/lib/formatter";
import loadScript from "discourse/lib/load-script";
import discourseDebounce from "discourse-common/lib/debounce";
import { makeArray } from "discourse-common/lib/helpers";
import { bind } from "discourse-common/utils/decorators";
import Report from "admin/models/report";
import Chart from "./chart";
@classNames("admin-report-chart")
export default class AdminReportChart extends Component {
limit = 8;
total = 0;
options = null;
get chartConfig() {
const { model, options } = this.args;
didInsertElement() {
super.didInsertElement(...arguments);
window.addEventListener("resize", this._resizeHandler);
}
willDestroyElement() {
super.willDestroyElement(...arguments);
window.removeEventListener("resize", this._resizeHandler);
this._resetChart();
}
didReceiveAttrs() {
super.didReceiveAttrs(...arguments);
discourseDebounce(this, this._scheduleChartRendering, 100);
}
_scheduleChartRendering() {
schedule("afterRender", () => {
this._renderChart(
this.model,
this.element && this.element.querySelector(".chart-canvas")
);
});
}
_renderChart(model, chartCanvas) {
if (!chartCanvas) {
return;
}
const context = chartCanvas.getContext("2d");
const chartData = this._applyChartGrouping(
const chartData = Report.collapse(
model,
makeArray(model.get("chartData") || model.get("data"), "weekly"),
this.options
makeArray(model.chartData || model.data, "weekly"),
options.chartGrouping
);
const prevChartData = makeArray(
model.get("prevChartData") || model.get("prev_data")
);
const prevChartData = makeArray(model.prevChartData || model.prev_data);
const labels = chartData.map((d) => d.x);
const data = {
@ -88,21 +44,6 @@ export default class AdminReportChart extends Component {
});
}
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
if (!this.element) {
return;
}
this._chart = new window.Chart(
context,
this._buildChartConfig(data, this.options)
);
});
}
_buildChartConfig(data, options) {
return {
type: "line",
data,
@ -164,19 +105,7 @@ export default class AdminReportChart extends Component {
};
}
_resetChart() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
}
_applyChartGrouping(model, data, options) {
return Report.collapse(model, data, options.chartGrouping);
}
@bind
_resizeHandler() {
discourseDebounce(this, this._scheduleChartRendering, 500);
}
<template>
<Chart @chartConfig={{this.chartConfig}} class="admin-report-chart" />
</template>
}

View File

@ -1,3 +0,0 @@
<div class="chart-canvas-container">
<canvas class="chart-canvas"></canvas>
</div>

View File

@ -0,0 +1,67 @@
import Component from "@glimmer/component";
import { makeArray } from "discourse-common/lib/helpers";
import hexToRGBA from "admin/lib/hex-to-rgba";
import Report from "admin/models/report";
import Chart from "./chart";
export default class AdminReportRadar extends Component {
get chartConfig() {
const { model } = this.args;
const chartData = makeArray(model.chartData || model.data).map((cd) => ({
label: cd.label,
color: cd.color,
data: Report.collapse(model, cd.data),
}));
const data = {
labels: chartData[0].data.mapBy("x"),
datasets: chartData.map((cd) => ({
label: cd.label,
data: cd.data.mapBy("y"),
fill: true,
backgroundColor: hexToRGBA(cd.color, 0.3),
borderColor: cd.color,
pointBackgroundColor: cd.color,
pointBorderColor: "#fff",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: cd.color,
})),
};
return {
type: "radar",
data,
options: {
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
hover: { mode: "index" },
animation: {
duration: 0,
},
plugins: {
tooltip: {
mode: "index",
intersect: false,
callbacks: {
beforeFooter: (tooltipItem) => {
const total = tooltipItem.reduce(
(sum, item) => sum + parseInt(item.parsed.r || 0, 10)
);
return `= ${total}`;
},
},
},
},
},
};
}
<template>
<Chart
@chartConfig={{this.chartConfig}}
class="admin-report-chart admin-report-radar"
/>
</template>
}

View File

@ -1,3 +0,0 @@
<div class="chart-canvas-container">
<canvas class="chart-canvas"></canvas>
</div>

View File

@ -1,137 +0,0 @@
import Component from "@ember/component";
import { schedule } from "@ember/runloop";
import { classNames } from "@ember-decorators/component";
import loadScript from "discourse/lib/load-script";
import discourseDebounce from "discourse-common/lib/debounce";
import { makeArray } from "discourse-common/lib/helpers";
import { bind } from "discourse-common/utils/decorators";
import Report from "admin/models/report";
@classNames("admin-report-chart", "admin-report-radar")
export default class AdminReportRadar extends Component {
didInsertElement() {
super.didInsertElement(...arguments);
window.addEventListener("resize", this._resizeHandler);
}
willDestroyElement() {
super.willDestroyElement(...arguments);
window.removeEventListener("resize", this._resizeHandler);
this._resetChart();
}
didReceiveAttrs() {
super.didReceiveAttrs(...arguments);
discourseDebounce(this, this._scheduleChartRendering, 100);
}
@bind
_resizeHandler() {
discourseDebounce(this, this._scheduleChartRendering, 500);
}
_scheduleChartRendering() {
schedule("afterRender", () => {
if (!this.element) {
return;
}
this._renderChart(
this.model,
this.element.querySelector(".chart-canvas")
);
});
}
_renderChart(model, chartCanvas) {
if (!chartCanvas) {
return;
}
const context = chartCanvas.getContext("2d");
const chartData = makeArray(model.chartData || model.data).map((cd) => {
return {
label: cd.label,
color: cd.color,
data: Report.collapse(model, cd.data),
};
});
const data = {
labels: chartData[0].data.mapBy("x"),
datasets: chartData.map((cd) => {
return {
label: cd.label,
data: cd.data.mapBy("y"),
fill: true,
backgroundColor: this._hexToRGBA(cd.color, 0.3),
borderColor: cd.color,
pointBackgroundColor: cd.color,
pointBorderColor: "#fff",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: cd.color,
};
}),
};
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
this._chart = new window.Chart(context, this._buildChartConfig(data));
});
}
_buildChartConfig(data) {
return {
type: "radar",
data,
options: {
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
hover: { mode: "index" },
animation: {
duration: 0,
},
plugins: {
tooltip: {
mode: "index",
intersect: false,
callbacks: {
beforeFooter: (tooltipItem) => {
let total = 0;
tooltipItem.forEach(
(item) => (total += parseInt(item.parsed.r || 0, 10))
);
return `= ${total}`;
},
},
},
},
},
};
}
_resetChart() {
this._chart?.destroy();
this._chart = null;
}
_hexToRGBA(hexCode, opacity) {
let hex = hexCode.replace("#", "");
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
const r = parseInt(hex.substring(0, 2), 16),
g = parseInt(hex.substring(2, 4), 16),
b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r},${g},${b}, ${opacity})`;
}
}

View File

@ -0,0 +1,102 @@
import Component from "@glimmer/component";
import { number } from "discourse/lib/formatter";
import { makeArray } from "discourse-common/lib/helpers";
import Report from "admin/models/report";
import Chart from "./chart";
export default class AdminReportStackedChart extends Component {
get chartConfig() {
const { model } = this.args;
const chartData = makeArray(model.chartData || model.data).map((cd) => ({
label: cd.label,
color: cd.color,
data: Report.collapse(model, cd.data),
}));
const data = {
labels: chartData[0].data.mapBy("x"),
datasets: chartData.map((cd) => ({
label: cd.label,
stack: "pageviews-stack",
data: cd.data,
backgroundColor: cd.color,
})),
};
return {
type: "bar",
data,
options: {
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
hover: { mode: "index" },
animation: {
duration: 0,
},
plugins: {
tooltip: {
mode: "index",
intersect: false,
callbacks: {
beforeFooter: (tooltipItem) => {
const total = tooltipItem.reduce(
(sum, item) => sum + parseInt(item.parsed.y || 0, 10)
);
return `= ${total}`;
},
title: (tooltipItem) =>
moment(tooltipItem[0].label, "YYYY-MM-DD").format("LL"),
},
},
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
},
scales: {
y: [
{
stacked: true,
display: true,
ticks: {
callback: (label) => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25,
},
},
],
x: [
{
display: true,
gridLines: { display: false },
type: "time",
time: {
unit: Report.unitForDatapoints(data.labels.length),
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50,
},
},
],
},
},
};
}
<template>
<Chart
@chartConfig={{this.chartConfig}}
class="admin-report-chart admin-report-stacked-chart"
/>
</template>
}

View File

@ -1,3 +0,0 @@
<div class="chart-canvas-container">
<canvas class="chart-canvas"></canvas>
</div>

View File

@ -1,159 +0,0 @@
import Component from "@ember/component";
import { schedule } from "@ember/runloop";
import { classNames } from "@ember-decorators/component";
import { number } from "discourse/lib/formatter";
import loadScript from "discourse/lib/load-script";
import discourseDebounce from "discourse-common/lib/debounce";
import { makeArray } from "discourse-common/lib/helpers";
import { bind } from "discourse-common/utils/decorators";
import Report from "admin/models/report";
@classNames("admin-report-chart", "admin-report-stacked-chart")
export default class AdminReportStackedChart extends Component {
didInsertElement() {
super.didInsertElement(...arguments);
window.addEventListener("resize", this._resizeHandler);
}
willDestroyElement() {
super.willDestroyElement(...arguments);
window.removeEventListener("resize", this._resizeHandler);
this._resetChart();
}
didReceiveAttrs() {
super.didReceiveAttrs(...arguments);
discourseDebounce(this, this._scheduleChartRendering, 100);
}
@bind
_resizeHandler() {
discourseDebounce(this, this._scheduleChartRendering, 500);
}
_scheduleChartRendering() {
schedule("afterRender", () => {
if (!this.element) {
return;
}
this._renderChart(
this.model,
this.element.querySelector(".chart-canvas")
);
});
}
_renderChart(model, chartCanvas) {
if (!chartCanvas) {
return;
}
const context = chartCanvas.getContext("2d");
const chartData = makeArray(model.chartData || model.data).map((cd) => {
return {
label: cd.label,
color: cd.color,
data: Report.collapse(model, cd.data),
};
});
const data = {
labels: chartData[0].data.mapBy("x"),
datasets: chartData.map((cd) => {
return {
label: cd.label,
stack: "pageviews-stack",
data: cd.data,
backgroundColor: cd.color,
};
}),
};
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
this._chart = new window.Chart(context, this._buildChartConfig(data));
});
}
_buildChartConfig(data) {
return {
type: "bar",
data,
options: {
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
hover: { mode: "index" },
animation: {
duration: 0,
},
plugins: {
tooltip: {
mode: "index",
intersect: false,
callbacks: {
beforeFooter: (tooltipItem) => {
let total = 0;
tooltipItem.forEach(
(item) => (total += parseInt(item.parsed.y || 0, 10))
);
return `= ${total}`;
},
title: (tooltipItem) =>
moment(tooltipItem[0].label, "YYYY-MM-DD").format("LL"),
},
},
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
},
scales: {
y: [
{
stacked: true,
display: true,
ticks: {
callback: (label) => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25,
},
},
],
x: [
{
display: true,
gridLines: { display: false },
type: "time",
time: {
unit: Report.unitForDatapoints(data.labels.length),
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50,
},
},
],
},
},
};
}
_resetChart() {
this._chart?.destroy();
this._chart = null;
}
}

View File

@ -0,0 +1,108 @@
import Component from "@glimmer/component";
import { number } from "discourse/lib/formatter";
import { makeArray } from "discourse-common/lib/helpers";
import hexToRGBA from "admin/lib/hex-to-rgba";
import Report from "admin/models/report";
import Chart from "./chart";
export default class AdminReportStackedLineChart extends Component {
get chartConfig() {
const { model } = this.args;
const chartData = makeArray(model.chartData || model.data).map((cd) => ({
label: cd.label,
color: cd.color,
data: Report.collapse(model, cd.data),
}));
return {
type: "line",
data: {
labels: chartData[0].data.mapBy("x"),
datasets: chartData.map((cd) => ({
label: cd.label,
stack: "pageviews-stack",
data: cd.data,
fill: true,
backgroundColor: hexToRGBA(cd.color, 0.3),
borderColor: cd.color,
pointBackgroundColor: cd.color,
pointBorderColor: "#fff",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: cd.color,
})),
},
options: {
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
hover: { mode: "index" },
animation: {
duration: 0,
},
plugins: {
tooltip: {
mode: "index",
intersect: false,
callbacks: {
beforeFooter: (tooltipItem) => {
const total = tooltipItem.reduce(
(sum, item) => sum + parseInt(item.parsed.y || 0, 10),
0
);
return `= ${total}`;
},
title: (tooltipItem) =>
moment(tooltipItem[0].label, "YYYY-MM-DD").format("LL"),
},
},
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
},
scales: {
y: [
{
stacked: true,
display: true,
ticks: {
callback: (label) => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25,
},
},
],
x: [
{
display: true,
gridLines: { display: false },
type: "time",
time: {
unit: Report.unitForDatapoints(chartData[0].data.length),
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50,
},
},
],
},
},
};
}
<template>
<Chart
@chartConfig={{this.chartConfig}}
class="admin-report-chart admin-report-stacked-line-chart"
/>
</template>
}

View File

@ -1,3 +0,0 @@
<div class="chart-canvas-container">
<canvas class="chart-canvas"></canvas>
</div>

View File

@ -1,179 +0,0 @@
import Component from "@ember/component";
import { schedule } from "@ember/runloop";
import { classNames } from "@ember-decorators/component";
import { number } from "discourse/lib/formatter";
import loadScript from "discourse/lib/load-script";
import discourseDebounce from "discourse-common/lib/debounce";
import { makeArray } from "discourse-common/lib/helpers";
import { bind } from "discourse-common/utils/decorators";
import Report from "admin/models/report";
@classNames("admin-report-chart", "admin-report-stacked-line-chart")
export default class AdminReportStackedLineChart extends Component {
didInsertElement() {
super.didInsertElement(...arguments);
window.addEventListener("resize", this._resizeHandler);
}
willDestroyElement() {
super.willDestroyElement(...arguments);
window.removeEventListener("resize", this._resizeHandler);
this._resetChart();
}
didReceiveAttrs() {
super.didReceiveAttrs(...arguments);
discourseDebounce(this, this._scheduleChartRendering, 100);
}
@bind
_resizeHandler() {
discourseDebounce(this, this._scheduleChartRendering, 500);
}
_scheduleChartRendering() {
schedule("afterRender", () => {
if (!this.element) {
return;
}
this._renderChart(
this.model,
this.element.querySelector(".chart-canvas")
);
});
}
_renderChart(model, chartCanvas) {
if (!chartCanvas) {
return;
}
const context = chartCanvas.getContext("2d");
const chartData = makeArray(model.chartData || model.data).map((cd) => {
return {
label: cd.label,
color: cd.color,
data: Report.collapse(model, cd.data),
};
});
const data = {
labels: chartData[0].data.mapBy("x"),
datasets: chartData.map((cd) => {
return {
label: cd.label,
stack: "pageviews-stack",
data: cd.data,
fill: true,
backgroundColor: this._hexToRGBA(cd.color, 0.3),
borderColor: cd.color,
pointBackgroundColor: cd.color,
pointBorderColor: "#fff",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: cd.color,
};
}),
};
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
this._chart = new window.Chart(context, this._buildChartConfig(data));
});
}
_buildChartConfig(data) {
return {
type: "line",
data,
options: {
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
hover: { mode: "index" },
animation: {
duration: 0,
},
plugins: {
tooltip: {
mode: "index",
intersect: false,
callbacks: {
beforeFooter: (tooltipItem) => {
let total = 0;
tooltipItem.forEach(
(item) => (total += parseInt(item.parsed.y || 0, 10))
);
return `= ${total}`;
},
title: (tooltipItem) =>
moment(tooltipItem[0].label, "YYYY-MM-DD").format("LL"),
},
},
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
},
scales: {
y: [
{
stacked: true,
display: true,
ticks: {
callback: (label) => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25,
},
},
],
x: [
{
display: true,
gridLines: { display: false },
type: "time",
time: {
unit: Report.unitForDatapoints(data.labels.length),
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50,
},
},
],
},
},
};
}
_resetChart() {
this._chart?.destroy();
this._chart = null;
}
_hexToRGBA(hexCode, opacity) {
let hex = hexCode.replace("#", "");
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
const r = parseInt(hex.substring(0, 2), 16),
g = parseInt(hex.substring(2, 4), 16),
b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r},${g},${b}, ${opacity})`;
}
}

View File

@ -0,0 +1,26 @@
import Component from "@glimmer/component";
import { modifier } from "ember-modifier";
import loadScript from "discourse/lib/load-script";
// args:
// chartConfig - object
export default class Chart extends Component {
renderChart = modifier((element) => {
loadScript("/javascripts/Chart.min.js").then(() => {
this.chart = new window.Chart(
element.getContext("2d"),
this.args.chartConfig
);
});
return () => this.chart?.destroy();
});
<template>
<div ...attributes>
<div class="chart-canvas-container">
<canvas {{this.renderChart}} class="chart-canvas"></canvas>
</div>
</div>
</template>
}

View File

@ -22,4 +22,42 @@ export default class AdminSearchLogsTermController extends Controller {
name: I18n.t("admin.logs.search_logs.types.click_through_only"),
},
];
get chartConfig() {
return {
type: "bar",
data: {
labels: this.model.data.map((r) => r.x),
datasets: [
{
data: this.model.data.map((r) => r.y),
label: this.model.title,
backgroundColor: "rgba(200,220,240,1)",
borderColor: "#08C",
},
],
},
options: {
responsive: true,
plugins: {
tooltip: {
callbacks: {
title: (context) =>
moment(context[0].label, "YYYY-MM-DD").format("LL"),
},
},
},
scales: {
y: [
{
display: true,
ticks: {
stepSize: 1,
},
},
],
},
},
};
}
}

View File

@ -0,0 +1,13 @@
export default function hexToRGBA(hexCode, opacity) {
let hex = hexCode.replace("#", "");
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r},${g},${b}, ${opacity})`;
}

View File

@ -16,7 +16,7 @@
</h2>
<ConditionalLoadingSpinner @condition={{this.refreshing}}>
<AdminGraph @model={{this.model}} @type="bar" />
<Chart @chartConfig={{this.chartConfig}} />
<br /><br />
<h2> {{i18n "admin.logs.search_logs.header_search_results"}} </h2>

View File

@ -78,6 +78,7 @@
.body {
display: flex;
flex-direction: column;
}
.main {