mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
rename frontend to web and update contributing guide (#2509)
* rename frontend to web and update contributing guide * rename this time * fix build * restructure rust code * update documentation * update descriptions * Update CONTRIBUTING.md Co-authored-by: J H <2364004+Blu-J@users.noreply.github.com> --------- Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: J H <2364004+Blu-J@users.noreply.github.com>
This commit is contained in:
27
web/projects/diagnostic-ui/src/app/app-routing.module.ts
Normal file
27
web/projects/diagnostic-ui/src/app/app-routing.module.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () =>
|
||||
import('./pages/home/home.module').then(m => m.HomePageModule),
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
loadChildren: () =>
|
||||
import('./pages/logs/logs.module').then(m => m.LogsPageModule),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
scrollPositionRestoration: 'enabled',
|
||||
preloadingStrategy: PreloadAllModules,
|
||||
useHash: true,
|
||||
}),
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
5
web/projects/diagnostic-ui/src/app/app.component.html
Normal file
5
web/projects/diagnostic-ui/src/app/app.component.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<tui-root>
|
||||
<ion-app>
|
||||
<ion-router-outlet></ion-router-outlet>
|
||||
</ion-app>
|
||||
</tui-root>
|
||||
8
web/projects/diagnostic-ui/src/app/app.component.scss
Normal file
8
web/projects/diagnostic-ui/src/app/app.component.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
tui-root {
|
||||
height: 100%;
|
||||
}
|
||||
10
web/projects/diagnostic-ui/src/app/app.component.ts
Normal file
10
web/projects/diagnostic-ui/src/app/app.component.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrls: ['app.component.scss'],
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor() {}
|
||||
}
|
||||
43
web/projects/diagnostic-ui/src/app/app.module.ts
Normal file
43
web/projects/diagnostic-ui/src/app/app.module.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { RouteReuseStrategy } from '@angular/router'
|
||||
import { IonicModule, IonicRouteStrategy } from '@ionic/angular'
|
||||
import { TuiRootModule } from '@taiga-ui/core'
|
||||
import { AppComponent } from './app.component'
|
||||
import { AppRoutingModule } from './app-routing.module'
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
import { ApiService } from './services/api/api.service'
|
||||
import { MockApiService } from './services/api/mock-api.service'
|
||||
import { LiveApiService } from './services/api/live-api.service'
|
||||
import { RELATIVE_URL, WorkspaceConfig } from '@start9labs/shared'
|
||||
|
||||
const {
|
||||
useMocks,
|
||||
ui: { api },
|
||||
} = require('../../../../config.json') as WorkspaceConfig
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
BrowserAnimationsModule,
|
||||
IonicModule.forRoot({
|
||||
mode: 'md',
|
||||
}),
|
||||
AppRoutingModule,
|
||||
TuiRootModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
||||
{
|
||||
provide: ApiService,
|
||||
useClass: useMocks ? MockApiService : LiveApiService,
|
||||
},
|
||||
{
|
||||
provide: RELATIVE_URL,
|
||||
useValue: `/${api.url}/${api.version}`,
|
||||
},
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { HomePage } from './home.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HomePage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class HomePageRoutingModule {}
|
||||
12
web/projects/diagnostic-ui/src/app/pages/home/home.module.ts
Normal file
12
web/projects/diagnostic-ui/src/app/pages/home/home.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { HomePage } from './home.page'
|
||||
import { HomePageRoutingModule } from './home-routing.module'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FormsModule, IonicModule, HomePageRoutingModule],
|
||||
declarations: [HomePage],
|
||||
})
|
||||
export class HomePageModule {}
|
||||
81
web/projects/diagnostic-ui/src/app/pages/home/home.page.html
Normal file
81
web/projects/diagnostic-ui/src/app/pages/home/home.page.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<ion-content>
|
||||
<div style="padding: 48px">
|
||||
<ng-container *ngIf="!restarted; else refresh">
|
||||
<h1
|
||||
class="ion-text-center"
|
||||
style="padding-bottom: 36px; font-size: calc(2vw + 14px)"
|
||||
>
|
||||
StartOS - Diagnostic Mode
|
||||
</h1>
|
||||
|
||||
<ng-container *ngIf="error">
|
||||
<h2
|
||||
style="
|
||||
padding-bottom: 16px;
|
||||
font-size: calc(1vw + 14px);
|
||||
font-weight: bold;
|
||||
"
|
||||
>
|
||||
StartOS launch error:
|
||||
</h2>
|
||||
<div class="code-block">
|
||||
<code>
|
||||
<ion-text color="warning">{{ error.problem }}</ion-text>
|
||||
<span *ngIf="error.details">
|
||||
<br />
|
||||
<br />
|
||||
<ion-text color="warning">{{ error.details }}</ion-text>
|
||||
</span>
|
||||
</code>
|
||||
</div>
|
||||
<ion-button routerLink="logs">View Logs</ion-button>
|
||||
<h2
|
||||
style="
|
||||
padding: 32px 0 16px 0;
|
||||
font-size: calc(1vw + 12px);
|
||||
font-weight: bold;
|
||||
"
|
||||
>
|
||||
Possible solutions:
|
||||
</h2>
|
||||
<div class="code-block">
|
||||
<code><ion-text color="success">{{ error.solution }}</ion-text></code>
|
||||
</div>
|
||||
<ion-button (click)="restart()">Restart Server</ion-button>
|
||||
<ion-button
|
||||
class="ion-padding-start"
|
||||
*ngIf="error.code === 15 || error.code === 25"
|
||||
(click)="forgetDrive()"
|
||||
>
|
||||
{{ error.code === 15 ? 'Setup Current Drive' : 'Enter Recovery Mode'
|
||||
}}
|
||||
</ion-button>
|
||||
|
||||
<div class="ion-padding-top">
|
||||
<ion-button (click)="presentAlertSystemRebuild()" color="warning">
|
||||
System Rebuild
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<div class="ion-padding-top">
|
||||
<ion-button (click)="presentAlertRepairDisk()" color="danger">
|
||||
Repair Drive
|
||||
</ion-button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #refresh>
|
||||
<h1
|
||||
class="ion-text-center"
|
||||
style="padding-bottom: 36px; font-size: calc(2vw + 12px)"
|
||||
>
|
||||
Server is restarting
|
||||
</h1>
|
||||
<h2 style="padding-bottom: 16px; font-size: calc(1vw + 12px)">
|
||||
Wait for the server to restart, then refresh this page.
|
||||
</h2>
|
||||
<ion-button (click)="refreshPage()">Refresh</ion-button>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,5 @@
|
||||
.code-block {
|
||||
background-color: rgb(69, 69, 69);
|
||||
padding: 12px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
210
web/projects/diagnostic-ui/src/app/pages/home/home.page.ts
Normal file
210
web/projects/diagnostic-ui/src/app/pages/home/home.page.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { AlertController, LoadingController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
templateUrl: 'home.page.html',
|
||||
styleUrls: ['home.page.scss'],
|
||||
})
|
||||
export class HomePage {
|
||||
error?: {
|
||||
code: number
|
||||
problem: string
|
||||
solution: string
|
||||
details?: string
|
||||
}
|
||||
solutions: string[] = []
|
||||
restarted = false
|
||||
|
||||
constructor(
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly api: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const error = await this.api.getError()
|
||||
// incorrect drive
|
||||
if (error.code === 15) {
|
||||
this.error = {
|
||||
code: 15,
|
||||
problem: 'Unknown storage drive detected',
|
||||
solution:
|
||||
'To use a different storage drive, replace the current one and click RESTART SERVER below. To use the current storage drive, click USE CURRENT DRIVE below, then follow instructions. No data will be erased during this process.',
|
||||
details: error.data?.details,
|
||||
}
|
||||
// no drive
|
||||
} else if (error.code === 20) {
|
||||
this.error = {
|
||||
code: 20,
|
||||
problem: 'Storage drive not found',
|
||||
solution:
|
||||
'Insert your StartOS storage drive and click RESTART SERVER below.',
|
||||
details: error.data?.details,
|
||||
}
|
||||
// drive corrupted
|
||||
} else if (error.code === 25) {
|
||||
this.error = {
|
||||
code: 25,
|
||||
problem:
|
||||
'Storage drive corrupted. This could be the result of data corruption or physical damage.',
|
||||
solution:
|
||||
'It may or may not be possible to re-use this drive by reformatting and recovering from backup. To enter recovery mode, click ENTER RECOVERY MODE below, then follow instructions. No data will be erased during this step.',
|
||||
details: error.data?.details,
|
||||
}
|
||||
// filesystem I/O error - disk needs repair
|
||||
} else if (error.code === 2) {
|
||||
this.error = {
|
||||
code: 2,
|
||||
problem: 'Filesystem I/O error.',
|
||||
solution:
|
||||
'Repairing the disk could help resolve this issue. Please DO NOT unplug the drive or server during this time or the situation will become worse.',
|
||||
details: error.data?.details,
|
||||
}
|
||||
// disk management error - disk needs repair
|
||||
} else if (error.code === 48) {
|
||||
this.error = {
|
||||
code: 48,
|
||||
problem: 'Disk management error.',
|
||||
solution:
|
||||
'Repairing the disk could help resolve this issue. Please DO NOT unplug the drive or server during this time or the situation will become worse.',
|
||||
details: error.data?.details,
|
||||
}
|
||||
} else {
|
||||
this.error = {
|
||||
code: error.code,
|
||||
problem: error.message,
|
||||
solution: 'Please contact support.',
|
||||
details: error.data?.details,
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.api.restart()
|
||||
this.restarted = true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
async forgetDrive(): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.api.forgetDrive()
|
||||
await this.api.restart()
|
||||
this.restarted = true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
async presentAlertSystemRebuild() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message:
|
||||
'<p>This action will tear down all service containers and rebuild them from scratch. No data will be deleted.</p><p>A system rebuild can be useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues.</p><p>It may take up to an hour to complete. During this time, you will lose all connectivity to your Start9 server.</p>',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Rebuild',
|
||||
handler: () => {
|
||||
try {
|
||||
this.systemRebuild()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
cssClass: 'alert-warning-message',
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentAlertRepairDisk() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message:
|
||||
'<p>This action should only be executed if directed by a Start9 support specialist.</p><p>If anything happens to the device during the reboot, such as losing power or unplugging the drive, the filesystem <i>will</i> be in an unrecoverable state. Please proceed with caution.</p>',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Repair',
|
||||
handler: () => {
|
||||
try {
|
||||
this.repairDisk()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
cssClass: 'alert-error-message',
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
refreshPage(): void {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
private async systemRebuild(): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.api.systemRebuild()
|
||||
await this.api.restart()
|
||||
this.restarted = true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async repairDisk(): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.api.repairDisk()
|
||||
await this.api.restart()
|
||||
this.restarted = true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
18
web/projects/diagnostic-ui/src/app/pages/logs/logs.module.ts
Normal file
18
web/projects/diagnostic-ui/src/app/pages/logs/logs.module.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { LogsPage } from './logs.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: LogsPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, RouterModule.forChild(routes)],
|
||||
declarations: [LogsPage],
|
||||
})
|
||||
export class LogsPageModule {}
|
||||
57
web/projects/diagnostic-ui/src/app/pages/logs/logs.page.html
Normal file
57
web/projects/diagnostic-ui/src/app/pages/logs/logs.page.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="/"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Logs</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content
|
||||
[scrollEvents]="true"
|
||||
(ionScrollEnd)="scrollEnd()"
|
||||
class="ion-padding"
|
||||
>
|
||||
<ion-infinite-scroll
|
||||
id="scroller"
|
||||
*ngIf="!loading && needInfinite"
|
||||
position="top"
|
||||
threshold="0"
|
||||
(ionInfinite)="doInfinite($event)"
|
||||
>
|
||||
<ion-infinite-scroll-content
|
||||
loadingSpinner="lines"
|
||||
></ion-infinite-scroll-content>
|
||||
</ion-infinite-scroll>
|
||||
|
||||
<div id="container">
|
||||
<div id="template" style="white-space: pre-line"></div>
|
||||
</div>
|
||||
|
||||
<div id="bottom-div"></div>
|
||||
|
||||
<div
|
||||
[ngStyle]="{
|
||||
'position': 'fixed',
|
||||
'bottom': '50px',
|
||||
'right': isOnBottom ? '-52px' : '30px',
|
||||
'border-radius': '100%',
|
||||
'transition': 'right 0.25s ease-out'
|
||||
}"
|
||||
>
|
||||
<ion-button
|
||||
style="
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
--padding-start: 0px;
|
||||
--padding-end: 0px;
|
||||
--border-radius: 100%;
|
||||
"
|
||||
color="dark"
|
||||
(click)="scrollToBottom()"
|
||||
strong
|
||||
>
|
||||
<ion-icon name="chevron-down"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-content>
|
||||
95
web/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts
Normal file
95
web/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { IonContent } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { ErrorToastService, toLocalIsoString } from '@start9labs/shared'
|
||||
|
||||
var Convert = require('ansi-to-html')
|
||||
var convert = new Convert({
|
||||
bg: 'transparent',
|
||||
})
|
||||
|
||||
@Component({
|
||||
selector: 'logs',
|
||||
templateUrl: './logs.page.html',
|
||||
styleUrls: ['./logs.page.scss'],
|
||||
})
|
||||
export class LogsPage {
|
||||
@ViewChild(IonContent) private content?: IonContent
|
||||
loading = true
|
||||
needInfinite = true
|
||||
startCursor?: string
|
||||
limit = 200
|
||||
isOnBottom = true
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.getLogs()
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
scrollEnd() {
|
||||
const bottomDiv = document.getElementById('bottom-div')
|
||||
this.isOnBottom =
|
||||
!!bottomDiv &&
|
||||
bottomDiv.getBoundingClientRect().top - 420 < window.innerHeight
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
this.content?.scrollToBottom(500)
|
||||
}
|
||||
|
||||
async doInfinite(e: any): Promise<void> {
|
||||
await this.getLogs()
|
||||
e.target.complete()
|
||||
}
|
||||
|
||||
private async getLogs() {
|
||||
try {
|
||||
const { 'start-cursor': startCursor, entries } = await this.api.getLogs({
|
||||
cursor: this.startCursor,
|
||||
before: !!this.startCursor,
|
||||
limit: this.limit,
|
||||
})
|
||||
|
||||
if (!entries.length) return
|
||||
|
||||
this.startCursor = startCursor
|
||||
|
||||
const container = document.getElementById('container')
|
||||
const newLogs = document.getElementById('template')?.cloneNode(true)
|
||||
|
||||
if (!(newLogs instanceof HTMLElement)) return
|
||||
|
||||
newLogs.innerHTML = entries
|
||||
.map(
|
||||
entry =>
|
||||
`<b>${toLocalIsoString(
|
||||
new Date(entry.timestamp),
|
||||
)}</b> ${convert.toHtml(entry.message)}`,
|
||||
)
|
||||
.join('\n')
|
||||
|
||||
const beforeContainerHeight = container?.scrollHeight || 0
|
||||
container?.prepend(newLogs)
|
||||
const afterContainerHeight = container?.scrollHeight || 0
|
||||
|
||||
// scroll down
|
||||
setTimeout(() => {
|
||||
this.content?.scrollToPoint(
|
||||
0,
|
||||
afterContainerHeight - beforeContainerHeight,
|
||||
)
|
||||
}, 50)
|
||||
|
||||
if (entries.length < this.limit) {
|
||||
this.needInfinite = false
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { LogsRes, ServerLogsReq } from '@start9labs/shared'
|
||||
|
||||
export abstract class ApiService {
|
||||
abstract getError(): Promise<GetErrorRes>
|
||||
abstract restart(): Promise<void>
|
||||
abstract forgetDrive(): Promise<void>
|
||||
abstract repairDisk(): Promise<void>
|
||||
abstract systemRebuild(): Promise<void>
|
||||
abstract getLogs(params: ServerLogsReq): Promise<LogsRes>
|
||||
}
|
||||
|
||||
export interface GetErrorRes {
|
||||
code: number
|
||||
message: string
|
||||
data: { details: string }
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
HttpService,
|
||||
isRpcError,
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
} from '@start9labs/shared'
|
||||
import { ApiService, GetErrorRes } from './api.service'
|
||||
import { LogsRes, ServerLogsReq } from '@start9labs/shared'
|
||||
|
||||
@Injectable()
|
||||
export class LiveApiService implements ApiService {
|
||||
constructor(private readonly http: HttpService) {}
|
||||
|
||||
async getError(): Promise<GetErrorRes> {
|
||||
return this.rpcRequest<GetErrorRes>({
|
||||
method: 'diagnostic.error',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.restart',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async forgetDrive(): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.disk.forget',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async repairDisk(): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.disk.repair',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async systemRebuild(): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.rebuild',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async getLogs(params: ServerLogsReq): Promise<LogsRes> {
|
||||
return this.rpcRequest<LogsRes>({
|
||||
method: 'diagnostic.logs',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
|
||||
const res = await this.http.rpcRequest<T>(opts)
|
||||
|
||||
const rpcRes = res.body
|
||||
|
||||
if (isRpcError(rpcRes)) {
|
||||
throw new RpcError(rpcRes.error)
|
||||
}
|
||||
|
||||
return rpcRes.result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { pauseFor } from '@start9labs/shared'
|
||||
import { ApiService, GetErrorRes } from './api.service'
|
||||
import { LogsRes, ServerLogsReq, Log } from '@start9labs/shared'
|
||||
|
||||
@Injectable()
|
||||
export class MockApiService implements ApiService {
|
||||
async getError(): Promise<GetErrorRes> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
code: 15,
|
||||
message: 'Unknown server',
|
||||
data: { details: 'Some details about the error here' },
|
||||
}
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async forgetDrive(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async repairDisk(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async systemRebuild(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async getLogs(params: ServerLogsReq): Promise<LogsRes> {
|
||||
await pauseFor(1000)
|
||||
let entries: Log[]
|
||||
if (Math.random() < 0.2) {
|
||||
entries = packageLogs
|
||||
} else {
|
||||
const arrLength = params.limit
|
||||
? Math.ceil(params.limit / packageLogs.length)
|
||||
: 10
|
||||
entries = new Array(arrLength)
|
||||
.fill(packageLogs)
|
||||
.reduce((acc, val) => acc.concat(val), [])
|
||||
}
|
||||
return {
|
||||
entries,
|
||||
'start-cursor': 'startCursor',
|
||||
'end-cursor': 'endCursor',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const packageLogs = [
|
||||
{
|
||||
timestamp: '2019-12-26T14:20:30.872Z',
|
||||
message: '****** START *****',
|
||||
},
|
||||
{
|
||||
timestamp: '2019-12-26T14:21:30.872Z',
|
||||
message: 'ServerLogs ServerLogs ServerLogs ServerLogs ServerLogs',
|
||||
},
|
||||
{
|
||||
timestamp: '2019-12-26T14:22:30.872Z',
|
||||
message: '****** FINISH *****',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
}
|
||||
16
web/projects/diagnostic-ui/src/environments/environment.ts
Normal file
16
web/projects/diagnostic-ui/src/environments/environment.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
}
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
||||
23
web/projects/diagnostic-ui/src/index.html
Normal file
23
web/projects/diagnostic-ui/src/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>StartOS Diagnostic UI</title>
|
||||
|
||||
<base href="/" />
|
||||
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
|
||||
<link rel="icon" type="image/png" href="assets/icon/favicon.ico" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
12
web/projects/diagnostic-ui/src/main.ts
Normal file
12
web/projects/diagnostic-ui/src/main.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { enableProdMode } from '@angular/core'
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
|
||||
import { AppModule } from './app/app.module'
|
||||
import { environment } from './environments/environment'
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode()
|
||||
}
|
||||
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err))
|
||||
64
web/projects/diagnostic-ui/src/polyfills.ts
Normal file
64
web/projects/diagnostic-ui/src/polyfills.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
*/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
import './zone-flags'
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone' // Included with Angular CLI.
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
41
web/projects/diagnostic-ui/src/styles.scss
Normal file
41
web/projects/diagnostic-ui/src/styles.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src: url('/assets/fonts/Montserrat/Montserrat-Regular.ttf');
|
||||
}
|
||||
|
||||
/** Ionic CSS Variables overrides **/
|
||||
:root {
|
||||
--ion-font-family: 'Montserrat';
|
||||
|
||||
--ion-color-primary: #0075e1;
|
||||
|
||||
--ion-color-medium: #989aa2;
|
||||
--ion-color-medium-rgb: 152,154,162;
|
||||
--ion-color-medium-contrast: #000000;
|
||||
--ion-color-medium-contrast-rgb: 0,0,0;
|
||||
--ion-color-medium-shade: #86888f;
|
||||
--ion-color-medium-tint: #a2a4ab;
|
||||
|
||||
--ion-color-light: #222428;
|
||||
--ion-color-light-rgb: 34,36,40;
|
||||
--ion-color-light-contrast: #ffffff;
|
||||
--ion-color-light-contrast-rgb: 255,255,255;
|
||||
--ion-color-light-shade: #1e2023;
|
||||
--ion-color-light-tint: #383a3e;
|
||||
|
||||
--ion-item-background: #2b2b2b;
|
||||
--ion-toolbar-background: #2b2b2b;
|
||||
--ion-card-background: #2b2b2b;
|
||||
|
||||
--ion-background-color: #282828;
|
||||
--ion-background-color-rgb: 30,30,30;
|
||||
--ion-text-color: var(--ion-color-dark);
|
||||
--ion-text-color-rgb: var(--ion-color-dark-rgb);
|
||||
}
|
||||
|
||||
.loader {
|
||||
--spinner-color: var(--ion-color-warning) !important;
|
||||
z-index: 40000 !important;
|
||||
}
|
||||
6
web/projects/diagnostic-ui/src/zone-flags.ts
Normal file
6
web/projects/diagnostic-ui/src/zone-flags.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Prevents Angular change detection from
|
||||
* running with certain Web Component callbacks
|
||||
*/
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
(window as any).__Zone_disable_customElements = true
|
||||
9
web/projects/diagnostic-ui/tsconfig.json
Normal file
9
web/projects/diagnostic-ui/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./"
|
||||
},
|
||||
"files": ["src/main.ts", "src/polyfills.ts"],
|
||||
"include": ["src/**/*.d.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user