2023-10-11 02:38:59 +08:00
|
|
|
|
import EmberObject from "@ember/object";
|
|
|
|
|
import { isEmpty } from "@ember/utils";
|
|
|
|
|
import { renderAvatar } from "discourse/helpers/user-avatar";
|
|
|
|
|
import { ajax } from "discourse/lib/ajax";
|
2019-11-02 01:06:50 +08:00
|
|
|
|
import { durationTiny, number } from "discourse/lib/formatter";
|
2023-10-11 02:38:59 +08:00
|
|
|
|
import round from "discourse/lib/round";
|
2019-09-12 21:17:34 +08:00
|
|
|
|
import {
|
2018-08-17 22:19:25 +08:00
|
|
|
|
escapeExpression,
|
2019-09-12 21:17:34 +08:00
|
|
|
|
fillMissingDates,
|
|
|
|
|
formatUsername,
|
|
|
|
|
toNumber,
|
|
|
|
|
} from "discourse/lib/utilities";
|
2020-06-04 00:45:26 +08:00
|
|
|
|
import getURL from "discourse-common/lib/get-url";
|
2018-07-20 02:33:11 +08:00
|
|
|
|
import { makeArray } from "discourse-common/lib/helpers";
|
2023-10-11 02:38:59 +08:00
|
|
|
|
import discourseComputed from "discourse-common/utils/decorators";
|
2023-10-18 18:07:09 +08:00
|
|
|
|
import I18n from "discourse-i18n";
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
|
|
|
|
// Change this line each time report format change
|
|
|
|
|
// and you want to ensure cache is reset
|
2019-04-26 18:17:10 +08:00
|
|
|
|
export const SCHEMA_VERSION = 4;
|
2015-06-23 01:46:51 +08:00
|
|
|
|
|
2023-03-17 18:18:42 +08:00
|
|
|
|
export default class Report extends EmberObject {
|
|
|
|
|
static groupingForDatapoints(count) {
|
|
|
|
|
if (count < DAILY_LIMIT_DAYS) {
|
|
|
|
|
return "daily";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (count >= DAILY_LIMIT_DAYS && count < WEEKLY_LIMIT_DAYS) {
|
|
|
|
|
return "weekly";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (count >= WEEKLY_LIMIT_DAYS) {
|
|
|
|
|
return "monthly";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static unitForDatapoints(count) {
|
|
|
|
|
if (count >= DAILY_LIMIT_DAYS && count < WEEKLY_LIMIT_DAYS) {
|
|
|
|
|
return "week";
|
|
|
|
|
} else if (count >= WEEKLY_LIMIT_DAYS) {
|
|
|
|
|
return "month";
|
|
|
|
|
} else {
|
|
|
|
|
return "day";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static unitForGrouping(grouping) {
|
|
|
|
|
switch (grouping) {
|
|
|
|
|
case "monthly":
|
|
|
|
|
return "month";
|
|
|
|
|
case "weekly":
|
|
|
|
|
return "week";
|
|
|
|
|
default:
|
|
|
|
|
return "day";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static collapse(model, data, grouping) {
|
|
|
|
|
grouping = grouping || Report.groupingForDatapoints(data.length);
|
|
|
|
|
|
|
|
|
|
if (grouping === "daily") {
|
|
|
|
|
return data;
|
|
|
|
|
} else if (grouping === "weekly" || grouping === "monthly") {
|
|
|
|
|
const isoKind = grouping === "weekly" ? "isoWeek" : "month";
|
|
|
|
|
const kind = grouping === "weekly" ? "week" : "month";
|
|
|
|
|
const startMoment = moment(model.start_date, "YYYY-MM-DD");
|
|
|
|
|
|
|
|
|
|
let currentIndex = 0;
|
|
|
|
|
let currentStart = startMoment.clone().startOf(isoKind);
|
|
|
|
|
let currentEnd = startMoment.clone().endOf(isoKind);
|
|
|
|
|
const transformedData = [
|
|
|
|
|
{
|
|
|
|
|
x: currentStart.format("YYYY-MM-DD"),
|
|
|
|
|
y: 0,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let appliedAverage = false;
|
|
|
|
|
data.forEach((d) => {
|
|
|
|
|
const date = moment(d.x, "YYYY-MM-DD");
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!date.isSame(currentStart) &&
|
|
|
|
|
!date.isBetween(currentStart, currentEnd)
|
|
|
|
|
) {
|
|
|
|
|
if (model.average) {
|
|
|
|
|
transformedData[currentIndex].y = applyAverage(
|
|
|
|
|
transformedData[currentIndex].y,
|
|
|
|
|
currentStart,
|
|
|
|
|
currentEnd
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
appliedAverage = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentIndex += 1;
|
|
|
|
|
currentStart = currentStart.add(1, kind).startOf(isoKind);
|
|
|
|
|
currentEnd = currentEnd.add(1, kind).endOf(isoKind);
|
|
|
|
|
} else {
|
|
|
|
|
appliedAverage = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (transformedData[currentIndex]) {
|
|
|
|
|
transformedData[currentIndex].y += d.y;
|
|
|
|
|
} else {
|
|
|
|
|
transformedData[currentIndex] = {
|
|
|
|
|
x: d.x,
|
|
|
|
|
y: d.y,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (model.average && !appliedAverage) {
|
|
|
|
|
transformedData[currentIndex].y = applyAverage(
|
|
|
|
|
transformedData[currentIndex].y,
|
|
|
|
|
currentStart,
|
|
|
|
|
moment(model.end_date).subtract(1, "day") // remove 1 day as model end date is at 00:00 of next day
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return transformedData;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ensure we return something if grouping is unknown
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static fillMissingDates(report, options = {}) {
|
|
|
|
|
const dataField = options.dataField || "data";
|
|
|
|
|
const filledField = options.filledField || "data";
|
|
|
|
|
const startDate = options.startDate || "start_date";
|
|
|
|
|
const endDate = options.endDate || "end_date";
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(report[dataField])) {
|
|
|
|
|
const startDateFormatted = moment
|
|
|
|
|
.utc(report[startDate])
|
|
|
|
|
.locale("en")
|
|
|
|
|
.format("YYYY-MM-DD");
|
|
|
|
|
const endDateFormatted = moment
|
|
|
|
|
.utc(report[endDate])
|
|
|
|
|
.locale("en")
|
|
|
|
|
.format("YYYY-MM-DD");
|
|
|
|
|
|
2024-08-03 00:40:27 +08:00
|
|
|
|
if (
|
|
|
|
|
report.modes[0] === "stacked_chart" ||
|
|
|
|
|
report.modes[0] === "stacked_line_chart"
|
|
|
|
|
) {
|
2023-03-17 18:18:42 +08:00
|
|
|
|
report[filledField] = report[dataField].map((rep) => {
|
|
|
|
|
return {
|
|
|
|
|
req: rep.req,
|
|
|
|
|
label: rep.label,
|
|
|
|
|
color: rep.color,
|
|
|
|
|
data: fillMissingDates(
|
|
|
|
|
JSON.parse(JSON.stringify(rep.data)),
|
|
|
|
|
startDateFormatted,
|
|
|
|
|
endDateFormatted
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
});
|
2023-11-08 21:48:12 +08:00
|
|
|
|
} else if (report.modes[0] !== "radar") {
|
2023-03-17 18:18:42 +08:00
|
|
|
|
report[filledField] = fillMissingDates(
|
|
|
|
|
JSON.parse(JSON.stringify(report[dataField])),
|
|
|
|
|
startDateFormatted,
|
|
|
|
|
endDateFormatted
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static find(type, startDate, endDate, categoryId, groupId) {
|
|
|
|
|
return ajax("/admin/reports/" + type, {
|
|
|
|
|
data: {
|
|
|
|
|
start_date: startDate,
|
|
|
|
|
end_date: endDate,
|
|
|
|
|
category_id: categoryId,
|
|
|
|
|
group_id: groupId,
|
|
|
|
|
},
|
|
|
|
|
}).then((json) => {
|
|
|
|
|
// don’t fill for large multi column tables
|
|
|
|
|
// which are not date based
|
|
|
|
|
const modes = json.report.modes;
|
|
|
|
|
if (modes.length !== 1 && modes[0] !== "table") {
|
|
|
|
|
Report.fillMissingDates(json.report);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const model = Report.create({ type });
|
|
|
|
|
model.setProperties(json.report);
|
|
|
|
|
|
|
|
|
|
if (json.report.related_report) {
|
|
|
|
|
// TODO: fillMissingDates if xaxis is date
|
|
|
|
|
const related = Report.create({
|
|
|
|
|
type: json.report.related_report.type,
|
|
|
|
|
});
|
|
|
|
|
related.setProperties(json.report.related_report);
|
|
|
|
|
model.set("relatedReport", related);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return model;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
average = false;
|
|
|
|
|
percent = false;
|
|
|
|
|
higher_is_better = true;
|
|
|
|
|
description_link = null;
|
|
|
|
|
description = null;
|
2018-05-03 21:41:41 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("type", "start_date", "end_date")
|
2018-05-14 09:33:36 +08:00
|
|
|
|
reportUrl(type, start_date, end_date) {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
start_date = moment.utc(start_date).locale("en").format("YYYY-MM-DD");
|
2018-07-20 02:33:11 +08:00
|
|
|
|
|
2018-05-18 04:44:33 +08:00
|
|
|
|
end_date = moment.utc(end_date).locale("en").format("YYYY-MM-DD");
|
2018-07-20 02:33:11 +08:00
|
|
|
|
|
2020-06-04 00:45:26 +08:00
|
|
|
|
return getURL(
|
2018-05-14 09:33:36 +08:00
|
|
|
|
`/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}`
|
|
|
|
|
);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2013-03-31 02:07:25 +08:00
|
|
|
|
|
2015-06-23 01:46:51 +08:00
|
|
|
|
valueAt(numDaysAgo) {
|
2013-03-31 02:07:25 +08:00
|
|
|
|
if (this.data) {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
const wantedDate = moment()
|
|
|
|
|
.subtract(numDaysAgo, "days")
|
|
|
|
|
.locale("en")
|
|
|
|
|
.format("YYYY-MM-DD");
|
2015-08-05 00:23:56 +08:00
|
|
|
|
const item = this.data.find((d) => d.x === wantedDate);
|
2013-03-31 02:07:25 +08:00
|
|
|
|
if (item) {
|
|
|
|
|
return item.y;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2013-03-31 02:07:25 +08:00
|
|
|
|
|
2015-08-05 00:23:56 +08:00
|
|
|
|
valueFor(startDaysAgo, endDaysAgo) {
|
2013-03-31 02:07:25 +08:00
|
|
|
|
if (this.data) {
|
2015-08-05 00:23:56 +08:00
|
|
|
|
const earliestDate = moment().subtract(endDaysAgo, "days").startOf("day");
|
|
|
|
|
const latestDate = moment().subtract(startDaysAgo, "days").startOf("day");
|
2018-05-18 04:44:33 +08:00
|
|
|
|
let d,
|
|
|
|
|
sum = 0,
|
|
|
|
|
count = 0;
|
2018-11-20 21:46:58 +08:00
|
|
|
|
this.data.forEach((datum) => {
|
2013-06-11 04:48:50 +08:00
|
|
|
|
d = moment(datum.x);
|
2015-08-05 00:23:56 +08:00
|
|
|
|
if (d >= earliestDate && d <= latestDate) {
|
2013-03-31 02:07:25 +08:00
|
|
|
|
sum += datum.y;
|
2015-08-05 00:23:56 +08:00
|
|
|
|
count++;
|
2013-03-31 02:07:25 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
2019-05-27 16:15:39 +08:00
|
|
|
|
if (this.method === "average" && count > 0) {
|
2015-08-10 16:06:33 +08:00
|
|
|
|
sum /= count;
|
|
|
|
|
}
|
2015-06-23 01:46:51 +08:00
|
|
|
|
return round(sum, -2);
|
2013-03-31 02:07:25 +08:00
|
|
|
|
}
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2013-03-31 02:07:25 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("data", "average")
|
2019-04-26 18:16:21 +08:00
|
|
|
|
todayCount() {
|
2018-05-03 21:41:41 +08:00
|
|
|
|
return this.valueAt(0);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2019-04-26 18:16:21 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("data", "average")
|
2019-04-26 18:16:21 +08:00
|
|
|
|
yesterdayCount() {
|
2018-05-03 21:41:41 +08:00
|
|
|
|
return this.valueAt(1);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2019-04-26 18:16:21 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("data", "average")
|
2019-04-26 18:16:21 +08:00
|
|
|
|
sevenDaysAgoCount() {
|
2018-05-03 21:41:41 +08:00
|
|
|
|
return this.valueAt(7);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2019-04-26 18:16:21 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("data", "average")
|
2019-04-26 18:16:21 +08:00
|
|
|
|
thirtyDaysAgoCount() {
|
2018-05-03 21:41:41 +08:00
|
|
|
|
return this.valueAt(30);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-05-18 04:44:33 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("data", "average")
|
2019-04-26 18:16:21 +08:00
|
|
|
|
lastSevenDaysCount() {
|
2018-05-03 21:41:41 +08:00
|
|
|
|
return this.averageCount(7, this.valueFor(1, 7));
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2019-04-26 18:16:21 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("data", "average")
|
2019-04-26 18:16:21 +08:00
|
|
|
|
lastThirtyDaysCount() {
|
2018-05-03 21:41:41 +08:00
|
|
|
|
return this.averageCount(30, this.valueFor(1, 30));
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-05-03 21:41:41 +08:00
|
|
|
|
|
|
|
|
|
averageCount(count, value) {
|
2019-05-27 16:15:39 +08:00
|
|
|
|
return this.average ? value / count : value;
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2013-04-03 02:40:00 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("yesterdayCount", "higher_is_better")
|
2018-05-28 16:55:42 +08:00
|
|
|
|
yesterdayTrend(yesterdayCount, higherIsBetter) {
|
|
|
|
|
return this._computeTrend(this.valueAt(2), yesterdayCount, higherIsBetter);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2013-03-31 02:07:25 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("lastSevenDaysCount", "higher_is_better")
|
2018-05-28 16:55:42 +08:00
|
|
|
|
sevenDaysTrend(lastSevenDaysCount, higherIsBetter) {
|
|
|
|
|
return this._computeTrend(
|
|
|
|
|
this.valueFor(8, 14),
|
|
|
|
|
lastSevenDaysCount,
|
|
|
|
|
higherIsBetter
|
|
|
|
|
);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2013-03-31 02:07:25 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("data")
|
2018-05-11 11:30:21 +08:00
|
|
|
|
currentTotal(data) {
|
2018-11-19 20:04:57 +08:00
|
|
|
|
return data.reduce((cur, pair) => cur + pair.y, 0);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-05-11 11:30:21 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("data", "currentTotal")
|
2018-05-11 11:30:21 +08:00
|
|
|
|
currentAverage(data, total) {
|
2019-11-01 01:55:01 +08:00
|
|
|
|
return makeArray(data).length === 0
|
2018-05-16 02:12:03 +08:00
|
|
|
|
? 0
|
|
|
|
|
: parseFloat((total / parseFloat(data.length)).toFixed(1));
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-05-16 02:12:03 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("trend", "higher_is_better")
|
2018-05-25 18:09:30 +08:00
|
|
|
|
trendIcon(trend, higherIsBetter) {
|
|
|
|
|
return this._iconForTrend(trend, higherIsBetter);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-05-25 18:09:30 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("sevenDaysTrend", "higher_is_better")
|
2018-05-25 18:09:30 +08:00
|
|
|
|
sevenDaysTrendIcon(sevenDaysTrend, higherIsBetter) {
|
|
|
|
|
return this._iconForTrend(sevenDaysTrend, higherIsBetter);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-05-25 18:09:30 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("thirtyDaysTrend", "higher_is_better")
|
2018-05-25 18:09:30 +08:00
|
|
|
|
thirtyDaysTrendIcon(thirtyDaysTrend, higherIsBetter) {
|
|
|
|
|
return this._iconForTrend(thirtyDaysTrend, higherIsBetter);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-05-25 18:09:30 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("yesterdayTrend", "higher_is_better")
|
2018-05-25 18:09:30 +08:00
|
|
|
|
yesterdayTrendIcon(yesterdayTrend, higherIsBetter) {
|
|
|
|
|
return this._iconForTrend(yesterdayTrend, higherIsBetter);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-05-11 11:30:21 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed(
|
|
|
|
|
"prev_period",
|
|
|
|
|
"currentTotal",
|
|
|
|
|
"currentAverage",
|
|
|
|
|
"higher_is_better"
|
|
|
|
|
)
|
2018-05-28 16:55:42 +08:00
|
|
|
|
trend(prev, currentTotal, currentAverage, higherIsBetter) {
|
2019-05-27 16:15:39 +08:00
|
|
|
|
const total = this.average ? currentAverage : currentTotal;
|
2018-05-28 16:55:42 +08:00
|
|
|
|
return this._computeTrend(prev, total, higherIsBetter);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-05-11 11:30:21 +08:00
|
|
|
|
|
2022-02-10 00:39:33 +08:00
|
|
|
|
@discourseComputed(
|
|
|
|
|
"prev30Days",
|
|
|
|
|
"prev_period",
|
|
|
|
|
"lastThirtyDaysCount",
|
|
|
|
|
"higher_is_better"
|
|
|
|
|
)
|
|
|
|
|
thirtyDaysTrend(
|
|
|
|
|
prev30Days,
|
|
|
|
|
prev_period,
|
|
|
|
|
lastThirtyDaysCount,
|
|
|
|
|
higherIsBetter
|
|
|
|
|
) {
|
|
|
|
|
return this._computeTrend(
|
|
|
|
|
prev30Days ?? prev_period,
|
|
|
|
|
lastThirtyDaysCount,
|
|
|
|
|
higherIsBetter
|
|
|
|
|
);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2013-04-17 06:37:35 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("type")
|
2018-03-16 05:10:45 +08:00
|
|
|
|
method(type) {
|
|
|
|
|
if (type === "time_to_first_response") {
|
2015-08-05 00:23:56 +08:00
|
|
|
|
return "average";
|
|
|
|
|
} else {
|
|
|
|
|
return "sum";
|
|
|
|
|
}
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2013-04-27 05:13:20 +08:00
|
|
|
|
|
2015-06-23 01:46:51 +08:00
|
|
|
|
percentChangeString(val1, val2) {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
const change = this._computeChange(val1, val2);
|
|
|
|
|
|
|
|
|
|
if (isNaN(change) || !isFinite(change)) {
|
2013-04-27 05:13:20 +08:00
|
|
|
|
return null;
|
2018-05-18 04:44:33 +08:00
|
|
|
|
} else if (change > 0) {
|
|
|
|
|
return "+" + change.toFixed(0) + "%";
|
2013-04-27 05:13:20 +08:00
|
|
|
|
} else {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
return change.toFixed(0) + "%";
|
2013-04-27 05:13:20 +08:00
|
|
|
|
}
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2013-04-27 05:13:20 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("prev_period", "currentTotal", "currentAverage")
|
2018-05-11 11:30:21 +08:00
|
|
|
|
trendTitle(prev, currentTotal, currentAverage) {
|
2019-05-27 16:15:39 +08:00
|
|
|
|
let current = this.average ? currentAverage : currentTotal;
|
2018-05-18 04:44:33 +08:00
|
|
|
|
let percent = this.percentChangeString(prev, current);
|
2018-05-11 11:30:21 +08:00
|
|
|
|
|
2019-05-27 16:15:39 +08:00
|
|
|
|
if (this.average) {
|
2018-05-14 09:12:52 +08:00
|
|
|
|
prev = prev ? prev.toFixed(1) : "0";
|
2019-05-27 16:15:39 +08:00
|
|
|
|
if (this.percent) {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
current += "%";
|
|
|
|
|
prev += "%";
|
2018-05-15 08:17:17 +08:00
|
|
|
|
}
|
2018-05-18 04:44:33 +08:00
|
|
|
|
} else {
|
|
|
|
|
prev = number(prev);
|
|
|
|
|
current = number(current);
|
2018-05-11 11:30:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
2018-05-18 04:44:33 +08:00
|
|
|
|
return I18n.t("admin.dashboard.reports.trend_title", {
|
|
|
|
|
percent,
|
|
|
|
|
prev,
|
|
|
|
|
current,
|
|
|
|
|
});
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-05-11 11:30:21 +08:00
|
|
|
|
|
2018-05-18 04:44:33 +08:00
|
|
|
|
changeTitle(valAtT1, valAtT2, prevPeriodString) {
|
|
|
|
|
const change = this.percentChangeString(valAtT1, valAtT2);
|
|
|
|
|
let title = "";
|
2018-05-29 02:35:22 +08:00
|
|
|
|
if (change) {
|
|
|
|
|
title += `${change} change. `;
|
|
|
|
|
}
|
|
|
|
|
title += `Was ${number(valAtT1)} ${prevPeriodString}.`;
|
2013-04-27 05:13:20 +08:00
|
|
|
|
return title;
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2013-04-27 05:13:20 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("yesterdayCount")
|
2018-05-03 21:41:41 +08:00
|
|
|
|
yesterdayCountTitle(yesterdayCount) {
|
2018-05-29 02:35:22 +08:00
|
|
|
|
return this.changeTitle(this.valueAt(2), yesterdayCount, "two days ago");
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2013-04-27 05:13:20 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("lastSevenDaysCount")
|
2018-05-18 04:44:33 +08:00
|
|
|
|
sevenDaysCountTitle(lastSevenDaysCount) {
|
2018-05-29 02:35:22 +08:00
|
|
|
|
return this.changeTitle(
|
|
|
|
|
this.valueFor(8, 14),
|
|
|
|
|
lastSevenDaysCount,
|
|
|
|
|
"two weeks ago"
|
|
|
|
|
);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2013-04-27 05:13:20 +08:00
|
|
|
|
|
2022-02-10 00:39:33 +08:00
|
|
|
|
@discourseComputed("prev30Days", "prev_period")
|
|
|
|
|
canDisplayTrendIcon(prev30Days, prev_period) {
|
|
|
|
|
return prev30Days ?? prev_period;
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2022-02-10 00:39:33 +08:00
|
|
|
|
|
|
|
|
|
@discourseComputed("prev30Days", "prev_period", "lastThirtyDaysCount")
|
|
|
|
|
thirtyDaysCountTitle(prev30Days, prev_period, lastThirtyDaysCount) {
|
2018-05-29 02:35:22 +08:00
|
|
|
|
return this.changeTitle(
|
2022-02-10 00:39:33 +08:00
|
|
|
|
prev30Days ?? prev_period,
|
2018-05-29 02:35:22 +08:00
|
|
|
|
lastThirtyDaysCount,
|
|
|
|
|
"in the previous 30 day period"
|
|
|
|
|
);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-03-16 05:10:45 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("data")
|
2018-03-16 05:10:45 +08:00
|
|
|
|
sortedData(data) {
|
2019-05-27 16:15:39 +08:00
|
|
|
|
return this.xAxisIsDate ? data.toArray().reverse() : data.toArray();
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2014-06-07 05:08:35 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("data")
|
2018-03-28 01:53:47 +08:00
|
|
|
|
xAxisIsDate() {
|
2020-09-22 22:28:28 +08:00
|
|
|
|
if (!this.data[0]) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2018-03-28 02:10:39 +08:00
|
|
|
|
return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/);
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2013-04-27 05:13:20 +08:00
|
|
|
|
|
2019-11-08 05:38:28 +08:00
|
|
|
|
@discourseComputed("labels")
|
2018-08-01 05:35:13 +08:00
|
|
|
|
computedLabels(labels) {
|
|
|
|
|
return labels.map((label) => {
|
2018-08-02 06:40:59 +08:00
|
|
|
|
const type = label.type || "string";
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
|
|
|
|
let mainProperty;
|
2020-09-22 22:28:28 +08:00
|
|
|
|
if (label.property) {
|
|
|
|
|
mainProperty = label.property;
|
|
|
|
|
} else if (type === "user") {
|
|
|
|
|
mainProperty = label.properties["username"];
|
|
|
|
|
} else if (type === "topic") {
|
|
|
|
|
mainProperty = label.properties["title"];
|
|
|
|
|
} else if (type === "post") {
|
2018-08-01 05:35:13 +08:00
|
|
|
|
mainProperty = label.properties["truncated_raw"];
|
2020-09-22 22:28:28 +08:00
|
|
|
|
} else {
|
|
|
|
|
mainProperty = label.properties[0];
|
|
|
|
|
}
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
title: label.title,
|
2021-04-22 19:38:10 +08:00
|
|
|
|
htmlTitle: label.html_title,
|
2018-08-01 05:35:13 +08:00
|
|
|
|
sortProperty: label.sort_property || mainProperty,
|
|
|
|
|
mainProperty,
|
2018-08-02 06:40:59 +08:00
|
|
|
|
type,
|
|
|
|
|
compute: (row, opts = {}) => {
|
FEATURE: Exposing a way to add a generic report filter (#6816)
* FEATURE: Exposing a way to add a generic report filter
## Why do we need this change?
Part of the work discussed [here](https://meta.discourse.org/t/gain-understanding-of-file-uploads-usage/104994), and implemented a first spike [here](https://github.com/discourse/discourse/pull/6809), I am trying to expose a single generic filter selector per report.
## How does this work?
We basically expose a simple, single generic filter that is computed and displayed based on backend values passed into the report.
This would be a simple contract between the frontend and the backend.
**Backend changes:** we simply need to return a list of dropdown / select options, and enable the report's newly introduced `custom_filtering` property.
For example, for our [Top Uploads](https://github.com/discourse/discourse/pull/6809/files#diff-3f97cbb8726f3310e0b0c386dbe89e22R1423) report, it can look like this on the backend:
```ruby
report.custom_filtering = true
report.custom_filter_options = [{ id: "any", name: "Any" }, { id: "jpg", name: "JPEG" } ]
```
In our javascript report HTTP call, it will look like:
```js
{
"custom_filtering": true,
"custom_filter_options": [
{
"id": "any",
"name": "Any"
},
{
"id": "jpg",
"name": "JPG"
}
]
}
```
**Frontend changes:** We introduced a generic `filter` param and a `combo-box` which hooks up into the existing framework for fetching a report.
This works alright, with the limitation of being a single custom filter per report. If we wanted to add, for an instance a `filesize filter`, this will not work for us. _I went through with this approach because it is hard to predict and build abstractions for requirements or problems we don't have yet, or might not have._
## How does it look like?
![a1ktg1odde](https://user-images.githubusercontent.com/45508821/50485875-f17edb80-09ee-11e9-92dd-1454ab041fbb.gif)
## More on the bigger picture
The major concern here I have is the solution I introduced might serve the `think small` version of the reporting work, but I don't think it serves the `think big`, I will try to shed some light into why.
Within the current design, It is hard to maintain QueryParams for dynamically generated params (based on the idea of introducing more than one custom filter per report).
To allow ourselves to have more than one generic filter, we will need to:
a. Use the Route's model to retrieve the report's payload (we are now dependent on changes of the QueryParams via computed properties)
b. After retrieving the payload, we can use the `setupController` to define our dynamic QueryParams based on the custom filters definitions we received from the backend
c. Load a custom filter specific Ember component based on the definitions we received from the backend
2019-03-15 20:15:38 +08:00
|
|
|
|
let value = null;
|
|
|
|
|
|
|
|
|
|
if (opts.useSortProperty) {
|
|
|
|
|
value = row[label.sort_property || mainProperty];
|
|
|
|
|
} else {
|
|
|
|
|
value = row[mainProperty];
|
|
|
|
|
}
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
2020-09-22 22:28:28 +08:00
|
|
|
|
if (type === "user") {
|
|
|
|
|
return this._userLabel(label.properties, row);
|
|
|
|
|
}
|
|
|
|
|
if (type === "post") {
|
|
|
|
|
return this._postLabel(label.properties, row);
|
|
|
|
|
}
|
|
|
|
|
if (type === "topic") {
|
|
|
|
|
return this._topicLabel(label.properties, row);
|
|
|
|
|
}
|
|
|
|
|
if (type === "seconds") {
|
|
|
|
|
return this._secondsLabel(value);
|
|
|
|
|
}
|
|
|
|
|
if (type === "link") {
|
|
|
|
|
return this._linkLabel(label.properties, row);
|
|
|
|
|
}
|
|
|
|
|
if (type === "percent") {
|
|
|
|
|
return this._percentLabel(value);
|
|
|
|
|
}
|
|
|
|
|
if (type === "bytes") {
|
|
|
|
|
return this._bytesLabel(value);
|
|
|
|
|
}
|
2018-08-02 06:40:59 +08:00
|
|
|
|
if (type === "number") {
|
|
|
|
|
return this._numberLabel(value, opts);
|
2018-08-01 05:35:13 +08:00
|
|
|
|
}
|
|
|
|
|
if (type === "date") {
|
2018-12-14 20:47:59 +08:00
|
|
|
|
const date = moment(value);
|
2020-09-22 22:28:28 +08:00
|
|
|
|
if (date.isValid()) {
|
|
|
|
|
return this._dateLabel(value, date);
|
|
|
|
|
}
|
2018-08-01 05:35:13 +08:00
|
|
|
|
}
|
2018-12-14 20:47:59 +08:00
|
|
|
|
if (type === "precise_date") {
|
|
|
|
|
const date = moment(value);
|
2020-09-22 22:28:28 +08:00
|
|
|
|
if (date.isValid()) {
|
|
|
|
|
return this._dateLabel(value, date, "LLL");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (type === "text") {
|
|
|
|
|
return this._textLabel(value);
|
2018-12-14 20:47:59 +08:00
|
|
|
|
}
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
value,
|
2018-08-02 06:40:59 +08:00
|
|
|
|
type,
|
|
|
|
|
property: mainProperty,
|
2022-06-21 02:02:05 +08:00
|
|
|
|
formattedValue: value ? escapeExpression(value) : "—",
|
2018-08-01 05:35:13 +08:00
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
});
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
|
|
|
|
_userLabel(properties, row) {
|
|
|
|
|
const username = row[properties.username];
|
|
|
|
|
|
2022-06-21 02:02:05 +08:00
|
|
|
|
const formattedValue = () => {
|
2018-08-02 06:40:59 +08:00
|
|
|
|
const userId = row[properties.id];
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
2019-10-30 03:23:50 +08:00
|
|
|
|
const user = EmberObject.create({
|
2018-08-02 06:40:59 +08:00
|
|
|
|
username,
|
|
|
|
|
name: formatUsername(username),
|
|
|
|
|
avatar_template: row[properties.avatar],
|
|
|
|
|
});
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
2020-06-04 00:45:26 +08:00
|
|
|
|
const href = getURL(`/admin/users/${userId}/${username}`);
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
2018-08-02 06:40:59 +08:00
|
|
|
|
const avatarImg = renderAvatar(user, {
|
|
|
|
|
imageSize: "tiny",
|
2020-07-24 02:57:03 +08:00
|
|
|
|
ignoreTitle: true,
|
|
|
|
|
siteSettings: this.siteSettings,
|
2018-08-02 06:40:59 +08:00
|
|
|
|
});
|
|
|
|
|
|
2019-06-26 23:09:52 +08:00
|
|
|
|
return `<a href='${href}'>${avatarImg}<span class='username'>${user.name}</span></a>`;
|
2018-08-02 06:40:59 +08:00
|
|
|
|
};
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
value: username,
|
2022-06-21 02:02:05 +08:00
|
|
|
|
formattedValue: username ? formattedValue() : "—",
|
2018-08-01 05:35:13 +08:00
|
|
|
|
};
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
|
|
|
|
_topicLabel(properties, row) {
|
|
|
|
|
const topicTitle = row[properties.title];
|
2018-08-02 20:01:17 +08:00
|
|
|
|
|
2022-06-21 02:02:05 +08:00
|
|
|
|
const formattedValue = () => {
|
2018-08-02 20:01:17 +08:00
|
|
|
|
const topicId = row[properties.id];
|
2020-06-04 00:45:26 +08:00
|
|
|
|
const href = getURL(`/t/-/${topicId}`);
|
2019-02-01 21:10:59 +08:00
|
|
|
|
return `<a href='${href}'>${escapeExpression(topicTitle)}</a>`;
|
2018-08-02 20:01:17 +08:00
|
|
|
|
};
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
value: topicTitle,
|
2022-06-21 02:02:05 +08:00
|
|
|
|
formattedValue: topicTitle ? formattedValue() : "—",
|
2018-08-01 05:35:13 +08:00
|
|
|
|
};
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
|
|
|
|
_postLabel(properties, row) {
|
|
|
|
|
const postTitle = row[properties.truncated_raw];
|
|
|
|
|
const postNumber = row[properties.number];
|
|
|
|
|
const topicId = row[properties.topic_id];
|
2020-06-04 00:45:26 +08:00
|
|
|
|
const href = getURL(`/t/-/${topicId}/${postNumber}`);
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
property: properties.title,
|
|
|
|
|
value: postTitle,
|
2022-06-21 02:02:05 +08:00
|
|
|
|
formattedValue:
|
2019-02-01 21:22:31 +08:00
|
|
|
|
postTitle && href
|
|
|
|
|
? `<a href='${href}'>${escapeExpression(postTitle)}</a>`
|
|
|
|
|
: "—",
|
2018-08-01 05:35:13 +08:00
|
|
|
|
};
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
2018-08-02 06:40:59 +08:00
|
|
|
|
_secondsLabel(value) {
|
2018-08-01 05:35:13 +08:00
|
|
|
|
return {
|
2019-09-12 21:17:34 +08:00
|
|
|
|
value: toNumber(value),
|
2022-06-21 02:02:05 +08:00
|
|
|
|
formattedValue: durationTiny(value),
|
2018-08-01 05:35:13 +08:00
|
|
|
|
};
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
2018-08-02 06:40:59 +08:00
|
|
|
|
_percentLabel(value) {
|
2018-08-01 05:35:13 +08:00
|
|
|
|
return {
|
2019-09-12 21:17:34 +08:00
|
|
|
|
value: toNumber(value),
|
2022-06-21 02:02:05 +08:00
|
|
|
|
formattedValue: value ? `${value}%` : "—",
|
2018-08-01 05:35:13 +08:00
|
|
|
|
};
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
2018-08-02 06:40:59 +08:00
|
|
|
|
_numberLabel(value, options = {}) {
|
2019-11-01 01:37:24 +08:00
|
|
|
|
const formatNumbers = isEmpty(options.formatNumbers)
|
2018-08-02 06:40:59 +08:00
|
|
|
|
? true
|
|
|
|
|
: options.formatNumbers;
|
|
|
|
|
|
2022-06-21 02:02:05 +08:00
|
|
|
|
const formattedValue = () => (formatNumbers ? number(value) : value);
|
2018-08-02 06:40:59 +08:00
|
|
|
|
|
2018-08-01 05:35:13 +08:00
|
|
|
|
return {
|
2019-09-12 21:17:34 +08:00
|
|
|
|
value: toNumber(value),
|
2022-06-21 02:02:05 +08:00
|
|
|
|
formattedValue: value ? formattedValue() : "—",
|
2018-08-01 05:35:13 +08:00
|
|
|
|
};
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
2018-12-29 03:48:54 +08:00
|
|
|
|
_bytesLabel(value) {
|
|
|
|
|
return {
|
2019-09-12 21:17:34 +08:00
|
|
|
|
value: toNumber(value),
|
2022-06-21 02:02:05 +08:00
|
|
|
|
formattedValue: I18n.toHumanSize(value),
|
2018-12-29 03:48:54 +08:00
|
|
|
|
};
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-12-29 03:48:54 +08:00
|
|
|
|
|
2018-12-14 20:47:59 +08:00
|
|
|
|
_dateLabel(value, date, format = "LL") {
|
2018-08-01 05:35:13 +08:00
|
|
|
|
return {
|
|
|
|
|
value,
|
2022-06-21 02:02:05 +08:00
|
|
|
|
formattedValue: value ? date.format(format) : "—",
|
2018-08-01 05:35:13 +08:00
|
|
|
|
};
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
2018-08-02 06:40:59 +08:00
|
|
|
|
_textLabel(value) {
|
2018-08-01 05:35:13 +08:00
|
|
|
|
const escaped = escapeExpression(value);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
value,
|
2022-06-21 02:02:05 +08:00
|
|
|
|
formattedValue: value ? escaped : "—",
|
2018-08-01 05:35:13 +08:00
|
|
|
|
};
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
|
|
|
|
_linkLabel(properties, row) {
|
|
|
|
|
const property = properties[0];
|
2020-06-04 00:45:26 +08:00
|
|
|
|
const value = getURL(row[property]);
|
2022-06-21 02:02:05 +08:00
|
|
|
|
const formattedValue = (href, anchor) => {
|
2018-08-02 06:40:59 +08:00
|
|
|
|
return `<a href="${escapeExpression(href)}">${escapeExpression(
|
|
|
|
|
anchor
|
|
|
|
|
)}</a>`;
|
|
|
|
|
};
|
|
|
|
|
|
2018-08-01 05:35:13 +08:00
|
|
|
|
return {
|
|
|
|
|
value,
|
2022-06-21 02:02:05 +08:00
|
|
|
|
formattedValue: value ? formattedValue(value, row[properties[1]]) : "—",
|
2018-08-01 05:35:13 +08:00
|
|
|
|
};
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-08-01 05:35:13 +08:00
|
|
|
|
|
2018-05-18 04:44:33 +08:00
|
|
|
|
_computeChange(valAtT1, valAtT2) {
|
|
|
|
|
return ((valAtT2 - valAtT1) / valAtT1) * 100;
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-05-18 04:44:33 +08:00
|
|
|
|
|
2018-05-28 16:55:42 +08:00
|
|
|
|
_computeTrend(valAtT1, valAtT2, higherIsBetter) {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
const change = this._computeChange(valAtT1, valAtT2);
|
|
|
|
|
|
|
|
|
|
if (change > 50) {
|
|
|
|
|
return higherIsBetter ? "high-trending-up" : "high-trending-down";
|
2018-05-28 16:55:42 +08:00
|
|
|
|
} else if (change > 2) {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
return higherIsBetter ? "trending-up" : "trending-down";
|
2018-05-28 16:55:42 +08:00
|
|
|
|
} else if (change <= 2 && change >= -2) {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
return "no-change";
|
|
|
|
|
} else if (change < -50) {
|
|
|
|
|
return higherIsBetter ? "high-trending-down" : "high-trending-up";
|
2018-05-28 16:55:42 +08:00
|
|
|
|
} else if (change < -2) {
|
2018-05-18 04:44:33 +08:00
|
|
|
|
return higherIsBetter ? "trending-down" : "trending-up";
|
|
|
|
|
}
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
2018-05-25 18:09:30 +08:00
|
|
|
|
|
|
|
|
|
_iconForTrend(trend, higherIsBetter) {
|
|
|
|
|
switch (trend) {
|
|
|
|
|
case "trending-up":
|
|
|
|
|
return higherIsBetter ? "angle-up" : "angle-down";
|
|
|
|
|
case "trending-down":
|
|
|
|
|
return higherIsBetter ? "angle-down" : "angle-up";
|
|
|
|
|
case "high-trending-up":
|
|
|
|
|
return higherIsBetter ? "angle-double-up" : "angle-double-down";
|
|
|
|
|
case "high-trending-down":
|
|
|
|
|
return higherIsBetter ? "angle-double-down" : "angle-double-up";
|
|
|
|
|
default:
|
2018-08-23 00:00:11 +08:00
|
|
|
|
return "minus";
|
2018-05-25 18:09:30 +08:00
|
|
|
|
}
|
2023-03-17 18:18:42 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2013-02-28 11:39:42 +08:00
|
|
|
|
|
2021-06-17 15:15:20 +08:00
|
|
|
|
export const WEEKLY_LIMIT_DAYS = 365;
|
2021-06-19 01:54:16 +08:00
|
|
|
|
export const DAILY_LIMIT_DAYS = 34;
|
2021-06-17 15:15:20 +08:00
|
|
|
|
|
2021-06-22 18:26:52 +08:00
|
|
|
|
function applyAverage(value, start, end) {
|
|
|
|
|
const count = end.diff(start, "day") + 1; // 1 to include start
|
|
|
|
|
return parseFloat((value / count).toFixed(2));
|
|
|
|
|
}
|