Skip to content
This repository has been archived by the owner on Mar 29, 2024. It is now read-only.

Commit

Permalink
Add news carousel to dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
ppacher committed Oct 17, 2023
1 parent 874f9aa commit 5804c0a
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
import { Inject, Injectable, InjectionToken, isDevMode, NgZone } from '@angular/core';
import { BehaviorSubject, Observable, Observer, of } from 'rxjs';
import { concatMap, delay, filter, map, retryWhen, takeWhile, tap } from 'rxjs/operators';
Expand Down Expand Up @@ -137,6 +137,28 @@ export class PortapiService {
return this.http.post(`${this.httpEndpoint}/v1/netquery/history/cleanup`, undefined, { observe: 'response', responseType: 'arraybuffer' })
}

/** Requests a resource from the portmaster as application/json and automatically parses the response body*/
getResource<T>(resource: string): Observable<T>;

/** Requests a resource from the portmaster as text */
getResource(resource: string, type: string): Observable<HttpResponse<string>>;

getResource(resource: string, type?: string): Observable<HttpResponse<string> | any> {
if (type !== undefined) {
const headers = new HttpHeaders({
'Accept': type
})

return this.http.get(`${this.httpEndpoint}/v1/updates/get/${resource}`, {
headers: headers,
observe: 'response',
responseType: 'text',
})
}

return this.http.get<any>(`${this.httpEndpoint}/v1/updates/get/${resource}`);
}

/**
* Injects an event into a module to trigger certain backend
* behavior.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
(keydown)="onKeydown($event)">

<!-- Tab Group Header -->
<div *ngFor="let tab of tabs; let index=index"
<div *ngFor="let tab of (tabs$ | async); let index=index"
class="flex flex-row items-center justify-center px-4 py-2 space-x-1 cursor-pointer hover:text-primary" #tabHeader
[ngClass]="{'text-primary': index === activeTabIndex, 'text-secondary': index !== activeTabIndex}"
(click)="activateTab(index)">
Expand Down
27 changes: 25 additions & 2 deletions modules/portmaster/projects/safing/ui/src/lib/tabs/tab-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AfterContentInit, AfterViewInit, ChangeDetectionStrategy, ChangeDetecto
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import { Observable, Subject } from "rxjs";
import { distinctUntilChanged, map } from "rxjs/operators";
import { distinctUntilChanged, map, startWith } from "rxjs/operators";
import { SfngTabComponent, TAB_ANIMATION_DIRECTION, TAB_PORTAL, TAB_SCROLL_HANDLER, TabOutletComponent } from "./tab";

export interface SfngTabContentScrollEvent {
Expand Down Expand Up @@ -91,6 +91,15 @@ export class SfngTabGroupComponent implements AfterContentInit, AfterViewInit, O
private tabActivate$ = new Subject<string>();
private destroyRef = inject(DestroyRef);

/** Emits the tab QueryList every time there are changes to the content-children */
get tabs$() {
return this.tabs?.changes
.pipe(
map(() => this.tabs),
startWith(this.tabs)
)
}

/** onActivate fires when a tab has been activated. */
get onActivate(): Observable<string> { return this.tabActivate$.asObservable() }

Expand All @@ -106,7 +115,7 @@ export class SfngTabGroupComponent implements AfterContentInit, AfterViewInit, O
/**
* pendingTabIdx holds the id or the index of a tab that should be activated after the component
* has been bootstrapped. We need to cache this value here because the ActivatedRoute might emit
* before we ar AfterViewInit.
* before we are AfterViewInit.
*/
private pendingTabIdx: string | null = null;

Expand Down Expand Up @@ -156,6 +165,20 @@ export class SfngTabGroupComponent implements AfterContentInit, AfterViewInit, O
.withTypeAhead()
.withWrap()

this.tabs!.changes
.subscribe(() => {
if (this.portalOutlet?.hasAttached()) {
if (this.tabs!.length === 0) {
this.portalOutlet.detach();
}
} else {
if (this.tabs!.length > 0) {
this.activateTab(0)
}
}

})

this.keymanager.change
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(change => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div class="grid grid-cols-2 gap-2">
<app-dashboard-widget label="Connections" style="min-height: 400px;">
<sfng-netquery-line-chart [data]="connectionChart"></sfng-netquery-line-chart>
</app-dashboard-widget>

<app-dashboard-widget label="Data Usage" beta="true" style="min-height: 400px;">
<sfng-netquery-line-chart [config]="bwChartConfig" [data]="bandwidthChart"></sfng-netquery-line-chart>
</app-dashboard-widget>

<app-dashboard-widget label="Countries" beta="true" style="min-height: 400px">
<sfng-netquery-circular-bar-chart class="block w-full h-full" [data]="countryData" [config]="countryBarConfig"></sfng-netquery-circular-bar-chart>
</app-dashboard-widget>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, Input, OnInit, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AppProfile, BandwidthChartResult, ChartResult, Netquery } from '@safing/portmaster-api';
import { repeat } from 'rxjs';
import { CircularBarChartConfig, splitQueryResult } from 'src/app/shared/netquery/circular-bar-chart/circular-bar-chart.component';
import { DefaultBandwidthChartConfig } from 'src/app/shared/netquery/line-chart/line-chart';

interface CountryBarData {
series: 'country';
value: number;
country: string;
}

