feat: move all frontend projects under the same Angular workspace (#1141)

* feat: move all frontend projects under the same Angular workspace

* Refactor/angular workspace (#1154)

* update frontend build steps

Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
This commit is contained in:
Aiden McClelland
2022-01-31 14:01:33 -07:00
committed by GitHub
parent 7e6c852ebd
commit 574539faec
504 changed files with 11569 additions and 78972 deletions

View File

@@ -0,0 +1,25 @@
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 { }

View File

@@ -0,0 +1,3 @@
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core'
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
constructor() {}
}

View File

@@ -0,0 +1,45 @@
import { ErrorHandler, NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { RouteReuseStrategy } from '@angular/router'
import { IonicModule, IonicRouteStrategy } from '@ionic/angular'
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 { HttpService } from './services/http.service'
import { GlobalErrorHandler } from './services/global-error-handler.service'
import { WorkspaceConfig } from '@shared'
const { useMocks } = require('../../../../config.json') as WorkspaceConfig
@NgModule({
declarations: [AppComponent],
entryComponents: [],
imports: [
HttpClientModule,
BrowserModule,
IonicModule.forRoot({
mode: 'md',
}),
AppRoutingModule,
],
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{
provide: ApiService,
useFactory: (http: HttpService) => {
if (useMocks) {
return new MockApiService()
} else {
return new LiveApiService(http)
}
},
deps: [HttpService],
},
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

@@ -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 {}

View File

@@ -0,0 +1,18 @@
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 {}

View File

@@ -0,0 +1,34 @@
<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);">EmbassyOS - Diagnostic Mode</h1>
<h2 style="padding-bottom: 16px; font-size: calc(1vw + 14px); font-weight: bold;">EmbassyOS launch error:</h2>
<div class="code-block">
<code><ion-text color="warning">{{ error.problem }}</ion-text></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 solution:</h2>
<div class="code-block">
<code><ion-text color="success">{{ error.solution }}</ion-text></code>
</div>
<ion-button (click)="restart()">
Restart Embassy
</ion-button>
<div *ngIf="error.code === 15 || error.code === 25" class="ion-padding-top">
<ion-button *ngIf="error.code === 15" (click)="forgetDrive()">
{{ error.code === 15 ? 'Setup Current Drive' : 'Enter Recovery Mode' }}
</ion-button>
</div>
</ng-container>
<ng-template #refresh>
<h1 class="ion-text-center" style="padding-bottom: 36px; font-size: calc(2vw + 12px);">Embassy is restarting</h1>
<h2 style="padding-bottom: 16px; font-size: calc(1vw + 12px);">Wait for Embassy restart, then refresh this page or click REFRESH below.</h2>
<ion-button (click)="refreshPage()">
Refresh
</ion-button>
</ng-template>
</div>
</ion-content>

View File

@@ -0,0 +1,5 @@
.code-block {
background-color: rgb(69, 69, 69);
padding: 12px;
margin-bottom: 32px;
}

View File

@@ -0,0 +1,98 @@
import { Component } from '@angular/core'
import { 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
} = { } as any
solutions: string[] = []
restarted = false
constructor (
private readonly loadingCtrl: LoadingController,
private readonly api: ApiService,
) { }
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 EMBASSY below. To use the current storage drive, click USE CURRENT DRIVE below, then follow instructions. No data will be erased during this process.'
}
// no drive
} else if (error.code === 20) {
this.error = {
code: 20,
problem: 'Storage drive not found',
solution: 'Insert your EmbassyOS storage drive and click RESTART EMBASSY below.'
}
// drive corrupted
} else if (error.code === 25) {
this.error = {
code: 25,
problem: 'Storage drive corrupted. This could be the result of data corruption or a 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.'
}
} else {
this.error = {
code: error.code,
problem: error.message,
solution: 'Please conact support.'
}
}
} catch (e) {
console.error(e)
}
}
async restart (): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
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({
spinner: 'lines',
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()
}
}
refreshPage (): void {
window.location.reload()
}
}

View File

@@ -0,0 +1,22 @@
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 { }

View File

