Move all into newgui/

This commit is contained in:
Jakob Borg
2021-02-10 13:58:08 +01:00
parent 777a30e870
commit 472affb6a3
120 changed files with 0 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
import { environment } from '../environments/environment'
export const deviceID = (): String => {
const dID: String = environment.production ? globalThis.metadata['deviceID'] : '12345';
return dID.substring(0, 5)
}
export const apiURL: String = '/'
export const apiRetry: number = 3;
+11
View File
@@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
+1
View File
@@ -0,0 +1 @@
<app-dashboard></app-dashboard>
+9
View File
@@ -0,0 +1,9 @@
/* Structure */
table {
width: 100%;
}
.mat-form-field {
font-size: 14px;
width: 100%;
}
+22
View File
@@ -0,0 +1,22 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});
+10
View File
@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
constructor() { }
}
+87
View File
@@ -0,0 +1,87 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatInputModule } from '@angular/material/input';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatDialogModule } from '@angular/material/dialog';
import { MatListModule } from '@angular/material/list'
import { MatButtonModule } from '@angular/material/button';
import { FlexLayoutModule } from '@angular/flex-layout';
import { httpInterceptorProviders } from './http-interceptors';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { StatusListComponent } from './lists/status-list/status-list.component';
import { DeviceListComponent } from './lists/device-list/device-list.component';
import { DonutChartComponent } from './charts/donut-chart/donut-chart.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { ListToggleComponent } from './list-toggle/list-toggle.component';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryConfigDataService } from './services/in-memory-config-data.service';
import { deviceID } from './api-utils';
import { environment } from '../environments/environment';
import { ChartItemComponent } from './charts/chart-item/chart-item.component';
import { ChartComponent } from './charts/chart/chart.component';
import { FolderListComponent } from './lists/folder-list/folder-list.component';
import { DialogComponent } from './dialog/dialog.component';
import { CardComponent, CardTitleComponent, CardContentComponent } from './card/card.component';
import { TrimPipe } from './trim.pipe';
@NgModule({
declarations: [
AppComponent,
StatusListComponent,
DeviceListComponent,
ListToggleComponent,
DashboardComponent,
DonutChartComponent,
ChartComponent,
ChartItemComponent,
FolderListComponent,
DialogComponent,
CardComponent,
CardTitleComponent,
CardContentComponent,
TrimPipe,
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
MatInputModule,
MatTableModule,
MatPaginatorModule,
MatSortModule,
MatButtonToggleModule,
MatCardModule,
MatProgressBarModule,
MatDialogModule,
MatListModule,
MatButtonModule,
FlexLayoutModule,
HttpClientModule,
HttpClientXsrfModule.withOptions({
headerName: 'X-CSRF-Token-' + deviceID(),
cookieName: 'CSRF-Token-' + deviceID(),
}),
environment.production ?
[] : HttpClientInMemoryWebApiModule.forRoot(InMemoryConfigDataService,
{ dataEncapsulation: false, delay: 10 }),
],
providers: [httpInterceptorProviders],
bootstrap: [AppComponent]
})
export class AppModule { }
+37
View File
@@ -0,0 +1,37 @@
// Import theming functions
@import '~@angular/material/theming';
@mixin tui-card-theme($theme) {
// Extract the palettes you need from the theme definition.
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.tui-card {
background-color: white;
border-radius: 4px;
height: 100%;
}
.tui-card-title {
padding: 16px 16px 0 16px;
font-size: mat-font-size($tech-ui-typography, subheading-2);
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
color: mat-color($primary);
}
.tui-card-content {
padding: 16px;
}
.tui-button-toggle .mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
line-height: 34px;
}
@media (prefers-color-scheme: dark) {
.tui-card {
background-color: map_get($mat-grey, 800);
}
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CardComponent } from './card.component';
describe('CardComponent', () => {
let component: CardComponent;
let fixture: ComponentFixture<CardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+35
View File
@@ -0,0 +1,35 @@
import { Component, OnInit } from '@angular/core';
import { cardElevation } from '../style';
@Component({
selector: 'app-card',
template: '<div class="{{elevation}} tui-card"><ng-content></ng-content></div>',
styleUrls: ['./card.component.scss']
})
export class CardComponent implements OnInit {
elevation: string = cardElevation;
constructor() { }
ngOnInit(): void {
}
}
@Component({
selector: 'app-card-title',
template: '<div class="tui-card-title"><ng-content></ng-content></div>',
styleUrls: ['./card.component.scss']
})
export class CardTitleComponent {
constructor() { }
}
@Component({
selector: 'app-card-content',
template: '<div class="tui-card-content"><ng-content></ng-content></div>',
styleUrls: ['./card.component.scss']
})
export class CardContentComponent {
constructor() { }
}
@@ -0,0 +1,4 @@
<div fxLayout="row" fxLayoutAlign="space-between start" [ngClass]="(_selected)?'item selected':'item'">
<div><a href="#">{{state}}</a>: &nbsp;</div>
<div>{{count}}</div>
</div>
@@ -0,0 +1,27 @@
@mixin chart-item-theme($theme) {
.item {
cursor: pointer;
padding: 3px 7px 3px 7px;
border-radius: 4px;
}
.selected {
background-color: #DDDDDD;
color: #303030;
}
.selected a {
color: #303030;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
.selected {
background-color: map_get($mat-grey, 900);
color: white;
}
.selected a {
color: white;
}
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChartItemComponent } from './chart-item.component';
describe('ChartItemComponent', () => {
let component: ChartItemComponent;
let fixture: ComponentFixture<ChartItemComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ChartItemComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChartItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,19 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-chart-item',
templateUrl: './chart-item.component.html',
styleUrls: ['./chart-item.component.scss']
})
export class ChartItemComponent {
@Input() state: string;
@Input() count: number;
@Input('selected')
set selected(s: boolean) {
this._selected = s;
}
_selected: boolean = true;
constructor() { }
}
@@ -0,0 +1,14 @@
<app-card>
<app-card-title>{{title | uppercase}}</app-card-title>
<app-card-content>
<div fxLayout="row" fxLayoutAlign="space-between stretch">
<app-donut-chart [elementID]="chartID" fxFlex="30" [title]="title" (stateEvent)="onItemSelect($event)">
</app-donut-chart>
<div class=" items" fxLayout="column" fxLayoutAlign="start end" fxFlex="70">
<app-chart-item *ngFor="let state of states" (click)="onItemSelect(state)" [state]="state.label"
[count]="state.count" [selected]="state.selected">
</app-chart-item>
</div>
</div>
</app-card-content>
</app-card>
@@ -0,0 +1,26 @@
import { async, TestBed } from '@angular/core/testing';
import { ChartComponent } from './chart.component';
import { HttpClientModule } from '@angular/common/http';
class MockService {
getEach() {
// unimplemented
}
};
describe('ChartComponent', () => {
let component: ChartComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
providers: [ChartComponent]
}).compileComponents();
component = TestBed.inject(ChartComponent);
}));
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,115 @@
import { Component, OnInit, ViewChild, Input, Type } from '@angular/core';
import Folder from '../../folder'
import { FolderService } from 'src/app/services/folder.service';
import { DonutChartComponent } from '../donut-chart/donut-chart.component';
import { DeviceService } from 'src/app/services/device.service';
import Device from 'src/app/device';
import { StType } from '../../type';
import { FilterService } from 'src/app/services/filter.service';
import { Observable } from 'rxjs';
export interface ChartItemState {
label: string,
count: number,
color: string,
selected: boolean,
}
@Component({
selector: 'app-chart',
templateUrl: './chart.component.html',
styleUrls: ['./chart.component.scss']
})
export class ChartComponent implements OnInit {
@ViewChild(DonutChartComponent) donutChart: DonutChartComponent;
@Input() type: StType;
title: string;
chartID: string;
states: ChartItemState[] = [];
private observer: Observable<any>;
private activeChartState: ChartItemState;
constructor(
private folderService: FolderService,
private deviceService: DeviceService,
private filterService: FilterService,
) { }
onItemSelect(s: ChartItemState) {
// Send chart item state to filter
this.filterService.changeFilter({ type: this.type, text: s.label });
// Deselect all other items
this.states.forEach(s => {
s.selected = false;
});
// Select item only
if (s !== this.activeChartState) {
s.selected = true;
this.activeChartState = s;
} else {
this.activeChartState = null;
this.filterService.changeFilter({ type: this.type, text: "" })
}
}
ngOnInit(): void {
switch (this.type) {
case StType.Folder:
this.title = "Folders";
this.chartID = 'foldersChart';
this.observer = this.folderService.folderAdded$;
break;
case StType.Device:
this.title = "Devices";
this.chartID = 'devicesChart';
this.observer = this.deviceService.deviceAdded$;
break;
}
}
ngAfterViewInit() {
let totalCount: number = 0;
this.observer.subscribe(
t => {
// Count the number of folders and set chart
totalCount++;
this.donutChart.count = totalCount;
// Get StateType and convert to string
const stateType = t.stateType;
const state = t.state;
let color;
switch (this.type) {
case StType.Folder:
color = Folder.stateTypeToColor(t.stateType);
break;
case StType.Device:
color = Device.stateTypeToColor(stateType);
break;
}
// Check if state exists
let found: boolean = false;
this.states.forEach(s => {
if (s.label === state) {
s.count = s.count + 1;
found = true;
}
});
if (!found) {
this.states.push({ label: state, count: 1, color: color, selected: false });
}
this.donutChart.updateData(this.states);
},
err => console.error('Observer got an error: ' + err),
() => {
}
);
}
}
@@ -0,0 +1,7 @@
<div class="chart-container">
<canvas id={{elementID}} width="100px" height="100px"></canvas>
<div class="center" fxLayout="column" fxLayoutAlign="center center">
<div class="{{_countClass}}">{{_count}}</div>
<div class="title">{{title}}</div>
</div>
</div>
@@ -0,0 +1,48 @@
.chart-container {
position: relative;
width: 100%;
height: 100%;
}
.center {
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
width: 50%;
height: 50%;
overflow: auto;
margin: auto;
}
.title {
font-size: calc(0.5rem + 0.625vw);
display:none;
}
.count-total {
font-size: calc(1rem + 0.625vw);
}
.large-count-total {
font-size: calc(0.5rem + 0.625vw);
}
@media (max-width: 800px) {
.count-total {
font-size: calc(1.00rem + 0.625vw);
}
}
@media (min-width: 800px) and (max-width: 1000px) {
.title {
font-size: calc(0.35rem + 0.625vw);
}
.count-total {
font-size: calc(1.35rem + 0.625vw);
}
}
@media (min-width:1000px) {
.title {
display: inline;
}
}
@@ -0,0 +1,21 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DonutChartComponent } from './donut-chart.component';
import { HttpClientModule } from '@angular/common/http';
describe('DonutChartComponent', () => {
let component: DonutChartComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DonutChartComponent],
providers: [DonutChartComponent]
}).compileComponents();
component = TestBed.inject(DonutChartComponent);
}));
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,87 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Chart } from 'chart.js'
import { tooltip } from '../tooltip'
import { FilterService } from 'src/app/services/filter.service';
import { ChartItemState } from '../chart/chart.component';
@Component({
selector: 'app-donut-chart',
templateUrl: './donut-chart.component.html',
styleUrls: ['./donut-chart.component.scss']
})
export class DonutChartComponent {
@Input() elementID: string;
@Input() title: number;
@Output() stateEvent = new EventEmitter<ChartItemState>();;
_count: number;
_countClass = "count-total";
set count(n: number) {
if (n >= 1000) { // use a smaller font
this._countClass = "large-count-total"
}
this._count = n;
}
private canvas: any;
private ctx: any;
private chart: Chart;
private states: ChartItemState[];
constructor(private filterService: FilterService) { }
updateData(states: ChartItemState[]): void {
this.states = states;
// Using object destructuring
for (let i = 0; i < states.length; i++) {
let s = states[i];
this.chart.data.labels[i] = s.label;
this.chart.data.datasets[0].data[i] = s.count;
this.chart.data.datasets[0].backgroundColor[i] = s.color;
}
this.chart.update();
}
removeAllData(withAnimation: boolean): void {
this.chart.data.labels.pop();
this.chart.data.datasets.forEach((dataset) => {
dataset.data = [];
});
this.chart.update(withAnimation);
}
ngAfterViewInit(): void {
this.canvas = document.getElementById(this.elementID);
this.ctx = this.canvas.getContext('2d');
this.chart = new Chart(this.ctx, {
type: 'doughnut',
data: {
datasets: [{
data: [],
backgroundColor: [],
borderWidth: 1
}]
},
options: {
cutoutPercentage: 77,
responsive: true,
onClick: (e) => {
var activePoints = this.chart.getElementsAtEvent(e);
if (activePoints.length > 0) {
const index = activePoints[0]["_index"];
this.stateEvent.emit(this.states[index]);
}
},
legend: {
display: false
},
tooltips: {
// Disable the on-canvas tooltip
enabled: false,
custom: tooltip(),
},
animation: false
}
});
}
}
+62
View File
@@ -0,0 +1,62 @@
// Adapted from https://www.chartjs.org/samples/latest/tooltips/custom-pie.html
export let tooltip: () => (tooltip: any) => void =
function (): (tooltip: any) => void {
return function (tooltip: any): void {
// Tooltip Element
const tooltipEl = document.getElementById('chartjs-tooltip');
// Hide if no tooltip
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = '0';
return;
}
// Set caret Position
tooltipEl.classList.remove('above', 'below', 'no-transform');
if (tooltip.yAlign) {
tooltipEl.classList.add(tooltip.yAlign);
} else {
tooltipEl.classList.add('no-transform');
}
function getBody(bodyItem) {
return bodyItem.lines;
}
// Set Text
if (tooltip.body) {
let titleLines = tooltip.title || [];
const bodyLines = tooltip.body.map(getBody);
let innerHtml = '<thead>';
titleLines.forEach(function (title) {
innerHtml += '<tr><th>' + title + '</th></tr>';
});
innerHtml += '</thead><tbody>';
bodyLines.forEach(function (body, i) {
let colors = tooltip.labelColors[i];
let style = 'background:' + colors.backgroundColor;
style += '; border-color:' + colors.borderColor;
style += '; border-width: 2px';
let span = '<span class="chartjs-tooltip-key" style="' + style + '"></span>';
innerHtml += '<tr><td>' + span + body + '</td></tr>';
});
innerHtml += '</tbody>';
let tableRoot = tooltipEl.querySelector('table');
tableRoot.innerHTML = innerHtml;
}
var position = this._chart.canvas.getBoundingClientRect();
// Display, position, and set styles for font
tooltipEl.style.opacity = '1';
tooltipEl.style.position = 'absolute';
tooltipEl.style.left = position.left + window.pageXOffset + tooltip.caretX + 'px';
tooltipEl.style.top = position.top + window.pageYOffset + tooltip.caretY + 'px';
tooltipEl.style.padding = tooltip.yPadding + 'px ' + tooltip.xPadding + 'px';
tooltipEl.style.pointerEvents = 'none';
}
};
+7
View File
@@ -0,0 +1,7 @@
export interface Completion {
completion: number;
globalBytes: number;
needBytes: number;
needDeletes: number;
needItems: number;
}
+16
View File
@@ -0,0 +1,16 @@
export interface SystemConnections {
connections: { deviceId?: Connection };
total: Connection;
}
export interface Connection {
address: string;
at: string;
clientVersion: string;
connected: boolean;
crypto: string;
inBytesTotal: number;
outBytesTotal: number;
paused: boolean;
type: string;
}
@@ -0,0 +1,21 @@
<!--<div class="grid-container" gdAreas="header header | folders devices | status-list status-list | footer footer"
gdGap="16px" gdRows="auto auto auto"> -->
<!--<div class="grid-container" fxLayout="row" fxLayoutGap="16px grid" fxLayoutAlign="stretch">-->
<div class="header" fxLayout="row" fxLayoutAlign="space-between center">
<div fxLayout="row" fxLayoutGap="16px" fxLayoutAlign="start center">
<img src="assets/logo-horizontal.svg" width="150px" />
<span>Tech UI</span>
</div>
</div>
<div class="progress">
<mat-progress-bar mode="determinate" value="{{progressValue}}" [@progressBar]="isLoading ? 'start' : 'done'">
</mat-progress-bar>
</div>
<div fxLayout="column" fxLayoutGap="16px" class="grid-container" [@loading]="isLoading ? 'start' : 'done'">
<div fxLayout="row" fxLayoutGap="16px" fxLayoutAlign="space-between stretch">
<app-chart [type]=folderChart fxFlex="50"></app-chart>
<app-chart [type]=deviceChart fxFlex="50"></app-chart>
</div>
<app-status-list gdArea="status-list"></app-status-list>
<div></div>
</div>
@@ -0,0 +1,13 @@
.header {
margin: 15px 3vw 12px 3vw;
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
}
.progress {
margin: 0 3vw 0 3vw;
}
.grid-container {
margin: 10px calc(10px + 3.3vw);
min-width: 600px;
}
@@ -0,0 +1,34 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
import { HttpClientModule } from '@angular/common/http';
import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform-browser/animations';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DashboardComponent],
imports: [
HttpClientModule,
NoopAnimationsModule,
BrowserAnimationsModule,
],
providers: [DashboardComponent]
}).compileComponents();
component = TestBed.inject(DashboardComponent);
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,117 @@
import { Component, OnInit, AfterViewInit, ViewChild, AfterViewChecked } from '@angular/core';
import {
trigger,
state,
style,
animate,
transition,
} from '@angular/animations';
import { SystemConfigService } from '../services/system-config.service';
import { StType } from '../type';
import { FilterService } from '../services/filter.service';
import { ProgressService } from '../services/progress.service';
import { MatProgressBar } from '@angular/material/progress-bar';
import { MessageService } from '../services/message.service';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { DialogComponent } from '../dialog/dialog.component';
import { FolderService } from '../services/folder.service';
import { DeviceService } from '../services/device.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'],
providers: [FilterService],
animations: [
trigger('loading', [
state('start', style({
marginTop: '20px',
})),
state('done', style({
marginTop: '0px',
})),
transition('start => done', [
animate('0.2s 0.2s')
]),
transition('done => start', [
animate('0.2s 0.2s')
]),
]),
trigger('progressBar', [
state('start', style({
opacity: 100,
visibility: 'visible'
})),
state('done', style({
opacity: 0,
visibility: 'hidden'
})),
transition('start => done', [
animate('0.35s')
]),
transition('done => start', [
animate('0.35s')
]),
]),
]
})
export class DashboardComponent implements OnInit, AfterViewInit {
@ViewChild(MatProgressBar) progressBar: MatProgressBar;
folderChart: StType = StType.Folder;
deviceChart: StType = StType.Device;
progressValue: number = 0;
isLoading = true;
private dialogRef: MatDialogRef<DialogComponent>;
constructor(
private systemConfigService: SystemConfigService,
private folderService: FolderService,
private deviceService: DeviceService,
private progressService: ProgressService,
private messageService: MessageService,
public dialog: MatDialog
) { }
ngOnInit() {
// Request data from Rest API
this.systemConfigService.getSystemConfig().subscribe(
_ => {
// Request devices and folders for charts and lists
this.folderService.requestFolders();
this.deviceService.requestDevices();
}
);
}
ngAfterViewInit() {
this.isLoading = true;
// Listen for progress service changes
let t = setInterval(() => {
if (this.progressService.isComplete()) {
clearInterval(t);
this.progressValue = 100;
this.isLoading = false;
}
this.progressValue = this.progressService.percentValue;
}, 100);
// Listen for messages from other services/components
this.messageService.messageAdded$
.subscribe(
_ => {
// Open dialog
if (!this.dialogRef)
this.dialogRef = this.dialog.open(DialogComponent);
this.dialogRef.afterClosed().subscribe(
_ => {
this.dialogRef = null;
this.messageService.clear();
}
);
});
}
}
+126
View File
@@ -0,0 +1,126 @@
import { colors } from './style';
import Folder from './folder';
import { Completion } from './completion';
interface Device {
deviceID: string;
name: string;
stateType: Device.StateType;
state: string;
paused: boolean;
connected: boolean;
completion: Completion;
used: boolean; // indicates if a folder is using the device
folders: Folder[];
}
namespace Device {
export enum StateType {
Insync = 1,
UnusedInsync,
Unknown,
Syncing,
Paused,
UnusedPaused,
Disconnected,
UnusedDisconnected,
}
export function stateTypeToString(s: StateType): string {
switch (s) {
case StateType.Insync:
return 'Up to Date';
case StateType.UnusedInsync:
return 'Connected (Unused)';
case StateType.Unknown:
return 'Unknown';
case StateType.Syncing:
return 'Syncing';
case StateType.Paused:
return 'Paused';
case StateType.UnusedPaused:
return 'Paused (Unused)';
case StateType.Disconnected:
return 'Disconnected';
case StateType.UnusedDisconnected:
return 'Disconnected (Unused)';
}
}
/**
* stateTypeToColor looks up a hex color string based on StateType
* @param s StateType
*/
export function stateTypeToColor(s: StateType): string {
switch (s) {
case StateType.Insync:
return colors.get("blue");
case StateType.UnusedInsync:
return colors.get("grey");
case StateType.Unknown:
return colors.get("grey");
case StateType.Syncing:
return colors.get("green");
case StateType.Paused:
return colors.get("grey");
case StateType.UnusedPaused:
return colors.get("grey");
case StateType.Disconnected:
return colors.get("yellow");
case StateType.UnusedDisconnected:
return colors.get("grey");
}
}
export function getStateType(d: Device): StateType {
// StateType Unknown is set in DeviceService
if (d.stateType === StateType.Unknown) {
return StateType.Unknown;
}
if (d.paused) {
return d.used ? StateType.Paused : StateType.UnusedPaused;
}
if (d.connected) {
if (d.completion.completion === 100) {
return d.used ? StateType.Insync : StateType.UnusedInsync;
} else {
return StateType.Syncing;
}
}
return d.used ? StateType.Disconnected : StateType.UnusedDisconnected;
}
export function recalcCompletion(d: Device) {
if (!d || !d.completion || !d.folders) {
return
}
var total = 0, needed = 0, deletes = 0, items = 0;
d.folders.forEach(folder => {
if (!folder || !folder.completion)
return
needed += folder.completion.needBytes;
items += folder.completion.needItems;
deletes += folder.completion.needDeletes;
});
if (total == 0) {
d.completion.completion = 100;
d.completion.needBytes = 0;
d.completion.needItems = 0;
} else {
d.completion.completion = Math.floor(100 * (1 - needed / total));
d.completion.needBytes = needed;
d.completion.needItems = items + deletes;
}
if (needed == 0 && deletes > 0) {
// We don't need any data, but we have deletes that we need
// to do. Drop down the completion percentage to indicate
// that we have stuff to do.
d.completion.completion = 95;
}
}
}
export default Device;
@@ -0,0 +1,6 @@
<div mat-dialog-content fxLayout="column" fxLayoutAlign="space-between center">
<mat-list>
<mat-list-item *ngFor='let message of messageService.messages'>{{message}}</mat-list-item>
</mat-list>
<button mat-stroked-button [mat-dialog-close] cdkFocusInitial>Close</button>
</div>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DialogComponent } from './dialog.component';
describe('DialogComponent', () => {
let component: DialogComponent;
let fixture: ComponentFixture<DialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DialogComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+26
View File
@@ -0,0 +1,26 @@
import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MessageService } from '../services/message.service';
export interface DialogData {
message: 'example message';
}
@Component({
selector: 'app-dialog',
templateUrl: './dialog.component.html',
styleUrls: ['./dialog.component.scss']
})
export class DialogComponent implements OnInit {
constructor(
public dialogRef: MatDialogRef<DialogComponent>,
public messageService: MessageService
) { }
ngOnInit(): void { }
onNoClick(): void {
this.dialogRef.close();
}
}
+200
View File
@@ -0,0 +1,200 @@
import Device from './device';
import { colors } from './style';
import { Completion } from './completion';
interface Folder {
id: string;
label: string;
devices: Device[];
status: Folder.Status;
stateType: Folder.StateType;
state: string;
paused: boolean;
completion: Completion;
path: string;
}
namespace Folder {
export enum StateType {
Paused = 1,
Unknown,
Unshared,
WaitingToScan,
Stopped,
Scanning,
Idle,
LocalAdditions,
WaitingToSync,
PreparingToSync,
Syncing,
OutOfSync,
FailedItems,
}
/**
* stateTypeToString returns a string representation of
* the StateType enum
* @param s StateType
*/
export function stateTypeToString(s: StateType): string {
switch (s) {
case StateType.Paused:
return 'Paused';
case StateType.Unknown:
return 'Unknown';
case StateType.Unshared:
return 'Unshared';
case StateType.WaitingToSync:
return 'Waiting to Sync';
case StateType.Stopped:
return 'Stopped';
case StateType.Scanning:
return 'Scanning';
case StateType.Idle:
return 'Up to Date';
case StateType.LocalAdditions:
return 'Local Additions';
case StateType.WaitingToScan:
return 'Waiting to Scan';
case StateType.PreparingToSync:
return 'Preparing to Sync';
case StateType.Syncing:
return 'Syncing';
case StateType.OutOfSync:
return 'Out of Sync';
case StateType.FailedItems:
return 'Failed Items';
}
}
/**
* stateTypeToColor looks up a hex color string based on StateType
* @param s StateType
*/
export function stateTypeToColor(s: StateType): string {
switch (s) {
case StateType.Paused:
return colors.get("grey");
case StateType.Unknown:
return colors.get("grey");
case StateType.Unshared:
return colors.get("grey");
case StateType.WaitingToSync:
return colors.get("yellow");
case StateType.Stopped:
return colors.get("grey");
case StateType.Scanning:
return colors.get("grey");
case StateType.Idle:
return colors.get("blue");
case StateType.LocalAdditions:
return colors.get("grey");
case StateType.WaitingToScan:
return colors.get("grey");
case StateType.PreparingToSync:
return colors.get("grey");
case StateType.Syncing:
return colors.get("green");
case StateType.OutOfSync:
return colors.get("grey");
case StateType.FailedItems:
return colors.get("red");
}
}
/**
* getStateType looks at a folder and determines the correct
* StateType to return
*
* Possible state values from API
* "idle", "scanning", "scan-waiting", "sync-waiting", "sync-preparing"
* "syncing", "error", "unknown"
*
* @param f Folder
*/
export function getStateType(f: Folder): StateType {
if (f.paused) {
return StateType.Paused;
}
if (!f.status || (Object.keys(f.status).length === 0)) {
return StateType.Unknown;
}
const fs: Folder.Status = f.status;
const state: string = fs.state;
// Match API string to StateType
switch (state) {
case "idle":
return StateType.Idle;
case "scanning":
return StateType.Scanning;
case "scan-waiting":
return StateType.WaitingToScan;
case "sync-waiting":
return StateType.WaitingToSync;
case "sync-preparing":
return StateType.PreparingToSync;
case "syncing":
return StateType.Syncing;
case "error":
// legacy, the state is called "stopped" in the gui
return StateType.Stopped;
case "unknown":
return StateType.Unknown;
}
if (fs.needTotalItems > 0) {
return StateType.OutOfSync;
}
if (fs.pullErrors > 0) {
return StateType.FailedItems;
}
if (fs.receiveOnlyTotalItems > 0) {
return StateType.LocalAdditions;
}
if (f.devices.length <= 1) {
return StateType.Unshared;
}
return StateType.Unknown;
}
export interface Status {
globalBytes: number;
globalDeleted: number;
globalDirectories: number;
globalFiles: number;
globalSymlinks: number;
globalTotalItems: number;
ignorePatterns: boolean;
inSyncBytes: number;
inSyncFiles: number;
invalid: string;
localBytes: number;
localDeleted: number;
localDirectories: number;
localFiles: number;
localSymlinks: number;
needBytes: number;
needDeletes: number;
needDirectories: number;
needFiles: number;
needSymlinks: number;
needTotalItems: number;
pullErrors: number;
receiveOnlyChangedBytes: number;
receiveOnlyChangedDeletes: number;
receiveOnlyChangedDirectories: number;
receiveOnlyChangedFiles: number;
receiveOnlyChangedSymlinks: number;
receiveOnlyTotalItems: number;
sequence: number;
state: string;
stateChanged: string;
version: number;
}
}
export default Folder;
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { CachingInterceptor } from './caching.interceptor';
describe('CachingInterceptor', () => {
beforeEach(() => TestBed.configureTestingModule({
providers: [
CachingInterceptor
]
}));
it('should be created', () => {
const interceptor: CachingInterceptor = TestBed.inject(CachingInterceptor);
expect(interceptor).toBeTruthy();
});
});
@@ -0,0 +1,58 @@
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpHeaders,
HttpResponse
} from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { RequestCacheService } from '../services/request-cache.service'
@Injectable()
export class CachingInterceptor implements HttpInterceptor {
constructor(private cache: RequestCacheService) { }
intercept(req: HttpRequest<any>, next: HttpHandler) {
// continue if not cachable.
if (!isCachable(req)) { return next.handle(req); }
const cachedResponse = this.cache.get(req);
return cachedResponse ?
of(cachedResponse) : sendRequest(req, next, this.cache);
}
}
/** Is this request cachable? */
function isCachable(req: HttpRequest<any>) {
// Only GET requests are cachable
return req.method === 'GET';
/*
return req.method === 'GET' &&
-1 < req.url.indexOf("url");
*/
}
/**
* Get server response observable by sending request to `next()`.
* Will add the response to the cache on the way out.
*/
function sendRequest(
req: HttpRequest<any>,
next: HttpHandler,
cache: RequestCacheService): Observable<HttpEvent<any>> {
// No headers allowed in npm search request
const noHeaderReq = req.clone({ headers: new HttpHeaders() });
return next.handle(noHeaderReq).pipe(
tap(event => {
// There may be other events besides the response.
if (event instanceof HttpResponse) {
// cache.put(req, event); // Update the cache.
}
})
);
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { CSRFInterceptor } from './csrf.interceptor';
describe('CsrfInterceptor', () => {
beforeEach(() => TestBed.configureTestingModule({
providers: [
CSRFInterceptor
]
}));
it('should be created', () => {
const interceptor: CSRFInterceptor = TestBed.inject(CSRFInterceptor);
expect(interceptor).toBeTruthy();
});
});
@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import { deviceID } from '../api-utils';
import {
HttpInterceptor, HttpHandler, HttpRequest, HttpHeaders
} from '@angular/common/http';
import { CookieService } from '../services/cookie.service';
@Injectable()
export class CSRFInterceptor implements HttpInterceptor {
constructor(private cookieService: CookieService) { }
intercept(req: HttpRequest<any>, next: HttpHandler) {
const dID: String = deviceID();
const csrfCookie = 'CSRF-Token-' + dID
// Clone the request and replace the original headers with
// cloned headers, updated with the CSRF information.
const csrfReq = req.clone({
headers: req.headers.set('X-CSRF-Token-' + dID,
this.cookieService.getCookie(csrfCookie))
});
// send cloned request with header to the next handler.
return next.handle(csrfReq);
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ErrorInterceptor } from './error.interceptor';
describe('ErrorInterceptor', () => {
beforeEach(() => TestBed.configureTestingModule({
providers: [
ErrorInterceptor
]
}));
it('should be created', () => {
const interceptor: ErrorInterceptor = TestBed.inject(ErrorInterceptor);
expect(interceptor).toBeTruthy();
});
});
@@ -0,0 +1,39 @@
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { apiRetry } from '../api-utils';
import { retry, catchError } from 'rxjs/operators';
import { MessageService } from '../services/message.service';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private messageService: MessageService) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request)
.pipe(
retry(apiRetry),
catchError((error: HttpErrorResponse) => {
let errorMsg: string;
if (error.error instanceof ErrorEvent) {
// Client side
errorMsg = `Error: ${error.error.message}`;
} else {
// Server side
errorMsg = `Error Status: ${error.status}\nMessage: ${error.message}`;
}
console.log(errorMsg);
this.messageService.add(errorMsg);
return throwError(errorMsg);
})
)
}
}
+14
View File
@@ -0,0 +1,14 @@
/* "Barrel" of Http Interceptors */
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { CSRFInterceptor } from './csrf.interceptor';
import { CachingInterceptor } from './caching.interceptor';
import { ErrorInterceptor } from './error.interceptor';
/** Http interceptor providers in outside-in order */
export const httpInterceptorProviders = [
{ provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
// CSRFInterceptor needs to be last
{ provide: HTTP_INTERCEPTORS, useClass: CSRFInterceptor, multi: true },
];
@@ -0,0 +1,4 @@
<mat-button-toggle-group class="tui-button-toggle" name="fontStyle" aria-label="Font Style" value="folders">
<mat-button-toggle value="folders" (click)="onSelect(listType.Folder)">Folders</mat-button-toggle>
<mat-button-toggle value="devices" (click)="onSelect(listType.Device)">Devices</mat-button-toggle>
</mat-button-toggle-group>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ListToggleComponent } from './list-toggle.component';
describe('ListToggleComponent', () => {
let component: ListToggleComponent;
let fixture: ComponentFixture<ListToggleComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ListToggleComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ListToggleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,26 @@
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core';
import { StType } from '../type';
import { MatButtonToggleGroup } from '@angular/material/button-toggle';
@Component({
selector: 'app-list-toggle',
templateUrl: './list-toggle.component.html',
styleUrls: ['./list-toggle.component.scss']
})
export class ListToggleComponent implements OnInit {
@ViewChild(MatButtonToggleGroup) group: MatButtonToggleGroup;
public listType = StType;
// public toggleValue: string = "folders";
@Output() listTypeEvent = new EventEmitter<StType>();
constructor() { }
ngOnInit(): void {
}
onSelect(t: StType): void {
this.listTypeEvent.emit(t);
}
}
@@ -0,0 +1,31 @@
<mat-form-field>
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. Up to Date">
</mat-form-field>
<table mat-table class="full-width-table" matSort aria-label="Devices" multiTemplateDataRows>
<ng-container matColumnDef="{{column}}" *ngFor="let column of displayedColumns">
<th mat-header-cell *matHeaderCellDef> {{column}} </th>
<td mat-cell *matCellDef="let device"> {{device[column]}} </td>
</ng-container>
<ng-container matColumnDef="expandedDetail">
<td mat-cell *matCellDef="let device" [attr.colspan]="displayedColumns.length">
<div class="table-detail" [@detailExpand]="device == expandedDevice ? 'expanded' : 'collapsed'">
<div class="detail-items">
<span>Folders: </span>
<span class="item-name" *ngFor="let folder of device.folders">{{folder.label | trim}}</span>
</div>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let device; columns: displayedColumns;" class="table-row"
[class.expanded-row]="expandedDevice === device"
(click)="expandedDevice = expandedDevice === device ? null : device">
</tr>
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="detail-row"></tr>
</table>
<mat-paginator #paginator [length]="dataSource?.data.length" [pageIndex]="0" [pageSize]="25"
[pageSizeOptions]="[25, 50, 100, 250]">
</mat-paginator>
@@ -0,0 +1,28 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { DeviceListComponent } from './device-list.component';
import { HttpClientModule } from '@angular/common/http';
import { ChangeDetectorRef } from '@angular/core';
describe('DeviceListComponent', () => {
let component: DeviceListComponent;
let fixture: ComponentFixture<DeviceListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DeviceListComponent],
imports: [HttpClientModule],
providers: [DeviceListComponent, ChangeDetectorRef]
}).compileComponents();
component = TestBed.inject(DeviceListComponent);
}));
it('should compile', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,96 @@
import { AfterViewInit, Component, OnInit, ViewChild, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTable, MatTableDataSource } from '@angular/material/table';
import Device from '../../device';
import { SystemConfigService } from '../../services/system-config.service';
import { FilterService } from 'src/app/services/filter.service';
import { StType } from 'src/app/type';
import { MatInput } from '@angular/material/input';
import { DeviceService } from 'src/app/services/device.service';
import { trigger, state, style, transition, animate } from '@angular/animations';
@Component({
selector: 'app-device-list',
templateUrl: './device-list.component.html',
styleUrls: ['../status-list/status-list.component.scss'],
animations: [
trigger('detailExpand', [
state('collapsed', style({ height: '0px', minHeight: '0' })),
state('expanded', style({ height: '*' })),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
]),
],
})
export class DeviceListComponent implements AfterViewInit, OnInit, OnDestroy {
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
@ViewChild(MatTable) table: MatTable<Device>;
@ViewChild(MatInput) input: MatInput;
dataSource: MatTableDataSource<Device>;
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['deviceID', 'name', 'state'];
expandedDevice: Device | null;
constructor(
private deviceService: DeviceService,
private filterService: FilterService,
private cdr: ChangeDetectorRef,
) { };
applyFilter(event: Event) {
// Set previous filter value
const filterValue = (event.target as HTMLInputElement).value;
this.filterService.previousInputs.set(StType.Device, filterValue);
this.dataSource.filter = filterValue.trim().toLowerCase();
}
ngOnInit() {
this.dataSource = new MatTableDataSource();
this.dataSource.data = [];
// Replace all data when requests are finished
this.deviceService.devicesUpdated$.subscribe(
devices => {
this.dataSource.data = devices;
}
);
// Add device as they come in
let devices: Device[] = [];
this.deviceService.deviceAdded$.subscribe(
device => {
devices.push(device);
this.dataSource.data = devices;
}
);
}
ngAfterViewInit() {
this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator;
this.table.dataSource = this.dataSource;
const changeText = (text: string) => {
this.dataSource.filter = text.trim().toLowerCase();
this.input.value = text;
this.cdr.detectChanges();
}
// Set previous value
changeText(this.filterService.previousInputs.get(StType.Device));
// Listen for filter changes from other components
this.filterService.filterChanged$
.subscribe(
input => {
if (input.type === StType.Device) {
changeText(input.text);
}
});
}
ngOnDestroy() { }
}
@@ -0,0 +1,32 @@
<mat-form-field>
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. Up to Date">
</mat-form-field>
<table mat-table class="full-width-table" matSort aria-label="Folders" multiTemplateDataRows>
<ng-container matColumnDef="{{column}}" *ngFor="let column of displayedColumns">
<th mat-header-cell *matHeaderCellDef> {{column}} </th>
<td mat-cell *matCellDef="let folder"> {{folder[column]}} </td>
</ng-container>
<!-- Expanded Content Column - The detail row is made up of this one column that spans across all columns -->
<ng-container matColumnDef="expandedDetail">
<td mat-cell *matCellDef="let folder" [attr.colspan]="displayedColumns.length">
<div class="table-detail" [@detailExpand]="folder == expandedFolder ? 'expanded' : 'collapsed'">
<div class="detail-items">
<span>Shared with: </span>
<span class="item-name" *ngFor="let device of folder.devices">{{device.name}}</span>
</div>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let folder; columns: displayedColumns;" class="table-row"
[class.expanded-row]="expandedFolder === folder"
(click)="expandedFolder = expandedFolder === folder ? null : folder">
</tr>
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="detail-row"></tr>
</table>
<mat-paginator #paginator [length]="dataSource?.data.length" [pageIndex]="0" [pageSize]="25"
[pageSizeOptions]="[25, 50, 100, 250]">
</mat-paginator>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FolderListComponent } from './folder-list.component';
import { HttpClientModule } from '@angular/common/http';
import { ChangeDetectorRef } from '@angular/core';
describe('FolderListComponent', () => {
let component: FolderListComponent;
let fixture: ComponentFixture<FolderListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [FolderListComponent],
imports: [HttpClientModule],
providers: [FolderListComponent, ChangeDetectorRef]
})
.compileComponents();
component = TestBed.inject(FolderListComponent);
}));
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,102 @@
import { AfterViewInit, Component, OnInit, ViewChild, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTable, MatTableDataSource } from '@angular/material/table';
import Folder from '../../folder';
import { SystemConfigService } from '../../services/system-config.service';
import { FilterService } from 'src/app/services/filter.service';
import { StType } from 'src/app/type';
import { MatInput } from '@angular/material/input';
import { FolderService } from 'src/app/services/folder.service';
import { trigger, state, style, transition, animate } from '@angular/animations';
@Component({
selector: 'app-folder-list',
templateUrl: './folder-list.component.html',
styleUrls: ['../status-list/status-list.component.scss'],
animations: [
trigger('detailExpand', [
state('collapsed', style({ height: '0px', minHeight: '0' })),
state('expanded', style({ height: '*' })),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
]),
],
})
export class FolderListComponent implements AfterViewInit, OnInit, OnDestroy {
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
@ViewChild(MatTable) table: MatTable<Folder>;
@ViewChild(MatInput) input: MatInput;
dataSource: MatTableDataSource<Folder>;
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = [
"id",
"label",
"path",
"state"
];
expandedFolder: Folder | null;
constructor(
private folderService: FolderService,
private filterService: FilterService,
private cdr: ChangeDetectorRef,
) {
};
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
this.filterService.previousInputs.set(StType.Folder, filterValue);
this.dataSource.filter = filterValue.trim().toLowerCase();
}
ngOnInit() {
this.dataSource = new MatTableDataSource();
this.dataSource.data = [];
// Replace all data when requests are finished
this.folderService.foldersUpdated$.subscribe(
folders => {
this.dataSource.data = folders;
}
);
// Add device as they come in
let folders: Folder[] = [];
this.folderService.folderAdded$.subscribe(
folder => {
folders.push(folder);
this.dataSource.data = folders;
}
);;
}
ngAfterViewInit() {
this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator;
this.table.dataSource = this.dataSource;
const changeText = (text: string) => {
this.dataSource.filter = text.trim().toLowerCase();
this.input.value = text;
this.cdr.detectChanges();
}
// Set previous value
changeText(this.filterService.previousInputs.get(StType.Folder));
// Listen for filter changes from other components
this.filterService.filterChanged$
.subscribe(
input => {
if (input.type === StType.Folder) {
changeText(input.text);
}
});
}
ngOnDestroy() { }
}
@@ -0,0 +1,10 @@
<app-card class="status-list">
<div fxLayout="row" fxLayoutAlign="space-between start">
<app-card-title>{{title | uppercase}}</app-card-title>
<app-list-toggle (listTypeEvent)="onToggle($event)" class="tui-card-toggle"></app-list-toggle>
</div>
<app-card-content>
<app-folder-list *ngIf="currentListType===listType.Folder"></app-folder-list>
<app-device-list *ngIf="currentListType===listType.Device"> </app-device-list>
</app-card-content>
</app-card>
@@ -0,0 +1,70 @@
.status-list .tui-card-toggle {
padding: 16px 16px 0 16px;
}
.full-width-table {
width: 100%;
}
.mat-form-field {
font-size: 14px;
width: 100%;
}
tr.detail-row {
height: 0;
}
tr.table-row:not(.expanded-row):hover {
background: whitesmoke;
color: #303030;
}
tr.table-row:not(.expanded-row):active {
background: #DDDDDD;
color: #303030;
}
.expanded-row {
background: #DDDDDD;
color: #303030;
}
.table-row td {
border-bottom-width: 0;
}
.table-detail {
overflow: hidden;
display: flex;
}
.detail-items {
padding: 16px;
}
// Hide empty name
.item-name:empty {
display: none;
}
.item-name:not(:last-child):after {
content: ", ";
}
@media (prefers-color-scheme: dark) {
tr.table-row:not(.expanded-row):hover {
background: #212121;
color: white;
}
tr.table-row:not(.expanded-row):active {
background: #212121;
color: white;
}
.expanded-row {
background: #212121;
color: white;
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { StatusListComponent } from './status-list.component';
describe('StatusListComponent', () => {
let component: StatusListComponent;
let fixture: ComponentFixture<StatusListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [StatusListComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(StatusListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,47 @@
import { Component, ViewChild, AfterViewInit, ChangeDetectorRef } from '@angular/core';
import { StType } from '../../type';
import { cardElevation } from '../../style';
import { FilterService } from 'src/app/services/filter.service';
import { ListToggleComponent } from 'src/app/list-toggle/list-toggle.component';
@Component({
selector: 'app-status-list',
templateUrl: './status-list.component.html',
styleUrls: ['./status-list.component.scss']
})
export class StatusListComponent {
@ViewChild(ListToggleComponent) toggle: ListToggleComponent;
currentListType: StType = StType.Folder;
listType = StType; // used in html
elevation: string = cardElevation;
title: string = 'Status';
constructor(
private filterService: FilterService,
private cdr: ChangeDetectorRef,
) { }
ngAfterViewInit() {
// Listen for filter changes from other components
this.filterService.filterChanged$.subscribe(
input => {
this.currentListType = input.type;
switch (input.type) {
case StType.Folder:
this.toggle.group.value = "folders";
break;
case StType.Device:
this.toggle.group.value = "devices";
break;
}
});
this.cdr.detectChanges(); // manually detect changes
}
onToggle(t: StType) {
this.currentListType = t;
}
}
@@ -0,0 +1,26 @@
export const dbCompletion =
[
{
"device": "YZJBJFX-RDBL7WY-6ZGKJ2D-4MJB4E7-ZATSDUY-LD6Y3L3-MLFUYWE-AEMXJAC",
"completion": 100,
"globalBytes": 156793013575,
"needBytes": 0,
"needDeletes": 0,
"needItems": 0
},
{
"completion": 80,
"globalBytes": 3013575,
"needBytes": 100,
"needDeletes": 0,
"needItems": 0
}
]
/*
{
"completion": 100,
"globalBytes": 156793013575,
"needBytes": 0,
"needDeletes": 0
}
*/
+46
View File
@@ -0,0 +1,46 @@
export const dbStatus =
[
{ "folder": "GXWxf-3zgnU", "state": "active" },
{ "folder": "Tyeho-ncvqp", "state": "idle" },
{ "folder": "Ihpqp-3zgnq", "state": "idle" },
{ "folder": "Abqqp-3zgnU", "state": "idle" },
{ "folder": "Bawer-3zgnU", "state": "idle" },
{ "folder": "Zpohq-3zgnU", "state": "idle" },
{ "folder": "Lkmbn-3zgnU", "state": "idle" },
{ "folder": "Poqff-3zgnU", "state": "idle" }
]
/*[{
"folder": "GXWxf-3zgnU",
"globalBytes": 0,
"globalDeleted": 0,
"globalDirectories": 0,
"globalFiles": 0,
"globalSymlinks": 0,
"globalTotalItems": 0,
"ignorePatterns": false,
"inSyncBytes": 0,
"inSyncFiles": 0,
"invalid": "",
"localBytes": 0,
"localDeleted": 0,
"localDirectories": 0,
"localFiles": 0,
"localSymlinks": 0,
"localTotalItems": 0,
"needBytes": 0,
"needDeletes": 0,
"needDirectories": 0,
"needFiles": 0,
"needSymlinks": 0,
"needTotalItems": 0,
"pullErrors": 0,
"receiveOnlyChangedBytes": 0,
"receiveOnlyChangedDeletes": 0,
"receiveOnlyChangedDirectories": 0,
"receiveOnlyChangedFiles": 0,
"receiveOnlyChangedSymlinks": 0,
"sequence": 0,
"state": "idle",
"stateChanged": "2018-08-08T07:04:57.301064781+02:00",
"version": 0
}]*/;
@@ -0,0 +1,67 @@
export const config = {
"version": 15,
"folders": [
{ "id": "GXWxf-3zgnU", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "YZJBJFX-RDBL7WY-6ZGKJ2D-4MJB4E7-ZATSDUY-LD6Y3L3-MLFUYWE-AEMXJAC" }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
{ "id": "Tyeho-ncvqp", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "..." }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
{ "id": "Ihpqp-3zgnq", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "..." }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
{ "id": "Abqqp-3zgnU", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "..." }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
{ "id": "Bawer-3zgnU", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "..." }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
{ "id": "Zpohq-3zgnU", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "..." }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
{ "id": "Lkmbn-3zgnU", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "..." }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
{ "id": "Poqff-3zgnU", "label": "MyFolder", "path": "...", "type": "sendreceive", "devices": [{ "deviceID": "..." }], "rescanIntervalS": 60, "ignorePerms": false, "autoNormalize": true, "minDiskFreePct": 1, "versioning": { "type": "simple", "params": { "keep": "5" } }, "copiers": 0, "pullers": 0, "hashers": 0, "order": "random", "ignoreDelete": false, "scanProgressIntervalS": 0, "pullerSleepS": 0, "pullerPauseS": 0, "maxConflicts": 10, "disableSparseFiles": false, "disableTempIndexes": false, "fsync": false, "invalid": "" },
],
"devices": [
{ "deviceID": "YZJBJFX-RDBL7WY-6ZGKJ2D-4MJB4E7-ZATSDUY-LD6Y3L3-MLFUYWE-AEMXJAC", "name": "Laptop", "addresses": ["dynamic", "tcp://192.168.1.2:22000"], "compression": "metadata", "certName": "", "introducer": false },
{ "deviceID": "...", "name": "Server", "addresses": ["dynamic", "tcp://192.168.1.3:22000"], "compression": "metadata", "certName": "", "introducer": false },
],
"gui": {
"enabled": true,
"address": "127.0.0.1:8384",
"user": "Username",
"password": "$2a$10$ZFws69T4FlvWwsqeIwL.TOo5zOYqsa/.TxlUnsGYS.j3JvjFTmxo6",
"useTLS": false,
"apiKey": "pGahcht56664QU5eoFQW6szbEG6Ec2Cr",
"insecureAdminAccess": false,
"theme": "default"
},
"options": {
"listenAddresses": [
"default"
],
"globalAnnounceServers": [
"default"
],
"globalAnnounceEnabled": true,
"localAnnounceEnabled": true,
"localAnnouncePort": 21027,
"localAnnounceMCAddr": "[ff12::8384]:21027",
"maxSendKbps": 0,
"maxRecvKbps": 0,
"reconnectionIntervalS": 60,
"relaysEnabled": true,
"relayReconnectIntervalM": 10,
"startBrowser": false,
"natEnabled": true,
"natLeaseMinutes": 60,
"natRenewalMinutes": 30,
"natTimeoutSeconds": 10,
"urAccepted": -1,
"urUniqueId": "",
"urURL": "https://data.syncthing.net/newdata",
"urPostInsecurely": false,
"urInitialDelayS": 1800,
"restartOnWakeup": true,
"autoUpgradeIntervalH": 12,
"keepTemporariesH": 24,
"cacheIgnoredFiles": false,
"progressUpdateIntervalS": 5,
"limitBandwidthInLan": false,
"minHomeDiskFreePct": 1,
"releasesURL": "https://upgrades.syncthing.net/meta.json",
"alwaysLocalNets": [],
"overwriteRemoteDeviceNamesOnConnect": false,
"tempIndexMinBlocks": 10
},
"ignoredDevices": [],
"ignoredFolders": []
}
@@ -0,0 +1,44 @@
export const connections = {
"total": {
"paused": false,
"clientVersion": "",
"at": "2015-11-07T17:29:47.691637262+01:00",
"connected": false,
"inBytesTotal": 1479,
"type": "",
"outBytesTotal": 1318,
"address": ""
},
"connections": {
"YZJBJFX-RDBL7WY-6ZGKJ2D-4MJB4E7-ZATSDUY-LD6Y3L3-MLFUYWE-AEMXJAC": {
"connected": true,
"inBytesTotal": 556,
"paused": false,
"at": "2015-11-07T17:29:47.691548971+01:00",
"clientVersion": "v0.12.1",
"address": "127.0.0.1:22002",
"type": "TCP (Client)",
"outBytesTotal": 550
},
"DOVII4U-SQEEESM-VZ2CVTC-CJM4YN5-QNV7DCU-5U3ASRL-YVFG6TH-W5DV5AA": {
"outBytesTotal": 0,
"type": "",
"address": "",
"at": "0001-01-01T00:00:00Z",
"clientVersion": "",
"paused": false,
"inBytesTotal": 0,
"connected": false
},
"UYGDMA4-TPHOFO5-2VQYDCC-7CWX7XW-INZINQT-LE4B42N-4JUZTSM-IWCSXA4": {
"address": "",
"type": "",
"outBytesTotal": 0,
"connected": false,
"inBytesTotal": 0,
"paused": false,
"at": "0001-01-01T00:00:00Z",
"clientVersion": ""
}
}
}
@@ -0,0 +1,59 @@
export const systemStatus = {
"alloc": 30618136,
"connectionServiceStatus": {
"dynamic+https://relays.syncthing.net/endpoint": {
"error": null,
"lanAddresses": [
"relay://23.92.71.120:443/?id=53STGR7-YBM6FCX-PAZ2RHM-YPY6OEJ-WYHVZO7-PCKQRCK-PZLTP7T-434XCAD&pingInterval=1m0s&networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=:22070&providedBy=canton7"
],
"wanAddresses": [
"relay://23.92.71.120:443/?id=53STGR7-YBM6FCX-PAZ2RHM-YPY6OEJ-WYHVZO7-PCKQRCK-PZLTP7T-434XCAD&pingInterval=1m0s&networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=:22070&providedBy=canton7"
]
},
"tcp://0.0.0.0:22000": {
"error": null,
"lanAddresses": [
"tcp://0.0.0.0:22000"
],
"wanAddresses": [
"tcp://0.0.0.0:22000"
]
}
},
"cpuPercent": 0,
"discoveryEnabled": true,
"discoveryErrors": {
"global@https://discovery-v4-1.syncthing.net/v2/": "500 Internal Server Error",
"global@https://discovery-v4-2.syncthing.net/v2/": "Post https://discovery-v4-2.syncthing.net/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)",
"global@https://discovery-v4-3.syncthing.net/v2/": "Post https://discovery-v4-3.syncthing.net/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)",
"global@https://discovery-v6-1.syncthing.net/v2/": "Post https://discovery-v6-1.syncthing.net/v2/: dial tcp [2001:470:28:4d6::5]:443: connect: no route to host",
"global@https://discovery-v6-2.syncthing.net/v2/": "Post https://discovery-v6-2.syncthing.net/v2/: dial tcp [2604:a880:800:10::182:a001]:443: connect: no route to host",
"global@https://discovery-v6-3.syncthing.net/v2/": "Post https://discovery-v6-3.syncthing.net/v2/: dial tcp [2400:6180:0:d0::d9:d001]:443: connect: no route to host"
},
"discoveryMethods": 8,
"goroutines": 49,
"lastDialStatus": {
"tcp://10.20.30.40": {
"when": "2019-05-16T07:41:23Z",
"error": "dial tcp 10.20.30.40:22000: i/o timeout"
},
"tcp://172.16.33.3:22000": {
"when": "2019-05-16T07:40:43Z",
"ok": true
},
"tcp://83.233.120.221:22000": {
"when": "2019-05-16T07:41:13Z",
"error": "dial tcp 83.233.120.221:22000: connect: connection refused"
}
},
"myID": "YZJBJFX-RDBL7WY-6ZGKJ2D-4MJB4E7-ZATSDUY-LD6Y3L3-MLFUYWE-AEMXJAC",
"pathSeparator": "/",
"startTime": "2016-06-06T19:41:43.039284753+02:00",
"sys": 42092792,
"themes": [
"default",
"dark"
],
"tilde": "/Users/jb",
"uptime": 2635
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { CookieService } from './cookie.service';
describe('CookieService', () => {
let service: CookieService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CookieService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
+35
View File
@@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CookieService {
constructor() { }
getCookie(name: string): string {
let ca: Array<string> = document.cookie.split(';');
let caLen: number = ca.length;
let cookieName = `${name}=`;
let c: string;
for (let i: number = 0; i < caLen; i += 1) {
c = ca[i].replace(/^\s+/g, '');
if (c.indexOf(cookieName) == 0) {
return c.substring(cookieName.length, c.length);
}
}
return '';
}
deleteCookie(name): void {
this.setCookie(name, "", -1);
}
setCookie(name: string, value: string, expireDays: number, path: string = ""): void {
let d: Date = new Date();
d.setTime(d.getTime() + expireDays * 24 * 60 * 60 * 1000);
let expires: string = "expires=" + d.toUTCString();
document.cookie = name + "=" + value + "; " + expires + (path.length > 0 ? "; path=" + path : "");
}
}
@@ -0,0 +1,20 @@
import { TestBed } from '@angular/core/testing';
import { DbCompletionService } from './db-completion.service';
import { HttpClientModule } from '@angular/common/http';
describe('DbCompletionService', () => {
let service: DbCompletionService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
providers: [DbCompletionService]
});
service = TestBed.inject(DbCompletionService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,51 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from 'src/environments/environment';
import { apiURL } from '../api-utils';
import { Completion } from '../completion';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { StType } from '../type';
@Injectable({
providedIn: 'root'
})
export class DbCompletionService {
private dbStatusUrl = environment.production ? apiURL + 'rest/db/completion' : 'api/dbCompletion';
constructor(private http: HttpClient) { }
getCompletion(type: StType, id: string): Observable<Completion> {
let httpOptions: { params: HttpParams };
if (id) {
switch (type) {
case StType.Device:
httpOptions = {
params: new HttpParams().set('device', id)
};
break;
case StType.Folder:
httpOptions = {
params: new HttpParams().set('folder', id)
};
break;
}
} else { }
return this.http
.get<Completion>(this.dbStatusUrl, httpOptions)
.pipe(
map(res => {
// Remove from array in developement
// in-memory-web-api returns arrays
if (!environment.production) {
const a: any = res as any;
if (a.length > 0) {
res = res[0];
}
}
return res;
})
);
}
}
@@ -0,0 +1,21 @@
import { TestBed } from '@angular/core/testing';
import { DbStatusService } from './db-status.service';
import { HttpClientModule } from '@angular/common/http';
describe('DbStatusService', () => {
let service: DbStatusService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
providers: [DbStatusService]
});
TestBed.configureTestingModule({});
service = TestBed.inject(DbStatusService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,43 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from '../../environments/environment'
import { apiURL } from '../api-utils'
import Folder from '../folder'
@Injectable({
providedIn: 'root'
})
export class DbStatusService {
private dbStatusUrl = environment.production ? apiURL + 'rest/db/status' : 'api/dbStatus';
constructor(private http: HttpClient) { }
getFolderStatus(id: string): Observable<Folder.Status> {
let httpOptions: { params: HttpParams };
if (id) {
httpOptions = {
params: new HttpParams().set('folder', id)
};
} else { }
return this.http
.get<Folder.Status>(this.dbStatusUrl, httpOptions)
.pipe(
map(res => {
// Remove from array in developement
// in-memory-web-api returns arrays
if (!environment.production) {
const a: any = res as any;
if (a.length > 0) {
res = res[0];
}
}
return res;
})
);
}
}
@@ -0,0 +1,20 @@
import { TestBed } from '@angular/core/testing';
import { DeviceService } from './device.service';
import { HttpClientModule } from '@angular/common/http';
describe('DeviceService', () => {
let service: DeviceService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
providers: [DeviceService]
});
service = TestBed.inject(DeviceService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
+128
View File
@@ -0,0 +1,128 @@
import { Injectable } from '@angular/core';
import Device from '../device';
import { Observable, Subscriber, ReplaySubject, Subject } from 'rxjs';
import { SystemConfigService } from './system-config.service';
import { SystemConnectionsService } from './system-connections.service';
import { DbCompletionService } from './db-completion.service';
import { SystemConnections } from '../connections';
import { SystemStatusService } from './system-status.service';
import { ProgressService } from './progress.service';
import { StType } from '../type';
@Injectable({
providedIn: 'root'
})
export class DeviceService {
private devices: Device[];
private sysConns: SystemConnections;
private devicesSubject: ReplaySubject<Device[]> = new ReplaySubject(1);
devicesUpdated$ = this.devicesSubject.asObservable();
private thisDevice: Device;
private deviceAddedSource = new Subject<Device>();
deviceAdded$ = this.deviceAddedSource.asObservable();
constructor(
private systemConfigService: SystemConfigService,
private systemConnectionsService: SystemConnectionsService,
private dbCompletionService: DbCompletionService,
private systemStatusService: SystemStatusService,
private progressService: ProgressService,
) { }
getDeviceStatusInOrder(startIndex: number) {
// Return if there aren't any device at the index
if (startIndex >= (this.devices.length)) {
this.devicesSubject.next(this.devices);
// this.devicesSubject.complete();
// this.deviceAddedSource.complete();
return;
}
const device: Device = this.devices[startIndex];
startIndex = startIndex + 1;
// Check if device in the connections
if (this.sysConns.connections[device.deviceID] === undefined) {
device.stateType = Device.StateType.Unknown;
} else {
// Set connected
device.connected = this.sysConns.connections[device.deviceID].connected;
// TODO ? temporarily set to connected
if (device.deviceID === this.thisDevice.deviceID) {
device.connected = true;
}
}
this.dbCompletionService.getCompletion(StType.Device, device.deviceID).subscribe(
c => {
device.completion = c;
Device.recalcCompletion(device);
device.stateType = Device.getStateType(device);
device.state = Device.stateTypeToString(device.stateType);
this.deviceAddedSource.next(device);
this.progressService.addToProgress(1);
// recursively get the status of the next device
this.getDeviceStatusInOrder(startIndex);
});
}
/**
* getEach() returns each device
*/
requestDevices() {
this.systemConfigService.getDevices().subscribe(
devices => {
this.devices = devices;
// First check to see which device is local 'thisDevice'
this.systemStatusService.getSystemStatus().subscribe(
status => {
this.devices.forEach(device => {
if (device.deviceID === status.myID) {
// TODO Determine if it should ignore thisDevice
this.thisDevice = device;
}
});
// Check folder devices to see if the device is used
this.systemConfigService.getFolders().subscribe(
folders => {
// Loop through all folder devices to see if the device is used
this.devices.forEach(device => {
// Alloc array if needed
if (!device.folders) {
device.folders = [];
}
folders.forEach(folder => {
folder.devices.forEach(fdevice => {
if (device.deviceID === fdevice.deviceID) {
// The device is used by a folder
device.used = true;
// Add a reference to the folder to the device
device.folders.push(folder);
}
});
});
});
// See if the connection is connected or undefined
this.systemConnectionsService.getSystemConnections().subscribe(
c => {
this.sysConns = c;
// Synchronously get the status of each device
this.getDeviceStatusInOrder(0);
}
);
});
}
)
}
);
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { FilterService } from './filter.service';
describe('FilterService', () => {
let service: FilterService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(FilterService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
+30
View File
@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { StType } from '../type';
import { Subject } from 'rxjs';
export interface FilterInput {
type: StType;
text: string
}
@Injectable({
providedIn: 'root'
})
export class FilterService {
previousInputs = new Map<StType, string>(
[
[StType.Folder, ""],
[StType.Device, ""],
]
)
constructor() { }
private filterChangeSource = new Subject<FilterInput>();
filterChanged$ = this.filterChangeSource.asObservable();
changeFilter(input: FilterInput) {
this.previousInputs.set(input.type, input.text)
this.filterChangeSource.next(input);
}
}
@@ -0,0 +1,20 @@
import { TestBed } from '@angular/core/testing';
import { FolderService } from './folder.service';
import { HttpClientModule } from '@angular/common/http';
describe('FolderService', () => {
let service: FolderService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
providers: [FolderService]
});
service = TestBed.inject(FolderService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
+98
View File
@@ -0,0 +1,98 @@
import { Injectable } from '@angular/core';
import { SystemConfigService } from './system-config.service';
import { Observable, Subscriber, Subject, ReplaySubject } from 'rxjs';
import Folder from '../folder';
import { DbStatusService } from './db-status.service';
import { ProgressService } from './progress.service';
import { DbCompletionService } from './db-completion.service';
import { StType } from '../type';
import { DeviceService } from './device.service';
@Injectable({
providedIn: 'root'
})
export class FolderService {
private folders: Folder[];
private foldersSubject: ReplaySubject<Folder[]> = new ReplaySubject(1);
foldersUpdated$ = this.foldersSubject.asObservable();
private folderAddedSource = new Subject<Folder>();
folderAdded$ = this.folderAddedSource.asObservable();
constructor(
private systemConfigService: SystemConfigService,
private deviceService: DeviceService,
private dbStatusService: DbStatusService,
private dbCompletionService: DbCompletionService,
private progressService: ProgressService,
) { }
getFolderStatusInOrder(startIndex: number) {
// Return if there aren't any folders at the index
if (startIndex >= (this.folders.length)) {
this.foldersSubject.next(this.folders);
// this.folderAddedSource.complete();
return;
}
const folder: Folder = this.folders[startIndex];
startIndex = startIndex + 1;
// Folder devices array only has deviceID
// and we want all the device info
this.systemConfigService.getDevices().subscribe(
devices => {
devices.forEach(device => {
// Update any device this folder
// has reference to
folder.devices.forEach((folderDevice, index) => {
if (folderDevice.deviceID === device.deviceID) {
console.log("find device match?", device.name)
folder.devices[index] = device;
console.log("update?", folder.devices);
}
});
});
// Gather the folder information from the status and
// completion services
this.dbStatusService.getFolderStatus(folder.id).subscribe(
status => {
folder.status = status;
this.dbCompletionService.getCompletion(StType.Folder, folder.id).subscribe(
c => {
folder.completion = c;
folder.stateType = Folder.getStateType(folder);
folder.state = Folder.stateTypeToString(folder.stateType);
this.folderAddedSource.next(folder);
this.progressService.addToProgress(1);
// Now that we have all the folder information
// recursively get the status of the next folder
this.getFolderStatusInOrder(startIndex);
});
}
);
}
)
}
/**
* requestFolders() requests each folder and uses db status service to
* set all their statuses and db completion service to find
* completion in order. Updating folderAdded$ and foldersUpdate$
* observers
*/
requestFolders() {
this.systemConfigService.getFolders().subscribe(
folders => {
this.folders = folders;
// Synchronously get the status of each folder
this.getFolderStatusInOrder(0);
}
);
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { InMemoryConfigDataService } from './in-memory-config-data.service';
describe('InMemoryDataService', () => {
let service: InMemoryConfigDataService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(InMemoryConfigDataService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,17 @@
import { Injectable } from '@angular/core';
import { config } from '../mocks/mock-system-config';
import { dbStatus } from '../mocks/mock-db-status';
import { connections } from '../mocks/mock-system-connections';
import { dbCompletion } from '../mocks/mock-db-completion';
import { systemStatus } from '../mocks/mock-system-status';
@Injectable({
providedIn: 'root'
})
export class InMemoryConfigDataService {
createDb() {
return { config, dbStatus, connections, dbCompletion, systemStatus };
}
constructor() { }
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { MessageService } from './message.service';
describe('MessageService', () => {
let service: MessageService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(MessageService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class MessageService {
messages: string[] = [];
private messageAddedSource = new Subject<string>();
messageAdded$ = this.messageAddedSource.asObservable();
add(message: string) {
this.messages.push(message);
this.messageAddedSource.next(message);
}
clear() {
this.messages = [];
}
}
@@ -0,0 +1,40 @@
import { TestBed } from '@angular/core/testing';
import { ProgressService } from './progress.service';
import { stringToKeyValue } from '@angular/flex-layout/extended/typings/style/style-transforms';
describe('ProgressService', () => {
let service: ProgressService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ProgressService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('#percentValue should return 0 - 100', () => {
interface iTest {
total: number,
progress: number,
expected: number,
}
const tests: Map<string, iTest> = new Map([
["default", { total: 0, progress: 0, expected: 0 }],
["NaN return 0", { total: 0, progress: 100, expected: 0 }],
["greater than 100 return 100", { total: 10, progress: 100, expected: 100 }],
["valid", { total: 100, progress: 100, expected: 100 }],
["valid", { total: 100, progress: 50, expected: 50 }],
["test floor", { total: 133, progress: 41, expected: 30 }],
]);
service = new ProgressService();
for (let test of tests.values()) {
service.total = test.total;
service.updateProgress(test.progress);
expect(service.percentValue).toBe(test.expected);
}
});
});
@@ -0,0 +1,50 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ProgressService {
private progress: number = 0;
private _total: number = 0;
set total(t: number) {
this._total = t;
}
get percentValue(): number {
let p: number = Math.floor((this.progress / this._total) * 100);
if (p < 0 || isNaN(p) || p === Infinity) {
p = 0;
} else if (p > 100) {
p = 100;
}
return p;
}
constructor() { }
addToProgress(n: number) {
if (n < 0 || isNaN(n) || n === Infinity) {
n = 0;
}
this.progress += n;
}
updateProgress(n: number) {
if (n < 0 || isNaN(n) || n === Infinity) {
n = 0
} else if (n > 100) {
n = 100
}
this.progress = n;
}
isComplete(): boolean {
if (this.progress >= this._total && this.progress > 0 && this._total > 0) {
return true;
}
return false;
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { RequestCacheService } from './request-cache.service';
describe('RequestCacheService', () => {
let service: RequestCacheService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(RequestCacheService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,50 @@
import { Injectable } from '@angular/core';
import { HttpResponse, HttpRequest } from '@angular/common/http';
export interface RequestCacheEntry {
url: string;
response: HttpResponse<any>;
lastRead: number;
}
const maxAge = 30000; // milliseconds
@Injectable({
providedIn: 'root'
})
export class RequestCacheService {
private cache: Map<string, RequestCacheEntry> = new Map();
constructor() { }
get(req: HttpRequest<any>): HttpResponse<any> | undefined {
const url = req.urlWithParams;
const cached = this.cache.get(url);
if (!cached) {
return undefined;
}
const isExpired = cached.lastRead < (Date.now() - maxAge);
return isExpired ? undefined : cached.response;
}
put(req: HttpRequest<any>, response: HttpResponse<any>): void {
const url = req.urlWithParams;
const entry = { url, response, lastRead: Date.now() };
this.cache.set(url, entry);
// Remove expired cache entries
const expired = Date.now() - maxAge;
this.cache.forEach(entry => {
if (entry.lastRead < expired) {
this.cache.delete(entry.url);
}
});
}
clearAll(): void {
this.cache = new Map();
}
}
@@ -0,0 +1,20 @@
import { TestBed } from '@angular/core/testing';
import { SystemConfigService } from './system-config.service';
import { HttpClientModule } from '@angular/common/http';
describe('SystemConfigService', () => {
let service: SystemConfigService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
providers: [SystemConfigService]
});
service = TestBed.inject(SystemConfigService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,55 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, ReplaySubject } from 'rxjs';
import { map } from 'rxjs/operators';
import Folder from '../folder';
import Device from '../device';
import { environment } from '../../environments/environment'
import { apiURL } from '../api-utils'
import { ProgressService } from './progress.service';
@Injectable({
providedIn: 'root'
})
export class SystemConfigService {
private folders: Folder[];
private devices: Device[];
private foldersSubject: ReplaySubject<Folder[]> = new ReplaySubject(1);
private devicesSubject: ReplaySubject<Device[]> = new ReplaySubject(1);
private systemConfigUrl = environment.production ? apiURL + 'rest/system/config' : 'api/config';
constructor(
private http: HttpClient,
private progressService: ProgressService,
) { }
getSystemConfig(): Observable<any> {
return this.http
.get(this.systemConfigUrl)
.pipe(
map(res => {
this.folders = res['folders'];
this.devices = res['devices'];
// Set the total for the progress service
this.progressService.total = this.folders.length + this.devices.length;
this.foldersSubject.next(this.folders);
this.devicesSubject.next(this.devices);
return res;
})
);
}
getFolders(): Observable<Folder[]> {
return this.foldersSubject.asObservable();
}
getDevices(): Observable<Device[]> {
return this.devicesSubject.asObservable();
}
}
@@ -0,0 +1,20 @@
import { TestBed } from '@angular/core/testing';
import { SystemConnectionsService } from './system-connections.service';
import { HttpClientModule } from '@angular/common/http';
describe('SystemConnectionsService', () => {
let service: SystemConnectionsService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
providers: [SystemConnectionsService]
});
service = TestBed.inject(SystemConnectionsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { apiURL } from '../api-utils';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { SystemConnections } from '../connections';
@Injectable({
providedIn: 'root'
})
export class SystemConnectionsService {
private systemConfigUrl = environment.production ? apiURL + 'rest/system/connections' : 'api/connections';
constructor(private http: HttpClient) { }
getSystemConnections(): Observable<SystemConnections> {
return this.http
.get<SystemConnections>(this.systemConfigUrl)
.pipe(
map(res => {
return res;
})
);
}
}
@@ -0,0 +1,20 @@
import { TestBed } from '@angular/core/testing';
import { SystemStatusService } from './system-status.service';
import { HttpClientModule } from '@angular/common/http';
describe('SystemStatusService', () => {
let service: SystemStatusService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
providers: [SystemStatusService]
});
service = TestBed.inject(SystemStatusService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { apiURL } from '../api-utils';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
import { SystemStatus } from '../system-status';
@Injectable({
providedIn: 'root'
})
export class SystemStatusService {
private systemStatusUrl = environment.production ? apiURL + 'rest/system/status' : 'api/systemStatus';
constructor(private http: HttpClient) { }
getSystemStatus(): Observable<SystemStatus> {
return this.http
.get<SystemStatus>(this.systemStatusUrl)
.pipe(
map(res => {
return res;
})
);
}
}
+11
View File
@@ -0,0 +1,11 @@
// material design elevation for all dashboard components
export const cardElevation: string = "mat-elevation-z2";
export const dataTableElevation: string = "mat-elevation-z0";
export const colors: Map<string, string> = new Map([
["blue", "#0891D1"],
["yellow", "#FFC400"],
["green", "#56C568"],
["grey", "#DDDDDD"],
["red", "#EB5757"]
]);
+17
View File
@@ -0,0 +1,17 @@
export interface SystemStatus {
alloc: number;
connectionServiceStatus: any;
cpuPercent: number; // allows returns 0
discoveryEnabled: boolean;
discoveryErrors: any;
discoveryMethods: number;
goroutines: number;
lastDialStatus: any;
myID: string;
pathSeparator: string;
startTime: string;
sys: number;
themes: string[];
tilde: string;
uptime: number;
}
+8
View File
@@ -0,0 +1,8 @@
import { TrimPipe } from './trim.pipe';
describe('TrimPipe', () => {
it('create an instance', () => {
const pipe = new TrimPipe();
expect(pipe).toBeTruthy();
});
});
+12
View File
@@ -0,0 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'trim'
})
export class TrimPipe implements PipeTransform {
transform(value: string, ...args: string[]): string {
return value.trim();
}
}
+4
View File
@@ -0,0 +1,4 @@
export enum StType {
Folder = 1,
Device,
}
View File
+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 429 117.3" enable-background="new 0 0 429 117.3" xml:space="preserve">
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="58.666" y1="117.332" x2="58.666" y2="-9.094947e-13">
<stop offset="0" style="stop-color:#0882C8"/>
<stop offset="1" style="stop-color:#26B6DB"/>
</linearGradient>
<circle fill="url(#SVGID_1_)" cx="58.7" cy="58.7" r="58.7"/>
<g>
<circle fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" cx="58.7" cy="58.5" r="43.7"/>
<g>
<path fill="#FFFFFF" d="M94.7,47.8c4.7,1.6,9.8-0.9,11.4-5.6c1.6-4.7-0.9-9.8-5.6-11.4c-4.7-1.6-9.8,0.9-11.4,5.6
C87.5,41.1,90,46.2,94.7,47.8z"/>
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="97.6" y1="39.4" x2="67.5" y2="64.4"/>
</g>
<g>
<path fill="#FFFFFF" d="M77.6,91c-0.4,4.9,3.2,9.3,8.2,9.8c5,0.4,9.3-3.2,9.8-8.2c0.4-4.9-3.2-9.3-8.2-9.8
C82.4,82.4,78,86,77.6,91z"/>
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="86.5" y1="91.8" x2="67.5" y2="64.4"/>
</g>
<path fill="#FFFFFF" d="M60,69.3c2.7,4.2,8.3,5.4,12.4,2.7c4.2-2.7,5.4-8.3,2.7-12.4c-2.7-4.2-8.3-5.4-12.4-2.7
C58.5,59.5,57.3,65.1,60,69.3z"/>
<g>
<path fill="#FFFFFF" d="M21.2,61.4c-4.3-2.5-9.8-1.1-12.3,3.1c-2.5,4.3-1.1,9.8,3.1,12.3c4.3,2.5,9.8,1.1,12.3-3.1
C26.8,69.5,25.4,64,21.2,61.4z"/>
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="16.6" y1="69.1" x2="67.5" y2="64.4"/>
</g>
</g>
</g>
<g>
<path fill="#0891D1" d="M163.8,50.2c-0.6-0.7-6.3-4.1-11.4-4.1c-3.4,0-5.2,1.2-5.2,3.5c0,2.9,3.2,3.7,8.9,5.2
c8.2,2.2,13.3,5,13.3,12.9c0,9.7-7.8,13-16,13c-6.2,0-13.1-2-18.2-5.3l4.3-8.6c0.8,0.8,7.5,5,14,5c3.5,0,5.2-1.1,5.2-3.2
c0-3.2-4.4-4-10.3-5.8c-7.9-2.4-11.5-5.3-11.5-11.8c0-9,7.2-13.9,15.7-13.9c6.1,0,11.6,2.5,15.4,4.7L163.8,50.2z"/>
<path fill="#0891D1" d="M175,85.1c1.7,0.5,3.3,0.8,4.4,0.8c2,0,3.3-1.5,4.2-5.5l-11.9-31.5h9.8l7.4,23.3l6.3-23.3h8.9l-12.1,36.6
c-1.7,5.3-6.2,8.7-11.8,8.8c-1.7,0-3.5-0.2-5.3-0.9V85.1z"/>
<path fill="#0891D1" d="M239.3,80.3h-9.6V62.6c0-4.1-1.7-5.9-4.3-5.9c-2.6,0-5.8,2.3-7,5.6v18.1h-9.6V48.8h8.6v5.3
c2.3-3.7,6.8-5.9,12.2-5.9c8.2,0,9.5,6.7,9.5,11.9V80.3z"/>
<path fill="#0891D1" d="M261.6,48.2c7.2,0,12.3,3.4,14.8,8.3l-9.4,2.8c-1.2-1.9-3.1-3-5.5-3c-4,0-7,3.2-7,8.2c0,5,3.1,8.3,7,8.3
c2.4,0,4.6-1.3,5.5-3.1l9.4,2.9c-2.3,4.9-7.6,8.3-14.8,8.3c-10.6,0-16.9-7.7-16.9-16.4S250.9,48.2,261.6,48.2z"/>
<path fill="#0891D1" d="M302.1,78.7c-2.6,1.1-6.2,2.3-9.7,2.3c-4.7,0-8.8-2.3-8.8-8.4V56.1h-4v-7.3h4v-10h9.6v10h6.4v7.3h-6.4v13.1
c0,2.1,1.2,2.9,2.8,2.9c1.4,0,3-0.6,4.2-1.1L302.1,78.7z"/>
<path fill="#0891D1" d="M337.2,80.3h-9.6V62.6c0-4.1-1.8-5.9-4.6-5.9c-2.3,0-5.5,2.2-6.7,5.6v18.1h-9.6V36.5h9.6v17.6
c2.3-3.7,6.3-5.9,10.9-5.9c8.5,0,9.9,6.5,9.9,11.9V80.3z"/>
<path fill="#0891D1" d="M343.4,45.2v-8.7h9.6v8.7H343.4z M343.4,80.3V48.8h9.6v31.5H343.4z"/>
<path fill="#0891D1" d="M389.9,80.3h-9.6V62.6c0-4.1-1.7-5.9-4.3-5.9c-2.6,0-5.8,2.3-7,5.6v18.1h-9.6V48.8h8.6v5.3
c2.3-3.7,6.8-5.9,12.2-5.9c8.2,0,9.5,6.7,9.5,11.9V80.3z"/>
<path fill="#0891D1" d="M395.5,64.6c0-9.2,6-16.3,14.6-16.3c4.7,0,8.4,2.2,10.6,5.8v-5.2h8.3v29.3c0,9.6-7.5,15.5-18.2,15.5
c-6.8,0-11.5-2.3-15-6.3l5.1-5.2c2.3,2.6,6,4.3,9.9,4.3c4.6,0,8.6-2.4,8.6-8.3v-3.1c-1.9,3.5-5.9,5.3-10,5.3
C401.1,80.5,395.5,73.3,395.5,64.6z M419.4,68.5v-6.6c-1.3-3.3-4.2-5.5-7.1-5.5c-4.1,0-7,4-7,8.4c0,4.6,3.2,8,7.5,8
C415.7,72.8,418.1,71,419.4,68.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -0,0 +1,3 @@
export const environment = {
production: true
};
+16
View File
@@ -0,0 +1,16 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

+20
View File
@@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>TechUi</title>
<base href="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<script type="text/javascript" src="/meta.js"></script>
</head>
<body>
<div id="chartjs-tooltip">
<table></table>
</div>
<app-root></app-root>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
+63
View File
@@ -0,0 +1,63 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

Some files were not shown because too many files have changed in this diff Show More