init
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<router-outlet></router-outlet>
|
||||
@@ -0,0 +1,6 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector : 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls : ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { NgModule, enableProdMode } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ExtraOptions, PreloadAllModules, RouterModule } from '@angular/router';
|
||||
import { MarkdownModule } from 'ngx-markdown';
|
||||
import { TreoModule } from '@treo';
|
||||
import { TreoConfigModule } from '@treo/services/config';
|
||||
import { TreoMockApiModule } from '@treo/lib/mock-api';
|
||||
import { CoreModule } from 'app/core/core.module';
|
||||
import { appConfig } from 'app/core/config/app.config';
|
||||
import { mockDataServices } from 'app/data/mock';
|
||||
import { LayoutModule } from 'app/layout/layout.module';
|
||||
import { AppComponent } from 'app/app.component';
|
||||
import { appRoutes } from 'app/app.routing';
|
||||
|
||||
const routerConfig: ExtraOptions = {
|
||||
scrollPositionRestoration: 'enabled',
|
||||
preloadingStrategy : PreloadAllModules
|
||||
};
|
||||
|
||||
let dev = [
|
||||
TreoMockApiModule.forRoot(mockDataServices),
|
||||
];
|
||||
|
||||
// if production clear dev imports and set to prod mode
|
||||
// @ts-ignore
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
dev = [];
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
],
|
||||
imports : [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
RouterModule.forRoot(appRoutes, routerConfig),
|
||||
|
||||
// Treo & Treo Mock API
|
||||
TreoModule,
|
||||
TreoConfigModule.forRoot(appConfig),
|
||||
...dev,
|
||||
|
||||
// Core
|
||||
CoreModule,
|
||||
|
||||
// Layout
|
||||
LayoutModule,
|
||||
|
||||
// 3rd party modules
|
||||
MarkdownModule.forRoot({})
|
||||
],
|
||||
bootstrap : [
|
||||
AppComponent
|
||||
]
|
||||
})
|
||||
export class AppModule
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Route } from '@angular/router';
|
||||
import { LayoutComponent } from 'app/layout/layout.component';
|
||||
import { EmptyLayoutComponent } from 'app/layout/layouts/empty/empty.component';
|
||||
|
||||
// @formatter:off
|
||||
// tslint:disable:max-line-length
|
||||
export const appRoutes: Route[] = [
|
||||
|
||||
// Redirect empty path to '/example'
|
||||
{path: '', pathMatch : 'full', redirectTo: 'dashboard'},
|
||||
|
||||
|
||||
// Landing routes
|
||||
{
|
||||
path: '',
|
||||
component: EmptyLayoutComponent,
|
||||
children : [
|
||||
{path: 'home', loadChildren: () => import('app/modules/landing/home/home.module').then(m => m.LandingHomeModule)},
|
||||
]
|
||||
},
|
||||
|
||||
// Admin routes
|
||||
{
|
||||
path : '',
|
||||
component : LayoutComponent,
|
||||
children : [
|
||||
|
||||
// Example
|
||||
{path: 'dashboard', loadChildren: () => import('app/modules/admin/dashboard/dashboard.module').then(m => m.DashboardModule)},
|
||||
{path: 'device/:wwn', loadChildren: () => import('app/modules/admin/detail/detail.module').then(m => m.DetailModule)}
|
||||
|
||||
// 404 & Catch all
|
||||
// {path: '404-not-found', pathMatch: 'full', loadChildren: () => import('app/modules/admin/pages/errors/error-404/error-404.module').then(m => m.Error404Module)},
|
||||
// {path: '**', redirectTo: '404-not-found'}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Layout } from "app/layout/layout.types";
|
||||
|
||||
// Theme type
|
||||
export type Theme = "light" | "dark";
|
||||
|
||||
/**
|
||||
* AppConfig interface. Update this interface to strictly type your config
|
||||
* object.
|
||||
*/
|
||||
export interface AppConfig
|
||||
{
|
||||
theme: Theme;
|
||||
layout: Layout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default configuration for the entire application. This object is used by
|
||||
* "ConfigService" to set the default configuration.
|
||||
*
|
||||
* If you need to store global configuration for your app, you can use this
|
||||
* object to set the defaults. To access, update and reset the config, use
|
||||
* "ConfigService".
|
||||
*/
|
||||
export const appConfig: AppConfig = {
|
||||
theme : "light",
|
||||
layout: "material"
|
||||
};
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NgModule, Optional, SkipSelf } from '@angular/core';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { MatIconRegistry } from '@angular/material/icon';
|
||||
|
||||
@NgModule({
|
||||
imports : [
|
||||
HttpClientModule
|
||||
],
|
||||
providers: []
|
||||
})
|
||||
export class CoreModule
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {DomSanitizer} _domSanitizer
|
||||
* @param {MatIconRegistry} _matIconRegistry
|
||||
* @param parentModule
|
||||
*/
|
||||
constructor(
|
||||
private _domSanitizer: DomSanitizer,
|
||||
private _matIconRegistry: MatIconRegistry,
|
||||
@Optional() @SkipSelf() parentModule?: CoreModule
|
||||
)
|
||||
{
|
||||
// Do not allow multiple injections
|
||||
if ( parentModule )
|
||||
{
|
||||
throw new Error('CoreModule has already been loaded. Import this module in the AppModule only.');
|
||||
}
|
||||
|
||||
// Register icon sets
|
||||
this._matIconRegistry.addSvgIconSet(this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/material-twotone.svg'));
|
||||
this._matIconRegistry.addSvgIconSetInNamespace('mat_outline', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/material-outline.svg'));
|
||||
this._matIconRegistry.addSvgIconSetInNamespace('iconsmind', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/iconsmind.svg'));
|
||||
this._matIconRegistry.addSvgIconSetInNamespace('dripicons', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/dripicons.svg'));
|
||||
this._matIconRegistry.addSvgIconSetInNamespace('feather', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/feather.svg'));
|
||||
this._matIconRegistry.addSvgIconSetInNamespace('heroicons_outline', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/heroicons-outline.svg'));
|
||||
this._matIconRegistry.addSvgIconSetInNamespace('heroicons_solid', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/heroicons-solid.svg'));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import * as _ from 'lodash';
|
||||
import { TreoMockApi } from '@treo/lib/mock-api/mock-api.interfaces';
|
||||
import { TreoMockApiService } from '@treo/lib/mock-api/mock-api.service';
|
||||
import { details as detailsData } from 'app/data/mock/device/details/data';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DetailsMockApi implements TreoMockApi
|
||||
{
|
||||
// Private
|
||||
private _details: any;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param _treoMockApiService
|
||||
*/
|
||||
constructor(
|
||||
private _treoMockApiService: TreoMockApiService
|
||||
)
|
||||
{
|
||||
// Set the data
|
||||
this._details = detailsData;
|
||||
|
||||
// Register the API endpoints
|
||||
this.register();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Register
|
||||
*/
|
||||
register(): void
|
||||
{
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Sales - GET
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
this._treoMockApiService
|
||||
.onGet('/api/device/:wwn/details')
|
||||
.reply(() => {
|
||||
|
||||
return [
|
||||
200,
|
||||
_.cloneDeep(this._details)
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SummaryMockApi } from 'app/data/mock/summary';
|
||||
import { DetailsMockApi } from 'app/data/mock/device/details';
|
||||
|
||||
export const mockDataServices = [
|
||||
SummaryMockApi,
|
||||
DetailsMockApi,
|
||||
];
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import * as _ from 'lodash';
|
||||
import { TreoMockApi } from '@treo/lib/mock-api/mock-api.interfaces';
|
||||
import { TreoMockApiService } from '@treo/lib/mock-api/mock-api.service';
|
||||
import { summary as summaryData } from 'app/data/mock/summary/data';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SummaryMockApi implements TreoMockApi
|
||||
{
|
||||
// Private
|
||||
private _summary: any;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param _treoMockApiService
|
||||
*/
|
||||
constructor(
|
||||
private _treoMockApiService: TreoMockApiService
|
||||
)
|
||||
{
|
||||
// Set the data
|
||||
this._summary = summaryData;
|
||||
|
||||
// Register the API endpoints
|
||||
this.register();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Register
|
||||
*/
|
||||
register(): void
|
||||
{
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Sales - GET
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
this._treoMockApiService
|
||||
.onGet('/api/summary')
|
||||
.reply(() => {
|
||||
|
||||
return [
|
||||
200,
|
||||
_.cloneDeep(this._summary)
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<!-- Open button, 'bar' only -->
|
||||
<button class="search-toggle-open"
|
||||
mat-icon-button
|
||||
*ngIf="appearance === 'bar' && !opened"
|
||||
(click)="open()">
|
||||
<mat-icon [svgIcon]="'search'"></mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Search container -->
|
||||
<div class="search-container"
|
||||
*ngIf="appearance === 'basic' || (appearance === 'bar' && opened)"
|
||||
[@.disabled]="appearance === 'basic'"
|
||||
@slideInTop
|
||||
@slideOutTop>
|
||||
|
||||
<mat-form-field class="treo-mat-no-subscript search-input"
|
||||
#searchInput>
|
||||
<mat-icon matPrefix
|
||||
[svgIcon]="'search'"></mat-icon>
|
||||
<input matInput
|
||||
[formControl]="searchControl"
|
||||
[placeholder]="'Search for a page or a contact'"
|
||||
[matAutocomplete]="matAutocomplete"
|
||||
(keydown)="onKeydown($event)">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-autocomplete [class]="'search-results search-results-appearance-' + appearance"
|
||||
#matAutocomplete="matAutocomplete"
|
||||
[disableRipple]="true">
|
||||
|
||||
<mat-option class="no-results"
|
||||
*ngIf="results && !results.length">
|
||||
No results found!
|
||||
</mat-option>
|
||||
|
||||
<mat-option *ngFor="let result of results"
|
||||
[routerLink]="result.link">
|
||||
|
||||
<!-- Page result -->
|
||||
<div class="result page-result"
|
||||
*ngIf="result.resultType === 'page'">
|
||||
<div class="badge">Page</div>
|
||||
<div class="title">
|
||||
<span [innerHTML]="result.title"></span>
|
||||
<span class="link"
|
||||
[routerLink]="result.link">{{result.link}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact result -->
|
||||
<div class="result contact-result"
|
||||
*ngIf="result.resultType === 'contact'">
|
||||
<div class="badge">Contact</div>
|
||||
<div class="title">
|
||||
<span [innerHTML]="result.title"></span>
|
||||
</div>
|
||||
<div class="image">
|
||||
<img *ngIf="result.avatar"
|
||||
[src]="result.avatar">
|
||||
<mat-icon *ngIf="!result.avatar"
|
||||
[svgIcon]="'account_circle'"></mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-option>
|
||||
|
||||
</mat-autocomplete>
|
||||
|
||||
<!-- Close button, 'bar' only -->
|
||||
<button class="search-toggle-close"
|
||||
mat-icon-button
|
||||
*ngIf="appearance === 'bar'"
|
||||
(click)="close()">
|
||||
<mat-icon [svgIcon]="'close'"></mat-icon>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,335 @@
|
||||
@import 'treo';
|
||||
|
||||
search {
|
||||
display: flex;
|
||||
|
||||
// Bar appearance
|
||||
&.search-appearance-bar {
|
||||
|
||||
.search-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 0 auto;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 99;
|
||||
|
||||
.search-input {
|
||||
flex: 1 0 auto;
|
||||
height: 100%;
|
||||
|
||||
.mat-form-field-wrapper {
|
||||
height: 100%;
|
||||
|
||||
.mat-form-field-flex {
|
||||
height: 100%;
|
||||
padding: 0 72px 0 32px;
|
||||
border: none;
|
||||
border-radius: 0 !important;
|
||||
|
||||
@include treo-breakpoint('xs') {
|
||||
padding: 0 56px 0 24px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-toggle-close {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 32px;
|
||||
margin-top: -20px;
|
||||
min-width: 40px;
|
||||
width: 40px;
|
||||
min-height: 40px;
|
||||
height: 40px;
|
||||
|
||||
@include treo-breakpoint('xs') {
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Basic appearance
|
||||
&.search-appearance-basic {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 0 auto;
|
||||
overflow: hidden;
|
||||
|
||||
.search-icon {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search results panel
|
||||
.search-results {
|
||||
max-height: 512px !important;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
bottom: 100%;
|
||||
left: 30px;
|
||||
border: solid transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:before {
|
||||
border-width: 9px;
|
||||
margin-left: -9px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-width: 8px;
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
// Bar appearance
|
||||
&.search-results-appearance-bar {
|
||||
border-top-width: 1px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
@include treo-elevation('md', true);
|
||||
|
||||
.mat-option {
|
||||
padding: 0 40px;
|
||||
|
||||
@include treo-breakpoint('xs') {
|
||||
padding: 0 24px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Basic appearance
|
||||
&.search-results-appearance-basic {
|
||||
margin-top: 8px;
|
||||
border-radius: 4px;
|
||||
@include treo-elevation('2xl', true);
|
||||
|
||||
.mat-option {
|
||||
padding: 0 32px;
|
||||
|
||||
@include treo-breakpoint('xs') {
|
||||
padding: 0 24px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-option {
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
font-size: 14px;
|
||||
|
||||
&.no-results {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mat-option-text {
|
||||
|
||||
.result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.contact-result {
|
||||
|
||||
.image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
max-width: 32px;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
margin-left: auto;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
|
||||
.mat-icon {
|
||||
margin: 0;
|
||||
@include treo-icon-size(20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.page-result {
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.link {
|
||||
margin-top: 4px;
|
||||
line-height: normal;
|
||||
font-size: 12px;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 3px 6px;
|
||||
margin-right: 16px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
mark {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Theming
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@include treo-theme {
|
||||
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$primary: map-get($theme, primary);
|
||||
$is-dark: map-get($theme, is-dark);
|
||||
|
||||
search {
|
||||
|
||||
// Basic appearance
|
||||
&.search-appearance-basic {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// Bar appearance
|
||||
&.search-appearance-bar {
|
||||
|
||||
.search-container {
|
||||
background: map-get($background, card);
|
||||
|
||||
.search-input {
|
||||
|
||||
.mat-form-field-wrapper {
|
||||
|
||||
.mat-form-field-flex {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search results panel
|
||||
.search-results {
|
||||
|
||||
&:before {
|
||||
border-color: transparent;
|
||||
border-bottom-color: map-get($foreground, divider);
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-color: transparent;
|
||||
border-bottom-color: map-get($background, card);
|
||||
}
|
||||
|
||||
.mat-option {
|
||||
|
||||
@include treo-breakpoint('xs') {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
@include treo-breakpoint('gt-xs') {
|
||||
&:hover:not(.mat-option-disabled),
|
||||
&:focus:not(.mat-option-disabled) {
|
||||
box-shadow: inset 4px 0 0 map-get($primary, default);
|
||||
}
|
||||
}
|
||||
|
||||
&.no-results {
|
||||
|
||||
.mat-option-text {
|
||||
color: map-get($foreground, secondary-text);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-option-text {
|
||||
|
||||
.result {
|
||||
|
||||
&.contact-result {
|
||||
|
||||
.badge {
|
||||
background: treo-color('blue', 500);
|
||||
color: treo-contrast('blue', 500);
|
||||
}
|
||||
}
|
||||
|
||||
&.page-result {
|
||||
|
||||
.badge {
|
||||
background: treo-color('purple', 500);
|
||||
color: treo-contrast('purple', 500);
|
||||
}
|
||||
|
||||
.title {
|
||||
|
||||
.link {
|
||||
color: map-get($foreground, secondary-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
@if ($is-dark) {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
} @else {
|
||||
background: map-get($primary, 100);
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
color: map-get($primary, default);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
|
||||
mark {
|
||||
background: transparent;
|
||||
color: map-get($primary, default);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, Renderer2, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { MatFormField } from '@angular/material/form-field';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime, filter, map, takeUntil } from 'rxjs/operators';
|
||||
import { TreoAnimations } from '@treo/animations/public-api';
|
||||
|
||||
@Component({
|
||||
selector : 'search',
|
||||
templateUrl : './search.component.html',
|
||||
styleUrls : ['./search.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
exportAs : 'treoSearch',
|
||||
animations : TreoAnimations
|
||||
})
|
||||
export class SearchComponent implements OnInit, OnDestroy
|
||||
{
|
||||
results: any[] | null;
|
||||
searchControl: FormControl;
|
||||
|
||||
// Debounce
|
||||
@Input()
|
||||
debounce: number;
|
||||
|
||||
// Min. length
|
||||
@Input()
|
||||
minLength: number;
|
||||
|
||||
// Search
|
||||
@Output()
|
||||
search: EventEmitter<any>;
|
||||
|
||||
// Private
|
||||
private _appearance: 'basic' | 'bar';
|
||||
private _opened: boolean;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {ElementRef} _elementRef
|
||||
* @param {HttpClient} _httpClient
|
||||
* @param {Renderer2} _renderer2
|
||||
*/
|
||||
constructor(
|
||||
private _elementRef: ElementRef,
|
||||
private _httpClient: HttpClient,
|
||||
private _renderer2: Renderer2
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
|
||||
// Set the defaults
|
||||
this.appearance = 'basic';
|
||||
this.debounce = this.debounce || 300;
|
||||
this.minLength = this.minLength || 2;
|
||||
this.opened = false;
|
||||
this.results = null;
|
||||
this.searchControl = new FormControl();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Setter and getter for appearance
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set appearance(value: 'basic' | 'bar')
|
||||
{
|
||||
// If the value is the same, return...
|
||||
if ( this._appearance === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the search is closed, before
|
||||
// changing the appearance to prevent issues
|
||||
this.close();
|
||||
|
||||
let appearanceClassName;
|
||||
|
||||
// Remove the previous appearance class
|
||||
appearanceClassName = 'search-appearance-' + this.appearance;
|
||||
this._renderer2.removeClass(this._elementRef.nativeElement, appearanceClassName);
|
||||
|
||||
// Store the appearance
|
||||
this._appearance = value;
|
||||
|
||||
// Add the new appearance class
|
||||
appearanceClassName = 'search-appearance-' + this.appearance;
|
||||
this._renderer2.addClass(this._elementRef.nativeElement, appearanceClassName);
|
||||
}
|
||||
|
||||
get appearance(): 'basic' | 'bar'
|
||||
{
|
||||
return this._appearance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for opened
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
set opened(value: boolean)
|
||||
{
|
||||
// If the value is the same, return...
|
||||
if ( this._opened === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the opened status
|
||||
this._opened = value;
|
||||
|
||||
// If opened...
|
||||
if ( value )
|
||||
{
|
||||
// Add opened class
|
||||
this._renderer2.addClass(this._elementRef.nativeElement, 'search-opened');
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove opened class
|
||||
this._renderer2.removeClass(this._elementRef.nativeElement, 'search-opened');
|
||||
}
|
||||
}
|
||||
|
||||
get opened(): boolean
|
||||
{
|
||||
return this._opened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for search input
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@ViewChild('searchInput')
|
||||
set searchInput(value: MatFormField)
|
||||
{
|
||||
// Return if the appearance is basic, since we don't want
|
||||
// basic search to be focused as soon as the page loads
|
||||
if ( this.appearance === 'basic' )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If the value exists, it means that the search input
|
||||
// is now in the DOM and we can focus on the input..
|
||||
if ( value )
|
||||
{
|
||||
// Give Angular time to complete the change detection cycle
|
||||
setTimeout(() => {
|
||||
|
||||
// Focus to the input element
|
||||
value._inputContainerRef.nativeElement.children[0].focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Subscribe to the search field value changes
|
||||
this.searchControl.valueChanges
|
||||
.pipe(
|
||||
debounceTime(this.debounce),
|
||||
takeUntil(this._unsubscribeAll),
|
||||
map((value) => {
|
||||
|
||||
// Set the search results to null if there is no value or
|
||||
// the length of the value is smaller than the minLength
|
||||
// so the autocomplete panel can be closed
|
||||
if ( !value || value.length < this.minLength )
|
||||
{
|
||||
this.results = null;
|
||||
}
|
||||
|
||||
// Continue
|
||||
return value;
|
||||
}),
|
||||
filter((value) => {
|
||||
|
||||
// Filter out undefined/null/false statements and also
|
||||
// filter out the values that are smaller than minLength
|
||||
return value && value.length >= this.minLength;
|
||||
})
|
||||
)
|
||||
.subscribe((value) => {
|
||||
this._httpClient.post('api/common/search', {query: value})
|
||||
.subscribe((response: any) => {
|
||||
this.results = response.results;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On keydown of the search input
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
onKeydown(event): void
|
||||
{
|
||||
// Listen for escape to close the search
|
||||
// if the appearance is 'bar'
|
||||
if ( this.appearance === 'bar' )
|
||||
{
|
||||
// Escape
|
||||
if ( event.keyCode === 27 )
|
||||
{
|
||||
// Close the search
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the search
|
||||
* Used in 'bar'
|
||||
*/
|
||||
open(): void
|
||||
{
|
||||
// Return, if it's already opened
|
||||
if ( this.opened )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the search
|
||||
this.opened = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the search
|
||||
* * Used in 'bar'
|
||||
*/
|
||||
close(): void
|
||||
{
|
||||
// Return, if it's already closed
|
||||
if ( !this.opened )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the search input
|
||||
this.searchControl.setValue('');
|
||||
|
||||
// Close the search
|
||||
this.opened = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { Overlay } from '@angular/cdk/overlay';
|
||||
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { SearchComponent } from 'app/layout/common/search/search.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
SearchComponent
|
||||
],
|
||||
imports : [
|
||||
RouterModule.forChild([]),
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
SharedModule
|
||||
],
|
||||
exports : [
|
||||
SearchComponent
|
||||
],
|
||||
providers : [
|
||||
{
|
||||
provide : MAT_AUTOCOMPLETE_SCROLL_STRATEGY,
|
||||
useFactory: (overlay: Overlay) => {
|
||||
return () => overlay.scrollStrategies.block();
|
||||
},
|
||||
deps : [Overlay]
|
||||
}
|
||||
]
|
||||
})
|
||||
export class SearchModule
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<!-- ----------------------------------------------------------------------------------------------------- -->
|
||||
<!-- Empty layout -->
|
||||
<!-- ----------------------------------------------------------------------------------------------------- -->
|
||||
<empty-layout *ngIf="layout === 'empty'"></empty-layout>
|
||||
|
||||
<!-- ----------------------------------------------------------------------------------------------------- -->
|
||||
<!-- Layouts with horizontal navigation -->
|
||||
<!-- ----------------------------------------------------------------------------------------------------- -->
|
||||
|
||||
<!-- Material -->
|
||||
<material-layout *ngIf="layout === 'material'"></material-layout>
|
||||
@@ -0,0 +1,7 @@
|
||||
layout {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { Component, Inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
||||
import { Subject } from 'rxjs';
|
||||
import { filter, takeUntil } from 'rxjs/operators';
|
||||
import { TreoConfigService } from '@treo/services/config';
|
||||
import { TreoDrawerService } from '@treo/components/drawer';
|
||||
import { Layout } from 'app/layout/layout.types';
|
||||
import { AppConfig, Theme } from 'app/core/config/app.config';
|
||||
|
||||
@Component({
|
||||
selector : 'layout',
|
||||
templateUrl : './layout.component.html',
|
||||
styleUrls : ['./layout.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class LayoutComponent implements OnInit, OnDestroy
|
||||
{
|
||||
config: AppConfig;
|
||||
layout: Layout;
|
||||
theme: Theme;
|
||||
|
||||
// Private
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {ActivatedRoute} _activatedRoute
|
||||
* @param {TreoConfigService} _treoConfigService
|
||||
* @param {TreoDrawerService} _treoDrawerService
|
||||
* @param {DOCUMENT} _document
|
||||
* @param {Router} _router
|
||||
*/
|
||||
constructor(
|
||||
private _activatedRoute: ActivatedRoute,
|
||||
private _treoConfigService: TreoConfigService,
|
||||
private _treoDrawerService: TreoDrawerService,
|
||||
@Inject(DOCUMENT) private _document: any,
|
||||
private _router: Router
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Subscribe to config changes
|
||||
this._treoConfigService.config$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((config: AppConfig) => {
|
||||
|
||||
// Store the config
|
||||
this.config = config;
|
||||
|
||||
// Store the theme
|
||||
this.theme = config.theme;
|
||||
|
||||
// Update the selected theme class name on body
|
||||
const themeName = 'treo-theme-' + config.theme;
|
||||
this._document.body.classList.forEach((className) => {
|
||||
if ( className.startsWith('treo-theme-') && className !== themeName )
|
||||
{
|
||||
this._document.body.classList.remove(className);
|
||||
this._document.body.classList.add(themeName);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Update the layout
|
||||
this._updateLayout();
|
||||
});
|
||||
|
||||
// Subscribe to NavigationEnd event
|
||||
this._router.events.pipe(
|
||||
filter(event => event instanceof NavigationEnd),
|
||||
takeUntil(this._unsubscribeAll)
|
||||
).subscribe(() => {
|
||||
|
||||
// Update the layout
|
||||
this._updateLayout();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Update the selected layout
|
||||
*/
|
||||
private _updateLayout(): void
|
||||
{
|
||||
// Get the current activated route
|
||||
let route = this._activatedRoute;
|
||||
while ( route.firstChild )
|
||||
{
|
||||
route = route.firstChild;
|
||||
}
|
||||
|
||||
// 1. Set the layout from the config
|
||||
this.layout = this.config.layout;
|
||||
|
||||
// 2. Get the query parameter from the current route and
|
||||
// set the layout and save the layout to the config
|
||||
const layoutFromQueryParam = (route.snapshot.queryParamMap.get('layout') as Layout);
|
||||
if ( layoutFromQueryParam )
|
||||
{
|
||||
this.config.layout = this.layout = layoutFromQueryParam;
|
||||
}
|
||||
|
||||
// 3. Iterate through the paths and change the layout as we find
|
||||
// a config for it.
|
||||
//
|
||||
// The reason we do this is that there might be empty grouping
|
||||
// paths or componentless routes along the path. Because of that,
|
||||
// we cannot just assume that the layout configuration will be
|
||||
// in the last path's config or in the first path's config.
|
||||
//
|
||||
// So, we get all the paths that matched starting from root all
|
||||
// the way to the current activated route, walk through them one
|
||||
// by one and change the layout as we find the layout config. This
|
||||
// way, layout configuration can live anywhere within the path and
|
||||
// we won't miss it.
|
||||
//
|
||||
// Also, this will allow overriding the layout in any time so we
|
||||
// can have different layouts for different routes.
|
||||
const paths = route.pathFromRoot;
|
||||
paths.forEach((path) => {
|
||||
|
||||
// Check if there is a 'layout' data
|
||||
if ( path.routeConfig && path.routeConfig.data && path.routeConfig.data.layout )
|
||||
{
|
||||
// Set the layout
|
||||
this.layout = path.routeConfig.data.layout;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Set the layout on the config
|
||||
*
|
||||
* @param layout
|
||||
*/
|
||||
setLayout(layout: string): void
|
||||
{
|
||||
// Clear the 'layout' query param to allow layout changes
|
||||
this._router.navigate([], {
|
||||
queryParams : {
|
||||
layout: null
|
||||
},
|
||||
queryParamsHandling: 'merge'
|
||||
}).then(() => {
|
||||
|
||||
// Set the config
|
||||
this._treoConfigService.config = {layout};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the theme on the config
|
||||
*
|
||||
* @param change
|
||||
*/
|
||||
setTheme(change: MatSlideToggleChange): void
|
||||
{
|
||||
this._treoConfigService.config = {theme: change.checked ? 'dark' : 'light'};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { TreoDrawerModule } from '@treo/components/drawer';
|
||||
import { LayoutComponent } from 'app/layout/layout.component';
|
||||
import { EmptyLayoutModule } from 'app/layout/layouts/empty/empty.module';
|
||||
import { MaterialLayoutModule } from 'app/layout/layouts/horizontal/material/material.module';
|
||||
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
|
||||
const modules = [
|
||||
// Empty
|
||||
EmptyLayoutModule,
|
||||
|
||||
// Horizontal navigation
|
||||
MaterialLayoutModule,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
LayoutComponent
|
||||
],
|
||||
imports : [
|
||||
TreoDrawerModule,
|
||||
SharedModule,
|
||||
...modules
|
||||
],
|
||||
exports : [
|
||||
...modules
|
||||
]
|
||||
})
|
||||
export class LayoutModule
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export type Layout = 'empty' |
|
||||
'centered' | 'enterprise' | 'material' | 'modern' |
|
||||
'basic' | 'classic' | 'classy' | 'compact' | 'dense' | 'futuristic' | 'thin';
|
||||
@@ -0,0 +1,13 @@
|
||||
<!-- Container -->
|
||||
<div class="container">
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content">
|
||||
|
||||
<!-- *ngIf="true" hack is required here for router-outlet to work correctly. Otherwise,
|
||||
it won't register the changes on the layout and won't update the view. -->
|
||||
<router-outlet *ngIf="true"></router-outlet>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
@import 'treo';
|
||||
|
||||
empty-layout {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
|
||||
// Container
|
||||
> .container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
|
||||
// Content
|
||||
> .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 0 auto;
|
||||
|
||||
> *:not(router-outlet) {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Theming
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@include treo-theme {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector : 'empty-layout',
|
||||
templateUrl : './empty.component.html',
|
||||
styleUrls : ['./empty.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class EmptyLayoutComponent implements OnInit, OnDestroy
|
||||
{
|
||||
// Private
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor()
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { EmptyLayoutComponent } from 'app/layout/layouts/empty/empty.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
EmptyLayoutComponent
|
||||
],
|
||||
imports : [
|
||||
RouterModule,
|
||||
SharedModule
|
||||
],
|
||||
exports : [
|
||||
EmptyLayoutComponent
|
||||
]
|
||||
})
|
||||
export class EmptyLayoutModule
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<!-- Navigation -->
|
||||
<treo-vertical-navigation class="bg-cool-gray-900 theme-dark"
|
||||
*ngIf="isScreenSmall"
|
||||
[appearance]="'classic'"
|
||||
[mode]="'over'"
|
||||
[name]="'mainNavigation'"
|
||||
[navigation]="data.navigation.default"
|
||||
[opened]="false">
|
||||
|
||||
<div treoVerticalNavigationContentHeader>
|
||||
<a class="logo" routerLink="/dashboard">
|
||||
<img src="assets/images/logo/scrutiny-logo-white-text.png">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</treo-vertical-navigation>
|
||||
|
||||
<!-- Wrapper -->
|
||||
<div class="wrapper">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
|
||||
<!-- Header container -->
|
||||
<div class="container">
|
||||
|
||||
<!-- Top bar -->
|
||||
<div class="top-bar">
|
||||
|
||||
<!-- Logo -->
|
||||
<a class="logo"
|
||||
routerLink="/dashboard"
|
||||
*ngIf="!isScreenSmall">
|
||||
<img class="logo-text"
|
||||
src="assets/images/logo/scrutiny-logo-dark-text.png">
|
||||
<img class="logo-text-on-dark"
|
||||
src="assets/images/logo/scrutiny-logo-white-text.png">
|
||||
</a>
|
||||
|
||||
<!-- Spacer -->
|
||||
<div class="spacer"></div>
|
||||
|
||||
|
||||
<!-- Shortcuts -->
|
||||
<!-- <shortcuts [shortcuts]="data.shortcuts"></shortcuts>-->
|
||||
|
||||
<!-- Notifications -->
|
||||
<!-- <notifications [notifications]="data.notifications"></notifications>-->
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content">
|
||||
|
||||
<!-- *ngIf="true" hack is required here for router-outlet to work correctly. Otherwise,
|
||||
it won't register the changes on the layout and won't update the view. -->
|
||||
<router-outlet *ngIf="true"></router-outlet>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,259 @@
|
||||
@import 'treo';
|
||||
|
||||
material-layout {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
|
||||
> treo-vertical-navigation {
|
||||
|
||||
.treo-vertical-navigation-content-header {
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 80px;
|
||||
min-height: 80px;
|
||||
max-height: 80px;
|
||||
padding: 24px 32px 0 32px;
|
||||
|
||||
img {
|
||||
max-width: 96px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
|
||||
> .header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 49;
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
max-width: 1440px;
|
||||
width: calc(100% - 64px);
|
||||
margin: 48px 32px 0 32px;
|
||||
padding: 16px 0 12px 0;
|
||||
border-bottom-width: 1px;
|
||||
border-radius: 12px 12px 0 0;
|
||||
box-shadow: 0 0 25px 0 rgba(0, 0, 0, 0.1), 0 0 10px 0 rgba(0, 0, 0, 0.04);
|
||||
overflow: hidden;
|
||||
|
||||
@include treo-breakpoint('lt-md') {
|
||||
margin-top: 32px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
@include treo-breakpoint('xs') {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.top-bar,
|
||||
.bottom-bar {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
max-height: 64px;
|
||||
min-height: 64px;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
position: relative;
|
||||
padding: 0 24px;
|
||||
|
||||
@include treo-breakpoint('lt-md') {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 8px;
|
||||
|
||||
img {
|
||||
width: 150px;
|
||||
min-width: 100px;
|
||||
max-width: 175px;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-toggle-button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
search {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
shortcuts {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
messages {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
notifications {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
max-width: 1440px;
|
||||
width: calc(100% - 64px);
|
||||
margin: 0 32px;
|
||||
box-shadow: 0 0 25px 0 rgba(0, 0, 0, 0.1), 0 0 10px 0 rgba(0, 0, 0, 0.04);
|
||||
|
||||
@include treo-breakpoint('xs') {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
> *:not(router-outlet) {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
> .footer {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
max-width: 1440px;
|
||||
width: calc(100% - 64px);
|
||||
height: 80px;
|
||||
max-height: 80px;
|
||||
min-height: 80px;
|
||||
margin: 0 32px;
|
||||
padding: 0 24px;
|
||||
z-index: 49;
|
||||
border-top-width: 1px;
|
||||
box-shadow: 0 0 25px 0 rgba(0, 0, 0, 0.1), 0 0 10px 0 rgba(0, 0, 0, 0.04);
|
||||
|
||||
@include treo-breakpoint('xs') {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@include treo-breakpoint('xs') {
|
||||
height: 56px;
|
||||
max-height: 56px;
|
||||
min-height: 56px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.fixed-header {
|
||||
|
||||
> .wrapper {
|
||||
|
||||
> .header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.fixed-footer {
|
||||
|
||||
> .wrapper {
|
||||
|
||||
> .footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Theming
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@include treo-theme {
|
||||
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$primary: map-get($theme, primary);
|
||||
$is-dark: map-get($theme, is-dark);
|
||||
|
||||
material-layout {
|
||||
|
||||
> .wrapper {
|
||||
@if ($is-dark) {
|
||||
background: map-get($background, card);
|
||||
} @else {
|
||||
background: treo-color('cool-gray', 200);
|
||||
}
|
||||
|
||||
> .header {
|
||||
background: map-get($primary, 700);
|
||||
|
||||
.container {
|
||||
background: map-get($background, card);
|
||||
|
||||
.logo {
|
||||
|
||||
.logo-text {
|
||||
@if ($is-dark) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-text-on-dark {
|
||||
@if (not $is-dark) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .content {
|
||||
background: map-get($background, background);
|
||||
}
|
||||
|
||||
> .footer {
|
||||
@if (not $is-dark) {
|
||||
background: map-get($background, card);
|
||||
}
|
||||
color: map-get($foreground, secondary-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Component, HostBinding, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { ActivatedRoute, Data, Router } from '@angular/router';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { TreoMediaWatcherService } from '@treo/services/media-watcher';
|
||||
import { TreoNavigationService } from '@treo/components/navigation';
|
||||
|
||||
@Component({
|
||||
selector : 'material-layout',
|
||||
templateUrl : './material.component.html',
|
||||
styleUrls : ['./material.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class MaterialLayoutComponent implements OnInit, OnDestroy
|
||||
{
|
||||
data: any;
|
||||
isScreenSmall: boolean;
|
||||
|
||||
@HostBinding('class.fixed-header')
|
||||
fixedHeader: boolean;
|
||||
|
||||
@HostBinding('class.fixed-footer')
|
||||
fixedFooter: boolean;
|
||||
|
||||
// Private
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {ActivatedRoute} _activatedRoute
|
||||
* @param {TreoMediaWatcherService} _treoMediaWatcherService
|
||||
* @param {TreoNavigationService} _treoNavigationService
|
||||
* @param {Router} _router
|
||||
*/
|
||||
constructor(
|
||||
private _activatedRoute: ActivatedRoute,
|
||||
private _treoMediaWatcherService: TreoMediaWatcherService,
|
||||
private _treoNavigationService: TreoNavigationService,
|
||||
private _router: Router
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
|
||||
// Set the defaults
|
||||
this.fixedHeader = false;
|
||||
this.fixedFooter = false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Getter for current year
|
||||
*/
|
||||
get currentYear(): number
|
||||
{
|
||||
return new Date().getFullYear();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Subscribe to the resolved route data
|
||||
this._activatedRoute.data.subscribe((data: Data) => {
|
||||
this.data = data.initialData;
|
||||
});
|
||||
|
||||
// Subscribe to media changes
|
||||
this._treoMediaWatcherService.onMediaChange$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe(({matchingAliases}) => {
|
||||
|
||||
// Check if the breakpoint is 'lt-md'
|
||||
this.isScreenSmall = matchingAliases.includes('lt-md');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Toggle navigation
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
toggleNavigation(key): void
|
||||
{
|
||||
// Get the navigation
|
||||
const navigation = this._treoNavigationService.getComponent(key);
|
||||
|
||||
if ( navigation )
|
||||
{
|
||||
// Toggle the opened status
|
||||
navigation.toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { TreoNavigationModule } from '@treo/components/navigation';
|
||||
import { SearchModule } from 'app/layout/common/search/search.module';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { MaterialLayoutComponent } from 'app/layout/layouts/horizontal/material/material.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
MaterialLayoutComponent
|
||||
],
|
||||
imports : [
|
||||
HttpClientModule,
|
||||
RouterModule,
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
TreoNavigationModule,
|
||||
SearchModule,
|
||||
SharedModule
|
||||
],
|
||||
exports : [
|
||||
MaterialLayoutComponent
|
||||
]
|
||||
})
|
||||
export class MaterialLayoutModule
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<div class="flex flex-col flex-auto w-full p-8 xs:p-2">
|
||||
|
||||
<div class="flex flex-wrap w-full">
|
||||
|
||||
<div class="flex items-center justify-between w-full my-4 px-4 xs:pr-0">
|
||||
<div class="mr-6">
|
||||
<h2 class="m-0">Dashboard</h2>
|
||||
<div class="text-secondary tracking-tight">Drive health at a glance</div>
|
||||
</div>
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center">
|
||||
<button class="xs:hidden"
|
||||
mat-stroked-button>
|
||||
<mat-icon class="icon-size-20"
|
||||
[svgIcon]="'save'"></mat-icon>
|
||||
<span class="ml-2">Export</span>
|
||||
</button>
|
||||
<button class="ml-2 xs:hidden"
|
||||
mat-stroked-button>
|
||||
<mat-icon class="icon-size-20 rotate-90 mirror"
|
||||
[svgIcon]="'tune'"></mat-icon>
|
||||
<span class="ml-2">Settings</span>
|
||||
</button>
|
||||
|
||||
<!-- Actions menu (visible on xs) -->
|
||||
<div class="hidden xs:flex">
|
||||
<button [matMenuTriggerFor]="actionsMenu"
|
||||
mat-icon-button>
|
||||
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #actionsMenu="matMenu">
|
||||
<button mat-menu-item>
|
||||
<mat-icon class="icon-size-20"
|
||||
[svgIcon]="'save'"></mat-icon>
|
||||
<span class="ml-2">Export</span>
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<mat-icon class="icon-size-20 rotate-90 mirror"
|
||||
[svgIcon]="'tune'"></mat-icon>
|
||||
<span class="ml-2">Settings</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap w-full">
|
||||
|
||||
<div *ngFor="let disk of data.data " class="flex flex-auto w-1/2 min-w-80 p-4">
|
||||
<div [ngClass]="{'border-green': disk.smart_results[0]?.smart_status == 'passed', 'border-red': disk.smart_results[0]?.smart_status == 'failed'}"
|
||||
class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden">
|
||||
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
|
||||
<mat-icon class="icon-size-96 opacity-12 text-green"
|
||||
*ngIf="disk.smart_results[0]?.smart_status == 'passed'"
|
||||
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
|
||||
<mat-icon class="icon-size-96 opacity-12 text-red"
|
||||
*ngIf="disk.smart_results[0]?.smart_status == 'failed'"
|
||||
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-col">
|
||||
<a [routerLink]="'/device/'+ disk.wwn"
|
||||
class="font-bold text-md text-secondary uppercase tracking-wider">/dev/{{disk.device_name}} - {{disk.model_name}}</a>
|
||||
<div class="text-green font-medium text-sm">
|
||||
Last Updated on {{disk.smart_results[0]?.date | date:'MMMM dd, yyyy' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto" *ngIf="disk.smart_results">
|
||||
<button mat-icon-button
|
||||
[matMenuTriggerFor]="previousStatementMenu">
|
||||
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #previousStatementMenu="matMenu">
|
||||
<a mat-menu-item [routerLink]="'/device/'+ disk.wwn">
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="icon-size-20 mr-3"
|
||||
[svgIcon]="'payment'"></mat-icon>
|
||||
<span>View Details</span>
|
||||
</span>
|
||||
</a>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap mt-4 -mx-6">
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">S.M.A.R.T</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none">{{ disk.smart_results[0]?.smart_status | titlecase}}</div>
|
||||
</div>
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Temperature</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none">{{ disk.smart_results[0]?.temp }}°C</div>
|
||||
</div>
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Capacity</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none">{{ disk.capacity | fileSize}}</div>
|
||||
</div>
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Powered On</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none">{{ humanizeHours(disk.smart_results[0]?.power_on_hours) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drive Temperatures -->
|
||||
<div class="flex flex-auto w-full min-w-80 h-90 p-4">
|
||||
<div class="flex flex-col flex-auto bg-card shadow-md rounded overflow-hidden">
|
||||
<div class="flex flex-col p-6 pr-4 pb-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-col">
|
||||
<div class="font-bold text-md text-secondary uppercase tracking-wider mr-4">Temperature</div>
|
||||
<div class="text-sm text-hint font-medium">Temperature history for each device </div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="h-8 min-h-8 px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="accountBalanceMenu">
|
||||
<span class="font-medium text-sm text-hint">12 months</span>
|
||||
</button>
|
||||
<mat-menu #accountBalanceMenu="matMenu">
|
||||
<button mat-menu-item>3 months</button>
|
||||
<button mat-menu-item>6 months</button>
|
||||
<button mat-menu-item>9 months</button>
|
||||
<button mat-menu-item>12 months</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-col flex-auto">
|
||||
<apx-chart class="flex-auto w-full h-full"
|
||||
[chart]="temperatureOptions.chart"
|
||||
[colors]="temperatureOptions.colors"
|
||||
[fill]="temperatureOptions.fill"
|
||||
[series]="temperatureOptions.series"
|
||||
[stroke]="temperatureOptions.stroke"
|
||||
[tooltip]="temperatureOptions.tooltip"
|
||||
[xaxis]="temperatureOptions.xaxis"></apx-chart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
@import 'treo';
|
||||
|
||||
example {
|
||||
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Theming
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@include treo-theme {
|
||||
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$primary: map-get($theme, primary);
|
||||
$accent: map-get($theme, accent);
|
||||
$warn: map-get($theme, warn);
|
||||
$is-dark: map-get($theme, is-dark);
|
||||
|
||||
example {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { ApexOptions } from 'ng-apexcharts';
|
||||
import { DashboardService } from 'app/modules/admin/dashboard/dashboard.service';
|
||||
import * as moment from "moment";
|
||||
|
||||
@Component({
|
||||
selector : 'example',
|
||||
templateUrl : './dashboard.component.html',
|
||||
styleUrls : ['./dashboard.component.scss'],
|
||||
encapsulation : ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
{
|
||||
data: any;
|
||||
temperatureOptions: ApexOptions;
|
||||
|
||||
// Private
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {SmartService} _smartService
|
||||
*/
|
||||
constructor(
|
||||
private _smartService: DashboardService
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the data
|
||||
this._smartService.data$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((data) => {
|
||||
|
||||
// Store the data
|
||||
this.data = data;
|
||||
|
||||
// Prepare the chart data
|
||||
this._prepareChartData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* After view init
|
||||
*/
|
||||
ngAfterViewInit(): void
|
||||
{}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
private _deviceDataTemperatureSeries() {
|
||||
var deviceTemperatureSeries = []
|
||||
|
||||
for(let device of this.data.data){
|
||||
var deviceSeriesMetadata = {
|
||||
name: `/dev/${device.device_name}`,
|
||||
data: []
|
||||
}
|
||||
for(let smartResults of device.smart_results){
|
||||
let newDate = new Date(smartResults.CreatedAt);
|
||||
deviceSeriesMetadata.data.push({
|
||||
x: newDate,
|
||||
y: smartResults.temp
|
||||
})
|
||||
}
|
||||
deviceTemperatureSeries.push(deviceSeriesMetadata)
|
||||
}
|
||||
return deviceTemperatureSeries
|
||||
}
|
||||
/**
|
||||
* Prepare the chart data from the data
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _prepareChartData(): void
|
||||
{
|
||||
// Account balance
|
||||
this.temperatureOptions = {
|
||||
chart : {
|
||||
animations: {
|
||||
speed : 400,
|
||||
animateGradually: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
fontFamily: 'inherit',
|
||||
foreColor : 'inherit',
|
||||
width : '100%',
|
||||
height : '100%',
|
||||
type : 'area',
|
||||
sparkline : {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
colors : ['#A3BFFA', '#667EEA'],
|
||||
fill : {
|
||||
colors : ['#CED9FB', '#AECDFD'],
|
||||
opacity: 0.5,
|
||||
type : 'solid'
|
||||
},
|
||||
series : this._deviceDataTemperatureSeries(),
|
||||
stroke : {
|
||||
curve: 'straight',
|
||||
width: 2
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'dark',
|
||||
x : {
|
||||
format: 'MMM dd, yyyy hh:mm:ss'
|
||||
},
|
||||
y : {
|
||||
formatter: (value) => {
|
||||
return value + '°C';
|
||||
}
|
||||
}
|
||||
},
|
||||
xaxis : {
|
||||
type: 'datetime'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any
|
||||
{
|
||||
return item.id || index;
|
||||
}
|
||||
|
||||
humanizeHours(hours: number): string {
|
||||
if(!hours){
|
||||
return '--'
|
||||
}
|
||||
|
||||
var value: number
|
||||
let unit = ""
|
||||
if(hours > (24*365)){ //more than a year
|
||||
value = Math.round((hours/(24*365)) * 10)/10
|
||||
unit = "years"
|
||||
} else if (hours > 24){
|
||||
value = Math.round((hours/24) *10 )/10
|
||||
unit = "days"
|
||||
} else{
|
||||
value = hours
|
||||
unit = "hours"
|
||||
}
|
||||
return `${value} ${unit}`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { DashboardComponent } from 'app/modules/admin/dashboard/dashboard.component';
|
||||
import { dashboardRoutes } from 'app/modules/admin/dashboard/dashboard.routing';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { NgApexchartsModule } from 'ng-apexcharts';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DashboardComponent
|
||||
],
|
||||
imports : [
|
||||
RouterModule.forChild(dashboardRoutes),
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
MatProgressBarModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
NgApexchartsModule,
|
||||
SharedModule
|
||||
]
|
||||
})
|
||||
export class DashboardModule
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { DashboardService } from 'app/modules/admin/dashboard/dashboard.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DashboardResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {FinanceService} _dashboardService
|
||||
*/
|
||||
constructor(
|
||||
private _dashboardService: DashboardService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>
|
||||
{
|
||||
return this._dashboardService.getData();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Route } from '@angular/router';
|
||||
import { DashboardComponent } from 'app/modules/admin/dashboard/dashboard.component';
|
||||
import {DashboardResolver} from "./dashboard.resolvers";
|
||||
|
||||
export const dashboardRoutes: Route[] = [
|
||||
{
|
||||
path : '',
|
||||
component: DashboardComponent,
|
||||
resolve : {
|
||||
sales: DashboardResolver
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DashboardService
|
||||
{
|
||||
// Observables
|
||||
private _data: BehaviorSubject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {HttpClient} _httpClient
|
||||
*/
|
||||
constructor(
|
||||
private _httpClient: HttpClient
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._data = new BehaviorSubject(null);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Getter for data
|
||||
*/
|
||||
get data$(): Observable<any>
|
||||
{
|
||||
return this._data.asObservable();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get data
|
||||
*/
|
||||
getData(): Observable<any>
|
||||
{
|
||||
return this._httpClient.get('/api/summary').pipe(
|
||||
tap((response: any) => {
|
||||
this._data.next(response);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
<div class="flex flex-col flex-auto w-full p-8 xs:p-2">
|
||||
|
||||
<div class="flex flex-wrap w-full">
|
||||
|
||||
<div class="flex items-center justify-between w-full my-4 px-4 xs:pr-0">
|
||||
<div class="mr-6">
|
||||
<h2 class="m-0">Drive Details</h2>
|
||||
<div class="text-secondary tracking-tight">Dive into S.M.A.R.T data</div>
|
||||
</div>
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center">
|
||||
<button class="xs:hidden"
|
||||
mat-stroked-button>
|
||||
<mat-icon class="icon-size-20"
|
||||
[svgIcon]="'save'"></mat-icon>
|
||||
<span class="ml-2">Export</span>
|
||||
</button>
|
||||
<button class="ml-2 xs:hidden"
|
||||
mat-stroked-button>
|
||||
<mat-icon class="icon-size-20 rotate-90 mirror"
|
||||
[svgIcon]="'tune'"></mat-icon>
|
||||
<span class="ml-2">Settings</span>
|
||||
</button>
|
||||
|
||||
<!-- Actions menu (visible on xs) -->
|
||||
<div class="hidden xs:flex">
|
||||
<button [matMenuTriggerFor]="actionsMenu"
|
||||
mat-icon-button>
|
||||
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #actionsMenu="matMenu">
|
||||
<button mat-menu-item>
|
||||
<mat-icon class="icon-size-20"
|
||||
[svgIcon]="'save'"></mat-icon>
|
||||
<span class="ml-2">Export</span>
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<mat-icon class="icon-size-20 rotate-90 mirror"
|
||||
[svgIcon]="'tune'"></mat-icon>
|
||||
<span class="ml-2">Settings</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Card -->
|
||||
<treo-card class="flex flex-col max-w-80 w-full mt-8 p-4 pt-6 filter-list">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-2xl font-semibold leading-tight">/dev/{{data.data.device_name}}</div>
|
||||
</div>
|
||||
<div class="flex flex-col my-2">
|
||||
<div *ngIf="data.data.manufacturer" class="my-2">
|
||||
<div>{{data.data.manufacturer}}</div>
|
||||
<div class="text-secondary text-md">Model Family</div>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<div>{{data.data.model_name}}</div>
|
||||
<div class="text-secondary text-md">Device Model</div>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<div>{{data.data.serial_number}}</div>
|
||||
<div class="text-secondary text-md">Serial Number</div>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<div>{{data.data.wwn}}</div>
|
||||
<div class="text-secondary text-md">LU WWN Device Id</div>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<div>{{data.data.firmware}}</div>
|
||||
<div class="text-secondary text-md">Firmware Version</div>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<div>{{data.data.capacity | fileSize}}</div>
|
||||
<div class="text-secondary text-md">Capacity</div>
|
||||
</div>
|
||||
<div *ngIf="data.data.rotational_speed" class="my-2">
|
||||
<div>{{data.data.rotational_speed}} RPM</div>
|
||||
<div class="text-secondary text-md">Rotation Rate</div>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<div>{{data.data.smart_results[0]?.power_cycle_count}}</div>
|
||||
<div class="text-secondary text-md">Power Cycle Count</div>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<div matTooltip="{{data.data.smart_results[0]?.power_on_hours}} hours">{{humanizeHours(data.data.smart_results[0]?.power_on_hours)}}</div>
|
||||
<div class="text-secondary text-md">Powered On</div>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<div>{{data.data.smart_results[0]?.temp}}°C</div>
|
||||
<div class="text-secondary text-md">Temperature</div>
|
||||
</div>
|
||||
</div>
|
||||
</treo-card>
|
||||
|
||||
<!-- S.M.A.R.T. Data table -->
|
||||
<div class="flex flex-auto w-1/3 p-8 lt-xl:w-full">
|
||||
<div class="flex flex-col flex-auto bg-card shadow-md rounded ">
|
||||
<div class="p-6">
|
||||
<div class="font-bold text-md text-secondary uppercase tracking-wider">S.M.A.R.T Attributes</div>
|
||||
<div class="text-sm text-hint font-medium">{{this.smartAttributeDataSource.data.length}} visible, {{this.data.data.smart_results[0]?.smart_attributes.length - this.smartAttributeDataSource.data.length}} hidden</div>
|
||||
</div>
|
||||
<div class="overflow-auto">
|
||||
<table class="w-full bg-transparent"
|
||||
mat-table
|
||||
matSort
|
||||
[dataSource]="smartAttributeDataSource"
|
||||
[trackBy]="trackByFn"
|
||||
#smartAttributeTable>
|
||||
|
||||
<!-- Status -->
|
||||
<ng-container matColumnDef="status">
|
||||
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
*matHeaderCellDef>
|
||||
<span class="whitespace-no-wrap">
|
||||
Status
|
||||
</span>
|
||||
</th>
|
||||
<td mat-cell
|
||||
*matCellDef="let attribute">
|
||||
<span class="inline-flex items-center font-bold text-xs px-2 py-2px rounded-full tracking-wide uppercase"
|
||||
[ngClass]="{'red-200': attribute.status === 'failed',
|
||||
'green-200': attribute.status === 'passed',
|
||||
'yellow-200': attribute.status === 'warn'
|
||||
}">
|
||||
<span class="w-2 h-2 rounded-full mr-2"
|
||||
[ngClass]="{'bg-red': attribute.status === 'failed',
|
||||
'bg-green': attribute.status === 'passed',
|
||||
'bg-yellow': attribute.status === 'warn'}"></span>
|
||||
<span class="pr-2px leading-relaxed whitespace-no-wrap" matTooltip="{{attribute.status_reason}}">{{attribute.status}}</span>
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- ID -->
|
||||
<ng-container matColumnDef="id">
|
||||
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
*matHeaderCellDef>
|
||||
<span class="whitespace-no-wrap">
|
||||
ID
|
||||
</span>
|
||||
</th>
|
||||
<td mat-cell
|
||||
*matCellDef="let attribute">
|
||||
<span class="pr-6 font-medium text-sm text-secondary whitespace-no-wrap">
|
||||
{{attribute.attribute_id}} ({{toHex(attribute.attribute_id)}})
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name -->
|
||||
<ng-container matColumnDef="name">
|
||||
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
*matHeaderCellDef>
|
||||
<span class="whitespace-no-wrap">
|
||||
Name
|
||||
</span>
|
||||
</th>
|
||||
<td mat-cell
|
||||
*matCellDef="let attribute">
|
||||
<span class="pr-6 whitespace-no-wrap" matTooltip="{{data.lookup[attribute.attribute_id]?.description}}">
|
||||
{{attribute.name}} <mat-icon class="icon-size-10" [svgIcon]="'info'"></mat-icon>
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Value -->
|
||||
<ng-container matColumnDef="value">
|
||||
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
*matHeaderCellDef>
|
||||
<span class="whitespace-no-wrap">
|
||||
Value
|
||||
</span>
|
||||
</th>
|
||||
<td mat-cell
|
||||
*matCellDef="let attribute">
|
||||
<span class="pr-6 whitespace-no-wrap" matTooltip="{{data.lookup[attribute.attribute_id].display_type}}">
|
||||
{{extractAttributeValue(data.lookup[attribute.attribute_id], attribute)}}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Worst -->
|
||||
<ng-container matColumnDef="worst">
|
||||
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
*matHeaderCellDef>
|
||||
<span class="whitespace-no-wrap">
|
||||
Worst
|
||||
</span>
|
||||
</th>
|
||||
<td mat-cell
|
||||
*matCellDef="let attribute">
|
||||
<span class="pr-6 whitespace-no-wrap">
|
||||
{{attribute.worst}}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Threshold -->
|
||||
<ng-container matColumnDef="thresh">
|
||||
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
*matHeaderCellDef>
|
||||
<span class="whitespace-no-wrap">
|
||||
Threshold
|
||||
</span>
|
||||
</th>
|
||||
<td mat-cell
|
||||
*matCellDef="let attribute">
|
||||
<span class="pr-6 whitespace-no-wrap">
|
||||
{{attribute.thresh}}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Ideal -->
|
||||
<ng-container matColumnDef="ideal">
|
||||
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
*matHeaderCellDef>
|
||||
<span class="whitespace-no-wrap">
|
||||
Ideal
|
||||
</span>
|
||||
</th>
|
||||
<td mat-cell
|
||||
*matCellDef="let attribute">
|
||||
<span class="pr-6 font-medium whitespace-no-wrap">
|
||||
{{data.lookup[attribute.attribute_id]?.display_type == "raw" ? data.lookup[attribute.attribute_id]?.ideal : '' }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Observed Failure Rate -->
|
||||
<ng-container matColumnDef="failure">
|
||||
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
*matHeaderCellDef>
|
||||
<span class="whitespace-no-wrap">
|
||||
Failure Rate <mat-icon [svgIcon]="'info'"></mat-icon>
|
||||
</span>
|
||||
</th>
|
||||
<td mat-cell
|
||||
*matCellDef="let attribute">
|
||||
<span class="pr-6 font-medium whitespace-no-wrap">
|
||||
{{attribute.failure_rate | percent}}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- History -->
|
||||
<ng-container matColumnDef="history">
|
||||
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
*matHeaderCellDef>
|
||||
<span class="whitespace-no-wrap">
|
||||
History
|
||||
</span>
|
||||
</th>
|
||||
<td mat-cell
|
||||
*matCellDef="let attribute">
|
||||
|
||||
<span class="font-medium whitespace-no-wrap">
|
||||
<apx-chart
|
||||
[series]="attribute.chartData"
|
||||
[chart]="commonSparklineOptions.chart"
|
||||
[tooltip]="commonSparklineOptions.tooltip"
|
||||
[stroke]="commonSparklineOptions.stroke"
|
||||
[annotations]="attribute.chartDataReferenceLine"
|
||||
></apx-chart>
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Footer -->
|
||||
<ng-container matColumnDef="recentOrdersTableFooter">
|
||||
<td class="px-3 border-none"
|
||||
mat-footer-cell
|
||||
*matFooterCellDef
|
||||
colspan="6">
|
||||
<button mat-button
|
||||
(click)="toggleOnlyCritical()"
|
||||
[color]="'primary'">
|
||||
<span *ngIf="onlyCritical">Show all attributes</span>
|
||||
<span *ngIf="!onlyCritical">Show critical attributes</span>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row
|
||||
*matHeaderRowDef="smartAttributeTableColumns"></tr>
|
||||
<tr class="attribute-row h-16"
|
||||
mat-row
|
||||
[ngClass]="{'yellow-50': data.lookup[row.attribute_id]?.critical}"
|
||||
*matRowDef="let row; columns: smartAttributeTableColumns;"></tr>
|
||||
<tr class="h-16"
|
||||
mat-footer-row
|
||||
*matFooterRowDef="['recentOrdersTableFooter']"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
@import 'treo';
|
||||
|
||||
detail {
|
||||
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Theming
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@include treo-theme {
|
||||
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$primary: map-get($theme, primary);
|
||||
$accent: map-get($theme, accent);
|
||||
$warn: map-get($theme, warn);
|
||||
$is-dark: map-get($theme, is-dark);
|
||||
|
||||
detail {
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DetailComponent } from './detail.component';
|
||||
|
||||
describe('DetailComponent', () => {
|
||||
let component: DetailComponent;
|
||||
let fixture: ComponentFixture<DetailComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DetailComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DetailComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,246 @@
|
||||
import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
||||
import {ApexOptions} from "ng-apexcharts";
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
import {MatSort} from "@angular/material/sort";
|
||||
import {Subject} from "rxjs";
|
||||
import {DetailService} from "../detail/detail.service";
|
||||
import {takeUntil} from "rxjs/operators";
|
||||
import {fadeOut} from "../../../../@treo/animations/fade";
|
||||
|
||||
@Component({
|
||||
selector: 'detail',
|
||||
templateUrl: './detail.component.html',
|
||||
styleUrls: ['./detail.component.scss']
|
||||
})
|
||||
|
||||
export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
onlyCritical: boolean = true;
|
||||
data: any;
|
||||
commonSparklineOptions: Partial<ApexOptions>;
|
||||
smartAttributeDataSource: MatTableDataSource<any>;
|
||||
smartAttributeTableColumns: string[];
|
||||
|
||||
|
||||
@ViewChild('smartAttributeTable', {read: MatSort})
|
||||
smartAttributeTableMatSort: MatSort;
|
||||
|
||||
// Private
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {DetailService} _detailService
|
||||
*/
|
||||
constructor(
|
||||
private _detailService: DetailService
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
|
||||
// Set the defaults
|
||||
this.smartAttributeDataSource = new MatTableDataSource();
|
||||
// this.recentTransactionsTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh'];
|
||||
this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh','ideal', 'failure', 'history'];
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the data
|
||||
this._detailService.data$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((data) => {
|
||||
|
||||
// Store the data
|
||||
this.data = data;
|
||||
|
||||
// Store the table data
|
||||
this.smartAttributeDataSource.data = this._generateSmartAttributeTableDataSource(data.data.smart_results);
|
||||
|
||||
// Prepare the chart data
|
||||
this._prepareChartData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* After view init
|
||||
*/
|
||||
ngAfterViewInit(): void
|
||||
{
|
||||
// Make the data source sortable
|
||||
this.smartAttributeDataSource.sort = this.smartAttributeTableMatSort;
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
extractAttributeValue(attribute_metadata, attribute_data){
|
||||
if(attribute_metadata.display_type == "raw"){
|
||||
return attribute_data.raw_value
|
||||
}
|
||||
else if(attribute_metadata.display_type == "transformed" && attribute_data.transformed_value ){
|
||||
return attribute_data.transformed_value
|
||||
} else {
|
||||
return attribute_data.value
|
||||
}
|
||||
}
|
||||
|
||||
private _generateSmartAttributeTableDataSource(smart_results){
|
||||
var smartAttributeDataSource = [];
|
||||
|
||||
if(smart_results.length == 0){
|
||||
return smartAttributeDataSource
|
||||
}
|
||||
|
||||
var latest_smart_result = smart_results[0];
|
||||
for(let attr of latest_smart_result.smart_attributes){
|
||||
|
||||
|
||||
|
||||
//chart history data
|
||||
if (!attr.chartData) {
|
||||
var rawHistory = (attr.history || []).map(hist_attr => this.extractAttributeValue(this.data.lookup[attr.attribute_id], hist_attr)).reverse()
|
||||
rawHistory.push(this.extractAttributeValue(this.data.lookup[attr.attribute_id], attr))
|
||||
attr.chartData = [
|
||||
{
|
||||
name: "chart-line-sparkline",
|
||||
// data: Array.from({length: 40}, () => Math.floor(Math.random() * 40))
|
||||
data: rawHistory
|
||||
}
|
||||
]
|
||||
|
||||
// //add the reference line showing the threshold
|
||||
// attr.chartDataReferenceLine = {
|
||||
// yaxis: [
|
||||
// {
|
||||
// y: attr.thresh,
|
||||
// borderColor: '#f05252',
|
||||
// label: {
|
||||
// borderColor: '#f05252',
|
||||
// style: {
|
||||
// color: '#fff',
|
||||
// background: '#f05252'
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
}
|
||||
//determine when to include the attributes in table.
|
||||
if(!this.onlyCritical || this.onlyCritical && this.data.lookup[attr.attribute_id]?.critical || attr.value <= attr.thresh){
|
||||
smartAttributeDataSource.push(attr)
|
||||
}
|
||||
}
|
||||
return smartAttributeDataSource
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the chart data from the data
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _prepareChartData(): void
|
||||
{
|
||||
|
||||
// Account balance
|
||||
this.commonSparklineOptions = {
|
||||
chart: {
|
||||
type: "bar",
|
||||
width: 100,
|
||||
height: 25,
|
||||
sparkline: {
|
||||
enabled: true
|
||||
},
|
||||
animations: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
fixed: {
|
||||
enabled: false
|
||||
},
|
||||
x: {
|
||||
show: false
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
formatter: function(seriesName) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
},
|
||||
marker: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
stroke: {
|
||||
width: 2,
|
||||
colors: ['#667EEA']
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
toHex(decimalNumb){
|
||||
return "0x" + Number(decimalNumb).toString(16).padStart(2, '0').toUpperCase()
|
||||
}
|
||||
toggleOnlyCritical(){
|
||||
this.onlyCritical = !this.onlyCritical
|
||||
this.smartAttributeDataSource.data = this._generateSmartAttributeTableDataSource(this.data.data.smart_results);
|
||||
|
||||
}
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any
|
||||
{
|
||||
return index;
|
||||
// return item.id || index;
|
||||
}
|
||||
|
||||
humanizeHours(hours: number): string {
|
||||
if(!hours){
|
||||
return '--'
|
||||
}
|
||||
|
||||
var value: number
|
||||
let unit = ""
|
||||
if(hours > (24*365)){ //more than a year
|
||||
value = Math.round((hours/(24*365)) * 10)/10
|
||||
unit = "years"
|
||||
} else if (hours > 24){
|
||||
value = Math.round((hours/24) *10 )/10
|
||||
unit = "days"
|
||||
} else{
|
||||
value = hours
|
||||
unit = "hours"
|
||||
}
|
||||
return `${value} ${unit}`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { DetailComponent } from 'app/modules/admin/detail/detail.component';
|
||||
import { detailRoutes } from 'app/modules/admin/detail/detail.routing';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip'
|
||||
import { NgApexchartsModule } from 'ng-apexcharts';
|
||||
import { TreoCardModule } from '@treo/components/card';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DetailComponent
|
||||
],
|
||||
imports : [
|
||||
RouterModule.forChild(detailRoutes),
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
MatTooltipModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
MatProgressBarModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
NgApexchartsModule,
|
||||
TreoCardModule,
|
||||
SharedModule,
|
||||
|
||||
]
|
||||
})
|
||||
export class DetailModule
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { DetailService } from 'app/modules/admin/detail/detail.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DetailResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {FinanceService} _detailService
|
||||
*/
|
||||
constructor(
|
||||
private _detailService: DetailService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>
|
||||
{
|
||||
return this._detailService.getData(route.params.wwn);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Route } from '@angular/router';
|
||||
import { DetailComponent } from 'app/modules/admin/detail/detail.component';
|
||||
import {DetailResolver} from "./detail.resolvers";
|
||||
|
||||
export const detailRoutes: Route[] = [
|
||||
{
|
||||
path : '',
|
||||
component: DetailComponent,
|
||||
resolve : {
|
||||
sales: DetailResolver
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DetailService
|
||||
{
|
||||
// Observables
|
||||
private _data: BehaviorSubject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {HttpClient} _httpClient
|
||||
*/
|
||||
constructor(
|
||||
private _httpClient: HttpClient
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._data = new BehaviorSubject(null);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Getter for data
|
||||
*/
|
||||
get data$(): Observable<any>
|
||||
{
|
||||
return this._data.asObservable();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get data
|
||||
*/
|
||||
getData(wwn): Observable<any>
|
||||
{
|
||||
return this._httpClient.get(`/api/device/${wwn}/details`).pipe(
|
||||
tap((response: any) => {
|
||||
this._data.next(response);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="flex flex-col items-center justify-center w-full h-full cool-gray-900">
|
||||
<div class="flex flex-col items-center max-w-lg rich-text">
|
||||
<img class="w-20"
|
||||
src="assets/images/logo/scrutiny-logo-white.svg">
|
||||
<h2>Landing Module</h2>
|
||||
<p>
|
||||
This can be the landing or the welcome page of your application. If you have an SSR (Server Side Rendering) setup, or if you don't need to have Search engine
|
||||
visibility and optimizations, you can even use this page as your primary landing page.
|
||||
</p>
|
||||
<p>
|
||||
This is a separate "module", it has its own directory and routing setup and also it's completely separated from the actual application. This is also a simple example of
|
||||
multiple applications setup. You can have different entry points and essentially have different applications within the same codebase.
|
||||
</p>
|
||||
<a class="mt-10 no-underline"
|
||||
mat-raised-button
|
||||
[color]="'primary'"
|
||||
[routerLink]="'/apps/analytics-dashboard'">
|
||||
Launch the App
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
@import 'treo';
|
||||
|
||||
landing-home {
|
||||
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Theming
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@include treo-theme {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Component, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector : 'landing-home',
|
||||
templateUrl : './home.component.html',
|
||||
styleUrls : ['./home.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class LandingHomeComponent
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { LandingHomeComponent } from 'app/modules/landing/home/home.component';
|
||||
import { landingHomeRoutes } from 'app/modules/landing/home/home.routing';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
LandingHomeComponent
|
||||
],
|
||||
imports : [
|
||||
RouterModule.forChild(landingHomeRoutes),
|
||||
MatButtonModule,
|
||||
SharedModule
|
||||
]
|
||||
})
|
||||
export class LandingHomeModule
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Route } from '@angular/router';
|
||||
import { LandingHomeComponent } from 'app/modules/landing/home/home.component';
|
||||
|
||||
export const landingHomeRoutes: Route[] = [
|
||||
{
|
||||
path : '',
|
||||
component: LandingHomeComponent
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright (c) 2019 Jonathan Catmull.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
type unit = 'bytes' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB';
|
||||
type unitPrecisionMap = {
|
||||
[u in unit]: number;
|
||||
};
|
||||
|
||||
const defaultPrecisionMap: unitPrecisionMap = {
|
||||
bytes: 0,
|
||||
KB: 0,
|
||||
MB: 1,
|
||||
GB: 1,
|
||||
TB: 2,
|
||||
PB: 2
|
||||
};
|
||||
|
||||
/*
|
||||
* Convert bytes into largest possible unit.
|
||||
* Takes an precision argument that can be a number or a map for each unit.
|
||||
* Usage:
|
||||
* bytes | fileSize:precision
|
||||
* @example
|
||||
* // returns 1 KB
|
||||
* {{ 1500 | fileSize }}
|
||||
* @example
|
||||
* // returns 2.1 GB
|
||||
* {{ 2100000000 | fileSize }}
|
||||
* @example
|
||||
* // returns 1.46 KB
|
||||
* {{ 1500 | fileSize:2 }}
|
||||
*/
|
||||
@Pipe({ name: 'fileSize' })
|
||||
export class FileSizePipe implements PipeTransform {
|
||||
private readonly units: unit[] = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
|
||||
transform(bytes: number = 0, precision: number | unitPrecisionMap = defaultPrecisionMap): string {
|
||||
if (isNaN(parseFloat(String(bytes))) || !isFinite(bytes)) return '?';
|
||||
|
||||
let unitIndex = 0;
|
||||
|
||||
while (bytes >= 1024) {
|
||||
bytes /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
const unit = this.units[unitIndex];
|
||||
|
||||
if (typeof precision === 'number') {
|
||||
return `${bytes.toFixed(+precision)} ${unit}`;
|
||||
}
|
||||
return `${bytes.toFixed(precision[unit])} ${unit}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import {FileSizePipe} from "./file-size.pipe";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
FileSizePipe
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
exports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
FileSizePipe
|
||||
]
|
||||
})
|
||||
export class SharedModule
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user