@@ -0,0 +1,47 @@
<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"
(ionScroll)="scrollEvent()"
style="height: 100%;"
id="ion-content"
class="ion-padding"
>
<ion-infinite-scroll id="scroller" *ngIf="!loading && needInfinite" position="top" threshold="0" (ionInfinite)="loadData($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="button-div" *ngIf="!loading" style="width: 100%; text-align: center;">
<ion-button *ngIf="!loadingMore" (click)="loadMore()" strong color="dark">
Load More
<ion-icon slot="end" name="refresh"></ion-icon>
</ion-button>
<ion-spinner *ngIf="loadingMore" name="lines" color="warning"></ion-spinner>
</div>
<div
*ngIf="!loading"
[ngStyle]="{
'position': 'fixed',
'bottom': '50px',
'right': isOnBottom ? '-52px' : '30px',
'border-radius': '100%',
'transition': 'right 0.4s 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>

View File

@@ -0,0 +1,113 @@
import { Component, ViewChild } from '@angular/core'
import { IonContent } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
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
loadingMore = false
logs: string
needInfinite = true
startCursor: string
endCursor: string
limit = 200
scrollToBottomButton = false
isOnBottom = true
constructor (
private readonly api: ApiService,
) { }
ngOnInit () {
this.getLogs()
}
async getLogs () {
try {
// get logs
const logs = await this.fetch()
if (!logs.length) return
const container = document.getElementById('container')
const beforeContainerHeight = container.scrollHeight
const newLogs = document.getElementById('template').cloneNode(true) as HTMLElement
newLogs.innerHTML = logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') + (logs.length ? '\n' : '')
container.prepend(newLogs)
const afterContainerHeight = container.scrollHeight
// scroll down
scrollBy(0, afterContainerHeight - beforeContainerHeight)
this.content.scrollToPoint(0, afterContainerHeight - beforeContainerHeight)
if (logs.length < this.limit) {
this.needInfinite = false
}
} catch (e) { }
}
async fetch (isBefore: boolean = true) {
try {
const cursor = isBefore ? this.startCursor : this.endCursor
const logsRes = await this.api.getLogs({
cursor,
before_flag: !!cursor ? isBefore : undefined,
limit: this.limit,
})
if ((isBefore || this.startCursor) && logsRes['start-cursor']) {
this.startCursor = logsRes['start-cursor']
}
if ((!isBefore || !this.endCursor) && logsRes['end-cursor']) {
this.endCursor = logsRes['end-cursor']
}
this.loading = false
return logsRes.entries
} catch (e) {
console.error(e)
}
}
async loadMore () {
try {
this.loadingMore = true
const logs = await this.fetch(false)
if (!logs.length) return this.loadingMore = false
const container = document.getElementById('container')
const newLogs = document.getElementById('template').cloneNode(true) as HTMLElement
newLogs.innerHTML = logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') + (logs.length ? '\n' : '')
container.append(newLogs)
this.loadingMore = false
this.scrollEvent()
} catch (e) { }
}
scrollEvent () {
const buttonDiv = document.getElementById('button-div')
this.isOnBottom = buttonDiv.getBoundingClientRect().top < window.innerHeight
}
scrollToBottom () {
this.content.scrollToBottom(500)
}
async loadData (e: any): Promise<void> {
await this.getLogs()
e.target.complete()
}
}

View File

@@ -0,0 +1,22 @@
export abstract class ApiService {
abstract getError (): Promise<GetErrorRes>
abstract restart (): Promise<void>
abstract forgetDrive (): Promise<void>
abstract getLogs (params: GetLogsReq): Promise<GetLogsRes>
}
export interface GetErrorRes {
code: number,
message: string,
data: { details: string }
}
export type GetLogsReq = { cursor?: string, before_flag?: boolean, limit?: number }
export type GetLogsRes = LogsRes
export type LogsRes = { entries: Log[], 'start-cursor'?: string, 'end-cursor'?: string }
export interface Log {
timestamp: string
message: string
}

View File

