Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/helpers/includes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { helper } from '@ember/component/helper';

export function includes([array, value]: [unknown[], unknown]): boolean {
return array.includes(value);
}

export default helper(includes);
52 changes: 52 additions & 0 deletions app/modifiers/metrics-chart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Modifier from 'ember-modifier';
import bb, { line, subchart } from 'billboard.js';

interface MetricsChartArgs {
positional: [];
named: {
dataColumns?: Array<Array<string|number>>,
dataRows?: Array<Array<string|number>>,
};
}

export default class MetricsChart extends Modifier<MetricsChartArgs> {
chart: any = null;

didReceiveArguments() {
if (this.chart) {
this.chart.destroy();
}
this.chart = bb.generate({
bindto: this.element,
data: {
type: line(),
x: 'report_date',
// columns: this.args.named.dataColumns,
rows: this.args.named.dataRows,
},
axis: {
x: {
type: 'timeseries',
tick: {
format: '%Y-%m-%d',
},
},
},
subchart: {
show: subchart(),
showHandle: true,
},
tooltip: {
grouped: false,
linked: true,
},
});
}

willRemove() {
if (this.chart) {
this.chart.destroy();
}
}
}

1 change: 1 addition & 0 deletions app/osf-metrics/loading.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<LoadingIndicator />
124 changes: 124 additions & 0 deletions app/osf-metrics/report-detail/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import Controller from '@ember/controller';
import { action, get } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { MetricsReportAttrs } from './route';


type ReportFields = {
keywordFields: string[],
numericFields: string[],
};


function gatherFields(obj: any): ReportFields {
const keywordFields: string[] = []
const numericFields: string[] = []
for (const fieldName in obj) {
if (fieldName === 'report_date' || fieldName === 'timestamp') {
continue;
}
const fieldValue = obj[fieldName];
switch (typeof fieldValue) {
case 'string':
keywordFields.push(fieldName);
break;
case 'number':
numericFields.push(fieldName);
break;
case 'object':
const nestedFields = gatherFields(fieldValue);
keywordFields.push(...nestedFields.keywordFields.map(
nestedFieldName => `${fieldName}.${nestedFieldName}`,
));
numericFields.push(...nestedFields.numericFields.map(
nestedFieldName => `${fieldName}.${nestedFieldName}`,
));
break;
default:
console.log(`ignoring unexpected ${fieldName}: ${fieldValue}`)
}
}
return {
keywordFields,
numericFields,
};
}


export default class MetricsReportDetailController extends Controller {
queryParams = [
{ daysBack: { scope: 'controller' as const } },
'yFields',
'xGroupField',
'xGroupFilter',
]

@tracked daysBack: string = '13';
@tracked model: MetricsReportAttrs[] = [];
@tracked yFields: string[] = [];
@tracked xGroupField?: string;
@tracked xField: string = 'report_date';
@tracked xGroupFilter: string = '';

get reportFields(): ReportFields {
const aReport: MetricsReportAttrs = this.model![0];
return gatherFields(aReport);
}

get chartRows(): Array<Array<string|number|null>>{
if (!this.xGroupField) {
const fieldNames = [this.xField, ...this.yFields];
const rows = this.model.map(
datum => fieldNames.map(
fieldName => (get(datum, fieldName) as string | number | undefined) ?? null,
),
);
return [fieldNames, ...rows];
}
const groupedFieldNames = new Set<string>();
const rowsByX: any = {};
for (const datum of this.model) {
const xValue = get(datum, this.xField) as string;
if (!rowsByX[xValue]) {
rowsByX[xValue] = {};
}
const groupName = get(datum, this.xGroupField) as string;
if (!this.xGroupFilter || groupName.includes(this.xGroupFilter)) {
this.yFields.forEach(fieldName => {
const groupedField = `${groupName} ${fieldName}`;
groupedFieldNames.add(groupedField);
const fieldValue = get(datum, fieldName);
rowsByX[xValue][groupedField] = fieldValue;
});
}
}
const rows = Object.entries(rowsByX).map(
([xValue, rowData]: [string, any]) => {
const yValues = [...groupedFieldNames].map(
groupedFieldName => (rowData[groupedFieldName] as string | number | undefined) ?? null,
);
return [xValue, ...yValues];
},
);
return [
[this.xField, ...groupedFieldNames],
...rows,
];
}

@action
yFieldToggle(fieldName: string) {
if (this.yFields.includes(fieldName)) {
this.yFields = this.yFields.filter(f => f !== fieldName);
} else {
this.yFields = [...this.yFields, fieldName];
}
}
}

declare module '@ember/controller' {
interface Registry {
'osf-metrics.report-detail': MetricsReportDetailController;
}
}

47 changes: 47 additions & 0 deletions app/osf-metrics/report-detail/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Route from '@ember/routing/route';
import config from 'ember-get-config';

const {
OSF: {
apiUrl,
},
} = config;

export interface MetricsReportAttrs {
report_date: string, // YYYY-MM-DD
[attr: string]: string | number | object,
}

interface MetricsReport {
id: string;
type: string;
attributes: MetricsReportAttrs;
}

interface RecentMetricsReportResponse {
data: MetricsReport[];
}

export default class OsfMetricsRoute extends Route {
queryParams = {
daysBack: {
refreshModel: true,
},
yFields: {
replace: true,
},
xGroupField: {
replace: true,
},
xGroupFilter: {
replace: true,
},
}

async model(params: { daysBack: string, reportName?: string }) {
const url = `${apiUrl}/_/metrics/reports/${params.reportName}/recent/?days_back=${params.daysBack}`
const response = await fetch(url);
const responseJson: RecentMetricsReportResponse = await response.json();
return responseJson.data.map(datum => datum.attributes);
}
}
43 changes: 43 additions & 0 deletions app/osf-metrics/report-detail/template.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<p>
days back
{{#each (array '7' '13' '31' '73' '371') as |daysBackOption|}}
| <LinkTo @query={{hash daysBack=daysBackOption}}>{{daysBackOption}}</LinkTo>
{{/each}}
</p>
<p>
data fields (y-axis)
{{#each this.reportFields.numericFields as |fieldName|}}
| <label>
<Input
@type='checkbox'
@checked={{includes this.yFields fieldName}}
{{on 'input' (fn this.yFieldToggle fieldName)}}
/>
{{fieldName}}
</label>
{{/each}}
</p>
{{#if this.reportFields.keywordFields.length}}
<p>
group by
<PowerSelect
@options={{this.reportFields.keywordFields}}
@selected={{this.xGroupField}}
@onChange={{fn (mut this.xGroupField)}}
as |item|
>
{{item}}
</PowerSelect>
</p>
<p>
<label>
filter groups
<Input @type='text' @value={{mut this.xGroupFilter}} />
</label>
</p>
{{/if}}
{{#if (and this.model.length this.yFields.length)}}
<section>
<div {{metrics-chart dataRows=this.chartRows}}></div>
</section>
{{/if}}
29 changes: 29 additions & 0 deletions app/osf-metrics/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Route from '@ember/routing/route';
import config from 'ember-get-config';

const {
OSF: {
apiUrl,
},
} = config;

interface MetricsReportName {
id: string;
type: 'metrics-report-name';
links: {
recent: string,
};
}

interface MetricsReportNameResponse {
data: MetricsReportName[];
}

export default class OsfMetricsRoute extends Route {
async model() {
const url = `${apiUrl}/_/metrics/reports/`;
const response = await fetch(url);
const responseJson: MetricsReportNameResponse = await response.json();
return responseJson.data.map(metricsReport => metricsReport.id);
}
}
17 changes: 17 additions & 0 deletions app/osf-metrics/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.OsfMetrics {
display: flex;
flex-direction: column;
align-items: center;
}

.OsfMetrics > p {
max-width: 62vw;
}

.OsfMetrics > section {
width: 87vw;
}

.OsfMetrics :global(.active) {
font-weight: bold;
}
12 changes: 12 additions & 0 deletions app/osf-metrics/template.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{{page-title 'osf metrics'}}
<section local-class='OsfMetrics'>
<h1>osf metrics</h1>
<p>
reports
{{#each @model as |reportName|}}
|
<LinkTo @route='osf-metrics.report-detail' @model={{reportName}}>{{reportName}}</LinkTo>
{{/each}}
</p>
{{outlet}}
</section>
3 changes: 3 additions & 0 deletions app/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ Router.map(function() {
});
});
this.route('support');
this.route('osf-metrics', function() {
this.route('report-detail', { path: '/:reportName' });
});
this.route('meetings', function() {
this.route('detail', { path: '/:meeting_id' });
});
Expand Down
2 changes: 2 additions & 0 deletions ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,7 @@ module.exports = function(defaults) {

app.import('node_modules/wicg-inert/dist/inert.min.js');

app.import('node_modules/billboard.js/dist/billboard.css');

return app.toTree();
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"babel-eslint": "^8.0.0",
"billboard.js": "3.6.3",
"bootstrap-sass": "^3.3.7",
"broccoli-asset-rev": "^3.0.0",
"chai": "^4.1.2",
Expand Down
Loading