Commit 1b70d572 authored by Olivia Madrid's avatar Olivia Madrid

(feat): analytics v2 channel search

1 merge request!579WIP: Entity centric metrics (analytics v2)
Pipeline #87227140 running with stages
......@@ -59,6 +59,10 @@ import { AnalyticsFilterComponent } from './v2/components/filter/filter.componen
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';
PlotlyModule.plotlyjs = PlotlyJS;
......@@ -88,13 +92,11 @@ const routes: Routes = [
],
},
{
path: 'dashboard',
component: AnalyticsDashboardComponent,
children: [
{ path: '', redirectTo: 'traffic', pathMatch: 'full' },
{ path: ':category', component: AnalyticsDashboardComponent },
],
path: 'dashboard/',
redirectTo: 'dashboard/traffic',
pathMatch: 'full',
},
{ path: 'dashboard/:category', component: AnalyticsDashboardComponent },
],
},
];
......@@ -105,6 +107,8 @@ const routes: Routes = [
CommonModule,
RouterModule.forChild(routes),
PlotlyModule,
SearchModule,
FormsModule,
],
exports: [
AdminAnalyticsComponent,
......@@ -165,6 +169,8 @@ const routes: Routes = [
AnalyticsFilterComponent,
AnalyticsChartComponent,
AnalyticsTableComponent,
AnalyticsSearchComponent,
AnalyticsSearchSuggestionsComponent,
],
providers: [AnalyticsDashboardService],
})
......
......@@ -101,7 +101,10 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
}
});
this.themeService.isDark$.subscribe(isDark => (this.isDark = isDark));
this.themeSubscription = this.themeService.isDark$.subscribe(isDark => {
this.isDark = isDark;
// TODO: relayout and restyle when theme changes
});
this.graphDiv = document.getElementById('graphDiv');
......@@ -124,10 +127,6 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
console.log('segments', this.segments);
this.segmentLength = this.segments[0].buckets.length;
// this.timespan = this.vm.timespans.find(
// timespan => timespan.id === this.vm.timespan
// );
// ----------------------------------------------
for (let i = 0; i < this.segmentLength; i++) {
this.markerOpacities[i] = 0;
......@@ -136,9 +135,9 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
type: 'line',
layer: 'below',
x0: this.segments[0].buckets[i].date.slice(0, 10),
y0: 0,
y0: 0, // this should be graph y min
x1: this.segments[0].buckets[i].date.slice(0, 10),
y1: this.segments[0].buckets[i].value,
y1: this.segments[0].buckets[i].value, // this should be graph y max
line: {
color: this.getColor('m-transparent'),
width: 2,
......@@ -259,13 +258,13 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
}
relayout() {
//const layoutUpdate = this.layout;
// const layoutUpdate = this.layout;
//Plotly.relayout('graphDiv', layoutUpdate);
this.cd.markForCheck();
this.cd.detectChanges();
}
drawGraph() {}
// drawGraph() {}
// UTILITY \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/
unpack(rows, key) {
......@@ -302,7 +301,6 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
}
setLineHeights() {
console.log(this.graphDiv.layout);
this.shapes.forEach(shape => {
shape.y0 = this.graphDiv.layout.yaxis.range[0];
shape.y1 = this.graphDiv.layout.yaxis.range[1];
......@@ -310,11 +308,11 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
}
onHover($event) {
console.log('hovering');
console.log($event);
// console.log('hovering');
// console.log($event);
this.hoverPoint = $event.points[0].pointIndex;
console.log(this.shapes);
// console.log(this.shapes);
this.markerOpacities[this.hoverPoint] = 1;
// SHOW VERTICAL LINE
......@@ -326,12 +324,7 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
onUnhover($event) {
// HIDE VERTICAL LINE
this.shapes[this.hoverPoint].line.color = this.getColor(
'm-grey-50-transparent'
);
// this.layout.shapes[this.hoverPoint].line.color = this.getColor(
// 'm-grey-50-transparent'
// );
this.shapes[this.hoverPoint].line.color = this.getColor('m-transparent');
// HIDE MARKER
this.hoverInfoDiv.style.opacity = 0;
......@@ -348,7 +341,6 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
@HostListener('window:resize')
applyDimensions() {
console.log('windowresize');
// this.layout = this.relayout();
// this.setLineHeights();
// this.layout = {
......@@ -361,8 +353,6 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
console.log('change detected');
// this.updateGraph(); // Does this run every time a change is made to vm$ as well?
}
ngOnDestroy() {
......
<div class="filterWrapper" [ngClass]="{ expanded: expanded }">
<div
class="filterWrapper"
[ngClass]="{ expanded: expanded }"
(blur)="expanded = false"
>
<div class="filterHeader" (click)="expanded = !expanded">
<span class="filterLabel">{{ filter.label }}</span>
<span class="option option--selected">
......
......@@ -8,7 +8,7 @@ m-analytics__filter {
cursor: pointer;
border-radius: 3px;
@include m-theme() {
background-color: white;
background-color: themed($m-white);
border: 1px solid themed($m-grey-100);
color: rgba(themed($m-grey-200), 0.7);
}
......@@ -58,6 +58,7 @@ m-analytics__filter {
}
}
.option--selected {
border-radius: 3px;
@include m-theme() {
color: themed($m-grey-500);
}
......@@ -76,4 +77,9 @@ m-analytics__filter {
border-top: 1px solid themed($m-grey-100);
background-color: themed($m-white);
}
.option:last-child {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
}
m-analytics__metrics {
position: relative;
z-index: 999;
}
.metricsContainer {
display: flex;
// position: relative;
width: 95%;
padding: 0 16px;
@include m-theme() {
box-shadow: 0px 0px 10px -3px rgba(themed($m-black-always), 0.3);
box-shadow: 0 7px 15px -7px rgba(themed($m-black-always), 0.1);
}
.metric {
width: 20%;
padding: 20px;
padding: 24px 20px 20px 20px;
font-size: 12px;
box-sizing: border-box;
@include m-theme() {
// border-bottom: 5px solid themed($m-white);
// border-bottom: 5px solid transparent;
color: themed($m-grey-200);
}
&.active {
......
<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 './analytics-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();
});
it('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="Search for a 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 {
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: 200;
}
}
}
input,
label {
padding: $minds-padding $minds-padding $minds-padding $minds-padding * 4;
box-sizing: border-box;
}
}
.m-search-bar--context {
display: none;
}
.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();
});
it('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="page" [ngClass]="{ isMobile: isMobile }">
<section class="sidebar">
<h3>Analytics</h3>
<div *ngIf="isMobile">{{ category$ | async }}</div>
<div *ngIf="isMobile">{{ category$ | async | titlecase }}</div>
<div class="catContainer">
<i class="material-icons">menu</i>
<div class="cat" *ngFor="let cat of cats">
<a [routerLink]="'./' + cat.id" routerLinkActive="selected">{{
<a [routerLink]="'../' + cat.id" routerLinkActive="selected">{{
cat?.label
}}</a>
</div>
......@@ -15,20 +15,16 @@
<section class="main">
<div class="mainHeader">
<h3 class="selectedCatLabel">
{{ category$ | async }}
{{ category$ | async | titlecase }}
</h3>
<div class="globalFilters">
<!-- TODO enable only show to admins -->
<!-- TODO: ngIf for global_filter bool -->
<!-- TODO: enable only show to admins -->
<!-- <div *ngIf="session.isAdmin()" class="channelSearch"> -->
<div class="channelSearch">
(todo: Channel search)
<!-- <input
type="text"
[formControl]="channelSearch"
placeholder="Search for a channel"
/> -->
<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>
......
......@@ -47,7 +47,7 @@
.mainHeader {
display: flex;
justify-content: space-between;
align-items: flex-end;
align-items: flex-start;
}
.globalFilters {
......@@ -55,7 +55,8 @@
display: flex;
align-items: baseline;
.channelSearch {
border-radius: 5px;
// border-radius: 5px;
margin-right: 8px;
@include m-theme() {
border: themed($m-grey-50);
color: themed($m-grey-100);
......@@ -74,13 +75,15 @@
}
}
}
.m-analytics__layout {
position: relative;
.layoutWrapper {
@include m-theme() {
box-shadow: 0px 0px 10px -3px rgba(themed($m-black-always), 0.3);
}
}
.m-analytics__layout {
position: relative;
}
}
// *************************************
......
......@@ -7,7 +7,12 @@ import {
ChangeDetectorRef,
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import {
ActivatedRoute,
Router,
ParamMap,
RoutesRecognized,
} from '@angular/router';
import { Subscription, Observable } from 'rxjs';
import { MindsTitle } from '../../../services/ux/title';
......@@ -29,7 +34,6 @@ import {
UserState,
} from './dashboard.service';
// import fakeData from './fake';
import categories from './categories.default';
import isMobileOrTablet from '../../../helpers/is-mobile-or-tablet';
......@@ -46,7 +50,7 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
subscription: Subscription;
paramsSubscription: Subscription;
category$ = this.analyticsService.category$;
selectedCat: Category;
selectedCat: string;
selectedTimespan; //string? or Timespan?
timespanFilter: Filter = {
......@@ -74,8 +78,8 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
this.isMobile = isMobileOrTablet();
this.title.setTitle('Analytics');
this.route.params.subscribe(params => {
this.updateCategory(params['category']);
this.route.paramMap.subscribe((params: ParamMap) => {
this.updateCategory(params.get('category'));
});
// TODO: implement channel filter
......@@ -85,10 +89,19 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
this.paramsSubscription = this.route.queryParams.subscribe(params => {
// TODO: do the same filter, metric, channel
//if (params['timespan'] && params['timespan'] !== this.vm.timespan) {
// this.updateTimespan(params['timespan']);
//}
//this.selectedCat = this.cats.find(cat => cat.id === this.vm.category);
if (params['timespan'] && params['timespan'] !== 'bork') {
// this.updateTimespan(params['timespan']);
console.log('bork');
}
// TODO: if (there's no channel filter in the url) {
if (!this.session.isAdmin()) {
const selection = 'self';
} else {
const selection = 'all';
}
// }
});
this.analyticsService.timespans$.subscribe(timespans => {
......@@ -104,9 +117,7 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
}
updateCategory(categoryId) {
// TODO
// update url
// this.analyticsService.updateCategory(categoryId);
this.analyticsService.updateCategory(categoryId);
}
detectChanges() {
......
......@@ -379,9 +379,9 @@ export class AnalyticsDashboardService {
vm$: Observable<UserState> = new BehaviorSubject(_state);
/**
* Watch 5 streams to trigger user loads and state updates
* Watch 4 streams to trigger user loads and state updates
*/
// TODO: remove one of these later
// TODO: remove one of these clients later
constructor(private client: Client, private httpClient: HttpClient) {
this.loadFromRemote();
}
......
......@@ -4,6 +4,5 @@
width: 95%;
@include m-theme() {
border-top: 1px solid themed($m-grey-50);
box-shadow: 0px 0px 10px -3px rgba(themed($m-black-always), 0.3);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment