...
 
Commits (2)
......@@ -7,17 +7,14 @@
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"favicon.ico"
],
"assets": ["assets", "favicon.ico"],
"index": "index.php",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"prefix": "m",
"styles": [
"../node_modules/material-design-lite/dist/material.blue_grey-amber.min.css",
"../node_modules/material-design-icons/iconfont/material-icons.css",
......
......@@ -16,10 +16,7 @@
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"assets": [
"src/assets",
"src/favicon.ico"
],
"assets": ["src/assets", "src/favicon.ico"],
"styles": [
"node_modules/material-design-lite/dist/material.blue_grey-amber.min.css",
"node_modules/material-design-icons/iconfont/material-icons.css",
......@@ -36,10 +33,10 @@
"optimization": true,
"outputHashing": "all",
"sourceMap": {
"hidden": true,
"scripts": true,
"styles": true
},
"hidden": true,
"scripts": true,
"styles": true
},
"extractCss": true,
"namedChunks": false,
"aot": true,
......@@ -83,22 +80,14 @@
"node_modules/medium-editor/dist/js/medium-editor.min.js",
"src/shims/jitsi-api.min.js"
],
"assets": [
"src/assets",
"src/favicon.ico"
]
"assets": ["src/assets", "src/favicon.ico"]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
"tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}
}
......@@ -118,12 +107,8 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"e2e/tsconfig.e2e.json"
],
"exclude": [
"**/node_modules/**"
]
"tsConfig": ["e2e/tsconfig.e2e.json"],
"exclude": ["**/node_modules/**"]
}
}
}
......@@ -132,11 +117,11 @@
"defaultProject": "v5.x",
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
"prefix": "m",
"styleext": "scss"
},
"@schematics/angular:directive": {
"prefix": "app"
"prefix": "m"
}
}
}
This diff is collapsed.
......@@ -42,10 +42,9 @@
<li
class="m-dropdownList__item m-user-menuDropdown__Item"
*ngIf="isAdmin()"
(click)="closeMenu()"
>
<a routerLink="/analytics/admin/network">
<a routerLink="/analytics/dashboard/traffic">
<i class="material-icons">timeline</i>
<span i18n>Analytics</span>
</a>
......
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;
timespan: string;
timespans: Timespan[];
metric: string;
metrics: Metric[];
filter: string[];
filters: Filter[];
}
export interface Filter {
id: string;
label: string;
options: Option[];
}
export interface Option {
id: string;
label: string;
available?: boolean;
selected?: boolean;
interval?: string;
comparison_interval?: number;
from_ts_ms?: number;
from_ts_iso?: string;
}
export interface Metric {
id: string;
label: string;
permissions: string[];
summary: Summary;
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: Array<Buckets>;
}
export interface Buckets {
buckets: Bucket[];
}
export interface Bucket {
key: number;
date: string;
value: number;
}
export interface Timespan {
id: string;
label: string;
interval: string;
comparison_interval: number;
from_ts_ms: number;
from_ts_iso: string;
}
export interface UserState {
category: string;
timespan: string;
timespans: Timespan[];
metric: string;
metrics: Metric[];
filter: string[];
filters: Filter[];
loading: boolean;
}
......@@ -49,6 +49,21 @@ import { ActiveUsersChartComponent } from './components/charts/active-users/acti
import { Graph } from './graph.component';
import { PageviewsCardComponent } from './components/cards/pageviews/pageviews.component';
import { PageviewsChartComponent } from './components/charts/pageviews/pageviews.component';
import { AnalyticsDashboardComponent } from './v2/dashboard.component';
import { AnalyticsLayoutChartComponent } from './v2/layouts/layout-chart/layout-chart.component';
import { AnalyticsLayoutTableComponent } from './v2/layouts/layout-table/layout-table.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';
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 { AnalyticsMenuComponent } from './v2/components/menu/menu.component';
PlotlyModule.plotlyjs = PlotlyJS;
......@@ -77,6 +92,12 @@ const routes: Routes = [
// { path: 'plus', component: OffChainBoostsCardComponent},
],
},
{
path: 'dashboard/',
redirectTo: 'dashboard/traffic',
pathMatch: 'full',
},
{ path: 'dashboard/:category', component: AnalyticsDashboardComponent },
],
},
];
......@@ -87,6 +108,8 @@ const routes: Routes = [
CommonModule,
RouterModule.forChild(routes),
PlotlyModule,
SearchModule,
FormsModule,
],
exports: [
AdminAnalyticsComponent,
......@@ -138,7 +161,19 @@ const routes: Routes = [
PageviewsChartComponent,
PageviewsCardComponent,
Graph,
AnalyticsDashboardComponent,
AnalyticsLayoutChartComponent,
AnalyticsLayoutTableComponent,
AnalyticsLayoutSummaryComponent,
AnalyticsMetricsComponent,
AnalyticsFiltersComponent,
AnalyticsFilterComponent,
AnalyticsChartComponent,
AnalyticsTableComponent,
AnalyticsSearchComponent,
AnalyticsSearchSuggestionsComponent,
AnalyticsMenuComponent,
],
providers: [],
providers: [AnalyticsDashboardService],
})
export class AnalyticsModule {}
const categories: Array<any> = [
// {
// id: 'summary',
// label: 'Summary',
// permissions: ['admin', 'user'],
// metrics: [],
// },
{
id: 'traffic',
label: 'Traffic',
permissions: ['admin', 'user'],
metrics: [
'active_users',
'signups',
'unique_visitors',
'pageviews',
'impressions',
'retention',
],
},
{
id: 'earnings',
label: 'Earnings',
permissions: ['admin', 'user'],
metrics: ['total', 'pageviews', 'active_referrals', 'customers'],
},
// {
// id: 'engagement',
// label: 'Engagement',
// permissions: ['admin', 'user'],
// metrics: ['posts', 'votes', 'comments', 'reminds', 'subscribers', 'tags'],
// },
{
id: 'trending',
label: 'Trending',
permissions: ['admin', 'user'],
metrics: ['top_content', 'top_channels'],
},
// {
// id: 'referrers',
// label: 'Referrers',
// permissions: ['admin', 'user'],
// metrics: ['top_referrers'],
// },
// {
// id: 'plus',
// label: 'Plus',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
// },
// {
// id: 'pro',
// label: 'Pro',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
// },
// {
// id: 'boost',
// label: 'Boost',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_tokens'],
// },
// {
// id: 'nodes',
// label: 'Nodes',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
// },
];
export default categories;
const chartPalette: Array<any> = [
{
id: 'm-white',
themeMap: ['#fff', '#161616'],
},
{
id: 'm-transparent',
themeMap: ['rgba(0,0,0,0)', 'rgba(0,0,0,0)'],
},
{
id: 'm-grey-50',
themeMap: ['rgba(232,232,232,1)', 'rgba(53,53,53,1)'],
},
{
id: 'm-grey-70',
themeMap: ['#eee', '#333'],
},
{
id: 'm-grey-130',
themeMap: ['#ccc', '#555'],
},
{
id: 'm-grey-160',
themeMap: ['#bbb', '#555'],
},
{
id: 'm-grey-300',
themeMap: ['#999', '#666'],
},
{
id: 'm-blue',
themeMap: ['#4690df', '#44aaff'],
},
{
id: 'm-red-dark',
themeMap: ['#c62828', '#e57373'],
},
{
id: 'm-amber-dark',
themeMap: ['#ffa000', '#ffecb3'],
},
{
id: 'm-green-dark',
themeMap: ['#388e3c', '#8bc34a'],
},
{
id: 'm-blue-grey-500',
themeMap: ['#607d8b', '#607d8b'],
},
];
export default chartPalette;
<!-- TODO: Make this into a different component -->
<!-- <m-chart [buckets]="(vm$.thecurrentvisualisation" | async)></m-chart> -->
<!-- TODO: then all this becomes m-plotlyChart -->
<!-- <div *ngIf="vm$ | async as vm"> -->
<div>
<div #graphDiv id="graphDiv"></div>
<!-- <plotly-plot
id="graphDiv"
[divId]="graphDiv"
[data]="data"
[layout]="layout"
[config]="config"
[useResizeHandler]="true"
[style]="{ position: 'relative' }"
(hover)="onHover($event)"
(unhover)="onUnhover($event)"
(afterPlot)="afterPlot()"
>
</plotly-plot> -->
<!-- <div class="hoverInfo__row">
{{ hoverInfo.date | date: selectedTimespan.datePipe }}
</div> -->
<div #hoverInfoDiv id="hoverInfoDiv" class="hoverInfoDiv">
<div class="hoverInfo__row">
{{ hoverInfo.date | date: datePipe }}
</div>
<div [ngSwitch]="selectedMetric.unit" class="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="hoverInfo__row" *ngIf="isComparison">
vs
<ng-container [ngSwitch]="selectedMetric.unit" class="hoverInfo__row">
<ng-template ngSwitchCase="number">
{{ hoverInfo.comparisonValue | number }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.comparisonValue | currency }} USD
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.comparisonValue | number: '1.1-3' }}
{{ selectedMetric.unit }}
</ng-template>
</ng-container>
on {{ hoverInfo.comparisonDate | date }}
</div>
</div>
</div>
m-analytics__chart {
position: relative;
.js-plotly-plot,
.plot-container {
height: 44vh;
display: block;
}
}
#graphDiv {
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%;
}
}
.hoverInfoDiv {
width: 160px;
padding: 12px;
position: absolute;
pointer-events: none;
border-radius: 3px;
font-size: 12px;
z-index: 9999999999; // TODO fix
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-200);
}
[class*='hoverInfo__row'] {
padding-bottom: 4px;
&:last-of-type {
padding-top: 2px;
}
}
.hoverInfo__row--primary {
font-size: 15px;
// font-weight: bold;
@include m-theme() {
color: themed($m-grey-600);
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsChartComponent } from './chart.component';
describe('AnalyticsChartComponent', () => {
let component: AnalyticsChartComponent;
let fixture: ComponentFixture<AnalyticsChartComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsChartComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
<ng-container *ngIf="true">
<div class="filterLabelWrapper" *ngIf="filter.id !== 'timespan'">
<span>{{ filter.label }}</span>
<m-tooltip icon="help">
<div class="filterDesc">{{ filter.description }}</div>
<ul class="filterOptions__descContainer">
<ng-container *ngFor="let option of filter.options">
<li class="filterOption__desc">
<span class="filterOption__descLabel">{{ option.label }}</span
><span class="filterOption__desc" *ngIf="option.description"
>: {{ option.description }}</span
>
</li>
</ng-container>
</ul>
</m-tooltip>
</div>
<div
class="filterWrapper"
[ngClass]="{
expanded: expanded,
isMobile: isMobile,
dropUp: dropUp
}"
(focus)="expanded = true"
(blur)="expanded = false"
>
<div class="filterHeader" (click)="expanded = !expanded">
<div class="row">
<span class="option option--selected">
{{ 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>
<div class="unselectedOptionsContainer">
<ng-container *ngFor="let option of filter.options">
<div
class="option row"
(click)="updateFilter(option)"
[ngClass]="{
unavailable: option.available === false
}"
>
<span>{{ option.label }}</span>
</div>
</ng-container>
</div>
</div>
</ng-container>
$rounded-top: 3px 3px 0 0;
$rounded-bottom: 0 0 3px 3px;
m-analytics__filter {
position: relative;
margin: 16px 24px 0 0;
z-index: 2;
}
.filterLabelWrapper {
position: absolute;
bottom: 110%;
white-space: nowrap;
@include m-theme() {
color: rgba(themed($m-grey-200), 0.9);
}
.m-tooltip {
i {
font-size: 12px;
@include m-theme() {
color: rgba(themed($m-grey-200), 0.7);
}
}
.m-tooltip--bubble {
letter-spacing: 1.2px;
line-height: 16px;
z-index: 9999;
font-size: 11px;
bottom: 85%;
left: 100%;
@include m-theme() {
color: themed($m-white);
background-color: themed($m-blue);
}
> * {
font-size: 11px;
font-weight: 300;
line-height: inherit;
letter-spacing: inherit;
}
ul {
padding-inline-start: 16px;
margin-block-end: 4px;
li {
padding-bottom: 8px;
.filterOption__descLabel {
// font-weight: bold;
}
}
}
}
}
}
.filterWrapper {
cursor: pointer;
&.expanded {
@include m-theme() {
box-shadow: 0px 1px 15px 0 rgba(themed($m-black), 0.15);
}
.filterHeader {
@include m-theme() {
border-color: themed($m-blue);
}
}
.unselectedOptionsContainer {
visibility: visible;
// @include m-theme() {
// box-shadow: 0px 1px 15px 0 rgba(themed($m-black), 0.15);
// }
}
&:not(.dropUp) {
.filterHeader {
@include m-theme() {
border-radius: $rounded-top;
}
}
.unselectedOptionsContainer {
border-top: none;
border-radius: $rounded-bottom;
}
}
&.dropUp {
.filterHeader {
border-radius: $rounded-bottom;
}
.unselectedOptionsContainer {
bottom: 100%;
border-radius: $rounded-top;
border-bottom: none;
}
}
}
.filterHeader {
position: relative;
width: 100%;
padding: 6px 6px 6px 8px;
border-radius: 3px;
transition: all 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
@include m-theme() {
background-color: themed($m-white);
color: rgba(themed($m-grey-200), 0.9);
}
@include m-theme() {
border: 1px solid themed($m-grey-100);
}
.filterLabel {
margin-right: 10px;
}
i {
flex-grow: 0;
}
.option--selected {
margin-right: 8px;
@include m-theme() {
color: themed($m-grey-500);
}
}
}
.unselectedOptionsContainer {
position: absolute;
// display: none;
visibility: hidden;
width: 100%;
padding: 6px 6px 6px 8px;
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);
}
.option {
padding: 5px 0;
}
}
.row {
display: flex;
justify-content: space-between;
}
.option {
border-radius: 3px;
white-space: nowrap;
@include m-theme() {
background-color: themed($m-white);
color: rgba(themed($m-grey-200), 0.9);
}
&.unavailable {
text-decoration: line-through;
@include m-theme() {
color: themed($m-grey-50);
}
}
&:hover:not(.unavailable) {
@include m-theme() {
color: themed($m-grey-500);
}
}
}
}
.isMobile {
.filterHeader {
i {
display: none;
}
}
.option--selected {
margin-right: 0;
}
}
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();
});
});
import {
Component,
OnInit,
Input,
Output,
EventEmitter,
ChangeDetectionStrategy,
} from '@angular/core';
import { Observable } from 'rxjs';
import {
AnalyticsDashboardService,
Category,
Response,
Dashboard,
Filter,
Option,
Metric,
Summary,
Visualisation,
Bucket,
Timespan,
UserState,
} from '../../dashboard.service';
import isMobileOrTablet from '../../../../../helpers/is-mobile-or-tablet';
import { Session } from '../../../../../services/session';
@Component({
selector: 'm-analytics__filter',
templateUrl: 'filter.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsFilterComponent implements OnInit {
@Input() filter: Filter;
@Input() dropUp: boolean = false;
isMobile: boolean;
expanded = false;
options: Array<any> = [];
selectedOption: Option;
// subscription;
constructor(
private analyticsService: AnalyticsDashboardService,
public session: Session
) {}
ngOnInit() {
// this.subscription = this.analyticsService.timespan$.subscribe(timespan => {
// if (this.filter.id === 'timespan') {
// this.selectedOption =
// this.filter.options.find(option => option.id === timespan) ||
// this.filter.options[0];
// // TODO: make selected option at top of array?
// } else {
// this.selectedOption =
// this.filter.options.find(option => option.selected === true) ||
// this.filter.options[0];
// }
// });
console.log(this.filter, this.filter.id);
this.selectedOption =
this.filter.options.find(option => option.selected === true) ||
this.filter.options[0];
this.isMobile = isMobileOrTablet();
}
updateFilter(option: Option) {
this.expanded = false;
this.selectedOption = option;
if (this.filter.id === 'timespan') {
this.analyticsService.updateTimespan(option.id);
return;
}
if (!this.selectedOption.available) {
return;
}
const selectedFilterStr = `${this.filter.id}::${option.id}`;
this.analyticsService.updateFilter(selectedFilterStr);
}
}
<div class="filtersContainer">
<ng-container *ngFor="let filter of filters$ | async">
<m-analytics__filter
class="filter"
[filter]="filter"
[dropUp]="true"
></m-analytics__filter>
</ng-container>
</div>
.filtersContainer {
display: flex;
flex-wrap: wrap;
padding: 16px;
position: relative;
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsFiltersComponent } from './filters.component';
describe('AnalyticsFiltersComponent', () => {
let component: AnalyticsFiltersComponent;
let fixture: ComponentFixture<AnalyticsFiltersComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsFiltersComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsFiltersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
} from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import {
AnalyticsDashboardService,
Category,
Response,
Dashboard,
Filter,
Option,
Metric,
Summary,
Visualisation,
Bucket,
Timespan,
UserState,
} from '../../dashboard.service';
@Component({
selector: 'm-analytics__filters',
templateUrl: './filters.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsFiltersComponent implements OnInit, OnDestroy {
subscription: Subscription;
filters$ = this.analyticsService.filters$;
constructor(private analyticsService: AnalyticsDashboardService) {}
ngOnInit() {
// TODO: remove subscription because everything is happening in html
// TODO: might even be fine to just get rid of this component and put it in dashboard.ts
}
ngOnDestroy() {}
}
<section class="menu" [ngClass]="{ isMobile: isMobile }">
<div class="topbar" *ngIf="isMobile">
<i class="material-icons" (click)="expanded = true">menu</i>
<div class="pageTitle">
Analytics
</div>
</div>
<div
class="overlay"
[ngClass]="{ expanded: expanded }"
(click)="expanded = false"
></div>
<div class="sidebar" [ngClass]="{ expanded: expanded }">
<a class="profile" *ngIf="isMobile" [routerLink]="['/', user.username]">
<img
class="avatar"
[src]="minds.cdn_url + 'icon/' + user.guid + '/small/' + user.icontime"
/>
<div class="details">
<div class="name">{{ user.name }}</div>
<!-- TODO: get subscriberCount -->
<div class="subscribers">
{{ user.subscribers_count | abbr }} subscribers
</div>
</div>
</a>
<div class="sidebarTitle">
<h3>Analytics</h3>
<i class="material-icons" *ngIf="isMobile" (click)="expanded = false"
>keyboard_arrow_up</i
>
</div>
<div class="catContainer">
<!-- TODO: apply permissions from categories.default to cats sidebar -->
<div class="cat" *ngFor="let cat of cats">
<a
(click)="expanded = false"
[routerLink]="'../' + cat.id"
routerLinkActive="selected"
>{{ cat?.label }}</a
>
</div>
</div>
</div>
</section>
// .m-sidebarMarkers__container,
// m-v2-topbar {
// display: none;
// }
m-analytics__menu {
// ----------------------------------------
// MOBILE
.isMobile {
.topbar {
z-index: 99999;
position: fixed;
top: 0;
left: 0;
width: 100%;
padding: 16px;
text-align: center;
@include m-theme() {
background-color: themed($m-grey-100);
color: themed($m-grey-800);
}
i {
display: block;
position: absolute;
top: 50%;
left: 16px;
transform: translateY(-50%);
@include m-theme() {
background-color: themed($m-grey-100);
color: themed($m-grey-700);
}
}
.pageTitle {
font-size: 20px;
margin: 0;
}
}
.overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: -1;
// display: none;
background-color: transparent;
transition: background-color 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
&.expanded {
// display: block;
z-index: 999998;
@include m-theme() {
background-color: rgba(themed($m-grey-700), 0.2);
}
}
}
.sidebar {
z-index: 999999;
position: fixed;
top: 0;
bottom: 0;
left: -340px;
transition: left 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
padding: 0 24px;
width: 300px;
max-width: 70%;
@include m-theme() {
background-color: themed($m-white);
}
&.expanded {
left: 0;
}
.sidebarTitle {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
font-size: 20px;
margin: 0;
}
i {
font-size: 20px;
@include m-theme() {
color: themed($m-grey-200);
}
}
}
.profile {
display: flex;
text-decoration: none;
margin: 24px 0;
@include m-theme() {
color: themed($m-grey-800);
}
.avatar {
border-radius: 50%;
margin-right: 16px;
}
.details {
& > {
padding: 8px 0;
}
.name {
font-weight: bold;
}
.subscribers {
font-size: 11px;
@include m-theme() {
color: themed($m-grey-300);
}
}
}
}
}
}
// ----------------------------------------
padding: 16px 16px 16px 16px;
flex: 1 1 0px;
i {
display: none;
}
.catContainer {
cursor: pointer;
.cat {
a {
display: block;
padding: 6px 0;
text-decoration: none;
font-weight: 400;
@include m-theme() {
color: themed($m-grey-200);
}
}
a.selected,
&:hover a {
@include m-theme() {
color: themed($m-blue);
}
}
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsMenuComponent } from './menu.component';
describe('AnalyticsMenuComponent', () => {
let component: AnalyticsMenuComponent;
let fixture: ComponentFixture<AnalyticsMenuComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsMenuComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
OnInit,
OnDestroy,
Input,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import {
ActivatedRoute,
Router,
ParamMap,
RoutesRecognized,
} from '@angular/router';
import { Subscription, Observable } from 'rxjs';
import { Client } from '../../../../../services/api';
import { Session } from '../../../../../services/session';
import {
AnalyticsDashboardService,
Category,
UserState,
} from '../../dashboard.service';
import categories from '../../categories.default';
import isMobileOrTablet from '../../../../../helpers/is-mobile-or-tablet';
@Component({
selector: 'm-analytics__menu',
templateUrl: './menu.component.html',
})
export class AnalyticsMenuComponent implements OnInit {
isMobile: boolean;
expanded: boolean = false;
minds;
user;
cats = categories;
// subscription: Subscription;
// paramsSubscription: Subscription;
// category$ = this.analyticsService.category$;
selectedCat: string;
constructor(
// public client: Client,
public route: ActivatedRoute,
// private router: Router,
// public analyticsService: AnalyticsDashboardService,
// private cd: ChangeDetectorRef
public session: Session
) {}
ngOnInit() {
this.minds = window.Minds;
this.isMobile = isMobileOrTablet();
this.user = this.session.getLoggedInUser();
}
// updateCategory(categoryId) {
// this.analyticsService.updateCategory(categoryId);
// }
// detectChanges() {
// this.cd.markForCheck();
// this.cd.detectChanges();
// }
}
<section class="metricsSection" [ngClass]="{ isMobile: isMobile }">
<!-- <div class="overflowFade--left"></div>
<div class="overflowScrollButton--left">
<i class="material-icons">chevron_left</i>
</div> -->
<div class="metricsWrapper">
<div class="metricsContainer" *ngIf="metrics$ | async as metrics">
<ng-container *ngFor="let metric of metrics">
<div
class="metric"
(click)="updateMetric(metric)"
[ngClass]="{ active: metric.visualisation }"
*ngIf="metric.permissionGranted"
>
<div class="metricLabel">
<span>{{ metric.label }}</span>
<m-tooltip [anchor]="top" icon="help">
{{ metric.description }}
</m-tooltip>
</div>
<div class="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="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>
<!-- <div class="overflowFade--right"></div>
<div class="overflowScrollButton--right">
<i class="material-icons">chevron_right</i>
</div> -->
</section>
m-analytics__metrics {
.metricsSection {
position: relative;
[class*='overflowFade--'] {
position: absolute;
top: 0;
bottom: 0;
width: 24px;
z-index: 1;
&.overflowFade--right {
@include m-theme() {
right: 0;
background: linear-gradient(
to right,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
&.overflowFade--left {
@include m-theme() {
left: 0;
background: linear-gradient(
to left,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
}
[class*='overflowScrollButton--'] {
position: absolute;
top: 50%;
border-radius: 50%;
box-sizing: border-box;
z-index: 2;
transform: translateY(-50%);
transition: all 0.2s ease-in;
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);
}
}
&.overflowScrollButton--right {
right: -12;
}
&.overflowScrollButton--left {
left: -12;
}
i {
@include m-theme() {
color: themed($m-grey-200);
}
}
}
}
}
.metricsWrapper {
position: relative;
overflow: hidden;
width: 100%;
@include m-theme() {
box-shadow: 0 7px 15px -7px rgba(themed($m-black-always), 0.1);
}
}
.metricsContainer {
scroll-snap-type: x mandatory;
position: relative;
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
// padding: 0 16px;
// &.metricsContainer::-webkit-scrollbar {
// display: none;
// }
.metric {
cursor: pointer;
scroll-snap-align: start;
flex: 0 0 auto;
width: 20%;
padding: 24px 20px 20px 20px;
font-size: 14px;
box-sizing: border-box;
@include m-theme() {
border-bottom: 8px solid themed($m-white);
}
@include m-theme() {
color: themed($m-grey-200);
}
&.active {
@include m-theme() {
background-color: rgba(themed($m-grey-100), 0.2);
border-bottom: 8px solid themed($m-blue);
}
}
&:first-child {
margin-left: 16px;
}
&:last-child {
margin-right: 16px;
}
&:hover:not(.active) {
@include m-theme() {
background-color: rgba(themed($m-grey-100), 0.2);
border-bottom: 8px solid rgba(0, 0, 0, 0);
transition: background-color 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}
}
.metricLabel {
white-space: nowrap;
}
m-tooltip {
vertical-align: middle;
margin-left: 4px;
}
.metricSummary {
font-size: 17px;
margin-top: 8px;
@include m-theme() {
color: themed($m-grey-800);
}
}
.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);
}
}
}
}
.m-tooltip {
i {
font-size: 12px;
@include m-theme() {
color: rgba(themed($m-grey-200), 0.7);
}
}
.m-tooltip--bubble {
z-index: 9999;
font-size: 11px;
@include m-theme() {
color: themed($m-white);
background-color: themed($m-blue);
}
}
}
}
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,
ChangeDetectionStrategy,
ChangeDetectorRef,
OnDestroy,
} from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import {
AnalyticsDashboardService,
Category,
Response,
Dashboard,
Filter,
Option,
Metric as MetricBase,
Summary,
Visualisation,
Bucket,
Timespan,
UserState,
} from '../../dashboard.service';
import { Session } from '../../../../../services/session';
import isMobileOrTablet from '../../../../../helpers/is-mobile-or-tablet';
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',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsMetricsComponent implements OnInit, OnDestroy {
data;
subscription: Subscription;
isMobile: boolean;
user;
userRoles: string[] = ['user'];
metrics$;
isOverflown = { left: false, right: false };
constructor(
private analyticsService: AnalyticsDashboardService,
public session: Session,
private cd: ChangeDetectorRef
) {}
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;
})
);
this.isMobile = isMobileOrTablet();
}
updateMetric(metric) {
this.analyticsService.updateMetric(metric.id);
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
ngOnDestroy() {
// this.subscription.unsubscribe();
}
checkOverflow() {
// element.scrollWidth - element.clientWidth
}
}
<div
class="m-analytics__searchSuggestions__list"
[hidden]="disabled || !active"
(mousedown)="mousedown($event)"
*ngIf="session.isLoggedIn()"
>
<ng-container *ngIf="!q">
<ng-container *ngFor="let suggestion of recent">
<a
class="m-analytics__searchSuggestions__suggestion"
*ngIf="suggestion.type == 'user'"
(click)="applyChannelFilter(suggestion)"
>
<img src="icon/{{ suggestion.guid }}/small" />
<div>
<div>{{ suggestion.name }}</div>
<div>@{{ suggestion.username }}</div>
</div>
</a>
</ng-container>
</ng-container>
<ng-container *ngIf="q">
<a
class="m-analytics__searchSuggestions__suggestion"
*ngFor="let suggestion of suggestions"
[routerLink]="['/', suggestion.username]"
>
<img src="icon/{{ suggestion.guid }}/small" />
<div>
<div>{{ suggestion.name }}</div>
<div>@{{ suggestion.username }}</div>
</div>
</a>
</ng-container>
</div>
m-analytics__searchSuggestions {
display: block;
.m-analytics__searchSuggestions__list {
padding: 0;
margin: 0;
position: absolute;
z-index: 5000;
box-sizing: border-box;
width: 100%;
@include m-theme() {
background-color: themed($m-white);
border: 1px solid themed($m-grey-50);
}
.m-analytics__searchSuggestions__suggestion {
cursor: pointer;
padding: 4px;
display: block;
text-decoration: none;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.5px;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
@include m-theme() {
border-bottom: 1px solid themed($m-grey-50);
color: themed($m-grey-700);
}
a {
display: flex;
flex-direction: row;
}
img {
border-radius: 50%;
margin: 0 8px 0 4px;
width: 36px;
height: 36px;
@include m-theme() {
background-color: themed($m-grey-800);
}
}
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsSearchSuggestionsComponent } from './search-suggestions.component';
describe('AnalyticsSearchSuggestionsComponent', () => {
let component: AnalyticsSearchSuggestionsComponent;
let fixture: ComponentFixture<AnalyticsSearchSuggestionsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsSearchSuggestionsComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsSearchSuggestionsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, Input, ChangeDetectorRef } from '@angular/core';
import {
AnalyticsDashboardService,
Filter,
Option,
} from '../../dashboard.service';
import { RecentService } from '../../../../../services/ux/recent';
import { Session } from '../../../../../services/session';
import { Client } from '../../../../../services/api';
@Component({
selector: 'm-analytics__searchSuggestions',
templateUrl: './search-suggestions.component.html',
})
export class AnalyticsSearchSuggestionsComponent implements OnInit {
suggestions: Array<any> = [];
recent: any[];
q: string = '';
private searchTimeout;
@Input() active: boolean;
@Input('q') set _q(value: string) {
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
this.q = value || '';
if (!value) {
this.loadRecent();
this.suggestions = [];
return;
}
this.searchTimeout = setTimeout(async () => {
this.loadRecent();
try {
const response: any = await this.client.get('api/v2/search/suggest', {
q: value,
limit: 4,
});
this.suggestions = response.entities;
console.log(response.entities);
} catch (e) {
console.error(e);
this.suggestions = [];
}
}, 300);
}
constructor(
private analyticsService: AnalyticsDashboardService,
private session: Session,
public client: Client,
public recentService: RecentService,
private cd: ChangeDetectorRef
) {}
ngOnInit() {
this.loadRecent();
}
applyChannelFilter(suggestion) {
// TODO: remove dummy data
let selection = suggestion.guid || 'test';
const selectedFilterStr = `channel::${selection}`;
// TODO: enable admin gate
// if (this.session.isAdmin()) {
this.analyticsService.updateFilter(selectedFilterStr);
// }
}
loadRecent() {
if (this.session.getLoggedInUser()) {
// TODO: Q - Does this only store channels?
this.recent = this.recentService.fetch('recent', 6);
}
}
mousedown(e) {
e.preventDefault();
setTimeout(() => {
this.active = false;
this.detectChanges();
}, 300);
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
<div class="mdl-textfield mdl-js-textfield">
<i class="material-icons" (click)="setFocus()">search</i>
<input
[(ngModel)]="q"
(focus)="focus()"
(blur)="blur()"
name="q"
class="mdl-textfield__input"
type="text"
id="search"
autocomplete="off"
placeholder="Filter by channel"
#searchInput
/>
<!-- <label class="mdl-textfield__label" for="search">abc</label> -->
<m-analytics__searchSuggestions
[q]="q"
[active]="active"
></m-analytics__searchSuggestions>
</div>
@import 'defaults';
m-analytics__search {
display: none;
width: 200px;
.mdl-textfield {
width: 100%;
.material-icons {
position: absolute;
margin: 6px;
font-size: 20px;
}
input {
@include m-theme() {
background-color: themed($m-white);
border: 1px solid rgba(themed($m-black), 0.12);
}
&::placeholder {
@include m-theme() {
color: themed($m-grey-200);
font-weight: 400;
letter-spacing: normal;
}
}
}
input,
label {
padding: $minds-padding $minds-padding $minds-padding $minds-padding * 4;
box-sizing: border-box;
}
}
.mdl-textfield .mdl-textfield__input {
height: 32px;
border-radius: 18px;
//text-indent: 22px;
font-family: 'Roboto', sans-serif;
font-size: 14px;
letter-spacing: 0.5px;
font-weight: 600;
//line-height: 38px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
}
// @media screen and (min-width: 769px) {
// .mdl-textfield.m-search-bar--has-context {
// display: flex;
// .m-search-bar--context {
// display: block;
// flex-grow: 1;
// align-self: center;
// max-width: 25em;
// height: 32px;
// border-radius: 3px;
// padding: 0 0 0 32px;
// font-family: Roboto, sans-serif;
// font-size: 12px;
// letter-spacing: 1.25px;
// //line-height: 38px;
// white-space: nowrap;
// text-transform: uppercase;
// border-right: none;
// box-sizing: border-box;
// text-rendering: optimizeLegibility;
// -webkit-font-smoothing: antialiased;
// @include m-theme() {
// border: 1px solid rgba(themed($m-black), 0.12);
// color: rgba(themed($m-grey-800), 0.5);
// }
// }
// .mdl-textfield__input {
// padding: 0 $minds-padding;
// //border-left: none;
// box-sizing: border-box;
// appearance: none;
// @include m-theme() {
// border: 1px solid rgba(themed($m-black), 0.12);
// }
// }
// }
// }
// }
// Search
// m-search {
// display: block;
// min-height: 100vh;
// padding-top: 42px;
// .m-toolbar {
// position: fixed;
// top: 0;
// position: fixed;
// top: 50px;
// width: 100%;
// z-index: 500;
// @include m-theme() {
// background-color: rgba(themed($m-grey-50), 0.8);
// }
// }
// .m-topbar--navigation--more {
// position: relative;
// cursor: pointer;
// .minds-dropdown-menu {
// width: auto;
// max-width: 300px;
// .mdl-menu__item {
// display: flex;
// align-items: center;
// overflow: visible;
// font-size: 12px;
// .m-dropdown--spacer {
// flex: 1;
// }
// > label.mdl-switch {
// width: 36px;
// }
// .m-tooltip i {
// font-size: 16px;
// margin: 0 8px;
// @include m-theme() {
// color: themed($m-grey-400);
// }
// }
// }
// }
// @media screen and (max-width: $max-mobile) {
// flex: 1;
// }
// }
// }
// Search Lists
// .m-search--list {
// max-width: 1280px;
// padding: $minds-padding * 2;
// margin: auto;
// .m-search--list-section {
// margin-bottom: $minds-padding * 3;
// &:last-child {
// margin-bottom: 0;
// }
// }
// .m-search--list-title {
// margin: 0;
// padding: 0 $minds-padding * 2;
// font-size: 14px;
// font-weight: 600;
// line-height: 1.25;
// letter-spacing: 2px;
// text-transform: uppercase;
// @include m-theme() {
// color: rgba(themed($m-black), 0.6);
// }
// }
// .m-search--list-entities {
// minds-card {
// background: transparent;
// }
// }
// }
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsSearchComponent } from './search.component';
describe('AnalyticsSearchComponent', () => {
let component: AnalyticsSearchComponent;
let fixture: ComponentFixture<AnalyticsSearchComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsSearchComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsSearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, ViewChild, Input, ElementRef } from '@angular/core';
// import { Observable, Subscription } from 'rxjs';
import {
AnalyticsDashboardService,
Filter,
Option,
} from '../../dashboard.service';
import { Session } from '../../../../../services/session';
@Component({
selector: 'm-analytics__search',
templateUrl: './search.component.html',
})
export class AnalyticsSearchComponent implements OnInit {
active: boolean;
q: string;
id: string;
hasSearchContext: boolean = true;
searchContext: string | Promise<string> = '';
@ViewChild('searchInput', { static: true }) searchInput: ElementRef;
constructor(
private analyticsService: AnalyticsDashboardService,
private session: Session
) {}
ngOnInit() {}
search() {
// const qs: { q; ref; id? } = { q: this.q, ref: 'top' };
// if (this.id) {
// qs.id = this.id;
// }
// if (this.featureService.has('top-feeds')) {
// this.router.navigate([
// '/newsfeed/global/top',
// { query: this.q, period: '24h' },
// ]);
// } else {
// this.router.navigate(['search', qs]);
// }
}
keyup(e) {
if (e.keyCode === 13 && this.session.isLoggedIn()) {
this.search();
this.unsetFocus();
}
// TODO: allow to tab through suggestions?
}
focus() {
this.active = true;
}
blur() {
setTimeout(() => (this.active = false), 100);
}
setFocus() {
if (this.searchInput.nativeElement) {
this.searchInput.nativeElement.focus();
}
}
unsetFocus() {
if (this.searchInput.nativeElement) {
this.searchInput.nativeElement.blur();
}
}
// protected getActiveSearchContext(fragments: string[]) {
// this.searchContext = ''; // this would be 'channels'
// this.id = ''; //this would be a guid
// fragments.forEach((fragment: string) => {
// let param = fragment.split('=');
// if (param[0] === 'q') {
// this.q = decodeURIComponent(param[1]);
// }
// if (param[0] === 'id') {
// this.id = param[1];
// this.searchContext = this.context.resolveLabel(
// decodeURIComponent(param[1])
// );
// }
// if (param[0] == 'type' && !this.searchContext) {
// this.searchContext = this.context.resolveStaticLabel(
// decodeURIComponent(param[1])
// );
// }
// });
// }
}
<div class="tableWrapper" [hidden]="loading">
<div class="header row">
<div
*ngFor="let col of visualisation.columns; let first = first"
[ngClass]="{
first: first
}"
class="cell"
>
{{ col.label }}
</div>
</div>
<div class="body">
<ng-container *ngFor="let row of reformattedBuckets">
<a class="row" [routerLink]="row.entity.route">
<div class="entity cell" *ngIf="row.entity">
<div class="entityTitle">
<span>{{ row.entity.title }}</span>
<i class="material-icons">open_in_new</i>
</div>
<div class="entityDetails">
<span class="usernameWrapper">
<a [routerLink]="'/' + row.entity.name"
>@{{ row.entity.username }}</a
>
</span>
<span *ngIf="!isMobile">{{ row.entity.type | titlecase }}</span>
<span *ngIf="!isMobile"
>Published {{ row.entity.time_created * 1000 | date }}</span
>
</div>
</div>
<div class="valuesContainer">
<ng-container *ngFor="let value of row.values">
<div class="value cell">{{ value | abbr }}</div>
</ng-container>
</div>
</a>
</ng-container>
</div>
</div>
.tableWrapper {
font-size: 13px;
font-weight: 400;
@include m-theme() {
color: themed($m-grey-800);
}
a {
text-decoration: none;
font-weight: normal;
@include m-theme() {
color: themed($m-grey-800);
}
}
.row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
&.header {
@include m-theme() {
border-bottom: 1px solid themed($m-grey-50);
color: themed($m-grey-200);
}
.cell {
flex: 1 1 0;
&.first {
flex: 7 2 0;
}
}
}
.cell {
flex: 1 1 0;
display: flex;
// align-items: center;
flex-direction: column;
overflow: hidden;
white-space: nowrap;
height: 50px;
padding: 8px 8px 8px 0;
&.entity {
padding-left: 5%;
min-width: 200px;
flex: 7 2 0;
@include m-theme() {
border-right: 1px solid themed($m-grey-50);
}
a {
font-weight: 400;
text-decoration: none;
}
.entityTitle {
display: flex;
// align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
i {
font-size: 13px;
display: none;
@include m-theme() {
color: themed($m-grey-800);
}
}
}
.entityDetails {
display: inline;
@include m-theme() {
color: themed($m-grey-200);
}
> * {
margin-right: $minds-margin;
}
.usernameWrapper {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
a {
@include m-theme() {
color: themed($m-grey-200);
}
}
}
}
&.value {
&:first-of-type {
padding-left: 8px;
}
&:last-of-type {
padding-right: 5%;
}
}
}
}
.valuesContainer {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
flex: 3 1 0;
}
.body {
.row {
transition: background-color 0.3s cubic-bezier(0.23, 1, 0.32, 1);
&:hover {
@include m-theme() {
background-color: rgba(themed($m-blue-grey-50), 0.5);
}
.entityTitle i {
display: inline-block;
}
}
}
}
}
.spinnerContainer {
height: 30%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
@include m-theme() {
background-color: themed($m-white);
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsTableComponent } from './table.component';
describe('AnalyticsTableComponent', () => {
let component: AnalyticsTableComponent;
let fixture: ComponentFixture<AnalyticsTableComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsTableComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
OnInit,
OnDestroy,
HostListener,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
} from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import {
AnalyticsDashboardService,
Visualisation,
} from '../../dashboard.service';
import isMobileOrTablet from '../../../../../helpers/is-mobile-or-tablet';
@Component({
selector: 'm-analytics__table',
templateUrl: './table.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsTableComponent implements OnInit, OnDestroy {
metricSubscription: Subscription;
visualisation: Visualisation;
columns: Array<any>;
rows: Array<any>;
reformattedBuckets: Array<any> = [];
minds = window.Minds;
user;
isMobile: boolean;
loadingSubscription: Subscription;
loading: boolean;
selectedMetric$ = this.analyticsService.metrics$.pipe(
map(metrics => {
return metrics.find(metric => metric.visualisation !== null);
})
);
selectedMetric;
constructor(
private analyticsService: AnalyticsDashboardService,
protected cd: ChangeDetectorRef // public session: Session
) {}
ngOnInit() {
this.isMobile = isMobileOrTablet();
this.metricSubscription = this.selectedMetric$.subscribe(metric => {
this.selectedMetric = metric;
this.visualisation = metric.visualisation;
this.columns = metric.visualisation.columns;
// .sort((a, b) =>
// a.order > b.order ? 1 : -1
// );
this.loadingSubscription = this.analyticsService.loading$.subscribe(
loading => {
this.loading = loading;
this.detectChanges();
}
);
this.reformatBuckets();
this.detectChanges();
});
}
reformatBuckets() {
this.reformattedBuckets = [];
this.visualisation.buckets.forEach(bucket => {
const reformattedBucket = {};
const reformattedValues = [];
if (!bucket.values['entity']) return;
this.columns.forEach((column, i) => {
if (i === 0) {
reformattedBucket['entity'] = this.reformatEntity(
bucket.values['entity']
);
} else {
reformattedValues.push(bucket.values[column.id]);
}
});
reformattedBucket['values'] = reformattedValues;
this.reformattedBuckets.push(reformattedBucket);
});
}
reformatEntity(entity) {
let type, username, name, titleType;
if (entity.remind_object) {
type = 'remind';
} else {
type = entity.urn.split(':')[1];
}
if (type === 'user') {
type = 'channel';
username = entity.username;
name = entity.name;
} else {
username = entity.ownerObj.username;
name = entity.ownerObj.name;
}
titleType = type.charAt(0).toUpperCase() + type.slice(1);
if (type === 'activity') {
titleType = 'Post';
}
const reformattedEntity = {
type: type,
time_created: entity.time_created || entity.time_published,
title: entity.title || entity.message || `${username}'s ${titleType}`,
route: this.getEntityRoute(type, entity),
username: username,
name: name,
};
return reformattedEntity;
}
getEntityRoute(type, entity) {
const routesByType = [
{
ids: ['image', 'video'],
route: '/media/' + entity.urn.split(':')[2],
},
{
ids: ['activity', 'remind'],
route: `/newsfeed/${entity.urn.split(':')[2]}`,
},
{
ids: ['blog'],
route: entity.route,
},
{
ids: ['channel'],
route: '/' + entity.name,
},
];
return routesByType.find(t => t.ids.indexOf(type) > -1).route;
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
ngOnDestroy() {
this.metricSubscription.unsubscribe();
this.loadingSubscription.unsubscribe();
}
}
<div class="page" [ngClass]="{ isMobile: isMobile }">
<m-analytics__menu></m-analytics__menu>
<!-- TODO: uncomment this -->
<!-- <section class="main" *ngIf="ready$ | async"> -->
<section class="main" [ngClass]="{ isMobile: isMobile }">
<div class="mainHeader">
<div class="dashboardTitle">
<h3 class="selectedCatLabel">
{{ category$ | async | titlecase }}
</h3>
<p
class="selectedCatDescription"
*ngIf="description$ | async as description"
>
{{ description }}
</p>
</div>
<div class="globalFilters">
<!-- TODO: *ngIf for global_filter bool -->
<!-- TODO: enable only show to admins -->
<!-- <div *ngIf="session.isAdmin()" class="channelSearch"> -->
<div class="channelSearch">
<m-analytics__search></m-analytics__search>
</div>
<!-- TODO: timespan filter will be a date filter -->
<div class="timespanFilter">
<m-analytics__filter [filter]="timespanFilter"></m-analytics__filter>
</div>
</div>
</div>
<div class="layoutWrapper">
<m-analytics__layout--chart
class="m-analytics__layout"
></m-analytics__layout--chart>
<!-- <m-analytics__layout--table
class="m-analytics__layout"
*ngIf="(category$ | async).type === 'table'"
></m-analytics__layout--table>
<m-analytics__layout--summary
class="m-analytics__layout"
*ngIf="(category$ | async).type === 'summary'"
></m-analytics__layout--summary> -->
</div>
</section>
</div>
// ----------------------------------------
// MOBILE
.isMobile {
&.page {
padding: 0;
}
.layoutWrapper {
box-shadow: none;
padding: 0;
}
.main {
padding: 0;
}
.m-analytics__menu {
padding: 0 8px;
}
}
// ----------------------------------------
.page {
padding: 16px;
display: flex;
flex-direction: row;
height: 100%;
// width: 100%;
@include m-theme() {
background-color: themed($m-white);
color: themed($m-grey-800);
}
h3 {
font-size: 26px;
font-weight: 500;
}
}
.main {
flex: 4 1 0px;
padding: 16px;
min-width: 600px;
.mainHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
h3 {
line-height: 0;
margin-top: 40px;
}
p {
@include m-theme() {
color: themed($m-grey-300);
}
}
}
.globalFilters {
margin: 24px 0;
display: flex;
align-items: baseline;
.channelSearch {
// border-radius: 5px;
margin-right: 8px;
@include m-theme() {
border: themed($m-grey-50);
color: themed($m-grey-100);
}
}
m-analytics__filter {
display: table;
}
.timespanFilter {
.filterWrapper {
margin-top: 0px;
}
.filterLabel {
display: none;
}
}
}
.layoutWrapper {
@include m-theme() {
box-shadow: 0px 0px 10px -3px rgba(themed($m-black-always), 0.3);
}
}
.m-analytics__layout {
position: relative;
}
}
// *************************************
// @media screen and (max-width: $min-tablet) {
@media screen and (max-width: 300px) {
.page {
flex-direction: column;
.sidebar {
.catContainer {
@include m-theme() {
background-color: themed($m-grey-200);
color: themed($m-white);
}
}
}
}
}
@media screen and (max-width: $max-mobile) {
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsDashboardComponent } from './dashboard.component';
describe('AnalyticsDashboardComponent', () => {
let component: AnalyticsDashboardComponent;
let fixture: ComponentFixture<AnalyticsDashboardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsDashboardComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsDashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
OnInit,
OnDestroy,
Input,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
import { Subscription, Observable } from 'rxjs';
import { MindsTitle } from '../../../services/ux/title';
import { Client } from '../../../services/api';
import { Session } from '../../../services/session';
import { AnalyticsDashboardService } from './dashboard.service';
import { Filter } from './../../../interfaces/dashboard';
// import categories from './categories.default';
import isMobileOrTablet from '../../../helpers/is-mobile-or-tablet';
@Component({
selector: 'm-analytics__dashboard',
templateUrl: './dashboard.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
isMobile: boolean;
// subscription: Subscription;
paramsSubscription: Subscription;
category$ = this.analyticsService.category$;
description$ = this.analyticsService.description$;
selectedCat: string;
selectedTimespan; //string? or Timespan?
timespanFilter: Filter = {
id: 'timespan',
label: 'Timespan',
options: [],
};
constructor(
public client: Client,
public route: ActivatedRoute,
private router: Router,
public session: Session,
public title: MindsTitle,
public analyticsService: AnalyticsDashboardService,
private cd: ChangeDetectorRef
) {}
ngOnInit() {
// TODO: why wasn't this working? didn't reroute
if (!this.session.isLoggedIn()) {
this.router.navigate(['/login']);
return;
}
this.isMobile = isMobileOrTablet();
this.title.setTitle('Analytics');
this.route.paramMap.subscribe((params: ParamMap) => {
this.updateCategory(params.get('category'));
});
this.paramsSubscription = this.route.queryParams.subscribe(params => {
// TODO: handleUrl
if (params['timespan']) {
// this.updateTimespan(params['timespan']);
}
});
this.analyticsService.timespans$.subscribe(timespans => {
this.timespanFilter.options = timespans;
this.detectChanges();
});
this.analyticsService.category$.subscribe(category => {
this.detectChanges();
});
this.analyticsService.metrics$.subscribe(metrics => {
this.detectChanges();
});
if (!this.session.isAdmin()) {
this.analyticsService.updateFilter('channel::self');
} else {
this.analyticsService.updateFilter('channel::all');
}
}
updateTimespan(timespanId) {
// TODO
// update url
// this.analyticsService.updateTimespan(timespanId);
}
updateCategory(categoryId) {
this.analyticsService.updateCategory(categoryId);
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
ngOnDestroy() {
this.paramsSubscription.unsubscribe();
}
}
import { TestBed } from '@angular/core/testing';
import {
AnalyticsDashboardService,
Category,
Response,
Dashboard,
Filter,
Option,
Metric,
Summary,
Visualisation,
Bucket,
Timespan,
UserState,
} from './dashboard.service';
describe('AnalyticsDashboardService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
xit('should be created', () => {
const service: AnalyticsDashboardService = TestBed.get(
AnalyticsDashboardService
);
expect(service).toBeTruthy();
});
});
import { Injectable } from '@angular/core';
import { FormControl } from '@angular/forms';
import { BehaviorSubject, Observable, combineLatest, of } from 'rxjs';
import {
map,
distinctUntilChanged,
switchMap,
startWith,
tap,
delay,
debounceTime,
throttleTime,
catchError,
} from 'rxjs/operators';
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;
timespan: string;
timespans: Timespan[];
metric: string;
metrics: Metric[];
filter: string[];
filters: Filter[];
}
export interface Filter {
id: string;
label: string;
options: Option[];
description: string;
}
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[];
loading: boolean;
}
// ʕ •ᴥ•ʔ
let _state: UserState = fakeData[0];
// {
// // loading: false,
// // filter: ['platform::browser'],
// // filters: [],
// // metric: 'views',
// // metrics: [],
// };
const deepDiff = (prev, curr) => JSON.stringify(prev) === JSON.stringify(curr);
// **********************************************************************
// **********************************************************************
@Injectable()
export class AnalyticsDashboardService {
/**
* Initialize the state subject and make it an observable
*/
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$ = this.state$.pipe(
map(state => state.loading),
distinctUntilChanged()
);
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);
}),
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,
loading: 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;
// }
// TODO: this in UpdateFilter() instead
// updateChannel(channel: string) {
// this.updateState({ ..._state, criteria, loading: true });
// }
updateCategory(category: string) {
this.updateState({
..._state,
category,
description: null,
metrics: [],
loading: true,
});
}
updateTimespan(timespan: string) {
this.updateState({
..._state,
timespan,
loading: true,
});
}
updateMetric(metric: string) {
this.updateState({ ..._state, metric, loading: true });
}
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);
}
console.log('update filter called: ' + selectedFilterStr);
console.log(filter);
this.updateState({ ..._state, filter, loading: true });
}
// // ------- Private Methods ------------------------
/** Update internal state cache and emit from store... */
private updateState(state: UserState) {
console.log('update state called');
this.store.next((_state = state));
}
/** Dashboard REST call */
private getDashboardResponse(
category: string,
timespan: string,
metric: string,
filter: string[]
): Observable<Response> {
return this.http
.get(`api/v2/analytics/dashboards/${category}`, {
metric,
timespan,
filter: filter.join(),
})
.pipe(
catchError(_ => of(null)),
map(response => response)
);
}
getData() {
console.warn('call was made to legacy function DashboardService.getData()');
}
}
This diff is collapsed.
<div class="spinnerContainer" *ngIf="loading$ | async">
<div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div>
</div>
<ng-container *ngIf="selectedMetric && selectedMetric.visualisation">
<m-analytics__metrics
*ngIf="selectedMetric.visualisation.type === 'chart'"
></m-analytics__metrics>
<div class="filterableChartWrapper" [ngClass]="{ isTable: isTable }">
<m-analytics__chart
*ngIf="selectedMetric.visualisation.type === 'chart'"
></m-analytics__chart>
<m-analytics__table
*ngIf="selectedMetric.visualisation.type === 'table'"
></m-analytics__table>
<m-analytics__filters
*ngIf="selectedMetric.visualisation.type === 'chart'"
></m-analytics__filters>
</div>
</ng-container>
.filterableChartWrapper {
position: relative;
padding: 16px;
width: 100%;
@include m-theme() {
border-top: 1px solid themed($m-grey-50);
}
&.isTable {
padding: 0;
width: 100%;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsLayoutChartComponent } from './layout-chart.component';
describe('AnalyticsLayoutChartComponent', () => {
let component: AnalyticsLayoutChartComponent;
let fixture: ComponentFixture<AnalyticsLayoutChartComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsLayoutChartComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsLayoutChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
OnInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import { Observable, Subscription, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { AnalyticsDashboardService } from '../../dashboard.service';
@Component({
selector: 'm-analytics__layout--chart',
templateUrl: './layout-chart.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsLayoutChartComponent implements OnInit {
subscription: Subscription;
loading$ = this.analyticsService.loading$;
selectedMetric$ = combineLatest(
this.analyticsService.metrics$,
this.analyticsService.metric$,
this.analyticsService.category$
).pipe(
map(([metrics, id, category]) => {
return metrics.find(metric => metric.id == id);
})
);
selectedMetric;
isTable: boolean = false;
constructor(
private analyticsService: AnalyticsDashboardService,
private cd: ChangeDetectorRef
) {}
ngOnInit() {
this.subscription = this.selectedMetric$.subscribe(metric => {
console.log('new metric');
this.selectedMetric = metric;
this.isTable =
this.selectedMetric &&
this.selectedMetric.visualisation &&
this.selectedMetric.visualisation.type === 'table';
this.detectChanges();
});
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
<p>
analytics__layout--summary works!
</p>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsLayoutSummaryComponent } from './layout-summary.component';
describe('AnalyticsLayoutSummaryComponent', () => {
let component: AnalyticsLayoutSummaryComponent;
let fixture: ComponentFixture<AnalyticsLayoutSummaryComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsLayoutSummaryComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsLayoutSummaryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'm-analytics__layout--summary',
templateUrl: './layout-summary.component.html',
})
export class AnalyticsLayoutSummaryComponent implements OnInit {
constructor() {}
ngOnInit() {}
}
<p>
analytics__layout--table works!
</p>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsLayoutTableComponent } from './layout-table.component';
describe('AnalyticsLayoutTableComponent', () => {
let component: AnalyticsLayoutTableComponent;
let fixture: ComponentFixture<AnalyticsLayoutTableComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsLayoutTableComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsLayoutTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'm-analytics__layout--table',
templateUrl: './layout-table.component.html',
})
export class AnalyticsLayoutTableComponent implements OnInit {
constructor() {}
ngOnInit() {}
}
{
"rulesDirectory": [
"node_modules/codelyzer"
],
"rulesDirectory": ["node_modules/codelyzer"],
"rules": {
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"comment-format": [true, "check-space"],
"curly": true,
"deprecation": {
"severity": "warn"
},
"eofline": true,
"forin": true,
"import-blacklist": [
true,
"rxjs/Rx"
],
"import-blacklist": [true, "rxjs/Rx"],
"import-spacing": true,
"indent": [
true,
"spaces",
2
],
"indent": [true, "spaces", 2],
"interface-over-type-literal": true,
"label-position": true,
"max-line-length": [
true,
140
],
"max-line-length": [true, 140],
"member-access": false,
"member-ordering": [
true,
......@@ -46,24 +31,14 @@
],
"no-arg": true,
"no-bitwise": true,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-empty": false,
"no-empty-interface": true,
"no-eval": true,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-inferrable-types": [true, "ignore-params"],
"no-misused-new": true,
"no-non-null-assertion": true,
"no-shadowed-variable": true,
......@@ -84,19 +59,10 @@
"check-whitespace"
],
"prefer-const": true,
"quotemark": [
true,
"single"
],
"quotemark": [true, "single"],
"radix": true,
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"semicolon": [true, "always"],
"triple-equals": [true, "allow-null-check"],
"typedef-whitespace": [
true,
{
......@@ -117,18 +83,8 @@
"check-separator",
"check-type"
],
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"minds",
"kebab-case"
],
"directive-selector": [true, "attribute", "m", "camelCase"],
"component-selector": [true, "element", "m", "kebab-case"],
"no-output-on-prefix": true,
"use-input-property-decorator": true,
"use-output-property-decorator": true,
......