@@ -0,0 +1,39 @@
import { Injectable } from "@angular/core"
import { HttpService } from "../http.service"
import { ApiService, GetErrorRes, GetLogsReq, GetLogsRes } from "./api.service"
@Injectable()
export class LiveApiService extends ApiService {
constructor (
private readonly http: HttpService,
) { super() }
getError (): Promise<GetErrorRes> {
return this.http.rpcRequest<GetErrorRes>({
method: 'diagnostic.error',
params: { },
})
}
restart (): Promise<void> {
return this.http.rpcRequest<void>({
method: 'diagnostic.restart',
params: { },
})
}
forgetDrive (): Promise<void> {
return this.http.rpcRequest<void>({
method: 'diagnostic.forget-disk',
params: { },
})
}
getLogs (params: GetLogsReq): Promise<GetLogsRes> {
return this.http.rpcRequest<GetLogsRes>({
method: 'diagnostic.logs',
params,
})
}
}

View File

@@ -0,0 +1,59 @@
import { Injectable } from "@angular/core"
import { pauseFor } from "../../util/misc.util"
import { ApiService, GetErrorRes, GetLogsReq, GetLogsRes, Log } from "./api.service"
@Injectable()
export class MockApiService extends ApiService {
constructor () { super() }
async getError (): Promise<GetErrorRes> {
await pauseFor(1000)
return {
code: 15,
message: 'Unknown Embassy',
data: { details: 'Some details about the error here' }
}
}
async restart (): Promise<void> {
await pauseFor(1000)
return null
}
async forgetDrive (): Promise<void> {
await pauseFor(1000)
return null
}
async getLogs (params: GetLogsReq): Promise<GetLogsRes> {
await pauseFor(1000)
let entries: Log[]
if (Math.random() < .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 *****',
},
]

View File

@@ -0,0 +1,13 @@
import { ErrorHandler, Injectable } from '@angular/core'
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
handleError (error: any): void {
const chunkFailedMessage = /Loading chunk [\d]+ failed/
if (chunkFailedMessage.test(error.message)) {
window.location.reload()
}
}
}

View File

@@ -0,0 +1,97 @@
import { Injectable } from '@angular/core'
import { HttpClient, HttpErrorResponse } from '@angular/common/http'
@Injectable({
providedIn: 'root',
})
export class HttpService {
constructor (
private readonly http: HttpClient,
) { }
async rpcRequest<T> (options: RPCOptions): Promise<T> {
const res = await this.httpRequest<RPCResponse<T>>(options)
if (isRpcError(res)) throw new RpcError(res.error)
if (isRpcSuccess(res)) return res.result
}
async httpRequest<T> (body: RPCOptions): Promise<T> {
const url = `${window.location.protocol}//${window.location.hostname}:${window.location.port}/rpc/v1`
return this.http.post(url, body)
.toPromise().then(a => a as T)
.catch(e => { throw new HttpError(e) })
}
}
function RpcError (e: RPCError['error']): void {
const { code, message, data } = e
this.code = code
this.message = message
if (typeof data === 'string') {
this.details = e.data
this.revision = null
} else {
this.details = data.details
}
}
function HttpError (e: HttpErrorResponse): void {
const { status, statusText } = e
this.code = status
this.message = statusText
this.details = null
this.revision = null
}
function isRpcError<Error, Result> (arg: { error: Error } | { result: Result}): arg is { error: Error } {
return !!(arg as any).error
}
function isRpcSuccess<Error, Result> (arg: { error: Error } | { result: Result}): arg is { result: Result } {
return !!(arg as any).result
}
export interface RPCOptions {
method: string
params: { [param: string]: Params }
}
export interface RequestError {
code: number
message: string
details: string
}
export type Params = string | number | boolean | object | string[] | number[]
interface RPCBase {
jsonrpc: '2.0'
id: string
}
export interface RPCRequest<T> extends RPCBase {
method: string
params?: T
}
export interface RPCSuccess<T> extends RPCBase {
result: T
}
export interface RPCError extends RPCBase {
error: {
code: number,
message: string
data?: {
details: string
} | string
}
}
export type RPCResponse<T> = RPCSuccess<T> | RPCError
type HttpError = HttpErrorResponse & { error: { code: string, message: string } }

View File

@@ -0,0 +1,3 @@
export function pauseFor (ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}