...
 
Commits (14)
### Summary
(What is the Merge request intending to do, in plain language)
(Be sure to associate any related issues or merge requests)
### Steps to test
(Steps to demonstrate merge achieves goal)
(Include any platform specific directions)
### Estimated Regression Scope
(What features do these changes effect in your estimation?)
......@@ -121,6 +121,7 @@ import { PageLayoutComponent } from './components/page-layout/page-layout.compon
import { DashboardLayoutComponent } from './components/dashboard-layout/dashboard-layout.component';
import { ShadowboxLayoutComponent } from './components/shadowbox-layout/shadowbox-layout.component';
import { ShadowboxHeaderComponent } from './components/shadowbox-header/shadowbox-header.component';
import { DropdownSelectorComponent } from './components/dropdown-selector/dropdown-selector.component';
import { ShadowboxSubmitButtonComponent } from './components/shadowbox-submit-button/shadowbox-submit-button.component';
import { FormDescriptorComponent } from './components/form-descriptor/form-descriptor.component';
import { FormToastComponent } from './components/form-toast/form-toast.component';
......@@ -238,6 +239,7 @@ PlotlyModule.plotlyjs = PlotlyJS;
DashboardLayoutComponent,
ShadowboxLayoutComponent,
ShadowboxHeaderComponent,
DropdownSelectorComponent,
FormDescriptorComponent,
FormToastComponent,
ShadowboxSubmitButtonComponent,
......@@ -338,9 +340,11 @@ PlotlyModule.plotlyjs = PlotlyJS;
PageLayoutComponent,
DashboardLayoutComponent,
ShadowboxLayoutComponent,
DropdownSelectorComponent,
FormDescriptorComponent,
FormToastComponent,
ShadowboxSubmitButtonComponent,
ShadowboxHeaderTabsComponent,
],
providers: [
SiteService,
......
......@@ -241,7 +241,7 @@ export class ChartV2Component implements OnInit, OnDestroy {
margin: {
t: this.isMini ? 0 : 16,
b: this.isMini ? 0 : 80,
l: this.isMini ? 0 : 0,
l: 0,
r: this.isMini ? 0 : 80,
pad: 16,
},
......@@ -377,7 +377,7 @@ export class ChartV2Component implements OnInit, OnDestroy {
return rows.map(row => {
if (key === 'date') {
return row[key].slice(0, 10);
} else if (this.segments[0].unit === 'usd') {
} else if (this.rawData.unit && this.rawData.unit === 'usd') {
return row[key] / 100;
} else {
return row[key];
......
<div class="m-analyticsFilter__labelWrapper" *ngIf="showLabel">
<div class="m-dropdownSelector__labelWrapper" *ngIf="showLabel">
<span>{{ filter.label }}</span>
<m-tooltip icon="help">
<div>{{ filter?.description }}</div>
......@@ -13,7 +13,7 @@
</m-tooltip>
</div>
<div
class="m-analyticsFilter__wrapper"
class="m-dropdownSelector__wrapper"
[ngClass]="{
expanded: expanded,
dropUp: dropUp
......@@ -21,27 +21,23 @@
(blur)="expanded = false"
tabindex="0"
>
<div
class="m-analyticsFilter__header m-analyticsFilter__row"
(click)="expanded = !expanded"
>
<span class="m-analyticsFilter__option m-analyticsFilter__option--selected">
<div class="m-dropdownSelector__header" (click)="expanded = !expanded">
<span class="m-dropdownSelector__option">
{{ selectedOption.label }}
</span>
<i class="material-icons" *ngIf="!expanded">keyboard_arrow_down</i>
<i class="material-icons" *ngIf="expanded">keyboard_arrow_up</i>
</div>
<div class="m-analyticsFilter__optionsContainer">
<div class="m-dropdownSelector__optionsContainer">
<ng-container *ngFor="let option of filter.options">
<div
class="m-analyticsFilter__option m-analyticsFilter__row"
class="m-dropdownSelector__option"
(click)="updateFilter(option)"
[ngClass]="{
unavailable: option.available === false
}"
>
{{ option.label }}
<!-- <span>{{ option.label }}</span> -->
</div>
</ng-container>
</div>
......
$rounded-top: 3px 3px 0 0;
$rounded-bottom: 0 0 3px 3px;
m-analytics__filter {
m-dropdownSelector {
position: relative;
margin: 0 24px 36px 0;
z-index: 2;
display: block;
}
.m-analyticsFilter__labelWrapper {
.m-dropdownSelector__labelWrapper {
position: absolute;
bottom: 115%;
white-space: nowrap;
......@@ -45,168 +45,179 @@ m-analytics__filter {
}
}
.m-analyticsFilter__wrapper {
.m-dropdownSelector__wrapper {
cursor: pointer;
&:focus {
outline: 0;
}
> * {
width: 180px;
box-sizing: border-box;
}
.m-analyticsFilter__optionsContainer {
padding: 8px 0;
.m-analyticsFilter__option {
transform: translateY(25%);
}
}
&.expanded {
@include m-theme() {
box-shadow: 0px 1px 15px 0 rgba(themed($m-black), 0.15);
}
.m-analyticsFilter__header {
.m-dropdownSelector__header {
@include m-theme() {
border-color: themed($m-blue);
}
}
.m-analyticsFilter__optionsContainer {
.m-dropdownSelector__optionsContainer {
display: block;
}
&:not(.dropUp) {
.m-analyticsFilter__header {
.m-dropdownSelector__header {
@include m-theme() {
border-radius: $rounded-top;
}
}
.m-analyticsFilter__optionsContainer {
.m-dropdownSelector__optionsContainer {
border-top: none;
border-radius: $rounded-bottom;
}
}
&.dropUp {
.m-analyticsFilter__header {
.m-dropdownSelector__header {
border-radius: $rounded-bottom;
}
.m-analyticsFilter__optionsContainer {
.m-dropdownSelector__optionsContainer {
bottom: 100%;
border-radius: $rounded-top;
border-bottom: none;
@include m-theme() {
box-shadow: 0px -4px 16px -4px rgba(themed($m-black), 0.15);
}
}
}
}
.m-analyticsFilter__header {
position: relative;
border-radius: 3px;
transition: all 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
> * {
width: 180px;
box-sizing: border-box;
}
}
.m-dropdownSelector__header {
position: relative;
border-radius: 3px;
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
@include m-theme() {
background-color: themed($m-white);
color: themed($m-grey-300);
}
@include m-theme() {
border: 1px solid themed($m-grey-100);
}
.m-dropdownSelector__label {
margin-right: 10px;
}
i {
flex-grow: 0;
width: 24px;
height: 24px;
padding-top: 2px;
}
.m-dropdownSelector__option {
@include m-theme() {
background-color: themed($m-white);
color: themed($m-grey-300);
}
@include m-theme() {
border: 1px solid themed($m-grey-100);
color: themed($m-grey-500);
}
.m-analyticsFilter__label {
margin-right: 10px;
}
i {
flex-grow: 0;
width: 24px;
height: 24px;
}
.m-analyticsFilter__option--selected {
}
}
.m-dropdownSelector__optionsContainer {
box-sizing: border-box;
position: absolute;
display: none;
border-radius: 3px;
left: 0px;
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
@include m-theme() {
border: 1px solid themed($m-blue);
border-top: 1px solid themed($m-blue);
background-color: themed($m-white);
box-shadow: 0px 8px 16px 0px rgba(themed($m-black), 0.15);
}
.m-dropdownSelector__option {
&:hover:not(.unavailable) {
@include m-theme() {
color: themed($m-grey-500);
background-color: rgba(themed($m-grey-100), 0.2);
}
}
}
.m-analyticsFilter__optionsContainer {
position: absolute;
display: none;
border-radius: 3px;
left: 0px;
transition: box-shadow 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
@include m-theme() {
border: 1px solid themed($m-blue);
border-top: 1px solid themed($m-blue);
background-color: themed($m-white);
&:first-child {
padding-top: 14px;
}
&:last-child {
padding-bottom: 14px;
}
}
}
.m-analyticsFilter__row {
display: flex;
justify-content: space-between;
align-items: center;
height: 46px;
padding: 0 20px;
&.m-analyticsFilter__header {
padding-right: 10px;
}
.m-dropdownSelector__header {
display: flex;
justify-content: space-between;
align-items: center;
padding-right: 10px;
}
.m-dropdownSelector__option {
display: inline-block;
padding: 10px 20px;
box-sizing: border-box;
width: inherit;
border-radius: 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@include m-theme() {
color: themed($m-grey-300);
}
.m-analyticsFilter__option {
display: inline-block;
box-sizing: border-box;
width: inherit;
border-radius: 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.unavailable {
display: none;
text-decoration: line-through;
@include m-theme() {
color: themed($m-grey-300);
}
&.unavailable {
text-decoration: line-through;
@include m-theme() {
color: themed($m-grey-50);
}
}
&:hover:not(.unavailable) {
@include m-theme() {
color: themed($m-grey-600);
}
color: themed($m-grey-50);
}
}
}
@media screen and (max-width: $min-tablet) {
m-analytics__filter {
.m-analyticsFilter__labelWrapper {
m-dropdownSelector {
.m-dropdownSelector__labelWrapper {
.m-tooltip--bubble {
width: 120px;
}
}
.m-analyticsFilter__wrapper {
> * {
width: 140px;
}
}
}
}
@media screen and (max-width: $max-mobile) {
m-analytics__filter {
.m-analyticsFilter__wrapper {
.m-analyticsFilter__header {
m-dropdownSelector {
.m-dropdownSelector__wrapper {
> * {
width: 160px;
}
.m-dropdownSelector__header {
padding-right: 10px;
i {
display: none;
}
}
.m-analyticsFilter__row {
padding: 0 18px;
height: 40px;
&.m-analyticsFilter__header {
padding-right: 10px;
.m-dropdownSelector__optionsContainer {
.m-dropdownSelector__option {
&:first-child {
padding-top: 11px;
}
&:last-child {
padding-bottom: 11px;
}
}
}
.m-analyticsFilter__option--selected {
.m-dropdownSelector__option {
margin-right: 0;
padding: 8px 18px;
}
}
}
......
......@@ -2,32 +2,30 @@ import {
Component,
OnInit,
Input,
Output,
ChangeDetectionStrategy,
EventEmitter,
} from '@angular/core';
import {
AnalyticsDashboardService,
Filter,
Option,
} from '../../dashboard.service';
import { Session } from '../../../../../services/session';
import { Session } from '../../../services/session';
import { Filter, Option } from '../../../interfaces/dashboard';
@Component({
selector: 'm-analytics__filter',
templateUrl: 'filter.component.html',
selector: 'm-dropdownSelector',
templateUrl: './dropdown-selector.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsFilterComponent implements OnInit {
export class DropdownSelectorComponent implements OnInit {
@Input() filter: Filter;
@Input() dropUp: boolean = false;
@Input() showLabel: boolean = true;
@Output() selectionMade: EventEmitter<any> = new EventEmitter();
expanded = false;
options: Array<any> = [];
selectedOption: Option;
constructor(
private analyticsService: AnalyticsDashboardService,
public session: Session
) {}
constructor(public session: Session) {}
ngOnInit() {
this.selectedOption =
......@@ -42,25 +40,9 @@ export class AnalyticsFilterComponent implements OnInit {
}
this.selectedOption = option;
if (this.filter.id === 'timespan') {
this.analyticsService.updateTimespan(option.id);
console.log('upDateFilter ', option.id);
return;
}
const selectedFilterStr = `${this.filter.id}::${option.id}`;
this.analyticsService.updateFilter(selectedFilterStr);
this.selectionMade.emit({
option: this.selectedOption,
filterId: this.filter.id,
});
}
// clickHeader() {
// if (this.expanded) {
// console.log('its expanded');
// setTimeout(() => {
// this.expanded = false;
// });
// } else {
// console.log('itsnot expanded');
// }
// document.getElementById("myAnchor").blur();
// }
}
<ng-container *ngIf="metrics$ | async as metrics">
<ng-container *ngFor="let metric of metrics">
<ng-container *ngFor="let tab of tabs">
<div
class="m-shadowboxHeaderTab"
(click)="changeTabs(tab)"
[ngClass]="{ active: tab.id === activeTabId }"
>
<div class="m-shadowboxHeaderTab__label">
<span>{{ tab.label }}</span>
<m-tooltip [anchor]="top" icon="help" *ngIf="tab.description">
{{ tab.description }}
</m-tooltip>
</div>
<div
class="m-analytics__metric m-shadowboxLayout__headerTab"
(click)="changeTabs(tab)"
[ngClass]="{ active: metric.visualisation }"
*ngIf="metric.permissionGranted"
class="m-shadowboxHeaderTab__value"
*ngIf="tab.value || tab.value === 0"
>
<div class="m-analytics__metricLabel">
<span>{{ metric.label }}</span>
<m-tooltip [anchor]="top" icon="help">
{{ metric.description }}
</m-tooltip>
</div>
<div class="m-analytics__metricSummary" *ngIf="metric.summary">
<ng-container *ngIf="metric.unit === 'number'">
{{ metric.summary.current_value | number }}
</ng-container>
<ng-container *ngIf="metric.unit === 'usd'">
<span>$</span
>{{ metric.summary.current_value / 100 | number: '1.2-2' }}
</ng-container>
</div>
<ng-container *ngIf="tab.unit === 'number'">
{{ tab.value | number }}
</ng-container>
<ng-container *ngIf="tab.unit === 'usd'">
<span>$</span>{{ tab.value / 100 | number: '1.2-2' }}
</ng-container>
</div>
<div
*ngIf="metric.summary"
class="m-analytics__metricDelta"
[ngClass]="{
goodChange: metric.hasChanged && metric.positiveTrend,
badChange: metric.hasChanged && !metric.positiveTrend
}"
>
<i class="material-icons" *ngIf="metric.delta > 0">arrow_upward</i>
<i class="material-icons" *ngIf="metric.delta < 0">arrow_downward</i>
<span>{{ metric.delta | percent: '1.0-1' }}</span>
</div>
<div
*ngIf="tab.delta || tab.delta === 0"
class="m-shadowboxHeaderTab__delta"
[ngClass]="{
goodChange: tab.hasChanged && tab.positiveTrend,
badChange: tab.hasChanged && !tab.positiveTrend
}"
>
<i class="material-icons" *ngIf="tab.delta > 0">arrow_upward</i>
<i class="material-icons" *ngIf="tab.delta < 0">arrow_downward</i>
<span>{{ tab.delta | percent: '1.0-1' }}</span>
</div>
</ng-container>
</div>
</ng-container>
.m-analytics__metric {
.m-shadowboxHeaderTab {
cursor: pointer;
flex: 0 0 auto;
width: 160px;
......@@ -32,20 +32,20 @@
border-bottom: 8px solid rgba(0, 0, 0, 0);
}
}
.m-analytics__metricLabel {
.m-shadowboxHeaderTab__label {
white-space: nowrap;
}
m-tooltip {
vertical-align: middle;
}
.m-analytics__metricSummary {
.m-shadowboxHeaderTab__value {
font-size: 17px;
margin-top: 8px;
@include m-theme() {
color: themed($m-grey-800);
}
}
.m-analytics__metricDelta {
.m-shadowboxHeaderTab__delta {
display: flex;
align-items: baseline;
padding-top: 4px;
......@@ -67,7 +67,7 @@
}
@media screen and (max-width: $min-tablet) {
.m-analytics__metric {
.m-shadowboxHeaderTab {
scroll-snap-align: start;
&:first-child {
margin-left: 16px;
......
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { ShadowboxHeaderTab } from '../../../interfaces/dashboard';
@Component({
selector: 'm-shadowboxHeader__tabs',
templateUrl: './shadowbox-header-tabs.component.html',
})
export class ShadowboxHeaderTabsComponent implements OnInit {
@Input() tabs;
export class ShadowboxHeaderTabsComponent {
@Input() tabs: ShadowboxHeaderTab[];
@Input() activeTabId = '';
@Output() tabChanged: EventEmitter<any> = new EventEmitter();
constructor() {}
ngOnInit() {}
changeTabs(tab) {
this.tabChanged.emit({ tabId: tab.id });
}
......
......@@ -2,8 +2,8 @@
<div class="m-shadowboxHeader__wrapper">
<ng-container *ngIf="isScrollable">
<div
*ngIf="isOverflown && !isAtScrollStart"
class="m-shadowboxHeader__overflowFade--left"
[ngClass]="{ visible: isOverflown && !isAtScrollStart }"
></div>
<div
[ngClass]="{ showButton: showButton.left }"
......@@ -22,8 +22,8 @@
</div>
<ng-container *ngIf="isScrollable">
<div
*ngIf="isOverflown && !isAtScrollEnd"
class="m-shadowboxHeader__overflowFade--right"
[ngClass]="{ visible: isOverflown && !isAtScrollEnd }"
></div>
<div
[ngClass]="{ showButton: showButton.right }"
......
......@@ -41,8 +41,10 @@ m-shadowboxHeader {
position: absolute;
top: 0;
bottom: 0;
width: 24px;
width: 0px;
z-index: 2;
opacity: 0;
transition: opacity 0.2s ease, width 0.4s ease;
&.m-shadowboxHeader__overflowFade--right {
@include m-theme() {
right: 0;
......@@ -63,6 +65,10 @@ m-shadowboxHeader {
);
}
}
&.visible {
opacity: 1;
width: 24px;
}
}
[class*='m-shadowboxHeader__overflowScrollButton--'] {
......@@ -132,7 +138,7 @@ m-shadowboxHeader {
.m-shadowboxHeader__container {
overflow-x: scroll;
scroll-snap-type: x mandatory;
.m-analytics__metric {
.m-shadowboxHeaderTab {
scroll-snap-align: start;
&:first-child {
margin-left: 16px;
......
......@@ -69,7 +69,7 @@ export class ShadowboxHeaderComponent implements AfterViewInit {
}
const firstMetric = <HTMLElement>(
document.querySelector('.m-shadowboxLayout__headerTab')
document.querySelector('.m-shadowboxHeaderTab')
);
// TODO: figure out how to avoid test failure "Cannot read property 'clientWidth' of null"
this.childClientWidth = firstMetric ? firstMetric.clientWidth : 160;
......
export interface Category {
id: string;
label: string;
metrics?: string[]; // TODO: remove this
permissions?: string[];
}
export interface Response {
status: string;
dashboard: Dashboard;
......@@ -12,6 +5,7 @@ export interface Response {
export interface Dashboard {
category: string;
description?: string;
timespan: string;
timespans: Timespan[];
metric: string;
......@@ -24,6 +18,8 @@ export interface Filter {
id: string;
label: string;
options: Option[];
description?: string;
expanded?: boolean;
}
export interface Option {
......@@ -31,6 +27,7 @@ export interface Option {
label: string;
available?: boolean;
selected?: boolean;
description?: string;
interval?: string;
comparison_interval?: number;
from_ts_ms?: number;
......@@ -40,11 +37,20 @@ export interface Option {
export interface Metric {
id: string;
label: string;
permissions: string[];
summary: Summary;
permissions?: string[];
summary?: Summary;
unit?: string;
description?: string;
visualisation: Visualisation | null;
tabs?: TopTab[];
value?: number;
}
export interface TopTab {
id: string;
label: string;
selected: boolean;
}
export interface Summary {
current_value: number;
comparison_value: number;
......@@ -54,16 +60,19 @@ export interface Summary {
export interface Visualisation {
type: string;
segments: Array<Buckets>;
segments?: Buckets[];
buckets?: Bucket[];
columns?: Array<any>;
}
export interface Buckets {
buckets: Bucket[];
}
export interface Bucket {
key: number;
date: string;
value: number;
key: number | string;
date?: string;
value?: number;
values?: {};
}
export interface Timespan {
......@@ -73,15 +82,28 @@ export interface Timespan {
comparison_interval: number;
from_ts_ms: number;
from_ts_iso: string;
selected: boolean;
}
export interface UserState {
category: string;
description?: string;
timespan: string;
timespans: Timespan[];
metric: string;
metrics: Metric[];
filter: string[];
filters: Filter[];
filter?: string[];
filters?: Filter[];
loading: boolean;
}
export interface ShadowboxHeaderTab {
id: string;
label: string;
value?: string | number;
unit?: string;
delta?: number;
hasChanged?: boolean;
positiveTrend?: boolean;
description?: string;
}
......@@ -52,9 +52,7 @@ import { PageviewsChartComponent } from './components/charts/pageviews/pageviews
import { AnalyticsDashboardComponent } from './v2/dashboard.component';
import { AnalyticsLayoutChartComponent } from './v2/layouts/layout-chart/layout-chart.component';
import { AnalyticsLayoutSummaryComponent } from './v2/layouts/layout-summary/layout-summary.component';
import { AnalyticsMetricsComponent } from './v2/components/metrics/metrics.component';
import { AnalyticsFiltersComponent } from './v2/components/filters/filters.component';
import { AnalyticsFilterComponent } from './v2/components/filter/filter.component';
import { AnalyticsChartComponent } from './v2/components/chart/chart.component';
import { AnalyticsTableComponent } from './v2/components/table/table.component';
import { AnalyticsDashboardService } from './v2/dashboard.service';
......@@ -154,9 +152,7 @@ const routes: Routes = [
AnalyticsDashboardComponent,
AnalyticsLayoutChartComponent,
AnalyticsLayoutSummaryComponent,
AnalyticsMetricsComponent,
AnalyticsFiltersComponent,
AnalyticsFilterComponent,
AnalyticsChartComponent,
AnalyticsTableComponent,
AnalyticsSearchComponent,
......
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsFilterComponent } from './filter.component';
describe('AnalyticsFilterComponent', () => {
let component: AnalyticsFilterComponent;
let fixture: ComponentFixture<AnalyticsFilterComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsFilterComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsFilterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
<div class="m-analytics__filtersContainer">
<!-- <ng-container *ngFor="let filter of filters$ | async"> -->
<ng-container *ngFor="let filter of filters">
<m-analytics__filter
class="filter"
<m-dropdownSelector
[filter]="filter"
[dropUp]="true"
></m-analytics__filter>
(selectionMade)="selectionMade($event)"
></m-dropdownSelector>
</ng-container>
</div>
......@@ -6,7 +6,8 @@ import {
ChangeDetectorRef,
} from '@angular/core';
import { Subscription } from 'rxjs';
import { AnalyticsDashboardService, Filter } from '../../dashboard.service';
import { AnalyticsDashboardService } from '../../dashboard.service';
import { Filter } from '../../../../../interfaces/dashboard';
@Component({
selector: 'm-analytics__filters',
......@@ -43,7 +44,12 @@ export class AnalyticsFiltersComponent implements OnInit, OnDestroy {
});
}
// TODO: remove all of this once channel search is ready
selectionMade($event) {
this.analyticsService.updateFilter(
`${$event.filterId}::${$event.option.id}`
);
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
......
<ng-container *ngIf="metrics$ | async as metrics">
<m-shadowboxHeader_tabs
[tabs]="metrics"
(tabChanged)="updateMetric($event)"
></m-shadowboxHeader_tabs>
<ng-container *ngFor="let metric of metrics">
<div
class="m-analytics__metric m-shadowboxLayout__headerTab"
(click)="updateMetric(metric)"
[ngClass]="{ active: metric.visualisation }"
*ngIf="metric.permissionGranted"
>
<div class="m-analytics__metricLabel">
<span>{{ metric.label }}</span>
<m-tooltip [anchor]="top" icon="help">
{{ metric.description }}
</m-tooltip>
</div>
<div class="m-analytics__metricSummary" *ngIf="metric.summary">
<ng-container *ngIf="metric.unit === 'number'">
{{ metric.summary.current_value | number }}
</ng-container>
<ng-container *ngIf="metric.unit === 'usd'">
<span>$</span
>{{ metric.summary.current_value / 100 | number: '1.2-2' }}
</ng-container>
</div>
<div
*ngIf="metric.summary"
class="m-analytics__metricDelta"
[ngClass]="{
goodChange: metric.hasChanged && metric.positiveTrend,
badChange: metric.hasChanged && !metric.positiveTrend
}"
>
<i class="material-icons" *ngIf="metric.delta > 0">arrow_upward</i>
<i class="material-icons" *ngIf="metric.delta < 0">arrow_downward</i>
<span>{{ metric.delta | percent: '1.0-1' }}</span>
</div>
</div>
</ng-container>
</ng-container>
.m-analytics__metric {
cursor: pointer;
flex: 0 0 auto;
width: 160px;
padding: 24px 20px 20px 20px;
font-size: 14px;
box-sizing: border-box;
overflow: visible;
@include m-theme() {
border-bottom: 8px solid themed($m-white);
}
@include m-theme() {
color: themed($m-grey-300);
}
&.active {
@include m-theme() {
background-color: rgba(themed($m-grey-100), 0.2);
border-bottom: 8px solid themed($m-blue);
}
}
&:first-child {
margin-left: 40px;
}
&:last-child {
margin-right: 40px;
}
&:hover:not(.active) {
transition: background-color 0.3s cubic-bezier(0.23, 1, 0.32, 1);
@include m-theme() {
background-color: rgba(themed($m-grey-100), 0.2);
border-bottom: 8px solid rgba(0, 0, 0, 0);
}
}
.m-analytics__metricLabel {
white-space: nowrap;
}
m-tooltip {
vertical-align: middle;
}
.m-analytics__metricSummary {
font-size: 17px;
margin-top: 8px;
@include m-theme() {
color: themed($m-grey-800);
}
}
.m-analytics__metricDelta {
display: flex;
align-items: baseline;
padding-top: 4px;
font-size: 11px;
.material-icons {
transform: scaleX(0.7);
font-size: 11px;
font-weight: bold;
}
@include m-theme() {
&.goodChange {
color: themed($m-green);
}
&.badChange {
color: themed($m-red);
}
}
}
}
@media screen and (max-width: $min-tablet) {
.m-analytics__metric {
scroll-snap-align: start;
&:first-child {
margin-left: 16px;
}
&:last-child {
margin-right: 16px;
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsMetricsComponent } from './metrics.component';
describe('AnalyticsMetricsComponent', () => {
let component: AnalyticsMetricsComponent;
let fixture: ComponentFixture<AnalyticsMetricsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsMetricsComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsMetricsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import {
AnalyticsDashboardService,
Metric as MetricBase,
} from '../../dashboard.service';
import { Session } from '../../../../../services/session';
interface MetricExtended extends MetricBase {
delta: number;
hasChanged: boolean;
positiveTrend: boolean;
permissionGranted: boolean;
}
export { MetricExtended as Metric };
@Component({
selector: 'm-analytics__metrics',
templateUrl: './metrics.component.html',
})
export class AnalyticsMetricsComponent implements OnInit {
subscription: Subscription;
user;
userRoles: string[] = ['user'];
metrics$;
constructor(
private analyticsService: AnalyticsDashboardService,
public session: Session
) {}
ngOnInit() {
this.user = this.session.getLoggedInUser();
if (this.session.isAdmin()) {
this.userRoles.push('admin');
}
if (this.user.pro) {
this.userRoles.push('pro');
}
this.metrics$ = this.analyticsService.metrics$.pipe(
map(_metrics => {
const metrics = _metrics.map(metric => ({ ...metric })); // Clone to avoid updating
for (const metric of metrics) {
metric['permissionGranted'] = metric.permissions.some(role =>
this.userRoles.includes(role)
);
if (metric.summary) {
let delta;
if (metric.summary.comparison_value !== 0) {
delta =
(metric.summary.current_value -
metric.summary.comparison_value) /
(metric.summary.comparison_value || 0);
} else {
delta = 1;
}
metric['delta'] = delta;
metric['hasChanged'] = delta === 0 ? false : true;
if (
(delta > 0 && metric.summary.comparison_positive_inclination) ||
(delta < 0 && !metric.summary.comparison_positive_inclination)
) {
metric['positiveTrend'] = true;
} else {
metric['positiveTrend'] = false;
}
}
}
return metrics;
})
);
}
updateMetric($event) {
// TODO: if clicked metric is not fully visible, slide() until it is
this.analyticsService.updateMetric($event.tabId);
}
}
import { Component, OnInit, Input, ChangeDetectorRef } from '@angular/core';
import {
AnalyticsDashboardService,
Filter,
Option,
} from '../../dashboard.service';
import { AnalyticsDashboardService } from '../../dashboard.service';
import { RecentService } from '../../../../../services/ux/recent';
import { Session } from '../../../../../services/session';
import { Client } from '../../../../../services/api';
import { Filter, Option } from '../../../../../interfaces/dashboard';
@Component({
selector: 'm-analytics__searchSuggestions',
......
import { Component, OnInit, ViewChild, Input, ElementRef } from '@angular/core';
// import { Observable, Subscription } from 'rxjs';
import {
AnalyticsDashboardService,
Filter,
Option,
} from '../../dashboard.service';
import { AnalyticsDashboardService } from '../../dashboard.service';
import { Session } from '../../../../../services/session';
import { Filter, Option } from '../../../../../interfaces/dashboard';
@Component({
selector: 'm-analytics__search',
......
......@@ -9,10 +9,8 @@ import {
} from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import {
AnalyticsDashboardService,
Visualisation,
} from '../../dashboard.service';
import { AnalyticsDashboardService } from '../../dashboard.service';
import { Visualisation } from '../../../../../interfaces/dashboard';
@Component({
selector: 'm-analytics__table',
......
<m-pageLayout [menu]="menu">
<div class="m-analyticsDashboard" *ngIf="ready$ | async" m-pageLayout__main>
<!-- TODOOOJM uncomment me -->
<!-- <div class="m-analyticsDashboard" *ngIf="ready$ | async" m-pageLayout__main> -->
<div class="m-analyticsDashboard" m-pageLayout__main>
<m-dashboardLayout>
<ng-container m-dashboardLayout__header>
<div>
......@@ -16,13 +18,14 @@
<m-analytics__search></m-analytics__search>
</div> -->
<!-- <div class="m-analyticsDashboard__channelFilter" *ngIf="session.isAdmin()">
<m-analytics__filter [filter]="channelFilter"></m-analytics__filter>
<m-dropdownSelector [filter]="channelFilter"></m-dropdownSelector>
</div> -->
<div class="m-analyticsDashboard__timespanFilter">
<m-analytics__filter
<m-dropdownSelector
[filter]="timespanFilter"
[showLabel]="false"
></m-analytics__filter>
(selectionMade)="filterSelectionMade($event)"
></m-dropdownSelector>
</div>
</div>
</ng-container>
......
......@@ -30,22 +30,14 @@ m-analytics__dashboard {
}
}
}
m-analytics__filter {
m-dropdownSelector {
margin: 0;
}
.m-analyticsFilter__wrapper {
.m-dropdownSelector__wrapper {
> * {
width: 180px;
}
}
.m-analyticsDashboard__timespanFilter {
.m-analytics__filterWrapper {
margin-top: 0px;
}
.m-analytics__filterLabel {
display: none;
}
}
}
.m-analyticsDashboard__description {
margin: 8px 16px 32px 0;
......@@ -57,11 +49,11 @@ m-analytics__dashboard {
@media screen and (max-width: $min-tablet) {
.m-dashboardLayout__header {
m-analytics__filter {
m-dropdownSelector {
margin: 0 16px 8px 0;
.m-analyticsFilter__wrapper {
.m-dropdownSelector__wrapper {
> * {
width: 160px;
width: 180px;
}
}
}
......@@ -73,9 +65,11 @@ m-analytics__dashboard {
@media screen and (max-width: $max-mobile) {
.m-dashboardLayout__header {
.m-analyticsFilter__wrapper {
> * {
width: 140px;
m-dropdownSelector {
.m-dropdownSelector__wrapper {
> * {
width: 160px;
}
}
}
}
......
......@@ -104,6 +104,12 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
}
}
filterSelectionMade($event) {
if ($event.filterId === 'timespan') {
this.analyticsService.updateTimespan($event.option.id);
}
}
updateTimespan(timespanId) {
// TODO: update url
// this.analyticsService.updateTimespan(timespanId);
......
import { TestBed } from '@angular/core/testing';
import {
AnalyticsDashboardService,
Category,
Response,
Dashboard,
Filter,
Option,
Metric,
Summary,
Visualisation,
Bucket,
Timespan,
UserState,
} from './dashboard.service';
import { AnalyticsDashboardService } from './dashboard.service';
describe('AnalyticsDashboardService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
......
......@@ -12,104 +12,7 @@ import {
import { MindsHttpClient } from '../../../common/api/client.service';
import fakeData from './fake-data';
export interface Category {
id: string;
label: string;
metrics?: string[]; // TODO: remove this
permissions?: string[];
}
export interface Response {
status: string;
dashboard: Dashboard;
}
export interface Dashboard {
category: string;
description?: string;
timespan: string;
timespans: Timespan[];
metric: string;
metrics: Metric[];
filter: string[];
filters: Filter[];
}
export interface Filter {
id: string;
label: string;
options: Option[];
description: string;
expanded?: boolean;
}
export interface Option {
id: string;
label: string;
available?: boolean;
selected?: boolean;
description?: string;
interval?: string;
comparison_interval?: number;
from_ts_ms?: number;
from_ts_iso?: string;
}
export interface Metric {
id: string;
label: string;
permissions?: string[];
summary?: Summary;
unit?: string;
description?: string;
visualisation: Visualisation | null;
}
export interface Summary {
current_value: number;
comparison_value: number;
comparison_interval: number;
comparison_positive_inclination: boolean;
}
export interface Visualisation {
type: string;
segments?: Buckets[];
buckets?: Bucket[];
columns?: Array<any>;
}
export interface Buckets {
buckets: Bucket[];
}
export interface Bucket {
key: number | string;
date?: string;
value?: number;
values?: {};
}
export interface Timespan {
id: string;
label: string;
interval: string;
comparison_interval: number;
from_ts_ms: number;
from_ts_iso: string;
selected: boolean;
}
export interface UserState {
category: string;
description?: string;
timespan: string;
timespans: Timespan[];
metric: string;
metrics: Metric[];
filter?: string[];
filters?: Filter[];
}
import { Response, UserState } from '../../../interfaces/dashboard';
let _state: UserState = fakeData[0];
......@@ -198,16 +101,17 @@ export class AnalyticsDashboardService {
const dashboard = response.dashboard;
this.ready$.next(true);
// TODOOJM uncomment me
this.updateState({
..._state,
category: dashboard.category,
description: dashboard.description,
timespan: dashboard.timespan,
timespans: dashboard.timespans,
filter: dashboard.filter,
filters: dashboard.filters,
metric: dashboard.metric,
metrics: dashboard.metrics,
// category: dashboard.category,
// description: dashboard.description,
// timespan: dashboard.timespan,
// timespans: dashboard.timespans,
// filter: dashboard.filter,
// filters: dashboard.filters,
// metric: dashboard.metric,
// metrics: dashboard.metrics,
});
this.loading$.next(false);
});
......@@ -238,13 +142,14 @@ export class AnalyticsDashboardService {
// return channelSearch;
// }
// TODOOJM uncomment me
updateCategory(category: string) {
this.updateState({
..._state,
category,
description: null,
metrics: [],
});
// this.updateState({
// ..._state,
// category,
// description: null,
// metrics: [],
// });
}
updateTimespan(timespan: string) {
this.updateState({
......@@ -273,8 +178,6 @@ export class AnalyticsDashboardService {
} else {
filter.push(selectedFilterStr);
}
// console.log('update filter called: ' + selectedFilterStr);
// console.log(filter);
this.updateState({ ..._state, filter });
}
......@@ -283,7 +186,6 @@ export class AnalyticsDashboardService {
/** Update internal state cache and emit from store... */
private updateState(state: UserState) {
// console.log('update state called');
this.store.next((_state = state));
}
......
......@@ -13,7 +13,7 @@ const fakeData: Array<any> = [
comparison_interval: 30,
from_ts_ms: 1567296000000,
from_ts_iso: '2019-09-01T00:00:00+00:00',
selected: false,
selected: true,
},
{
id: '12m',
......@@ -119,7 +119,7 @@ const fakeData: Array<any> = [
},
{
id: 'active_users',
label: 'Active UsersA',
label: 'Active Users B',
permissions: ['admin', 'user'],
summary: {
current_value: 120962,
......@@ -149,7 +149,7 @@ const fakeData: Array<any> = [
},
{
id: 'views',
label: 'Pageviews',
label: 'Pageviews USD',
permissions: ['admin', 'user'],
summary: {
current_value: 83898,
......
......@@ -5,10 +5,13 @@
*ngIf="selectedMetric && selectedMetric.visualisation"
[hasHeader]="selectedMetric.visualisation.type === 'chart'"
>
<m-analytics__metrics
<m-shadowboxHeader__tabs
class="m-shadowboxLayout__header"
*ngIf="selectedMetric.visualisation.type === 'chart'"
></m-analytics__metrics>
[tabs]="tabs"
[activeTabId]="activeTabId"
(tabChanged)="updateMetric($event)"
></m-shadowboxHeader__tabs>
<div
class="m-shadowboxLayout__body"
[ngClass]="{ isTable: isTable, isMobile: isMobile }"
......
......@@ -5,9 +5,11 @@ import {
ChangeDetectorRef,
OnDestroy,
} from '@angular/core';
import { Observable, Subscription, combineLatest } from 'rxjs';
import { Subscription, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { AnalyticsDashboardService } from '../../dashboard.service';
import { ShadowboxHeaderTab } from '../../../../../interfaces/dashboard';
import { Session } from '../../../../../services/session';
@Component({
selector: 'm-analytics__layout--chart',
......@@ -15,15 +17,23 @@ import { AnalyticsDashboardService } from '../../dashboard.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsLayoutChartComponent implements OnInit, OnDestroy {
subscription: Subscription;
user;
userRoles: string[] = ['user'];
tabs: ShadowboxHeaderTab[] = [];
activeTabId: string = '';
metricsSubscription: Subscription;
selectedMetricSubscription: Subscription;
loading$ = this.analyticsService.loading$;
metrics$;
selectedMetric$ = combineLatest(
this.analyticsService.metrics$,
this.analyticsService.metric$,
this.analyticsService.category$
this.analyticsService.metric$
).pipe(
map(([metrics, id, category]) => {
return metrics.find(metric => metric.id == id);
map(([metrics, id]) => {
return metrics.find(metric => metric.id === id);
})
);
selectedMetric;
......@@ -31,11 +41,20 @@ export class AnalyticsLayoutChartComponent implements OnInit, OnDestroy {
constructor(
private analyticsService: AnalyticsDashboardService,
public session: Session,
private cd: ChangeDetectorRef
) {}
ngOnInit() {
this.subscription = this.selectedMetric$.subscribe(metric => {
this.user = this.session.getLoggedInUser();
if (this.session.isAdmin()) {
this.userRoles.push('admin');
}
if (this.user.pro) {
this.userRoles.push('pro');
}
this.selectedMetricSubscription = this.selectedMetric$.subscribe(metric => {
this.selectedMetric = metric;
this.isTable =
......@@ -44,6 +63,56 @@ export class AnalyticsLayoutChartComponent implements OnInit, OnDestroy {
this.selectedMetric.visualisation.type === 'table';
this.detectChanges();
});
this.metricsSubscription = this.analyticsService.metrics$.subscribe(
metrics => {
for (const metric of metrics) {
const permissionGranted = metric.permissions.some(role =>
this.userRoles.includes(role)
);
const tab: ShadowboxHeaderTab = {
id: metric.id,
label: metric.label,
};
if (metric.visualisation) {
this.activeTabId = metric.id;
}
tab.unit = metric.unit ? metric.unit : null;
tab.description = metric.description ? metric.description : null;
if (metric.summary) {
tab.value = metric.summary.comparison_value;
if (metric.summary.comparison_value !== 0) {
tab.delta =
(metric.summary.current_value -
metric.summary.comparison_value) /
(metric.summary.comparison_value || 0);
} else {
tab.delta = 1;
}
tab.hasChanged = tab.delta === 0 ? false : true;
if (
(tab.delta > 0 &&
metric.summary.comparison_positive_inclination) ||
(tab.delta < 0 && !metric.summary.comparison_positive_inclination)
) {
tab.positiveTrend = true;
} else {
tab.positiveTrend = false;
}
}
console.log(tab);
if (permissionGranted) {
this.tabs.push(tab);
}
this.detectChanges();
}
}
);
}
detectChanges() {
......@@ -52,6 +121,7 @@ export class AnalyticsLayoutChartComponent implements OnInit, OnDestroy {
}
ngOnDestroy() {
this.subscription.unsubscribe();
this.selectedMetricSubscription.unsubscribe();
this.metricsSubscription.unsubscribe();
}
}
......@@ -17,7 +17,7 @@ export class AnalyticsLayoutSummaryComponent implements OnInit {
tiles = [
{
id: 'pageviews',
label: 'Daily pageviews',
label: 'Daily Pageviews',
unit: 'number',
interval: 'day',
endpoint:
......@@ -62,7 +62,7 @@ export class AnalyticsLayoutSummaryComponent implements OnInit {
},
{
id: 'earnings_total',
label: 'Total PRO Earnings',
label: 'Total Pro Earnings',
unit: 'usd',
interval: 'day',
endpoint: this.url + 'earnings',
......
......@@ -9,7 +9,6 @@ const sidebarMenu = {
id: 'summary',
label: 'Summary',
permissions: ['admin'],
// path: '/some/path/outside/analytics/dashboard',
},
{
id: 'traffic',
......
<form class="m-form">
<p>
You can receive Bitcoin (BTC) payments via wire by inputing a receiver
You can receive Bitcoin (BTC) payments via wire by inputting a receiver
address below. Note: You may want to rotate this address frequently to avoid
3rd parties tracking your transactions.
</p>
......
......@@ -336,6 +336,7 @@ export class ProSettingsComponent implements OnInit, OnDestroy {
for (const tag of tags) {
this.addTag(tag.label, tag.tag);
}
this.form.markAsDirty();
this.detectChanges();
}
......@@ -360,6 +361,7 @@ export class ProSettingsComponent implements OnInit, OnDestroy {
for (let link of links) {
this.addFooterLink(link.title, link.href);
}
this.form.markAsDirty();
this.detectChanges();
}
......
......@@ -2,7 +2,7 @@
<div class="m-walletDashboard" m-pageLayout__main *ngIf="settings">
<!-- *ngIf="ready$ | async" -->
<m-dashboardLayout>
<!-- <m-dashboardLayout>
<ng-container m-dashboardLayout__header>
<div>
<h3>
......@@ -12,30 +12,34 @@
</ng-container>
<ng-container m-dashboardLayout__body>
<!-- <div class="m-analytics__spinnerContainer" *ngIf="loading$ | async">
<div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div>
</div> -->
<m-shadowboxLayout>
<m-analytics__metrics
<m-shadowboxHeader__tabs
class="m-shadowboxLayout__header"
></m-analytics__metrics>
<m-walletBalance class="m-shadowboxLayout__body">
<!-- *ngIf="description$ | async as description" -->
</m-walletBalance>
></m-shadowboxHeader__tabs>
<m-walletBalance class="m-shadowboxLayout__body"> -->
<!-- *ngIf="description$ | async as description" -->
<!-- </m-walletBalance>
<div
class="m-shadowboxLayout__body"
[ngClass]="{ isTable: isTable, isMobile: isMobile }"
>
<m-walletChart>
<!-- <m-dropdownSelector
[filter]="timespanFilter"
[showLabel]="false"
></m-dropdownSelector> -->
</m-walletChart>
<m-walletTransactions></m-walletTransactions>
</div>
</m-shadowboxLayout>
</ng-container>
</m-dashboardLayout>
</m-dashboardLayout> -->
</div>
</m-pageLayout>
<!-- <div class="m-analytics__spinnerContainer" *ngIf="loading$ | async">
<div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div>
</div> -->
<!-- <m-dropdownSelector
[filter]="timespanFilter"
[showLabel]="false"
></m-dropdownSelector> -->
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, combineLatest, of } from 'rxjs';
import {
map,
distinctUntilChanged,
switchMap,
tap,
catchError,
} from 'rxjs/operators';
import { MindsHttpClient } from '../../../common/api/client.service';
import fakeData from './fake-data';
import { Response, UserState } from '../../../interfaces/dashboard';
// export interface UserState {}
let _state: UserState = fakeData[0];
const deepDiff = (prev, curr) => JSON.stringify(prev) === JSON.stringify(curr);
@Injectable()
export class WalletDashboardService {
constructor() {}
private store = new BehaviorSubject<UserState>(_state);
private state$ = this.store.asObservable();
// Make all the different variables within the UserState observables
// that are emitted only when something inside changes
category$ = this.state$.pipe(
map(state => state.category),
distinctUntilChanged(deepDiff)
);
description$ = this.state$.pipe(
map(state => state.description),
distinctUntilChanged()
);
timespan$ = this.state$.pipe(
map(state => state.timespan),
distinctUntilChanged(deepDiff)
);
timespans$ = this.state$.pipe(
map(state => state.timespans),
distinctUntilChanged(deepDiff)
);
metric$ = this.state$.pipe(
map(state => state.metric),
distinctUntilChanged(deepDiff)
);
metrics$ = this.state$.pipe(
map(state => state.metrics),
distinctUntilChanged(deepDiff)
);
filter$ = this.state$.pipe(
map(state => state.filter),
distinctUntilChanged(deepDiff)
);
filters$ = this.state$.pipe(
map(state => state.filters),
distinctUntilChanged(deepDiff)
);
loading$ = new BehaviorSubject<boolean>(false);
ready$ = new BehaviorSubject<boolean>(false);
/**
* Viewmodel that resolves once all the data is ready (or updated)
*/
vm$: Observable<UserState> = new BehaviorSubject(_state);
constructor(private http: MindsHttpClient) {
this.loadFromRemote();
}
loadFromRemote() {
combineLatest([this.category$, this.timespan$, this.metric$, this.filter$])
.pipe(
distinctUntilChanged(deepDiff),
catchError(_ => {
console.log('caught error');
return of(null);
}),
tap(() => this.loading$.next(true)),
switchMap(([category, timespan, metric, filter]) => {
// console.log(category, timespan, metric, filter);
try {
const response = this.getDashboardResponse(
category,
timespan,
metric,
filter
);
return response;
} catch (err) {
return null;
}
}),
catchError(_ => of(null))
)
.subscribe(response => {
if (!response) {
return;
}
const dashboard = response.dashboard;
this.ready$.next(true);
this.updateState({
..._state,
category: dashboard.category,
description: dashboard.description,
timespan: dashboard.timespan,
timespans: dashboard.timespans,
filter: dashboard.filter,
filters: dashboard.filters,
metric: dashboard.metric,
metrics: dashboard.metrics,
});
this.loading$.next(false);
});
}
// ------- Public Methods ------------------------
// Allows quick snapshot access to data for ngOnInit() purposes
getStateSnapshot(): UserState {
return {
..._state,
timespans: { ..._state.timespans },
metrics: { ..._state.metrics },
filters: { ..._state.filters },
};
}
// TODO: implement channel filter
// buildchannelSearchControl(): FormControl {
// const channelSearch = new FormControl();
// channelSearch.valueChanges
// .pipe(
// debounceTime(300),
// distinctUntilChanged()
// )
// .subscribe(value => this.updateSearchCriteria(value));
// return channelSearch;
// }
// TODOOJM uncomment me
updateCategory(category: string) {
// this.updateState({
// ..._state,
// category,
// description: null,
// metrics: [],
// });
}
updateTimespan(timespan: string) {
this.updateState({
..._state,
timespan,
});
}
updateMetric(metric: string) {
this.updateState({ ..._state, metric });
}
updateFilter(selectedFilterStr: string) {
if (_state.filter.includes(selectedFilterStr)) {
return;
}
const selectedFilterId = selectedFilterStr.split('::')[0];
const filter = _state.filter;
const activeFilterIds = filter.map(filterStr => {
return filterStr.split('::')[0];
});
const filterIndex = activeFilterIds.findIndex(
filterId => filterId === selectedFilterId
);
if (activeFilterIds.includes(selectedFilterId)) {
filter.splice(filterIndex, 1, selectedFilterStr);
} else {
filter.push(selectedFilterStr);
}
this.updateState({ ..._state, filter });
}
// // ------- Private Methods ------------------------
/** Update internal state cache and emit from store... */
private updateState(state: UserState) {
this.store.next((_state = state));
}
/** Dashboard REST call */
private getDashboardResponse(
category: string,
timespan: string,
metric: string,
filter: string[]
): Observable<Response> {
this.loading$.next(true);
return this.http
.get(`api/v2/analytics/dashboards/${category}`, {
metric,
timespan,
filter: filter.join(),
})
.pipe(
catchError(_ => of(null)),
map(response => response)
);
}
}
const fakeData: Array<any> = [
{
// CHART TESTS
loading: false,
category: 'wallet',
timespan: '30d',
timespans: [
{
id: '30d',
label: 'Last 30 days',
interval: 'day',
comparison_interval: 30,
from_ts_ms: 1567296000000,
from_ts_iso: '2019-09-01T00:00:00+00:00',
selected: true,
},
{
id: '12m',
label: 'Last 12 months',
interval: 'month',
comparison_interval: 365,
from_ts_ms: 1538352000000,
from_ts_iso: '2018-10-01T00:00:00+00:00',
selected: false,
},
],
filter: [],
filters: [],
metric: 'tokens',
metrics: [
{
id: 'tokens',
label: 'Tokens',
unit: 'tokens',
value: 1.450289,
visualisation: {
type: 'chart',
segments: [
{
buckets: [
{
key: 1567296000000,
date: '2019-09-01T00:00:00+00:00',
value: 11,
},
{
key: 1567382400000,
date: '2019-09-02T00:00:00+00:00',
value: 12,
},
{
key: 1567468800000,
date: '2019-09-03T00:00:00+00:00',
value: 13,
},
{
key: 1567555200000,
date: '2019-09-04T00:00:00+00:00',
value: 9,
},
{
key: 1567641600000,
date: '2019-09-05T00:00:00+00:00',
value: 1,
},
{
key: 1567296000000,
date: '2019-09-06T00:00:00+00:00',
value: 11,
},
{
key: 1567382400000,
date: '2019-09-07T00:00:00+00:00',
value: 12,
},
{
key: 1567468800000,
date: '2019-09-08T00:00:00+00:00',
value: 13,
},
{
key: 1567555200000,
date: '2019-09-09T00:00:00+00:00',
value: 9,
},
{
key: 1567641600000,
date: '2019-09-10T00:00:00+00:00',
value: 7,
},
{
key: 1567555200000,
date: '2019-09-11T00:00:00+00:00',
value: 9,
},
{
key: 1567641600000,
date: '2019-09-12T00:00:00+00:00',
value: 10.2,
},
],
},
],
},
},
{
id: 'active_users',
label: 'Active Users B',
permissions: ['admin', 'user'],
summary: {
current_value: 120962,
comparison_value: 120962,
comparison_interval: 28,
comparison_positive_inclination: true,
},
unit: 'number',
description:
'At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentiuti atque corrupti quos dolores',
visualisation: null,
},
{
id: 'signups',
label: 'Signups',
permissions: ['admin', 'user'],
summary: {
current_value: 53060,
comparison_value: 60577,
comparison_interval: 28,
comparison_positive_inclination: true,
},
unit: 'number',
description:
'At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentiuti atque corrupti quos dolores',
visualisation: null,
},
{
id: 'views',
label: 'Pageviews USD',
permissions: ['admin', 'user'],
summary: {
current_value: 83898,
comparison_value: 0,
comparison_interval: 28,
comparison_positive_inclination: true,
},
unit: 'usd',
visualisation: null,
},
],
},
];
export default fakeData;