2018-08-24 21:28:01 +08:00
|
|
|
import ReportLoader from "discourse/lib/reports-loader";
|
2018-07-26 01:16:35 +08:00
|
|
|
import Category from "discourse/models/category";
|
2018-07-20 02:33:11 +08:00
|
|
|
import { exportEntity } from "discourse/lib/export-csv";
|
|
|
|
import { outputExportResult } from "discourse/lib/export-result";
|
2018-08-01 05:35:13 +08:00
|
|
|
import { SCHEMA_VERSION, default as Report } from "admin/models/report";
|
2018-07-20 02:33:11 +08:00
|
|
|
import computed from "ember-addons/ember-computed-decorators";
|
|
|
|
|
|
|
|
const TABLE_OPTIONS = {
|
|
|
|
perPage: 8,
|
|
|
|
total: true,
|
2018-08-02 06:40:59 +08:00
|
|
|
limit: 20,
|
|
|
|
formatNumbers: true
|
2018-07-20 02:33:11 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
const CHART_OPTIONS = {};
|
|
|
|
|
|
|
|
function collapseWeekly(data, average) {
|
|
|
|
let aggregate = [];
|
|
|
|
let bucket, i;
|
|
|
|
let offset = data.length % 7;
|
|
|
|
for (i = offset; i < data.length; i++) {
|
|
|
|
if (bucket && i % 7 === offset) {
|
|
|
|
if (average) {
|
|
|
|
bucket.y = parseFloat((bucket.y / 7.0).toFixed(2));
|
|
|
|
}
|
|
|
|
aggregate.push(bucket);
|
|
|
|
bucket = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
bucket = bucket || { x: data[i].x, y: 0 };
|
|
|
|
bucket.y += data[i].y;
|
|
|
|
}
|
2019-01-21 22:17:04 +08:00
|
|
|
|
2018-07-20 02:33:11 +08:00
|
|
|
return aggregate;
|
|
|
|
}
|
|
|
|
|
|
|
|
export default Ember.Component.extend({
|
2018-08-17 22:19:25 +08:00
|
|
|
classNameBindings: ["isEnabled", "isLoading", "dasherizedDataSourceName"],
|
2018-07-20 02:33:11 +08:00
|
|
|
classNames: ["admin-report"],
|
|
|
|
isEnabled: true,
|
2018-11-08 00:59:42 +08:00
|
|
|
disabledLabel: I18n.t("admin.dashboard.disabled"),
|
2018-07-20 02:33:11 +08:00
|
|
|
isLoading: false,
|
2018-08-22 17:25:12 +08:00
|
|
|
rateLimitationString: null,
|
2018-07-20 02:33:11 +08:00
|
|
|
dataSourceName: null,
|
|
|
|
report: null,
|
|
|
|
model: null,
|
|
|
|
reportOptions: null,
|
|
|
|
forcedModes: null,
|
|
|
|
showAllReportsLink: false,
|
2018-07-27 13:22:00 +08:00
|
|
|
filters: null,
|
2018-07-20 02:33:11 +08:00
|
|
|
startDate: null,
|
|
|
|
endDate: null,
|
2018-07-27 07:08:06 +08:00
|
|
|
category: null,
|
2018-07-26 02:02:21 +08:00
|
|
|
groupId: null,
|
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
|
|
|
filter: null,
|
2018-07-20 02:33:11 +08:00
|
|
|
showTrend: false,
|
|
|
|
showHeader: true,
|
|
|
|
showTitle: true,
|
|
|
|
showFilteringUI: false,
|
|
|
|
showCategoryOptions: Ember.computed.alias("model.category_filtering"),
|
|
|
|
showDatesOptions: Ember.computed.alias("model.dates_filtering"),
|
|
|
|
showGroupOptions: Ember.computed.alias("model.group_filtering"),
|
|
|
|
showExport: Ember.computed.not("model.onlyTable"),
|
2018-07-26 01:16:35 +08:00
|
|
|
showRefresh: Ember.computed.or(
|
2018-07-20 02:33:11 +08:00
|
|
|
"showCategoryOptions",
|
|
|
|
"showDatesOptions",
|
2018-07-26 01:16:35 +08:00
|
|
|
"showGroupOptions"
|
2018-07-20 02:33:11 +08:00
|
|
|
),
|
2018-08-17 22:19:25 +08:00
|
|
|
shouldDisplayTrend: Ember.computed.and("showTrend", "model.prev_period"),
|
2018-07-20 02:33:11 +08:00
|
|
|
|
|
|
|
init() {
|
|
|
|
this._super(...arguments);
|
|
|
|
|
|
|
|
this._reports = [];
|
|
|
|
},
|
|
|
|
|
|
|
|
didReceiveAttrs() {
|
|
|
|
this._super(...arguments);
|
|
|
|
|
2018-07-27 13:22:00 +08:00
|
|
|
const state = this.get("filters") || {};
|
2018-08-17 22:19:25 +08:00
|
|
|
|
2018-07-26 01:16:35 +08:00
|
|
|
this.setProperties({
|
|
|
|
category: Category.findById(state.categoryId),
|
|
|
|
groupId: state.groupId,
|
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
|
|
|
filter: state.filter,
|
2018-07-26 01:16:35 +08:00
|
|
|
startDate: state.startDate,
|
|
|
|
endDate: state.endDate
|
|
|
|
});
|
|
|
|
|
2018-07-20 02:33:11 +08:00
|
|
|
if (this.get("report")) {
|
|
|
|
this._renderReport(
|
|
|
|
this.get("report"),
|
|
|
|
this.get("forcedModes"),
|
|
|
|
this.get("currentMode")
|
|
|
|
);
|
|
|
|
} else if (this.get("dataSourceName")) {
|
2018-08-24 21:28:01 +08:00
|
|
|
this._fetchReport();
|
2018-07-20 02:33:11 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-11-12 20:47:24 +08:00
|
|
|
showError: Ember.computed.or(
|
|
|
|
"showTimeoutError",
|
|
|
|
"showExceptionError",
|
|
|
|
"showNotFoundError"
|
|
|
|
),
|
|
|
|
showNotFoundError: Ember.computed.equal("model.error", "not_found"),
|
2018-08-01 09:23:28 +08:00
|
|
|
showTimeoutError: Ember.computed.equal("model.error", "timeout"),
|
|
|
|
showExceptionError: Ember.computed.equal("model.error", "exception"),
|
2018-07-20 02:33:11 +08:00
|
|
|
|
2018-07-26 02:28:41 +08:00
|
|
|
hasData: Ember.computed.notEmpty("model.data"),
|
|
|
|
|
2018-07-20 02:33:11 +08:00
|
|
|
@computed("dataSourceName", "model.type")
|
|
|
|
dasherizedDataSourceName(dataSourceName, type) {
|
|
|
|
return (dataSourceName || type || "undefined").replace(/_/g, "-");
|
|
|
|
},
|
|
|
|
|
|
|
|
@computed("dataSourceName", "model.type")
|
|
|
|
dataSource(dataSourceName, type) {
|
|
|
|
dataSourceName = dataSourceName || type;
|
|
|
|
return `/admin/reports/${dataSourceName}`;
|
|
|
|
},
|
|
|
|
|
|
|
|
@computed("displayedModes.length")
|
|
|
|
showModes(displayedModesLength) {
|
|
|
|
return displayedModesLength > 1;
|
|
|
|
},
|
|
|
|
|
2018-07-27 07:08:06 +08:00
|
|
|
categoryId: Ember.computed.alias("category.id"),
|
|
|
|
|
2018-07-20 02:33:11 +08:00
|
|
|
@computed("currentMode", "model.modes", "forcedModes")
|
|
|
|
displayedModes(currentMode, reportModes, forcedModes) {
|
|
|
|
const modes = forcedModes ? forcedModes.split(",") : reportModes;
|
|
|
|
|
|
|
|
return Ember.makeArray(modes).map(mode => {
|
2018-10-25 04:09:36 +08:00
|
|
|
const base = `btn-default mode-btn ${mode}`;
|
2018-08-17 22:19:25 +08:00
|
|
|
const cssClass = currentMode === mode ? `${base} is-current` : base;
|
2018-07-20 02:33:11 +08:00
|
|
|
|
|
|
|
return {
|
|
|
|
mode,
|
|
|
|
cssClass,
|
|
|
|
icon: mode === "table" ? "table" : "signal"
|
|
|
|
};
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
@computed()
|
|
|
|
groupOptions() {
|
|
|
|
const arr = [
|
|
|
|
{ name: I18n.t("admin.dashboard.reports.groups"), value: "all" }
|
|
|
|
];
|
|
|
|
return arr.concat(
|
2018-08-17 22:19:25 +08:00
|
|
|
(this.site.groups || []).map(i => {
|
2018-07-20 02:33:11 +08:00
|
|
|
return { name: i["name"], value: i["id"] };
|
|
|
|
})
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
@computed("currentMode")
|
|
|
|
modeComponent(currentMode) {
|
|
|
|
return `admin-report-${currentMode}`;
|
|
|
|
},
|
|
|
|
|
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
|
|
|
@computed("model.filter_options")
|
|
|
|
filterOptions(options) {
|
|
|
|
if (options) {
|
|
|
|
return options.map(option => {
|
|
|
|
if (option.allowAny) {
|
|
|
|
option.choices.unshift(I18n.t("admin.dashboard.report_filter_any"));
|
|
|
|
}
|
|
|
|
return option;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-07-20 07:30:13 +08:00
|
|
|
@computed("startDate")
|
|
|
|
normalizedStartDate(startDate) {
|
|
|
|
return startDate && typeof startDate.isValid === "function"
|
2018-08-17 22:19:25 +08:00
|
|
|
? moment
|
|
|
|
.utc(startDate.toISOString())
|
|
|
|
.locale("en")
|
|
|
|
.format("YYYYMMDD")
|
|
|
|
: moment(startDate)
|
|
|
|
.locale("en")
|
|
|
|
.format("YYYYMMDD");
|
2018-07-20 07:30:13 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
@computed("endDate")
|
|
|
|
normalizedEndDate(endDate) {
|
|
|
|
return endDate && typeof endDate.isValid === "function"
|
2018-08-17 22:19:25 +08:00
|
|
|
? moment
|
|
|
|
.utc(endDate.toISOString())
|
|
|
|
.locale("en")
|
|
|
|
.format("YYYYMMDD")
|
|
|
|
: moment(endDate)
|
|
|
|
.locale("en")
|
|
|
|
.format("YYYYMMDD");
|
2018-07-20 07:30:13 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
@computed(
|
|
|
|
"dataSourceName",
|
|
|
|
"categoryId",
|
|
|
|
"groupId",
|
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
|
|
|
"filter",
|
2018-07-20 07:30:13 +08:00
|
|
|
"normalizedStartDate",
|
|
|
|
"normalizedEndDate"
|
|
|
|
)
|
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
|
|
|
reportKey(dataSourceName, categoryId, groupId, filter, startDate, endDate) {
|
2018-07-20 07:30:13 +08:00
|
|
|
if (!dataSourceName || !startDate || !endDate) return null;
|
|
|
|
|
2018-08-01 05:35:13 +08:00
|
|
|
let reportKey = "reports:";
|
|
|
|
reportKey += [
|
|
|
|
dataSourceName,
|
|
|
|
categoryId,
|
|
|
|
startDate.replace(/-/g, ""),
|
|
|
|
endDate.replace(/-/g, ""),
|
|
|
|
groupId,
|
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
|
|
|
filter,
|
2018-08-01 05:35:13 +08:00
|
|
|
"[:prev_period]",
|
|
|
|
this.get("reportOptions.table.limit"),
|
|
|
|
SCHEMA_VERSION
|
|
|
|
]
|
|
|
|
.filter(x => x)
|
|
|
|
.map(x => x.toString())
|
|
|
|
.join(":");
|
2018-07-20 07:30:13 +08:00
|
|
|
|
|
|
|
return reportKey;
|
|
|
|
},
|
|
|
|
|
2018-07-20 02:33:11 +08:00
|
|
|
actions: {
|
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
|
|
|
filter(filterOptionId, value) {
|
|
|
|
let params = [];
|
|
|
|
let paramPairs = {};
|
|
|
|
let newParams = [];
|
|
|
|
|
|
|
|
if (this.get("filter")) {
|
|
|
|
const filter = this.get("filter").slice(1, -1);
|
|
|
|
params = filter.split("&") || [];
|
|
|
|
params.map(p => {
|
|
|
|
const pair = p.split("=");
|
|
|
|
paramPairs[pair[0]] = pair[1];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
paramPairs[filterOptionId] = value;
|
|
|
|
Object.keys(paramPairs).forEach(key => {
|
|
|
|
if (paramPairs[key] !== I18n.t("admin.dashboard.report_filter_any")) {
|
|
|
|
newParams.push(`${key}=${paramPairs[key]}`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.set("filter", `[${newParams.join("&")}]`);
|
|
|
|
},
|
|
|
|
|
2018-07-26 01:16:35 +08:00
|
|
|
refreshReport() {
|
|
|
|
this.attrs.onRefresh({
|
2018-07-27 07:08:06 +08:00
|
|
|
categoryId: this.get("categoryId"),
|
2018-07-26 01:16:35 +08:00
|
|
|
groupId: this.get("groupId"),
|
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
|
|
|
filter: this.get("filter"),
|
2018-07-26 01:16:35 +08:00
|
|
|
startDate: this.get("startDate"),
|
|
|
|
endDate: this.get("endDate")
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2018-07-20 02:33:11 +08:00
|
|
|
exportCsv() {
|
|
|
|
exportEntity("report", {
|
|
|
|
name: this.get("model.type"),
|
|
|
|
start_date: this.get("startDate"),
|
|
|
|
end_date: this.get("endDate"),
|
|
|
|
category_id:
|
|
|
|
this.get("categoryId") === "all" ? undefined : this.get("categoryId"),
|
|
|
|
group_id:
|
|
|
|
this.get("groupId") === "all" ? undefined : this.get("groupId")
|
|
|
|
}).then(outputExportResult);
|
|
|
|
},
|
|
|
|
|
|
|
|
changeMode(mode) {
|
|
|
|
this.set("currentMode", mode);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_computeReport() {
|
|
|
|
if (!this.element || this.isDestroying || this.isDestroyed) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this._reports || !this._reports.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// on a slow network _fetchReport could be called multiple times between
|
|
|
|
// T and T+x, and all the ajax responses would occur after T+(x+y)
|
|
|
|
// to avoid any inconsistencies we filter by period and make sure
|
|
|
|
// the array contains only unique values
|
|
|
|
let filteredReports = this._reports.uniqBy("report_key");
|
|
|
|
let report;
|
|
|
|
|
|
|
|
const sort = r => {
|
|
|
|
if (r.length > 1) {
|
|
|
|
return r.findBy("type", this.get("dataSourceName"));
|
|
|
|
} else {
|
|
|
|
return r;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-07-20 07:30:13 +08:00
|
|
|
if (!this.get("startDate") || !this.get("endDate")) {
|
2018-07-20 02:33:11 +08:00
|
|
|
report = sort(filteredReports)[0];
|
|
|
|
} else {
|
2018-11-12 20:47:24 +08:00
|
|
|
const reportKey = this.get("reportKey");
|
2018-07-20 02:33:11 +08:00
|
|
|
|
|
|
|
report = sort(
|
|
|
|
filteredReports.filter(r => r.report_key.includes(reportKey))
|
|
|
|
)[0];
|
|
|
|
|
2018-07-20 20:16:29 +08:00
|
|
|
if (!report) return;
|
2018-07-20 02:33:11 +08:00
|
|
|
}
|
|
|
|
|
2018-11-12 20:47:24 +08:00
|
|
|
if (report.error === "not_found") {
|
|
|
|
this.set("showFilteringUI", false);
|
|
|
|
}
|
|
|
|
|
2018-07-20 02:33:11 +08:00
|
|
|
this._renderReport(
|
|
|
|
report,
|
|
|
|
this.get("forcedModes"),
|
|
|
|
this.get("currentMode")
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
_renderReport(report, forcedModes, currentMode) {
|
|
|
|
const modes = forcedModes ? forcedModes.split(",") : report.modes;
|
2018-07-26 02:02:21 +08:00
|
|
|
currentMode = currentMode || (modes ? modes[0] : null);
|
2018-07-20 02:33:11 +08:00
|
|
|
|
|
|
|
this.setProperties({
|
|
|
|
model: report,
|
|
|
|
currentMode,
|
|
|
|
options: this._buildOptions(currentMode)
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
_fetchReport() {
|
2019-01-19 17:05:51 +08:00
|
|
|
this._super(...arguments);
|
2018-07-20 02:33:11 +08:00
|
|
|
|
2018-08-22 17:25:12 +08:00
|
|
|
this.setProperties({ isLoading: true, rateLimitationString: null });
|
2018-07-20 02:33:11 +08:00
|
|
|
|
2018-08-24 21:28:01 +08:00
|
|
|
Ember.run.next(() => {
|
|
|
|
let payload = this._buildPayload(["prev_period"]);
|
2018-07-20 02:33:11 +08:00
|
|
|
|
2018-08-24 21:28:01 +08:00
|
|
|
const callback = response => {
|
|
|
|
if (!this.element || this.isDestroying || this.isDestroyed) {
|
|
|
|
return;
|
2018-07-20 02:33:11 +08:00
|
|
|
}
|
2018-08-24 21:28:01 +08:00
|
|
|
|
|
|
|
this.set("isLoading", false);
|
|
|
|
|
|
|
|
if (response === 429) {
|
2018-08-23 00:13:29 +08:00
|
|
|
this.set(
|
|
|
|
"rateLimitationString",
|
|
|
|
I18n.t("admin.dashboard.too_many_requests")
|
|
|
|
);
|
2018-08-24 21:28:01 +08:00
|
|
|
} else if (response === 500) {
|
|
|
|
this.set("model.error", "exception");
|
|
|
|
} else if (response) {
|
|
|
|
this._reports.push(this._loadReport(response));
|
|
|
|
this._computeReport();
|
2018-08-22 17:25:12 +08:00
|
|
|
}
|
2018-08-24 21:28:01 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
ReportLoader.enqueue(this.get("dataSourceName"), payload.data, callback);
|
|
|
|
});
|
2018-07-20 02:33:11 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
_buildPayload(facets) {
|
|
|
|
let payload = { data: { cache: true, facets } };
|
|
|
|
|
|
|
|
if (this.get("startDate")) {
|
2018-08-17 22:19:25 +08:00
|
|
|
payload.data.start_date = moment
|
|
|
|
.utc(this.get("startDate"), "YYYY-MM-DD")
|
|
|
|
.toISOString();
|
2018-07-20 02:33:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.get("endDate")) {
|
2018-08-17 22:19:25 +08:00
|
|
|
payload.data.end_date = moment
|
|
|
|
.utc(this.get("endDate"), "YYYY-MM-DD")
|
|
|
|
.toISOString();
|
2018-07-20 02:33:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.get("groupId") && this.get("groupId") !== "all") {
|
|
|
|
payload.data.group_id = this.get("groupId");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.get("categoryId") && this.get("categoryId") !== "all") {
|
|
|
|
payload.data.category_id = this.get("categoryId");
|
|
|
|
}
|
|
|
|
|
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
|
|
|
if (this.get("filter") && this.get("filter") !== "all") {
|
|
|
|
payload.data.filter = this.get("filter");
|
|
|
|
}
|
|
|
|
|
2018-07-20 02:33:11 +08:00
|
|
|
if (this.get("reportOptions.table.limit")) {
|
|
|
|
payload.data.limit = this.get("reportOptions.table.limit");
|
|
|
|
}
|
|
|
|
|
|
|
|
return payload;
|
|
|
|
},
|
|
|
|
|
|
|
|
_buildOptions(mode) {
|
|
|
|
if (mode === "table") {
|
|
|
|
const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS));
|
|
|
|
return Ember.Object.create(
|
2018-08-02 06:40:59 +08:00
|
|
|
Object.assign(tableOptions, this.get("reportOptions.table") || {})
|
2018-07-20 02:33:11 +08:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
|
|
|
|
return Ember.Object.create(
|
2018-08-02 06:40:59 +08:00
|
|
|
Object.assign(chartOptions, this.get("reportOptions.chart") || {})
|
2018-07-20 02:33:11 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_loadReport(jsonReport) {
|
|
|
|
Report.fillMissingDates(jsonReport, { filledField: "chartData" });
|
|
|
|
|
2019-01-21 22:17:04 +08:00
|
|
|
if (jsonReport.chartData && jsonReport.modes[0] === "stacked_chart") {
|
|
|
|
jsonReport.chartData = jsonReport.chartData.map(chartData => {
|
|
|
|
if (chartData.length > 40) {
|
|
|
|
return {
|
|
|
|
data: collapseWeekly(chartData.data),
|
|
|
|
req: chartData.req,
|
2019-01-22 00:10:10 +08:00
|
|
|
label: chartData.label,
|
2019-01-21 22:17:04 +08:00
|
|
|
color: chartData.color
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return chartData;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else if (jsonReport.chartData && jsonReport.chartData.length > 40) {
|
2018-07-20 02:33:11 +08:00
|
|
|
jsonReport.chartData = collapseWeekly(
|
|
|
|
jsonReport.chartData,
|
|
|
|
jsonReport.average
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (jsonReport.prev_data) {
|
|
|
|
Report.fillMissingDates(jsonReport, {
|
|
|
|
filledField: "prevChartData",
|
|
|
|
dataField: "prev_data",
|
|
|
|
starDate: jsonReport.prev_start_date,
|
|
|
|
endDate: jsonReport.prev_end_date
|
|
|
|
});
|
|
|
|
|
|
|
|
if (jsonReport.prevChartData && jsonReport.prevChartData.length > 40) {
|
|
|
|
jsonReport.prevChartData = collapseWeekly(
|
|
|
|
jsonReport.prevChartData,
|
|
|
|
jsonReport.average
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Report.create(jsonReport);
|
|
|
|
}
|
|
|
|
});
|