This commit is contained in:
Jason Kulatunga
2020-08-19 16:04:21 -07:00
commit 8482272d45
336 changed files with 197309 additions and 0 deletions
@@ -0,0 +1 @@
<router-outlet></router-outlet>
@@ -0,0 +1,6 @@
:host {
display: flex;
flex: 1 1 auto;
width: 100%;
height: 100%;
}
+16
View File
@@ -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()
{
}
}
+61
View File
@@ -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
{
}
+37
View File
@@ -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
{
}