@Component({
selector: 'app-app-insights',
templateUrl: './app-insights.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppInsightsComponent implements OnInit {
private readonly netquery = inject(Netquery);
private readonly destroyRef = inject(DestroyRef);
private readonly cdr = inject(ChangeDetectorRef);

@Input()
profile!: AppProfile;

connectionChart: ChartResult[] = [];

bandwidthChart: BandwidthChartResult<any>[] = [];

bwChartConfig = DefaultBandwidthChartConfig;

countryData: CountryBarData[] = [];

readonly countryBarConfig: CircularBarChartConfig<CountryBarData> = {
stack: 'country',
seriesKey: 'series',
value: 'value',
ticks: 3,
colorAsClass: true,
series: {
'count': {
color: 'text-green-300 text-opacity-50',
},
},
}

ngOnInit() {
const key = `${this.profile.Source}/${this.profile.ID}`

this.netquery.batch({
countryData: {
select: [
'country',
{ $count: { field: '*', as: 'count' } },
],
query: {
internal: { $eq: false },
country: { $ne: '' }
},
groupBy: ['country']
}
})
.pipe(
repeat({ delay: 10000 }),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(result => {
this.countryData = splitQueryResult(result.countryData, ['count']) as CountryBarData[];
console.log(this.countryData)
this.cdr.markForCheck();
})

this.netquery.activeConnectionChart({ profile: key })
.pipe(
repeat({ delay: 10000 }),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(data => {
this.connectionChart = data;
this.cdr.markForCheck();
})

this.netquery.bandwidthChart({ profile: key }, undefined, 60)
.pipe(
repeat({ delay: 10000 }),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(data => {
this.bandwidthChart = data;
this.cdr.markForCheck();
})

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,56 @@ <h1>
</span>
</app-dashboard-widget>

<app-dashboard-widget class="flex-grow relative" id="news" label="News">

<div class="flex flex-col items-center justify-center w-full h-full gap-2 font-light" *ngIf="!news">
<span>News are only available if intel data updates are enabled</span>
<button [routerLink]="['/settings']" [queryParams]="{setting: 'core/automaticIntelUpdates'}">Open Settings</button>
</div>

<div class="flex flex-col items-center justify-center w-full h-full gap-2 font-light" *ngIf="news === 'pending'">
<span>Just a second, we're loading the latest news</span>
</div>

<ng-container *ngIf="!!news && news !== 'pending'">
<sfng-tab-group linkRouter="false" [customHeader]="true" #carousel>
<sfng-tab *ngFor="let card of news?.cards" [id]="card.title" [title]="card.title">
<section *sfngTabContent class="flex flex-col gap-2 p-2 h-full" (mouseenter)="onCarouselTabHover(card)" (mouseleave)="onCarouselTabHover(null)">
<h1 class="flex flex-row justify-between items-center w-full ml-2 mr-2">
<span>
{{ card.title }}
</span>
<a *ngIf="card.url" [attr.href]="card.url" target="_blank" class="bg-gray-400 text-xs rounded text-white hover:text-white py-0.5 px-1 hover:bg-gray-400 hover:bg-opacity-50 ml-2 flex flex-row items-center gap-2">
Open

<svg role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="text-white text-opacity-50 w-3 h-3">
<path fill="currentColor" d="M352 0c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9L370.7 96 201.4 265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L416 141.3l41.4 41.4c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V32c0-17.7-14.3-32-32-32H352zM80 32C35.8 32 0 67.8 0 112V432c0 44.2 35.8 80 80 80H400c44.2 0 80-35.8 80-80V320c0-17.7-14.3-32-32-32s-32 14.3-32 32V432c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V112c0-8.8 7.2-16 16-16H192c17.7 0 32-14.3 32-32s-14.3-32-32-32H80z"></path>
</svg>
</a>
</h1>

<markdown class="flex-grow" *ngIf="card.body" emoji [data]="card.body"></markdown>
<markdown *ngIf="card.footer" emoji [data]="card.footer"></markdown>

<div *ngIf="card.progress as progress" class="ml-2 mr-2">
<div class="overflow-hidden rounded border bg-gray-400 border-gray-100 h-3 w-full relative">
<div class="h-full" [style.backgroundColor]="progress.color" [style.width.%]="progress.percent"></div>
<div class="absolute top-0.5 bottom-0 left-0 right-0 flex flex-row justify-center items-center text-xxs text-background">
<span>{{ progress.percent }}%</span>
</div>
</div>
</div>

</section>
</sfng-tab>
</sfng-tab-group>

<div class="absolute bottom-2 left-0 right-0 flex flex-row items-center justify-center gap-2">
<span *ngFor="let dot of carousel.tabs; let index=index"
class="block w-2 h-2 transition-all duration-150 ease-in-out bg-opacity-50 rounded-full cursor-pointer bg-background"
[class.bg-blue]="carousel.activeTabIndex === index" (click)="carousel.activateTab(index)"></span>
</div>
</ng-container>

</app-dashboard-widget>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-template-areas:
"header header header header"
"feature feature feature feature"
"feature feature news news"
"feature feature news news"
"stats stats stats stats"
"stats stats stats stats"
"charts charts charts charts"
Expand All @@ -24,7 +25,8 @@
.dashboard-grid {
grid-template-areas:
"header header header header"
"feature feature feature feature"
"feature feature news news"
"feature feature news news"
"stats stats stats stats"
"charts charts charts charts"
"countries countries map map"
Expand Down Expand Up @@ -69,6 +71,10 @@
grid-area: bwvis-line;
}

#news {
grid-area: news;
}

.auto-grid-3 {
@apply grid gap-4;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
Expand All @@ -93,8 +99,28 @@ app-dashboard-widget {
@apply grid gap-3 w-full;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
}

&#news {

h1 {
@apply text-base;
@apply font-light;
}

::ng-deep markdown {
@apply font-light;

a {
@apply underline text-blue;
}

strong {
@apply font-medium;
}
}
}

}

::ng-deep #dashboard-map {
#world-group {
Expand Down
Loading

0 comments on commit 5804c0a

Please sign in to comment.