...
 
Commits (2)
......@@ -115,7 +115,14 @@ import { ToggleComponent } from './components/toggle/toggle.component';
import { MarketingAsFeaturedInComponent } from './components/marketing/as-featured-in.component';
import { SidebarMenuComponent } from './components/sidebar-menu/sidebar-menu.component';
import { ChartV2Component } from './components/chart-v2/chart-v2.component';
import { MiniChartComponent } from './components/mini-chart/mini-chart.component';
import * as PlotlyJS from 'plotly.js/dist/plotly.js';
import { PlotlyModule } from 'angular-plotly.js';
import { PageLayoutComponent } from './components/page-layout/page-layout.component';
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';
PlotlyModule.plotlyjs = PlotlyJS;
@NgModule({
imports: [
......@@ -124,6 +131,7 @@ import { MiniChartComponent } from './components/mini-chart/mini-chart.component
RouterModule,
FormsModule,
ReactiveFormsModule,
PlotlyModule,
],
declarations: [
MINDS_PIPES,
......@@ -220,7 +228,10 @@ import { MiniChartComponent } from './components/mini-chart/mini-chart.component
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
ChartV2Component,
MiniChartComponent,
PageLayoutComponent,
DashboardLayoutComponent,
ShadowboxLayoutComponent,
ShadowboxHeaderComponent,
],
exports: [
MINDS_PIPES,
......@@ -312,6 +323,10 @@ import { MiniChartComponent } from './components/mini-chart/mini-chart.component
MarketingComponent,
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
ChartV2Component,
PageLayoutComponent,
DashboardLayoutComponent,
ShadowboxLayoutComponent,
],
providers: [
SiteService,
......
<!-- <div
<div
#chartContainer
class="m-chartV2__chartContainer"
[ngClass]="{ isTouchDevice: isTouchDevice }"
[ngClass]="{ isTouchDevice: isTouchDevice, isMini: isMini }"
>
<plotly-plot
*ngIf="init"
#graphDiv
id="graphDiv"
[data]="data"
[layout]="layout"
[config]="config"
......@@ -14,48 +12,70 @@
[style]="{ position: 'relative' }"
(hover)="onHover($event)"
(unhover)="onUnhover($event)"
(plotly_click)="onClick($event)"
id="graphDiv"
>
</plotly-plot>
</div>
<div #hoverInfoDiv id="hoverInfoDiv" class="m-chartV2__hoverInfoDiv">
<i *ngIf="isTouchDevice" class="material-icons" (click)="onUnhover($event)"
<div #hoverInfoDiv class="m-chartV2__hoverInfoDiv">
<i
*ngIf="isTouchDevice"
class="material-icons m-chartV2__hoverInfo__closeBtn"
(click)="onUnhover($event)"
>close</i
>
<div class="m-chartV2__hoverInfo__row">
{{ hoverInfo.date | utcDate | date: datePipe }}
</div>
<div
[ngSwitch]="selectedMetric?.unit"
class="m-chartV2__hoverInfo__row--primary"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.value | number }} {{ selectedMetric.label | lowercase }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.value | currency }} USD
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.value | number: '1.1-3' }} {{ selectedMetric?.unit }}
</ng-template>
</div>
<div class="m-chartV2__hoverInfo__row" *ngIf="isComparison">
vs
<ng-container
[ngSwitch]="selectedMetric?.unit"
class="m-chartV2__hoverInfo__row"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.comparisonValue | number }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.comparisonValue | currency }}
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.comparisonValue | number: '1.1-3' }}
</ng-template>
</ng-container>
on {{ hoverInfo.comparisonDate | utcDate | date: datePipe }}
<div class="m-chartV2__hoverInfo__wrapper">
<div class="m-chartV2__hoverInfo__arrowContainer" *ngIf="isMini">
<i class="material-icons">arrow_upward</i>
</div>
<div class="m-chartV2__hoverInfo__rowsContainer">
<div class="m-chartV2__hoverInfo__row">
{{ hoverInfo.date | utcDate | date: datePipe }}
</div>
<div
[ngSwitch]="rawData?.unit"
class="m-chartV2__hoverInfo__row--primary"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.value | number: '1.0-0' | abbr }}
{{ rawData.label | lowercase }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.value | currency }} USD
</ng-template>
<ng-template ngSwitchCase="eth">
{{ hoverInfo.value | number: '1.3-3' }} ETH
</ng-template>
<ng-template ngSwitchCase="tokens">
{{ hoverInfo.value | number: '1.1-3' }} Tokens
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.value | number: '1.0-3' }} {{ rawData?.unit }}
</ng-template>
</div>
<div class="m-chartV2__hoverInfo__row" *ngIf="isComparison">
vs
<ng-container
[ngSwitch]="rawData?.unit"
class="m-chartV2__hoverInfo__row"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.comparisonValue | number: '1.0-0' | abbr }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.comparisonValue | currency }}
</ng-template>
<ng-template ngSwitchCase="eth">
{{ hoverInfo.value | number: '1.3-3' }} ETH
</ng-template>
<ng-template ngSwitchCase="tokens">
{{ hoverInfo.value | number: '1.1-3' }} Tokens
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.comparisonValue | number: '1.1-3' }}
</ng-template>
</ng-container>
on {{ hoverInfo.comparisonDate | utcDate | date: datePipe }}
</div>
</div>
</div>
</div> -->
</div>
m-chartV2 {
display: block;
position: relative;
margin-left: 40px;
}
.js-plotly-plot,
.plot-container {
height: 44vh;
min-height: 44vh;
display: block;
}
#graphDiv {
display: block;
position: relative;
g,
g > * {
cursor: default;
}
> * {
transition: background-color 0.3s cubic-bezier(0.23, 1, 0.32, 1),
color 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}
.main-svg {
max-width: 100%;
}
}
.m-chartV2__hoverInfoDiv {
width: 160px;
padding: 12px;
position: absolute;
pointer-events: none;
border-radius: 3px;
font-size: 12px;
z-index: 9999999999;
opacity: 0;
transition: opacity 0.2s ease-in;
@include m-theme() {
background-color: themed($m-white);
box-shadow: 0 0 4px rgba(themed($m-black), 0.3);
color: themed($m-grey-300);
}
[class*='m-chartV2__hoverInfo__row'] {
padding-bottom: 4px;
font-weight: 300;
&:last-of-type {
padding-top: 2px;
}
}
.m-chartV2__hoverInfo__row--primary {
font-weight: 400;
font-size: 15px;
@include m-theme() {
color: themed($m-grey-600);
}
}
.m-chartV2__hoverInfo__closeBtn {
display: none;
font-size: 15px;
position: absolute;
cursor: pointer;
top: 10px;
right: 10px;
transition: color 0.3s cubic-bezier(0.23, 1, 0.32, 1);
@include m-theme() {
color: themed($m-grey-300);
}
&:active {
@include m-theme() {
color: themed($m-grey-500);
}
}
}
}
// ----------------------------------------------------
.isTouchDevice .m-chartV2__hoverInfoDiv .m-chartV2__hoverInfo__closeBtn {
display: block;
}
@media screen and (max-width: $min-tablet) {
m-chartV2 {
margin-left: 16px;
}
}
// ----------------------------------------------------
m-chartV2.isMini {
margin-left: 0;
margin-top: 24px;
.js-plotly-plot,
.plot-container {
height: 40px;
min-height: 40px;
}
.m-chartV2__chartContainer {
// margin-right: 24px;
}
.m-chartV2__hoverInfoDiv {
width: 150px;
padding: 0px;
.m-chartV2__hoverInfo__wrapper {
display: flex;
}
.m-chartV2__hoverInfo__rowsContainer {
display: flex;
flex-direction: column;
padding: 14px 14px 14px 0;
}
.m-chartV2__hoverInfo__arrowContainer {
width: 20px;
i {
margin-left: -4px;
transform: rotate(-45deg) scaleX(0.5);
@include m-theme() {
color: themed($m-grey-600);
}
}
}
[class*='m-chartV2__hoverInfo__row'] {
line-height: 1.1;
}
.m-chartV2__hoverInfo__row--primary {
font-size: 12px;
}
}
@media screen and (max-width: $min-tablet) {
margin-left: 0;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { UtcDatePipe } from '../../pipes/utcdate';
import { AbbrPipe } from '../../pipes/abbr';
import { MockService } from '../../../utils/mock';
import { ThemeService } from '../../services/theme.service';
import { ChartV2Component } from './chart-v2.component';
describe('ChartV2Component', () => {
......@@ -8,14 +12,46 @@ describe('ChartV2Component', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ChartV2Component],
declarations: [ChartV2Component, UtcDatePipe, AbbrPipe],
providers: [
{
provide: ThemeService,
useValue: MockService(ThemeService),
},
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChartV2Component);
component = fixture.componentInstance;
fixture.detectChanges();
component.rawData = {
id: 'views',
label: 'Pageviews',
permissions: ['admin', 'user'],
unit: 'usd',
description: '',
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,
},
],
},
],
},
};
});
it('should create', () => {
......
<div class="m-dashboardLayout__header">
<ng-content select="[m-dashboardLayout__header]"></ng-content>
</div>
<div class="m-dashboardLayout__body">
<ng-content select="[m-dashboardLayout__body]"></ng-content>
</div>
m-dashboardLayout {
display: block;
width: 100%;
max-width: 100%;
}
.m-dashboardLayout__header {
h3 {
font-size: 26px;
font-weight: 500;
margin-top: 0;
margin-right: 24px;
}
}
.m-dashboardLayout__body {
position: relative;
display: block;
width: 100%;
}
@media screen and (max-width: $min-tablet) {
m-dashboardLayout {
display: block;
padding: 0;
max-width: none;
width: 100%;
}
.m-dashboardLayout__header {
padding-left: 24px;
}
}
@media screen and (max-width: $max-mobile) {
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MiniChartComponent } from './mini-chart.component';
import { DashboardLayoutComponent } from './dashboard-layout.component';
describe('MiniChartComponent', () => {
let component: MiniChartComponent;
let fixture: ComponentFixture<MiniChartComponent>;
describe('DashboardLayoutComponent', () => {
let component: DashboardLayoutComponent;
let fixture: ComponentFixture<DashboardLayoutComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MiniChartComponent],
declarations: [DashboardLayoutComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MiniChartComponent);
fixture = TestBed.createComponent(DashboardLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
......
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'm-miniChart',
templateUrl: './mini-chart.component.html',
selector: 'm-dashboardLayout',
templateUrl: './dashboard-layout.component.html',
})
export class MiniChartComponent implements OnInit {
export class DashboardLayoutComponent implements OnInit {
constructor() {}
ngOnInit() {}
......
<p>
mini-chart works!
</p>
<m-sidebarMenu [catId]="navId"></m-sidebarMenu>
<section class="m-pageLayout__main">
<ng-content select="[m-pageLayout__main]"></ng-content>
</section>
m-pageLayout {
display: block;
position: relative;
width: 100%;
padding-top: 56px;
margin-bottom: 48px;
@include m-theme() {
background-color: themed($m-white);
color: themed($m-grey-800);
}
.m-tooltip {
margin-left: 4px;
i {
font-size: 12px;
@include m-theme() {
color: rgba(themed($m-grey-300), 0.7);
}
}
.m-tooltip--bubble {
z-index: 9999;
font-size: 11px;
@include m-theme() {
color: themed($m-white);
background-color: themed($m-blue);
}
}
}
}
m-sidebarMenu {
display: block;
box-sizing: border-box;
padding-left: 105px;
width: 245px;
@include m-theme() {
background-color: themed($m-white);
}
}
.m-pageLayout__main {
margin-left: 350px;
margin-right: 24px;
@include m-theme() {
background-color: themed($m-white);
color: themed($m-grey-800);
}
}
@media screen and (max-width: $min-tablet) {
.m-pageLayout__main {
display: block;
margin: 0;
}
m-sidebarMenu {
margin-left: 0;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, Input } from '@angular/core';
import { PageLayoutComponent } from './page-layout.component';
@Component({
selector: 'm-sidebarMenu',
template: '',
})
class SidebarMenuComponentMock {
@Input() catId;
}
describe('PageLayoutComponent', () => {
let component: PageLayoutComponent;
let fixture: ComponentFixture<PageLayoutComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [PageLayoutComponent, SidebarMenuComponentMock],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PageLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'm-pageLayout',
templateUrl: './page-layout.component.html',
})
export class PageLayoutComponent implements OnInit {
@Input() navId: string;
constructor() {}
ngOnInit() {}
}
<section class="m-shadowboxHeader__section">
<div class="m-shadowboxHeader__wrapper">
<ng-container *ngIf="isScrollable">
<div
*ngIf="isOverflown && !isAtScrollStart"
class="m-shadowboxHeader__overflowFade--left"
></div>
<div
[ngClass]="{ showButton: showButton.left }"
class="m-shadowboxHeader__overflowScrollButton--left"
(click)="slide('left')"
>
<i class="material-icons">chevron_left</i>
</div>
</ng-container>
<div
#shadowboxHeaderContainer
class="m-shadowboxHeader__container disable-scrollbars"
(scroll)="onScroll($event)"
>
<ng-content select=".m-shadowboxLayout__header"></ng-content>
</div>
<ng-container *ngIf="isScrollable">
<div
*ngIf="isOverflown && !isAtScrollEnd"
class="m-shadowboxHeader__overflowFade--right"
></div>
<div
[ngClass]="{ showButton: showButton.right }"
class="m-shadowboxHeader__overflowScrollButton--right"
(click)="slide('right')"
>
<i class="material-icons">chevron_right</i>
</div>
</ng-container>
</div>
</section>
m-shadowboxHeader {
min-height: 116px;
display: block;
}
.m-shadowboxHeader__section {
position: relative;
}
.m-shadowboxHeader__wrapper {
position: relative;
z-index: 1;
height: 124px;
@include m-theme() {
box-shadow: 0 7px 15px -7px rgba(themed($m-black-always), 0.1);
}
}
.m-shadowboxHeader__container {
overflow-x: hidden;
overflow-y: hidden;
// display: flex;
// flex-wrap: nowrap;
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
&.disable-scrollbars {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 10+ */
&::-webkit-scrollbar {
width: 0px;
background: transparent; /* Chrome/Safari/Webkit */
}
}
.m-tooltip--bubble {
width: 160px;
}
}
[class*='m-shadowboxHeader__overflowFade--'] {
position: absolute;
top: 0;
bottom: 0;
width: 24px;
z-index: 2;
&.m-shadowboxHeader__overflowFade--right {
@include m-theme() {
right: 0;
background: linear-gradient(
to right,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
&.m-shadowboxHeader__overflowFade--left {
@include m-theme() {
left: 0;
background: linear-gradient(
to left,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
}
[class*='m-shadowboxHeader__overflowScrollButton--'] {
position: absolute;
top: 50%;
border-radius: 50%;
box-sizing: border-box;
z-index: 2;
transform: translateY(-50%);
opacity: 0;
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
cursor: pointer;
&.showButton {
opacity: 1;
}
@include m-theme() {
background-color: themed($m-white);
box-shadow: 0px 0px 10px -3px rgba(themed($m-black-always), 0.3);
border: 1px solid themed($m-white);
}
&:hover {
@include m-theme() {
border: 1px solid themed($m-blue);
}
}
&.m-shadowboxHeader__overflowScrollButton--right {
right: -12;
}
&.m-shadowboxHeader__overflowScrollButton--left {
left: -12;
}
i {
@include m-theme() {
color: themed($m-grey-200);
}
}
}
@media screen and (max-width: $min-tablet) {
.m-shadowboxHeader__section {
[class*='m-shadowboxHeader__overflowScrollButton--'] {
display: none;
}
.m-shadowboxHeader__container {
overflow-x: scroll;
scroll-snap-type: x mandatory;
.m-analytics__metric {
scroll-snap-align: start;
&:first-child {
margin-left: 16px;
}
&:last-child {
margin-right: 16px;
}
}
}
}
.m-shadowboxHeader__wrapper {
@include m-theme() {
box-shadow: none;
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {
Component,
Input,
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
ViewChild,
ElementRef,
HostListener,
} from '@angular/core';
import { ShadowboxHeaderComponent } from './shadowbox-header.component';
describe('ShadowboxHeaderComponent', () => {
let component: ShadowboxHeaderComponent;
let fixture: ComponentFixture<ShadowboxHeaderComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ShadowboxHeaderComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ShadowboxHeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
Input,
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
ViewChild,
ElementRef,
HostListener,
} from '@angular/core';
@Component({
selector: 'm-shadowboxHeader',
templateUrl: './shadowbox-header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShadowboxHeaderComponent implements AfterViewInit {
@Input() isScrollable: boolean = true;
@Input() metricActivated;
@ViewChild('shadowboxHeaderContainer', { static: false })
shadowboxHeaderContainerEl: ElementRef;
shadowboxHeaderContainer;
childClientWidth: number;
faderWidth = 24;
isOverflown: boolean = false;
isAtScrollEnd = false;
isAtScrollStart = true;
showButton = { left: false, right: false };
constructor(private cd: ChangeDetectorRef) {}
ngAfterViewInit() {
this.checkOverflow();
// const activeMetric = ;//get the index of the metric with .active
// this.slideToActiveMetric();
}
// updateMetric(metric) {
// // TODO: if clicked metric is not fully visible, slide() until it is
// this.analyticsService.updateMetric(metric.id);
// }
// ----------------------------------------------------
@HostListener('click', ['$event.target'])
onClick(target) {
console.log('***Clicked on: ', target);
// this.slideToActiveMetric(metricIndex);
}
slideToActiveMetric(metricIndex) {
// TODOOJM
}
// ----------------------------------------------------
@HostListener('window:resize')
onResize() {
this.checkOverflow();
}
onScroll($event) {
this.checkOverflow();
}
checkOverflow() {
if (!this.isScrollable) {
return;
}
const firstMetric = <HTMLElement>(
document.querySelector('.m-shadowboxLayout__headerItem')
);
// TODO: figure out how to avoid test failure "Cannot read property 'clientWidth' of null"
this.childClientWidth = firstMetric ? firstMetric.clientWidth : 160;
this.shadowboxHeaderContainer = this.shadowboxHeaderContainerEl.nativeElement;
this.isOverflown =
this.shadowboxHeaderContainer.scrollWidth -
this.shadowboxHeaderContainer.clientWidth >
0;
this.isAtScrollStart =
this.shadowboxHeaderContainer.scrollLeft < this.faderWidth;
this.showButton.left = this.isOverflown && !this.isAtScrollStart;
this.isAtScrollEnd =
!this.isOverflown ||
this.shadowboxHeaderContainer.scrollWidth -
(this.shadowboxHeaderContainer.scrollLeft +
this.shadowboxHeaderContainer.clientWidth) <
this.faderWidth;
this.showButton.right =
this.isOverflown &&
this.shadowboxHeaderContainer.scrollLeft >= 0 &&
!this.isAtScrollEnd;
this.detectChanges();
}
slide(direction) {
let currentScrollLeft = this.shadowboxHeaderContainer.scrollLeft;
let targetScrollLeft;
let scrollEndOffset = 0;
const partiallyVisibleMetricWidth =
this.shadowboxHeaderContainer.clientWidth % this.childClientWidth;
const completelyVisibleMetricsWidth =
this.shadowboxHeaderContainer.clientWidth - partiallyVisibleMetricWidth;
if (direction === 'right') {
if (currentScrollLeft < this.faderWidth) {
currentScrollLeft = this.faderWidth;
}
targetScrollLeft = Math.min(
currentScrollLeft + completelyVisibleMetricsWidth,
this.shadowboxHeaderContainer.scrollWidth -
completelyVisibleMetricsWidth
);
} else {
if (this.isAtScrollEnd) {
scrollEndOffset = partiallyVisibleMetricWidth - this.faderWidth;
}
targetScrollLeft = Math.max(
currentScrollLeft - completelyVisibleMetricsWidth + scrollEndOffset,
0
);
}
this.shadowboxHeaderContainer.scrollTo({
top: 0,
left: targetScrollLeft,
behavior: 'smooth',
});
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
<m-shadowboxHeader
*ngIf="hasHeader"
[isScrollable]="scrollableHeader"
[ngClass]="{ isScrollable: scrollableHeader }"
><ng-content
select=".m-shadowboxLayout__header"
ngProjectAs=".m-shadowboxLayout__header"
></ng-content
></m-shadowboxHeader>
<div class="m-shadowboxLayout__bottom">
<ng-content select=".m-shadowboxLayout__body"></ng-content>
<ng-content select=".m-shadowboxLayout__footer"></ng-content>
</div>
m-shadowboxLayout {
display: block;
@include m-theme() {
box-shadow: 0px 0px 10px -3px rgba(themed($m-black-always), 0.3);
}
}
m-shadowboxHeader.isScrollable {
.m-shadowboxLayout__header {
display: flex;
flex-flow: row nowrap;
}
}
.m-shadowboxLayout__bottom {
position: relative;
@include m-theme() {
border-top: 1px solid rgba(themed($m-grey-50), 0.5);
background-color: themed($m-white);
}
}
.m-shadowboxLayout__footer {
min-height: 104px;
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
width: 100%;
@include m-theme() {
background-color: rgba(themed($m-grey-50), 0.25);
border-top: 1px solid rgba(themed($m-grey-50), 0.6);
}
> * {
margin: 30px 68px 30px 0;
}
}
@media screen and (max-width: $min-tablet) {
m-shadowboxLayout {
@include m-theme() {
box-shadow: none;
}
}
.m-shadowboxLayout__bottom {
@include m-theme() {
border-top: 1px solid themed($m-grey-100);
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, Input } from '@angular/core';
import { ShadowboxLayoutComponent } from './shadowbox-layout.component';
@Component({
selector: 'm-shadowboxHeader',
template: '',
})
class ShadowboxHeaderComponentMock {
@Input() isScrollable;
}
describe('ShadowboxLayoutComponent', () => {
let component: ShadowboxLayoutComponent;
let fixture: ComponentFixture<ShadowboxLayoutComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ShadowboxLayoutComponent, ShadowboxHeaderComponentMock],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ShadowboxLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'm-shadowboxLayout',
templateUrl: './shadowbox-layout.component.html',
})
export class ShadowboxLayoutComponent implements OnInit {
@Input() scrollableHeader: boolean = true;
@Input() hasHeader: boolean = true;
constructor() {}
ngOnInit() {}
}
const sidebarMenuCategories = [
{
header: {
id: 'analytics',
label: 'Analytics',
path: '/analytics/dashboard/',
permissions: ['admin', 'user'],
},
links: [
{
id: 'summary',
label: 'Summary',
permissions: ['admin'],
},
{
id: 'traffic',
label: 'Traffic',
permissions: ['admin', 'user'],
},
{
id: 'earnings',
label: 'Earnings',
permissions: ['admin', 'user'],
},
{
id: 'engagement',
label: 'Engagement',
permissions: ['admin', 'user'],
// path: '/some/path/outside/header/path',
},
{
id: 'trending',
label: 'Trending',
permissions: ['admin', 'user'],
},
// {
// id: 'referrers',
// label: 'Referrers',
// permissions: ['admin', 'user'],
// },
// {
// id: 'plus',
// label: 'Plus',
// permissions: ['admin'],
// },
// {
// id: 'pro',
// label: 'Pro',
// permissions: ['admin'],
// {
// id: 'boost',
// label: 'Boost',
// permissions: ['admin'],
// },
// {
// id: 'nodes',
// label: 'Nodes',
// permissions: ['admin'],
// },
],
},
];
export default sidebarMenuCategories;
<section class="m-sidebarMenu">
<div class="m-sidebarMenu__topbar">
<i class="material-icons" (click)="mobileMenuExpanded = true">menu</i>
<div class="m-sidebarMenu__topbarCatLabel" *ngIf="activeCat">
{{ activeCat.category.label }}
</div>
<div class="m-sidebarMenu__topbar">
<i class="material-icons" (click)="mobileMenuExpanded = true">menu</i>
<div class="m-sidebarMenu__topbarHeader">
{{ cat.header.label }}
</div>
<div
class="m-sidebarMenu__overlay"
[ngClass]="{ mobileMenuExpanded: mobileMenuExpanded }"
(click)="mobileMenuExpanded = false"
></div>
<div
class="m-sidebarMenu__sidebar"
[ngClass]="{ mobileMenuExpanded: mobileMenuExpanded }"
>
<a class="m-sidebarMenu__userWrapper" [routerLink]="['/', user.username]">
<img
class="m-sidebarMenu__userAvatar"
[src]="minds.cdn_url + 'icon/' + user.guid + '/small/' + user.icontime"
/>
<div class="m-sidebarMenu__userDetails">
<div class="m-sidebarMenu__userDetails__name">{{ user.name }}</div>
<div class="m-sidebarMenu__userDetails__username">
@{{ user.username }}
</div>
<!-- TODO: get subscriberCount and remove username -->
<!-- <div class="m-sidebarMenu__userDetails__subscribers">
</div>
<div
class="m-sidebarMenu__overlay"
[ngClass]="{ mobileMenuExpanded: mobileMenuExpanded }"
(click)="mobileMenuExpanded = false"
></div>
<div
class="m-sidebarMenu__sidebar"
[ngClass]="{ mobileMenuExpanded: mobileMenuExpanded }"
>
<a class="m-sidebarMenu__userWrapper" [routerLink]="['/', user.username]">
<img
class="m-sidebarMenu__userAvatar"
[src]="minds.cdn_url + 'icon/' + user.guid + '/medium/' + user.icontime"
/>
<div class="m-sidebarMenu__userDetails">
<div class="m-sidebarMenu__userDetails__name">{{ user.name }}</div>
<div class="m-sidebarMenu__userDetails__username">
@{{ user.username }}
</div>
<!-- TODO: get subscriberCount and remove username -->
<!-- <div class="m-sidebarMenu__userDetails__subscribers">
{{ user.subscribers_count | abbr }} subscribers
</div> -->
</div>
</a>
<ng-container *ngFor="let cat of cats">
<div
class="m-sidebarMenu__catContainer"
*ngIf="cat.category.permissionGranted"
[ngClass]="{ expanded: cat.category.expanded }"
>
<div class="m-sidebarMenu__catLabel">
<h3>{{ cat.category.label }}</h3>
<i
class="material-icons"
*ngIf="cat.category.expanded && cat.subcategories"
(click)="cat.category.expanded = false"
>keyboard_arrow_up</i
>
<i
class="material-icons"
*ngIf="!cat.category.expanded && cat.subcategories"
(click)="cat.category.expanded = true"
>keyboard_arrow_down</i
>
</div>
</div>
</a>
<!-- <ng-container *ngFor="let cat of cats"> -->
<div
class="m-sidebarMenu__catContainer expanded"
*ngIf="cat.header.permissionGranted"
>
<!-- [ngClass]="{ expanded: cat.header.expanded }" -->
<div class="m-sidebarMenu__header">
<h3>{{ cat.header.label }}</h3>
<!-- <i
class="material-icons"
*ngIf="cat.header.expanded && cat.links"
(click)="cat.header.expanded = false"
>keyboard_arrow_up</i
> -->
<!-- <i class="material-icons" *ngIf="!cat.header.expanded && cat.links"
>keyboard_arrow_down</i
> -->
<!-- (click)="cat.header.expanded = true" -->
</div>
<div class="m-sidebarMenu__subcatContainer" *ngIf="cat.subcategories">
<div
class="m-sidebarMenu__subcat"
*ngFor="let subcat of cat.subcategories"
>
<a
*ngIf="subcat.permissionGranted"
class="m-sidebarMenu__subcatLabel"
(click)="mobileMenuExpanded = false"
[routerLink]="'../' + subcat.id"
routerLinkActive="selected"
>{{ subcat.label }}</a
>
</div>
</div>
<nav class="m-sidebarMenu__linksContainer" *ngIf="cat.links">
<div class="m-sidebarMenu__link" *ngFor="let link of cat.links">
<a
*ngIf="link.permissionGranted"
(click)="mobileMenuExpanded = false"
[routerLink]="link.path ? '/' + link.path : '../' + link.id"
routerLinkActive="selected"
>{{ link.label }}</a
>
</div>
</ng-container>
</nav>
</div>
</section>
<!-- </ng-container> -->
</div>
m-sidebarMenu {
display: block;
// min-width: 180px;
// padding: 16px 16px 16px 80px;
// flex: 1 1 0px;
.m-sidebarMenu {
padding: 16px 16px 16px 80px;
}
i {
display: none;
cursor: pointer;
......@@ -15,31 +8,35 @@ m-sidebarMenu {
display: none;
}
.m-sidebarMenu__catContainer {
.m-sidebarMenu__subcatContainer {
.m-sidebarMenu__linksContainer {
display: block;
cursor: pointer;
}
}
// .m-sidebarMenu__sidebar {
// position: relative;
// position: -webkit-sticky;
// position: sticky;
// top: 0;
// }
.m-sidebarMenu__catLabel {
.m-sidebarMenu__sidebar {
position: fixed;
top: 157px;
}
.m-sidebarMenu__header {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin-top: 0;
font-size: 26px;
font-weight: 500;
}
i {
display: none;
}
}
.m-sidebarMenu__subcatContainer {
.m-sidebarMenu__linksContainer {
cursor: pointer;
display: none;
.m-sidebarMenu__subcat {
.m-sidebarMenu__link {
a {
display: block;
padding: 8px 0;
......@@ -63,12 +60,9 @@ m-sidebarMenu {
// --------------------------------------------------
@media screen and (max-width: $min-tablet) {
m-sidebarMenu {
margin-right: -32px;
// margin-right: -32px;
flex: 0 1 0px;
padding: 0;
.m-sidebarMenu {
padding: 0 8px;
}
.m-sidebarMenu__topbar {
display: block;
......@@ -94,7 +88,7 @@ m-sidebarMenu {
color: themed($m-grey-300);
}
}
.m-sidebarMenu__topbarCatLabel {
.m-sidebarMenu__topbarHeader {
font-size: 20px;
margin: 0 0 0 -24px;
min-height: 20px;
......@@ -134,13 +128,13 @@ m-sidebarMenu {
left: 0;
}
.m-sidebarMenu__catContainer {
.m-sidebarMenu__subcatContainer {
.m-sidebarMenu__linksContainer {
display: none;
}
&.expanded {
.m-sidebarMenu__subcatContainer {
.m-sidebarMenu__linksContainer {
display: block;
.m-sidebarMenu__subcat {
.m-sidebarMenu__link {
a {
padding: 6px 0;
}
......@@ -148,7 +142,7 @@ m-sidebarMenu {
}
}
}
.m-sidebarMenu__catLabel {
.m-sidebarMenu__header {
display: flex;
justify-content: space-between;
align-items: center;
......@@ -176,6 +170,9 @@ m-sidebarMenu {
.m-sidebarMenu__userAvatar {
border-radius: 50%;
margin-right: 16px;
height: 40px;
width: 40px;
object-fit: contain;
}
.m-sidebarMenu__userDetails {
& > {
......@@ -200,3 +197,10 @@ m-sidebarMenu {
}
}
}
@media screen and (min-width: 992px) {
m-sidebarMenu {
.m-sidebarMenu__sidebar {
top: 109px;
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, Input } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { Session } from '../../../services/session';
import { sessionMock } from '../../../../tests/session-mock.spec';
import { SidebarMenuComponent } from './sidebar-menu.component';
import sidebarMenuCategories from './sidebar-menu-categories.default';
describe('SidebarMenuComponent', () => {
let component: SidebarMenuComponent;
......@@ -19,6 +21,7 @@ describe('SidebarMenuComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(SidebarMenuComponent);
component = fixture.componentInstance;
component.catId = 'analytics';
// component.user = sessionMock.user;
fixture.detectChanges();
});
......
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, Input } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Session } from '../../../services/session';
import menuCategories from './categories.default';
import sidebarMenuCategories from './sidebar-menu-categories.default';
interface MenuCategory {
category: MenuLink;
subcategories?: MenuLink[];
header: MenuLink;
links?: MenuLink[];
expanded?: boolean;
}
export { MenuCategory };
......@@ -24,9 +24,11 @@ export { MenuLink };
templateUrl: './sidebar-menu.component.html',
})
export class SidebarMenuComponent implements OnInit {
cats: MenuCategory[] = menuCategories;
@Input() catId: string;
cat: MenuCategory;
mobileMenuExpanded = false;
activeCat;
// activeCat;
minds: Minds;
user;
userRoles: string[] = ['user'];
......@@ -36,6 +38,7 @@ export class SidebarMenuComponent implements OnInit {
ngOnInit() {
this.minds = window.Minds;
this.user = this.session.getLoggedInUser();
this.cat = sidebarMenuCategories.find(cat => cat.header.id === this.catId);
this.getUserRoles();
this.grantPermissionsAndFindActiveCat();
}
......@@ -48,25 +51,25 @@ export class SidebarMenuComponent implements OnInit {
}
grantPermissionsAndFindActiveCat() {
this.cats.forEach(catObj => {
catObj.category['permissionGranted'] = catObj.category.permissions
? this.checkForRoleMatch(catObj.category.permissions)
: true;
// this.cat.forEach(this.cat => {
this.cat.header['permissionGranted'] = this.cat.header.permissions
? this.checkForRoleMatch(this.cat.header.permissions)
: true;
if (catObj.subcategories) {
catObj.subcategories.forEach(subCat => {
subCat['permissionGranted'] = subCat.permissions
? this.checkForRoleMatch(subCat.permissions)
: true;
});
}
if (location.pathname.indexOf(catObj.category.path) !== -1) {
catObj.category['expanded'] = true;
this.activeCat = catObj;
} else {
catObj.category['expanded'] = false;
}
});
if (this.cat.links) {
this.cat.links.forEach(link => {
link['permissionGranted'] = link.permissions
? this.checkForRoleMatch(link.permissions)
: true;
});
}
// if (location.pathname.indexOf(this.cats.header.path) !== -1) {
// this.cats.header['expanded'] = true;
// this.activeCat = this.cat;
// } else {
// this.cat.header['expanded'] = false;
// }
// });
}
checkForRoleMatch(permissionsArray) {
......
......@@ -62,6 +62,7 @@ import { SearchModule } from '../search/search.module';
import { AnalyticsSearchComponent } from './v2/components/search/search.component';
import { FormsModule } from '@angular/forms';
import { AnalyticsSearchSuggestionsComponent } from './v2/components/search-suggestions/search-suggestions.component';
import { AnalyticsBenchmarkComponent } from './v2/components/benchmark/benchmark.component';
PlotlyModule.plotlyjs = PlotlyJS;
......@@ -169,6 +170,7 @@ const routes: Routes = [
AnalyticsTableComponent,
AnalyticsSearchComponent,
AnalyticsSearchSuggestionsComponent,
AnalyticsBenchmarkComponent,
],
providers: [AnalyticsDashboardService],
})
......
<div class="m-analytics__benchmarkContainer" [ngClass]="{ noChart: noChart }">
<div class="m-analytics__benchmarkLabelWrapper">
<div class="m-analytics__benchmarkLabel">{{ label }}</div>
<m-tooltip icon="help" *ngIf="description">
{{ description }}
</m-tooltip>
</div>
<div class="m-analytics__benchmarkValueWrapper">
<ng-container
[ngSwitch]="unit"
*ngIf="isNumber(value); else placeholderValue"
>
<ng-template ngSwitchCase="usd">
<div class="m-analytics__benchmarkValue">
{{ value / 100 | currency }}
</div>
<div class="m-analytics__benchmarkUnit">USD</div>
</ng-template>
<ng-template ngSwitchCase="eth">
<div class="m-analytics__benchmarkValue">
{{ value | number: '1.3-3' }}
</div>
<div class="m-analytics__benchmarkUnit">ETH</div>
</ng-template>
<ng-template ngSwitchCase="tokens">
<div class="m-analytics__benchmarkValue">
{{ value | number: '1.3-3' }}
</div>
<div class="m-analytics__benchmarkUnit">
{{ value !== 1 ? 'Tokens' : 'Token' }}
</div>
</ng-template>
<ng-template ngSwitchCase="hours">
<div class="m-analytics__benchmarkValue">
{{ value | number: '1.0-0' }}
</div>
<div class="m-analytics__benchmarkUnit">
{{ value !== 1 ? 'hrs' : 'hr' }}
</div>
</ng-template>
<ng-template ngSwitchDefault>
<div class="m-analytics__benchmarkValue">
{{ value | number: '1.0-0' }}
</div>
</ng-template>
</ng-container>
</div>
</div>
<!-- TODO: delete this when active users placeholder is removed -->
<ng-template #placeholderValue>
<div class="m-analytics__benchmarkValue">{{ value }}</div>
</ng-template>
<!-- <div [ngSwitch]="unit" class="m-chartV2__hoverInfo__row--primary">
<ng-template ngSwitchCase="number">
{{ hoverInfo.value | number: '1.0-0' | abbr }}
{{ rawData.label | lowercase }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.value | currency }} USD
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.value | number: '1.1-3' }} {{ rawData?.unit }}
</ng-template>
</div>
</div> -->
.m-analytics__benchmarkValueWrapper > div {
display: inline-block;
}
.m-analytics__benchmarkLabelWrapper {
padding-bottom: 12px;
min-width: 190px;
& > div {
display: inline-block;
}
}
.m-analytics__benchmarkLabel {
font-size: 18px;
@include m-theme() {
color: themed($m-grey-300);
}
}
.m-analytics__benchmarkValue {
font-size: 24px;
@include m-theme() {
color: themed($m-grey-800);
}
}
.m-analytics__benchmarkUnit {
font-size: 15px;
padding-left: 6px;
}
.m-analytics__benchmarkContainer {
&.noChart {
.m-analytics__benchmarkValue {
font-size: 42px;
}
.m-analytics__benchmarkLabelWrapper {
padding-bottom: 18px;
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TooltipComponentMock } from '../../../../../mocks/common/components/tooltip/tooltip.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { AnalyticsBenchmarkComponent } from './benchmark.component';
describe('AnalyticsBenchmarkComponent', () => {
let component: AnalyticsBenchmarkComponent;
let fixture: ComponentFixture<AnalyticsBenchmarkComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsBenchmarkComponent, TooltipComponentMock],
// schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsBenchmarkComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'm-analytics__benchmark',
templateUrl: './benchmark.component.html',
})
export class AnalyticsBenchmarkComponent implements OnInit {
@Input() label: string;
@Input() description: string;
@Input() value: string | number;
@Input() unit: string;
@Input() noChart: boolean = false;
isCurrency: boolean = false;
constructor() {}
ngOnInit() {
if (this.unit && (this.unit === 'eth' || this.unit === 'usd')) {
this.isCurrency = true;
}
}
isNumber(val) {
return typeof val === 'number';
}
}
<!-- TODO: Make this into a different component -->
<!-- TODO: then all this becomes m-plotlyChart -->
<div
#chartContainer
class="m-analyticsChart__chartContainer"
[ngClass]="{ isTouchDevice: isTouchDevice }"
>
<plotly-plot
*ngIf="init"
#graphDiv
id="graphDiv"
[data]="data"
[layout]="layout"
[config]="config"
[style]="{ position: 'relative' }"
[useResizeHandler]="true"
(hover)="onHover($event)"
(unhover)="onUnhover($event)"
(plotly_click)="onClick($event)"
>
</plotly-plot>
</div>
<div #hoverInfoDiv id="hoverInfoDiv" class="m-analyticsChart__hoverInfoDiv">
<i *ngIf="isTouchDevice" class="material-icons" (click)="onUnhover($event)"
>close</i
>
<div class="m-analyticsChart__hoverInfo__row">
{{ hoverInfo.date | utcDate | date: datePipe }}
</div>
<div
[ngSwitch]="selectedMetric?.unit"
class="m-analyticsChart__hoverInfo__row--primary"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.value | number }} {{ selectedMetric.label | lowercase }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.value | currency }} USD
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.value | number: '1.1-3' }} {{ selectedMetric?.unit }}
</ng-template>
</div>
<div class="m-analyticsChart__hoverInfo__row" *ngIf="isComparison">
vs
<ng-container
[ngSwitch]="selectedMetric?.unit"
class="m-analyticsChart__hoverInfo__row"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.comparisonValue | number }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.comparisonValue | currency }}
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.comparisonValue | number: '1.1-3' }}
</ng-template>
</ng-container>
on {{ hoverInfo.comparisonDate | utcDate | date: datePipe }}
</div>
</div>
<m-chartV2
*ngIf="selectedMetric && selectedTimespan"
[rawData]="selectedMetric"
[interval]="selectedTimespan.interval"
></m-chartV2>
m-analytics__chart {
display: block;
position: relative;
margin-left: 40px;
.js-plotly-plot,
.plot-container {
......
......@@ -14,25 +14,14 @@ m-analytics__filter {
@include m-theme() {
color: themed($m-grey-300);
}
m-tooltip {
margin-left: 4px;
}
> * {
display: inline-block;
}
.m-tooltip {
i {
font-size: 12px;
@include m-theme() {
color: rgba(themed($m-grey-300), 0.7);
}
}
}
.m-tooltip--bubble {
letter-spacing: 1.2px;
line-height: 16px;
z-index: 9999;
font-size: 11px;
bottom: 110%;
left: 0;
width: 160px;
......
......@@ -2,11 +2,8 @@ import {
Component,
OnInit,
Input,
Output,
EventEmitter,
ChangeDetectionStrategy,
} from '@angular/core';
import { Observable } from 'rxjs';
import {
AnalyticsDashboardService,
Filter,
......
......@@ -12,6 +12,6 @@ m-analytics__filters {
@media screen and (max-width: $min-tablet) {
.m-analytics__filtersContainer {
margin-left: 16px;
margin-left: 24px;
}
}
<section class="m-analytics__metricsSection">
<div class="m-analytics__metricsWrapper" #metricsWrapper>
<ng-container *ngIf="metrics$ | async as metrics">
<ng-container *ngFor="let metric of metrics">
<div
*ngIf="isOverflown && !isAtScrollStart"
class="m-analytics__metricsOverflowFade--left"
></div>
<div
[ngClass]="{ showButton: showButton.left }"
class="m-analytics__metricsOverflowScrollButton--left"
(click)="slide('left')"
>
<i class="material-icons">chevron_left</i>
</div>
<div
#metricsContainer
*ngIf="metrics$ | async as metrics"
class="m-analytics__metricsContainer disable-scrollbars"
(scroll)="onScroll($event)"
>
<ng-container *ngFor="let metric of metrics">
<div
class="m-analytics__metric"
(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>
</div>
<div
*ngIf="isOverflown && !isAtScrollEnd"
class="m-analytics__metricsOverflowFade--right"
></div>
<div
[ngClass]="{ showButton: showButton.right }"
class="m-analytics__metricsOverflowScrollButton--right"
(click)="slide('right')"
>
<i class="material-icons">chevron_right</i>
</div>
</div>
</section>
<!--
<section class="m-analytics__metricsSection">
<div
*ngIf="isOverflown && !isAtScrollStart"
class="m-analytics__metricsOverflowFade--left"
></div>
<div
[ngClass]="{ showButton: showButton.left }"
class="m-analytics__metricsOverflowScrollButton--left"
(click)="slide('left')"
>
<i class="material-icons">chevron_left</i>
</div>
<div class="m-analytics__metricsWrapper" #metricsWrapper>
<div
#metricsContainer
*ngIf="metrics$ | async as metrics"
class="m-analytics__metricsContainer disable-scrollbars"
(scroll)="onScroll($event)"
class="m-analytics__metric m-shadowboxLayout__headerItem"
(click)="updateMetric(metric)"
[ngClass]="{ active: metric.visualisation }"
*ngIf="metric.permissionGranted"
>
<ng-container *ngFor="let metric of metrics">
<div
class="m-analytics__metric"
(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 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>
<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>
</div>
<div
*ngIf="isOverflown && !isAtScrollEnd"
class="m-analytics__metricsOverflowFade--right"
></div>
<div
[ngClass]="{ showButton: showButton.right }"
class="m-analytics__metricsOverflowScrollButton--right"
(click)="slide('right')"
>
<i class="material-icons">chevron_right</i>
</div>
</section> -->
</ng-container>
</ng-container>
m-analytics__metrics {
display: block;
.m-analytics__metricsSection {
position: relative;
[class*='m-analytics__metricsOverflowFade--'] {
position: absolute;
top: 0;
bottom: 0;
width: 24px;
z-index: 2;
&.m-analytics__metricsOverflowFade--right {
@include m-theme() {
right: 0;
background: linear-gradient(
to right,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
&.m-analytics__metricsOverflowFade--left {
@include m-theme() {
left: 0;
background: linear-gradient(
to left,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
}
[class*='m-analytics__metricsOverflowScrollButton--'] {
position: absolute;
top: 50%;
border-radius: 50%;
box-sizing: border-box;
z-index: 2;
transform: translateY(-50%);
opacity: 0;
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
&.showButton {
opacity: 1;
}
cursor: pointer;
@include m-theme() {
background-color: themed($m-white);
box-shadow: 0px 0px 10px -3px rgba(themed($m-black-always), 0.3);
border: 1px solid themed($m-white);
}
&:hover {
@include m-theme() {
border: 1px solid themed($m-blue);
}
}
&.m-analytics__metricsOverflowScrollButton--right {
right: -12;
}
&.m-analytics__metricsOverflowScrollButton--left {
left: -12;
}
i {
@include m-theme() {
color: themed($m-grey-200);
}
}
}
.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);
}
}
.m-analytics__metricsWrapper {
position: relative;
z-index: 1;
@include m-theme() {
box-shadow: 0 7px 15px -7px rgba(themed($m-black-always), 0.1);
color: themed($m-grey-300);
}
}
.m-analytics__metricsContainer {
overflow-x: hidden;
overflow-y: hidden;
display: flex;
flex-wrap: nowrap;
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
&.disable-scrollbars {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 10+ */
&::-webkit-scrollbar {
width: 0px;
background: transparent; /* Chrome/Safari/Webkit */
&.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;
}
.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;
&:hover:not(.active) {
transition: background-color 0.3s cubic-bezier(0.23, 1, 0.32, 1);
@include m-theme() {
border-bottom: 8px solid themed($m-white);
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-300);
color: themed($m-grey-800);
}
&.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;
margin-left: 4px;
}
.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;
}
.m-analytics__metricDelta {
display: flex;
align-items: baseline;
padding-top: 4px;
font-size: 11px;
.material-icons {
transform: scaleX(0.7);
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);
}
}
font-weight: bold;
}
}
.m-tooltip {
i {
font-size: 12px;
@include m-theme() {
color: rgba(themed($m-grey-300), 0.7);
@include m-theme() {
&.goodChange {
color: themed($m-green);
}
}
.m-tooltip--bubble {
z-index: 9999;
font-size: 11px;
width: 160px;
@include m-theme() {
color: themed($m-white);
background-color: themed($m-blue);
&.badChange {
color: themed($m-red);
}
}
}
}
@media screen and (max-width: $min-tablet) {
.m-analytics__metricsSection {
[class*='m-analytics__metricsOverflowScrollButton--'] {
display: none;
.m-analytics__metric {
scroll-snap-align: start;
&:first-child {
margin-left: 16px;
}
.m-analytics__metricsContainer {
overflow-x: scroll;
position: relative;
scroll-snap-type: x mandatory;
.m-analytics__metric {
scroll-snap-align: start;
&:first-child {
margin-left: 16px;
}
&:last-child {
margin-right: 16px;
}
}
&:last-child {
margin-right: 16px;
}
}
}
import {
Component,
OnInit,
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
ViewChild,
ElementRef,
HostListener,
} from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
......@@ -28,31 +19,16 @@ export { MetricExtended as Metric };
@Component({
selector: 'm-analytics__metrics',
templateUrl: './metrics.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsMetricsComponent implements OnInit, AfterViewInit {
@ViewChild('metricsContainer', { static: false })
metricsContainerEl: ElementRef;
metricsContainer;
data;
export class AnalyticsMetricsComponent implements OnInit {
subscription: Subscription;
user;
userRoles: string[] = ['user'];
init = false;
metrics$;
metricClientWidth: number;
faderWidth = 24;
isOverflown: boolean = false;
isAtScrollEnd = false;
isAtScrollStart = true;
showButton = { left: false, right: false };
constructor(
private analyticsService: AnalyticsDashboardService,
public session: Session,
private cd: ChangeDetectorRef
public session: Session
) {}
ngOnInit() {
......@@ -98,86 +74,10 @@ export class AnalyticsMetricsComponent implements OnInit, AfterViewInit {
return metrics;
})
);
// TODO: if selected metric is not fully visible, slide() until it is
}
ngAfterViewInit() {
this.checkOverflow();
}
updateMetric(metric) {
// TODO: if clicked metric is not fully visible, slide() until it is
this.analyticsService.updateMetric(metric.id);
}
@HostListener('window:resize')
onResize() {
this.checkOverflow();
}
onScroll($event) {
this.checkOverflow();
}
checkOverflow() {
const firstMetric = document.querySelector('.m-analytics__metric');
this.metricClientWidth = firstMetric.clientWidth;
this.metricsContainer = this.metricsContainerEl.nativeElement;
this.isOverflown =
this.metricsContainer.scrollWidth - this.metricsContainer.clientWidth > 0;
this.isAtScrollStart = this.metricsContainer.scrollLeft < this.faderWidth;
this.showButton.left = this.isOverflown && !this.isAtScrollStart;
this.isAtScrollEnd =
!this.isOverflown ||
this.metricsContainer.scrollWidth -
(this.metricsContainer.scrollLeft + this.metricsContainer.clientWidth) <
this.faderWidth;
this.showButton.right =
this.isOverflown &&
this.metricsContainer.scrollLeft >= 0 &&
!this.isAtScrollEnd;
this.detectChanges();
}
slide(direction) {
let currentScrollLeft = this.metricsContainer.scrollLeft;
let targetScrollLeft;
let scrollEndOffset = 0;
const partiallyVisibleMetricWidth =
this.metricsContainer.clientWidth % this.metricClientWidth;
const completelyVisibleMetricsWidth =
this.metricsContainer.clientWidth - partiallyVisibleMetricWidth;
if (direction === 'right') {
if (currentScrollLeft < this.faderWidth) {
currentScrollLeft = this.faderWidth;
}
targetScrollLeft = Math.min(
currentScrollLeft + completelyVisibleMetricsWidth,
this.metricsContainer.scrollWidth - completelyVisibleMetricsWidth
);
} else {
if (this.isAtScrollEnd) {
scrollEndOffset = partiallyVisibleMetricWidth - this.faderWidth;
}
targetScrollLeft = Math.max(
currentScrollLeft - completelyVisibleMetricsWidth + scrollEndOffset,
0
);
}
this.metricsContainer.scrollTo({
top: 0,
left: targetScrollLeft,
behavior: 'smooth',
});
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
......@@ -167,6 +167,7 @@
.m-analyticsTable__wrapper {
.m-analyticsTable__col--entity {
flex: 3 1 0;
padding-left: 24px;
.m-analyticsTable__entityType,
.m-analyticsTable__entityPublishDate {
display: none;
......
<div class="m-analytics__dashboard">
<m-sidebarMenu></m-sidebarMenu>
<section class="m-analytics__main" *ngIf="ready$ | async">
<div class="m-analytics__mainHeader">
<div class="m-analytics__dashboardTitle">
<h3 class="m-analytics__selectedCatLabel">
{{ category$ | async | titlecase }}
</h3>
</div>
<div class="m-analytics__globalFilters">
<!-- <div *ngIf="session.isAdmin()" class="m-analytics__channelSearch"> -->
<!-- <div class="m-analytics__channelSearch">
<m-analytics__search></m-analytics__search>
</div> -->
<!-- <div class="m-analytics__channelFilter" *ngIf="session.isAdmin()">
<m-analytics__filter [filter]="channelFilter"></m-analytics__filter>
</div> -->
<div class="m-analytics__timespanFilter">
<m-analytics__filter
[filter]="timespanFilter"
[showLabel]="false"
></m-analytics__filter>
<m-pageLayout navId="analytics">
<div class="m-analyticsDashboard" *ngIf="ready$ | async" m-pageLayout__main>
<m-dashboardLayout>
<ng-container m-dashboardLayout__header>
<div>
<h3>
{{ category$ | async | titlecase }}
</h3>
</div>
</div>
</div>
<p
class="m-analytics__selectedCatDescription"
*ngIf="description$ | async as description"
>
{{ description }}
<ng-container *ngIf="(category$ | async) === 'earnings'">
<a *ngIf="!session.getLoggedInUser().pro" routerLink="/pro"
>Upgrade to PRO</a
<div
class="m-analyticsDashboard__headerFilters"
*ngIf="layout === 'chart'"
>
<a
*ngIf="
session.getLoggedInUser().pro &&
!(
session.getLoggedInUser().merchant &&
session.getLoggedInUser().merchant['id']
)
"
routerLink="/wallet/usd"
>Enable payouts</a
>.
<!-- <div *ngIf="session.isAdmin()" class="m-analytics__channelSearch"> -->
<!-- <div class="m-analyticsDashboard__channelSearch">
<m-analytics__search></m-analytics__search>
</div> -->
<!-- <div class="m-analyticsDashboard__channelFilter" *ngIf="session.isAdmin()">
<m-analytics__filter [filter]="channelFilter"></m-analytics__filter>
</div> -->
<div class="m-analyticsDashboard__timespanFilter">
<m-analytics__filter
[filter]="timespanFilter"
[showLabel]="false"
></m-analytics__filter>
</div>
</div>
</ng-container>
</p>
<div class="m-analytics__layoutWrapper">
<p
m-dashboardLayout__body
class="m-analyticsDashboard__description"
*ngIf="description$ | async as description"
>
{{ description }}
<ng-container *ngIf="(category$ | async) === 'earnings'">
<a *ngIf="!session.getLoggedInUser().pro" routerLink="/pro"
>Upgrade to PRO</a
>
<a
*ngIf="
session.getLoggedInUser().pro &&
!(
session.getLoggedInUser().merchant &&
session.getLoggedInUser().merchant['id']
)
"
routerLink="/wallet/usd"
>Enable payouts</a
>
</ng-container>
</p>
<m-analytics__layout--chart
m-dashboardLayout__body
*ngIf="layout === 'chart'"
class="m-analytics__layout"
></m-analytics__layout--chart>
<!-- <m-analytics__layout--summary
class="m-analytics__layout"
*ngIf="(category$ | async).type === 'summary'"
></m-analytics__layout--summary> -->
</div>
</section>
</div>
<m-analytics__layout--summary
m-dashboardLayout__body
*ngIf="layout === 'summary'"
class="m-analytics__layout"
></m-analytics__layout--summary>
</m-dashboardLayout>
</div>
</m-pageLayout>
......@@ -4,55 +4,22 @@ m-analytics__dashboard {
width: 100%;
}
.m-analytics__dashboard {
padding: 16px;
// display: flex;
// box-sizing: border-box;
// flex-direction: row;
.m-analytics__spinnerContainer {
height: 30%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
@include m-theme() {
background-color: themed($m-white);
color: themed($m-grey-800);
}
h3 {
font-size: 26px;
font-weight: 500;
margin: 24px 16px 24px 0;
}
}
m-sidebarMenu {
display: inline-block;
vertical-align: top;
width: 29%;
}
.m-analytics__main {
display: inline-block;
width: 65%;
// flex: 4 1 0px;
padding: 16px;
@include m-theme() {
background-color: themed($m-white);
}
}
.m-analytics__mainHeader {
.m-dashboardLayout__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
m-analytics__filter {
margin: 0;
.m-analyticsFilter__wrapper {
> * {
width: 180px;
}
}
}
.m-analytics__globalFilters {
margin: 24px 0;
.m-analyticsDashboard__headerFilters {
display: flex;
align-items: baseline;
.m-analytics__channelSearch {
......@@ -62,18 +29,25 @@ m-sidebarMenu {
color: themed($m-grey-100);
}
}
.m-analytics__timespanFilter {
.m-analytics__filterWrapper {
margin-top: 0px;
}
.m-analytics__filterLabel {
display: none;
}
}
m-analytics__filter {
margin: 0;
}
.m-analyticsFilter__wrapper {
> * {
width: 180px;
}
}
.m-analyticsDashboard__timespanFilter {
.m-analytics__filterWrapper {
margin-top: 0px;
}
.m-analytics__filterLabel {
display: none;
}
}
}
.m-analytics__selectedCatDescription {
.m-analyticsDashboard__description {
margin: 8px 16px 32px 0;
font-weight: 400;
@include m-theme() {
......@@ -87,68 +61,28 @@ m-sidebarMenu {
}
}
}
.m-analytics__layoutWrapper {
@include m-theme() {
box-shadow: 0px 0px 10px -3px rgba(themed($m-black-always), 0.3);
}
}
.m-analytics__layout {
position: relative;
display: block;
width: 100%;
}
@media screen and (max-width: $min-tablet) {
m-analytics__dashboard {
.m-analytics__dashboard {
display: block;
padding: 0;
// flex-direction: column;
.m-analytics__main {
padding: 0;
max-width: none;
width: 100%;
.m-analytics__mainHeader {
padding-left: 24px;
m-analytics__filter {
margin: 0 32px 8px 0;
.m-analyticsFilter__wrapper {
> * {
width: 160px;
}
}
}
}
.m-analytics__selectedCatDescription {
margin: 0 24px 24px 24px;
}
.m-analytics__layoutWrapper {
padding: 0;
@include m-theme() {
box-shadow: none;
}
.m-dashboardLayout__header {
m-analytics__filter {
margin: 0 16px 8px 0;
.m-analyticsFilter__wrapper {
> * {
width: 160px;
}
}
}
}
.m-analyticsDashboard__description {
margin: 0 24px 24px 24px;
}
}
// :(
@media screen and (max-width: $max-mobile) {
m-analytics__dashboard {
.m-analytics__dashboard {
.m-analytics__main {
.m-analytics__mainHeader {
m-analytics__filter {
.m-analyticsFilter__wrapper {
> * {
width: 140px;
}
}
}
}
.m-dashboardLayout__header {
.m-analyticsFilter__wrapper {
> * {
width: 140px;
}
}
}
......
......@@ -38,6 +38,7 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
options: [],
};
channelFilter: Filter;
layout = 'chart';
constructor(
public client: Client,
......@@ -58,7 +59,13 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
this.title.setTitle('Analytics');
this.route.paramMap.subscribe((params: ParamMap) => {
this.updateCategory(params.get('category'));
const cat = params.get('category');
this.updateCategory(cat);
if (cat === 'summary') {
this.layout = 'summary';
} else {
this.layout = 'chart';
}
});
this.paramsSubscription = this.route.queryParams.subscribe(params => {
......
......@@ -6,11 +6,7 @@ import {
map,
distinctUntilChanged,
switchMap,
startWith,
tap,
delay,
debounceTime,
throttleTime,
catchError,
} from 'rxjs/operators';
......
This diff is collapsed.
<div class="m-analytics__spinnerContainer" *ngIf="loading$ | async">
<div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div>
</div>
<ng-container *ngIf="selectedMetric && selectedMetric.visualisation">
<m-shadowboxLayout
*ngIf="selectedMetric && selectedMetric.visualisation"
[hasHeader]="selectedMetric.visualisation.type === 'chart'"
>
<m-analytics__metrics
class="m-shadowboxLayout__header"
*ngIf="selectedMetric.visualisation.type === 'chart'"
></m-analytics__metrics>
<div
class="m-analytics__filterableChartWrapper"
class="m-shadowboxLayout__body"
[ngClass]="{ isTable: isTable, isMobile: isMobile }"
>
<m-analytics__chart
......@@ -19,4 +23,4 @@
*ngIf="selectedMetric.visualisation.type === 'chart'"
></m-analytics__filters>
</div>
</ng-container>
</m-shadowboxLayout>
.m-analytics__filterableChartWrapper {
position: relative;
margin-bottom: 48px;
@include m-theme() {
border-top: 1px solid rgba(themed($m-grey-50), 0.5);
background-color: themed($m-white);
}
.m-shadowboxLayout__body {
&.isTable {
width: 100%;
min-width: 420px;
}
}
.m-analytics__spinnerContainer {
height: 30%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
@include m-theme() {
background-color: themed($m-white);
}
}
<p>
analytics__layout--summary works!
</p>
<ng-container m-pageLayout__main>
<div
class="m-analytics__spinnerContainer"
*ngIf="loading && this.session.isAdmin()"
>
<div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div>
</div>
<!-- TILES CONTAINER -->
<div
class="m-analyticsSummary__tilesContainer"
*ngIf="!loading && this.session.isAdmin()"
>
<!-- ACTIVE USERS TILE (TEMP PLACEHOLDER) -->
<div class="m-analyticsSummary__tile">
<m-analytics__benchmark
[label]="'Active Users On Site'"
[description]="
'Coming soon! Realtime count of all users on web and mobile'
"
[value]="'🚧'"
[noChart]="true"
></m-analytics__benchmark>
</div>
<!-- CHART TILES -->
<ng-container *ngFor="let tile of tiles">
<div class="m-analyticsSummary__tile">
<m-analytics__benchmark
[label]="tile.label"
[description]="tile.description"
[value]="tile.value"
[unit]="tile?.unit"
[noChart]="!tile.visualisation"
></m-analytics__benchmark>
<m-chartV2
*ngIf="tile.visualisation"
[rawData]="tile"
[interval]="day"
[isMini]="true"
></m-chartV2>
</div>
</ng-container>
</div>
<!-- BOOST BACKLOG -->
<div class="m-analyticsSummary__boostBacklogWrapper" *ngIf="boosts">
<div class="m-analyticsSummary__boostBacklogTitle">Boost Backlog</div>
<div class="m-analyticsSummary__boostRowsContainer">
<ng-container *ngFor="let boostRow of boostRows">
<div class="m-analyticsSummary__boostRow">
<ng-container *ngFor="let boostType of boostRow">
<div class="m-analyticsSummary__boostType">
<m-analytics__benchmark
[label]="boostType.label"
[value]="boostType.value"
[unit]="'hours'"
></m-analytics__benchmark>
</div>
</ng-container>
</div>
</ng-container>
</div>
</div>
</ng-container>