mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
feat(portal): add notifications sidebar (#2516)
* feat(portal): add notifications sidebar * chore: add service * chore: simplify style * chore: fix comments * WIP, moving notifications to patch-db * revamp notifications * chore: small adjustments --------- Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
62
web/package-lock.json
generated
62
web/package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.3.5.1",
|
"version": "0.3.5.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^16.1.4",
|
"@angular/animations": "^16.1.4",
|
||||||
|
"@angular/cdk": "^16.1.4",
|
||||||
"@angular/common": "^16.1.4",
|
"@angular/common": "^16.1.4",
|
||||||
"@angular/compiler": "^16.1.4",
|
"@angular/compiler": "^16.1.4",
|
||||||
"@angular/core": "^16.1.4",
|
"@angular/core": "^16.1.4",
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
"@start9labs/emver": "^0.1.5",
|
"@start9labs/emver": "^0.1.5",
|
||||||
"@start9labs/start-sdk": "0.4.0-rev0.lib0.rc8.beta2",
|
"@start9labs/start-sdk": "0.4.0-rev0.lib0.rc8.beta2",
|
||||||
"@taiga-ui/addon-charts": "3.53.0",
|
"@taiga-ui/addon-charts": "3.53.0",
|
||||||
|
"@taiga-ui/addon-mobile": "3.53.0",
|
||||||
"@taiga-ui/cdk": "3.53.0",
|
"@taiga-ui/cdk": "3.53.0",
|
||||||
"@taiga-ui/core": "3.53.0",
|
"@taiga-ui/core": "3.53.0",
|
||||||
"@taiga-ui/experimental": "3.53.0",
|
"@taiga-ui/experimental": "3.53.0",
|
||||||
@@ -31,6 +33,7 @@
|
|||||||
"@taiga-ui/kit": "3.53.0",
|
"@taiga-ui/kit": "3.53.0",
|
||||||
"@taiga-ui/styles": "3.53.0",
|
"@taiga-ui/styles": "3.53.0",
|
||||||
"@tinkoff/ng-dompurify": "4.0.0",
|
"@tinkoff/ng-dompurify": "4.0.0",
|
||||||
|
"@tinkoff/ng-event-plugins": "3.1.0",
|
||||||
"ansi-to-html": "^0.7.2",
|
"ansi-to-html": "^0.7.2",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
||||||
@@ -440,6 +443,46 @@
|
|||||||
"@angular/core": "16.2.12"
|
"@angular/core": "16.2.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular/cdk": {
|
||||||
|
"version": "16.2.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.12.tgz",
|
||||||
|
"integrity": "sha512-wT8/265zm2WKY0BDaRoYbrAT4kadrmejTRLjuimQIEUKnw4vBsJMWCwQkpFo3s6zr6eznGqYVAFb8KKPVLKGBg==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"parse5": "^7.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": "^16.0.0 || ^17.0.0",
|
||||||
|
"@angular/core": "^16.0.0 || ^17.0.0",
|
||||||
|
"rxjs": "^6.5.3 || ^7.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@angular/cdk/node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@angular/cdk/node_modules/parse5": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@angular/cli": {
|
"node_modules/@angular/cli": {
|
||||||
"version": "16.2.10",
|
"version": "16.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.10.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.10.tgz",
|
||||||
@@ -4312,6 +4355,25 @@
|
|||||||
"rxjs": ">=6.0.0"
|
"rxjs": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@taiga-ui/addon-mobile": {
|
||||||
|
"version": "3.53.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-3.53.0.tgz",
|
||||||
|
"integrity": "sha512-MHfG7I488fyT5NCOvQvCCgCMzIcYFO0MgfNGowcm/nyVaXcTzINF2JWMmdSbMJHCVBFy5xl2CQl0kjOFLYYg9g==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": ">=2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/cdk": ">=12.0.0",
|
||||||
|
"@angular/common": ">=12.0.0",
|
||||||
|
"@angular/core": ">=12.0.0",
|
||||||
|
"@ng-web-apis/common": ">=3.0.0",
|
||||||
|
"@taiga-ui/cdk": ">=3.53.0",
|
||||||
|
"@taiga-ui/core": ">=3.53.0",
|
||||||
|
"@taiga-ui/kit": ">=3.53.0",
|
||||||
|
"@tinkoff/ng-polymorpheus": ">=4.0.0",
|
||||||
|
"rxjs": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@taiga-ui/cdk": {
|
"node_modules/@taiga-ui/cdk": {
|
||||||
"version": "3.53.0",
|
"version": "3.53.0",
|
||||||
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.53.0.tgz",
|
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.53.0.tgz",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"check:install": "tsc --project projects/install-wizard/tsconfig.json --noEmit --skipLibCheck",
|
"check:install": "tsc --project projects/install-wizard/tsconfig.json --noEmit --skipLibCheck",
|
||||||
"check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
|
"check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
|
||||||
"check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck",
|
"check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck",
|
||||||
"build:deps": "rm -rf .angular/cache && cd ../patch-db/client && npm ci && npm run build",
|
"build:deps": "npx rimraf .angular/cache && cd ../patch-db/client && npm ci && npm run build",
|
||||||
"build:install": "ng run install-wizard:build",
|
"build:install": "ng run install-wizard:build",
|
||||||
"build:setup": "ng run setup-wizard:build",
|
"build:setup": "ng run setup-wizard:build",
|
||||||
"build:ui": "ng run ui:build",
|
"build:ui": "ng run ui:build",
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^16.1.4",
|
"@angular/animations": "^16.1.4",
|
||||||
|
"@angular/cdk": "^16.1.4",
|
||||||
"@angular/common": "^16.1.4",
|
"@angular/common": "^16.1.4",
|
||||||
"@angular/compiler": "^16.1.4",
|
"@angular/compiler": "^16.1.4",
|
||||||
"@angular/core": "^16.1.4",
|
"@angular/core": "^16.1.4",
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"@start9labs/emver": "^0.1.5",
|
"@start9labs/emver": "^0.1.5",
|
||||||
"@start9labs/start-sdk": "0.4.0-rev0.lib0.rc8.beta2",
|
"@start9labs/start-sdk": "0.4.0-rev0.lib0.rc8.beta2",
|
||||||
"@taiga-ui/addon-charts": "3.53.0",
|
"@taiga-ui/addon-charts": "3.53.0",
|
||||||
|
"@taiga-ui/addon-mobile": "3.53.0",
|
||||||
"@taiga-ui/cdk": "3.53.0",
|
"@taiga-ui/cdk": "3.53.0",
|
||||||
"@taiga-ui/core": "3.53.0",
|
"@taiga-ui/core": "3.53.0",
|
||||||
"@taiga-ui/experimental": "3.53.0",
|
"@taiga-ui/experimental": "3.53.0",
|
||||||
@@ -53,6 +55,7 @@
|
|||||||
"@taiga-ui/kit": "3.53.0",
|
"@taiga-ui/kit": "3.53.0",
|
||||||
"@taiga-ui/styles": "3.53.0",
|
"@taiga-ui/styles": "3.53.0",
|
||||||
"@tinkoff/ng-dompurify": "4.0.0",
|
"@tinkoff/ng-dompurify": "4.0.0",
|
||||||
|
"@tinkoff/ng-event-plugins": "3.1.0",
|
||||||
"ansi-to-html": "^0.7.2",
|
"ansi-to-html": "^0.7.2",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
StartOSDiskInfo,
|
StartOSDiskInfo,
|
||||||
Log,
|
Log,
|
||||||
SetupStatus,
|
SetupStatus,
|
||||||
|
FollowLogsRes,
|
||||||
|
FollowLogsReq,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||||
@@ -19,7 +21,7 @@ export abstract class ApiService {
|
|||||||
abstract execute(setupInfo: ExecuteReq): Promise<void> // setup.execute
|
abstract execute(setupInfo: ExecuteReq): Promise<void> // setup.execute
|
||||||
abstract complete(): Promise<CompleteRes> // setup.complete
|
abstract complete(): Promise<CompleteRes> // setup.complete
|
||||||
abstract exit(): Promise<void> // setup.exit
|
abstract exit(): Promise<void> // setup.exit
|
||||||
abstract followLogs(): Promise<string> // setup.logs.follow
|
abstract followServerLogs(params: FollowLogsReq): Promise<FollowLogsRes> // setup.logs.follow
|
||||||
abstract openLogsWebsocket$(
|
abstract openLogsWebsocket$(
|
||||||
config: WebSocketSubjectConfig<Log>,
|
config: WebSocketSubjectConfig<Log>,
|
||||||
): Observable<Log>
|
): Observable<Log>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
RpcError,
|
RpcError,
|
||||||
RPCOptions,
|
RPCOptions,
|
||||||
SetupStatus,
|
SetupStatus,
|
||||||
|
FollowLogsRes,
|
||||||
|
FollowLogsReq,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
ApiService,
|
ApiService,
|
||||||
@@ -90,8 +92,8 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async followLogs(): Promise<string> {
|
async followServerLogs(params: FollowLogsReq): Promise<FollowLogsRes> {
|
||||||
return this.rpcRequest({ method: 'setup.logs.follow', params: {} })
|
return this.rpcRequest({ method: 'setup.logs.follow', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
openLogsWebsocket$({ url }: WebSocketSubjectConfig<Log>): Observable<Log> {
|
openLogsWebsocket$({ url }: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import {
|
import {
|
||||||
encodeBase64,
|
encodeBase64,
|
||||||
|
FollowLogsReq,
|
||||||
|
FollowLogsRes,
|
||||||
getSetupStatusMock,
|
getSetupStatusMock,
|
||||||
Log,
|
Log,
|
||||||
pauseFor,
|
pauseFor,
|
||||||
@@ -134,9 +136,12 @@ export class MockApiService extends ApiService {
|
|||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
async followLogs(): Promise<string> {
|
async followServerLogs(params: FollowLogsReq): Promise<FollowLogsRes> {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
return 'fake-guid'
|
return {
|
||||||
|
'start-cursor': 'fakestartcursor',
|
||||||
|
guid: 'fake-guid',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { StaticClassProvider } from '@angular/core'
|
import { StaticClassProvider } from '@angular/core'
|
||||||
import { defer, Observable, switchMap } from 'rxjs'
|
import { defer, Observable, switchMap } from 'rxjs'
|
||||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||||
import { Log } from '../types/api'
|
import { FollowLogsReq, FollowLogsRes, Log } from '../types/api'
|
||||||
import { Constructor } from '../types/constructor'
|
import { Constructor } from '../types/constructor'
|
||||||
|
|
||||||
interface Api {
|
interface Api {
|
||||||
followLogs: () => Promise<string>
|
followServerLogs: (params: FollowLogsReq) => Promise<FollowLogsRes>
|
||||||
openLogsWebsocket$: (config: WebSocketSubjectConfig<Log>) => Observable<Log>
|
openLogsWebsocket$: (config: WebSocketSubjectConfig<Log>) => Observable<Log>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,8 +20,10 @@ export function provideSetupLogsService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SetupLogsService extends Observable<Log> {
|
export class SetupLogsService extends Observable<Log> {
|
||||||
private readonly log$ = defer(() => this.api.followLogs()).pipe(
|
private readonly log$ = defer(() => this.api.followServerLogs({})).pipe(
|
||||||
switchMap(url => this.api.openLogsWebsocket$({ url })),
|
switchMap(({ guid }) =>
|
||||||
|
this.api.openLogsWebsocket$({ url: `/rpc/${guid}` }),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
constructor(private readonly api: Api) {
|
constructor(private readonly api: Api) {
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
export type ServerLogsReq = {
|
export type FollowLogsReq = {}
|
||||||
|
export type FollowLogsRes = {
|
||||||
|
'start-cursor': string
|
||||||
|
guid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FetchLogsReq = {
|
||||||
before: boolean
|
before: boolean
|
||||||
cursor?: string
|
cursor?: string
|
||||||
limit?: number
|
limit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LogsRes = {
|
export type FetchLogsRes = {
|
||||||
entries: Log[]
|
entries: Log[]
|
||||||
'start-cursor'?: string
|
'start-cursor'?: string
|
||||||
'end-cursor'?: string
|
'end-cursor'?: string
|
||||||
|
|||||||
@@ -96,3 +96,8 @@ tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[tuiSidebar] > div.t-wrapper {
|
||||||
|
backdrop-filter: blur(1rem);
|
||||||
|
background: rgb(34 34 34 / 80%);
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
class="left-menu"
|
class="left-menu"
|
||||||
>
|
>
|
||||||
<ion-content color="light" scrollY="false" class="menu">
|
<ion-content color="light" scrollY="false" class="menu">
|
||||||
<app-menu *ngIf="authService.isVerified$ | async"></app-menu>
|
<app-menu *ngIf="authService.isVerified$ | async" />
|
||||||
</ion-content>
|
</ion-content>
|
||||||
</ion-menu>
|
</ion-menu>
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
(click)="onResize(drawer)"
|
(click)="onResize(drawer)"
|
||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
<widgets *ngIf="drawer.open" [wide]="drawer.width === 600"></widgets>
|
<widgets *ngIf="drawer.open" [wide]="drawer.width === 600" />
|
||||||
</ion-menu>
|
</ion-menu>
|
||||||
|
|
||||||
<ion-router-outlet
|
<ion-router-outlet
|
||||||
@@ -68,22 +68,23 @@
|
|||||||
!(sidebarOpen$ | async)
|
!(sidebarOpen$ | async)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<connection-bar></connection-bar>
|
<connection-bar />
|
||||||
</ion-footer>
|
</ion-footer>
|
||||||
<toast-container></toast-container>
|
<toast-container />
|
||||||
</ion-app>
|
</ion-app>
|
||||||
|
<sidebar-host ngProjectAs="tuiOverContent" />
|
||||||
</tui-root>
|
</tui-root>
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="authService.isVerified$ | async; else defaultTheme"
|
*ngIf="authService.isVerified$ | async; else defaultTheme"
|
||||||
[ngSwitch]="theme$ | async"
|
[ngSwitch]="theme$ | async"
|
||||||
>
|
>
|
||||||
<ng-container *ngSwitchCase="'Dark'">
|
<ng-container *ngSwitchCase="'Dark'">
|
||||||
<tui-theme-night></tui-theme-night>
|
<tui-theme-night />
|
||||||
<dark-theme></dark-theme>
|
<dark-theme />
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<light-theme *ngSwitchCase="'Light'"></light-theme>
|
<light-theme *ngSwitchCase="'Light'" />
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #defaultTheme>
|
<ng-template #defaultTheme>
|
||||||
<tui-theme-night></tui-theme-night>
|
<tui-theme-night />
|
||||||
<dark-theme></dark-theme>
|
<dark-theme />
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { ConnectionBarComponentModule } from './app/connection-bar/connection-ba
|
|||||||
import { WidgetsPageModule } from 'src/app/apps/ui/pages/widgets/widgets.module'
|
import { WidgetsPageModule } from 'src/app/apps/ui/pages/widgets/widgets.module'
|
||||||
import { ServiceWorkerModule } from '@angular/service-worker'
|
import { ServiceWorkerModule } from '@angular/service-worker'
|
||||||
import { environment } from '../environments/environment'
|
import { environment } from '../environments/environment'
|
||||||
|
import { SidebarHostComponent } from './app/sidebar-host.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AppComponent],
|
declarations: [AppComponent],
|
||||||
@@ -72,6 +73,7 @@ import { environment } from '../environments/environment'
|
|||||||
}),
|
}),
|
||||||
LoadingModule,
|
LoadingModule,
|
||||||
QRComponentModule,
|
QRComponentModule,
|
||||||
|
SidebarHostComponent,
|
||||||
],
|
],
|
||||||
providers: APP_PROVIDERS,
|
providers: APP_PROVIDERS,
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ export class MenuComponent {
|
|||||||
|
|
||||||
readonly notificationCount$ = this.patch.watch$(
|
readonly notificationCount$ = this.patch.watch$(
|
||||||
'server-info',
|
'server-info',
|
||||||
'unread-notification-count',
|
'unreadNotifications',
|
||||||
|
'count',
|
||||||
)
|
)
|
||||||
|
|
||||||
readonly snekScore$ = this.patch.watch$('ui', 'gaming', 'snake', 'high-score')
|
readonly snekScore$ = this.patch.watch$('ui', 'gaming', 'snake', 'high-score')
|
||||||
|
|||||||
35
web/projects/ui/src/app/app/sidebar-host.component.ts
Normal file
35
web/projects/ui/src/app/app/sidebar-host.component.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Directive,
|
||||||
|
Injectable,
|
||||||
|
} from '@angular/core'
|
||||||
|
import {
|
||||||
|
AbstractTuiPortalHostComponent,
|
||||||
|
AbstractTuiPortalService,
|
||||||
|
TuiDropdownPortalService,
|
||||||
|
} from '@taiga-ui/cdk'
|
||||||
|
|
||||||
|
@Injectable({ providedIn: `root` })
|
||||||
|
export class SidebarService extends AbstractTuiPortalService {}
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[tuiSidebar]',
|
||||||
|
standalone: true,
|
||||||
|
providers: [
|
||||||
|
{ provide: TuiDropdownPortalService, useExisting: SidebarService },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class SidebarDirective {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'sidebar-host',
|
||||||
|
template: '<ng-container #viewContainer></ng-container>',
|
||||||
|
styles: [':host { position: fixed; top: 0; }'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true,
|
||||||
|
providers: [
|
||||||
|
{ provide: AbstractTuiPortalService, useExisting: SidebarService },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class SidebarHostComponent extends AbstractTuiPortalHostComponent {}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LogsRes, ServerLogsReq } from '@start9labs/shared'
|
import { FetchLogsReq, FetchLogsRes } from '@start9labs/shared'
|
||||||
|
|
||||||
export abstract class DiagnosticService {
|
export abstract class DiagnosticService {
|
||||||
abstract getError(): Promise<GetErrorRes>
|
abstract getError(): Promise<GetErrorRes>
|
||||||
@@ -6,7 +6,7 @@ export abstract class DiagnosticService {
|
|||||||
abstract forgetDrive(): Promise<void>
|
abstract forgetDrive(): Promise<void>
|
||||||
abstract repairDisk(): Promise<void>
|
abstract repairDisk(): Promise<void>
|
||||||
abstract systemRebuild(): Promise<void>
|
abstract systemRebuild(): Promise<void>
|
||||||
abstract getLogs(params: ServerLogsReq): Promise<LogsRes>
|
abstract getLogs(params: FetchLogsReq): Promise<FetchLogsRes>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetErrorRes {
|
export interface GetErrorRes {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
RpcError,
|
RpcError,
|
||||||
RPCOptions,
|
RPCOptions,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { LogsRes, ServerLogsReq } from '@start9labs/shared'
|
import { FetchLogsReq, FetchLogsRes } from '@start9labs/shared'
|
||||||
import { DiagnosticService, GetErrorRes } from './diagnostic.service'
|
import { DiagnosticService, GetErrorRes } from './diagnostic.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -47,8 +47,8 @@ export class LiveDiagnosticService implements DiagnosticService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLogs(params: ServerLogsReq): Promise<LogsRes> {
|
async getLogs(params: FetchLogsReq): Promise<FetchLogsRes> {
|
||||||
return this.rpcRequest<LogsRes>({
|
return this.rpcRequest<FetchLogsRes>({
|
||||||
method: 'diagnostic.logs',
|
method: 'diagnostic.logs',
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { pauseFor } from '@start9labs/shared'
|
import { pauseFor } from '@start9labs/shared'
|
||||||
import { LogsRes, ServerLogsReq, Log } from '@start9labs/shared'
|
import { FetchLogsReq, FetchLogsRes, Log } from '@start9labs/shared'
|
||||||
import { DiagnosticService, GetErrorRes } from './diagnostic.service'
|
import { DiagnosticService, GetErrorRes } from './diagnostic.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -30,7 +30,7 @@ export class MockDiagnosticService implements DiagnosticService {
|
|||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLogs(params: ServerLogsReq): Promise<LogsRes> {
|
async getLogs(params: FetchLogsReq): Promise<FetchLogsRes> {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
let entries: Log[]
|
let entries: Log[]
|
||||||
if (Math.random() < 0.2) {
|
if (Math.random() < 0.2) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
empty: empty
|
empty: empty
|
||||||
"
|
"
|
||||||
appCard
|
appCard
|
||||||
[badge]="item.key | toNotifications | async"
|
[badge]="item.key | toBadge | async"
|
||||||
[drawerItem]="item.key"
|
[drawerItem]="item.key"
|
||||||
[id]="item.key"
|
[id]="item.key"
|
||||||
[title]="item.value.title"
|
[title]="item.value.title"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { ServicesService } from '../../services/services.service'
|
|||||||
import { toRouterLink } from '../../utils/to-router-link'
|
import { toRouterLink } from '../../utils/to-router-link'
|
||||||
import { DrawerItemDirective } from './drawer-item.directive'
|
import { DrawerItemDirective } from './drawer-item.directive'
|
||||||
import { SYSTEM_UTILITIES } from '../../constants/system-utilities'
|
import { SYSTEM_UTILITIES } from '../../constants/system-utilities'
|
||||||
import { ToNotificationsPipe } from '../../pipes/to-notifications'
|
import { ToBadgePipe } from '../../pipes/to-badge'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-drawer',
|
selector: 'app-drawer',
|
||||||
@@ -45,7 +45,7 @@ import { ToNotificationsPipe } from '../../pipes/to-notifications'
|
|||||||
TuiFilterPipeModule,
|
TuiFilterPipeModule,
|
||||||
CardComponent,
|
CardComponent,
|
||||||
DrawerItemDirective,
|
DrawerItemDirective,
|
||||||
ToNotificationsPipe,
|
ToBadgePipe,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DrawerComponent {
|
export class DrawerComponent {
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
|
import {
|
||||||
|
TuiDataListModule,
|
||||||
|
TuiHostedDropdownModule,
|
||||||
|
TuiSvgModule,
|
||||||
|
} from '@taiga-ui/core'
|
||||||
|
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||||
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
|
import { AuthService } from 'src/app/services/auth.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'header-menu',
|
||||||
|
template: `
|
||||||
|
<tui-hosted-dropdown
|
||||||
|
[content]="content"
|
||||||
|
[tuiDropdownMaxHeight]="9999"
|
||||||
|
(click.stop.prevent)="(0)"
|
||||||
|
(pointerdown.stop)="(0)"
|
||||||
|
>
|
||||||
|
<button tuiIconButton appearance="">
|
||||||
|
<img style="max-width: 62%" src="assets/img/icon.png" alt="StartOS" />
|
||||||
|
</button>
|
||||||
|
<ng-template #content>
|
||||||
|
<tui-data-list>
|
||||||
|
<h3 class="title">StartOS</h3>
|
||||||
|
<button tuiOption class="item" (click)="({})">
|
||||||
|
<tui-svg src="tuiIconInfo"></tui-svg>
|
||||||
|
About this server
|
||||||
|
</button>
|
||||||
|
<tui-opt-group>
|
||||||
|
<button tuiOption class="item" (click)="({})">
|
||||||
|
<tui-svg src="tuiIconBookOpen"></tui-svg>
|
||||||
|
User Manual
|
||||||
|
<tui-svg class="external" src="tuiIconArrowUpRight"></tui-svg>
|
||||||
|
</button>
|
||||||
|
<button tuiOption class="item" (click)="({})">
|
||||||
|
<tui-svg src="tuiIconHeadphones"></tui-svg>
|
||||||
|
Contact Support
|
||||||
|
<tui-svg class="external" src="tuiIconArrowUpRight"></tui-svg>
|
||||||
|
</button>
|
||||||
|
<button tuiOption class="item" (click)="({})">
|
||||||
|
<tui-svg src="tuiIconDollarSign"></tui-svg>
|
||||||
|
Donate to Start9
|
||||||
|
<tui-svg class="external" src="tuiIconArrowUpRight"></tui-svg>
|
||||||
|
</button>
|
||||||
|
</tui-opt-group>
|
||||||
|
<tui-opt-group>
|
||||||
|
<button tuiOption class="item" (click)="({})">
|
||||||
|
<tui-svg src="tuiIconTool"></tui-svg>
|
||||||
|
System Rebuild
|
||||||
|
</button>
|
||||||
|
<button tuiOption class="item" (click)="({})">
|
||||||
|
<tui-svg src="tuiIconRefreshCw"></tui-svg>
|
||||||
|
Restart
|
||||||
|
</button>
|
||||||
|
<button tuiOption class="item" (click)="({})">
|
||||||
|
<tui-svg src="tuiIconPower"></tui-svg>
|
||||||
|
Shutdown
|
||||||
|
</button>
|
||||||
|
</tui-opt-group>
|
||||||
|
<tui-opt-group>
|
||||||
|
<button tuiOption class="item" (click)="logout()">
|
||||||
|
<tui-svg src="tuiIconLogOut"></tui-svg>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</tui-opt-group>
|
||||||
|
</tui-data-list>
|
||||||
|
</ng-template>
|
||||||
|
</tui-hosted-dropdown>
|
||||||
|
`,
|
||||||
|
styles: [
|
||||||
|
`
|
||||||
|
.item {
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0.5rem 0.25rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
font: var(--tui-font-text-l);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external {
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
standalone: true,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
TuiHostedDropdownModule,
|
||||||
|
TuiDataListModule,
|
||||||
|
TuiSvgModule,
|
||||||
|
TuiButtonModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class HeaderMenuComponent {
|
||||||
|
private readonly api = inject(ApiService)
|
||||||
|
private readonly auth = inject(AuthService)
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.api.logout({}).catch(e => console.error('Failed to log out', e))
|
||||||
|
this.auth.setUnverified()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
<tui-hosted-dropdown
|
|
||||||
[content]="content"
|
|
||||||
[tuiDropdownMaxHeight]="9999"
|
|
||||||
(click.stop.prevent)="(0)"
|
|
||||||
(pointerdown.stop)="(0)"
|
|
||||||
>
|
|
||||||
<button tuiIconButton appearance="">
|
|
||||||
<img style="max-width: 62%" src="assets/img/icon.png" alt="StartOS" />
|
|
||||||
</button>
|
|
||||||
<ng-template #content>
|
|
||||||
<tui-data-list>
|
|
||||||
<h3 class="title">StartOS</h3>
|
|
||||||
<button tuiOption class="item" (click)="({})">
|
|
||||||
<tui-svg src="tuiIconInfo"></tui-svg>
|
|
||||||
About this server
|
|
||||||
</button>
|
|
||||||
<tui-opt-group>
|
|
||||||
<button tuiOption class="item" (click)="({})">
|
|
||||||
<tui-svg src="tuiIconBookOpen"></tui-svg>
|
|
||||||
User Manual
|
|
||||||
<tui-svg class="external" src="tuiIconArrowUpRight"></tui-svg>
|
|
||||||
</button>
|
|
||||||
<button tuiOption class="item" (click)="({})">
|
|
||||||
<tui-svg src="tuiIconHeadphones"></tui-svg>
|
|
||||||
Contact Support
|
|
||||||
<tui-svg class="external" src="tuiIconArrowUpRight"></tui-svg>
|
|
||||||
</button>
|
|
||||||
<button tuiOption class="item" (click)="({})">
|
|
||||||
<tui-svg src="tuiIconDollarSign"></tui-svg>
|
|
||||||
Donate to Start9
|
|
||||||
<tui-svg class="external" src="tuiIconArrowUpRight"></tui-svg>
|
|
||||||
</button>
|
|
||||||
</tui-opt-group>
|
|
||||||
<tui-opt-group>
|
|
||||||
<button tuiOption class="item" (click)="({})">
|
|
||||||
<tui-svg src="tuiIconTool"></tui-svg>
|
|
||||||
System Rebuild
|
|
||||||
</button>
|
|
||||||
<button tuiOption class="item" (click)="({})">
|
|
||||||
<tui-svg src="tuiIconRefreshCw"></tui-svg>
|
|
||||||
Restart
|
|
||||||
</button>
|
|
||||||
<button tuiOption class="item" (click)="({})">
|
|
||||||
<tui-svg src="tuiIconPower"></tui-svg>
|
|
||||||
Shutdown
|
|
||||||
</button>
|
|
||||||
</tui-opt-group>
|
|
||||||
<tui-opt-group>
|
|
||||||
<button tuiOption class="item" (click)="logout()">
|
|
||||||
<tui-svg src="tuiIconLogOut"></tui-svg>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</tui-opt-group>
|
|
||||||
</tui-data-list>
|
|
||||||
</ng-template>
|
|
||||||
</tui-hosted-dropdown>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
.item {
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 0.5rem 0.25rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
font: var(--tui-font-text-l);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.external {
|
|
||||||
margin-left: auto;
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
|
||||||
import {
|
|
||||||
TuiDataListModule,
|
|
||||||
TuiHostedDropdownModule,
|
|
||||||
TuiSvgModule,
|
|
||||||
} from '@taiga-ui/core'
|
|
||||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|
||||||
import { AuthService } from 'src/app/services/auth.service'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'header-menu',
|
|
||||||
templateUrl: 'header-menu.component.html',
|
|
||||||
styleUrls: ['header-menu.component.scss'],
|
|
||||||
standalone: true,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [
|
|
||||||
TuiHostedDropdownModule,
|
|
||||||
TuiDataListModule,
|
|
||||||
TuiSvgModule,
|
|
||||||
TuiButtonModule,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class HeaderMenuComponent {
|
|
||||||
private readonly api = inject(ApiService)
|
|
||||||
private readonly auth = inject(AuthService)
|
|
||||||
|
|
||||||
logout() {
|
|
||||||
this.api.logout({}).catch(e => console.error('Failed to log out', e))
|
|
||||||
this.auth.setUnverified()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
inject,
|
||||||
|
Input,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { TuiSvgModule } from '@taiga-ui/core'
|
||||||
|
import { TuiButtonModule, TuiTitleModule } from '@taiga-ui/experimental'
|
||||||
|
import { TuiLineClampModule } from '@taiga-ui/kit'
|
||||||
|
import { ServerNotification } from 'src/app/services/api/api.types'
|
||||||
|
import { NotificationService } from '../../services/notification.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'header-notification',
|
||||||
|
template: `
|
||||||
|
<tui-svg
|
||||||
|
style="align-self: flex-start; margin: 0.25rem 0;"
|
||||||
|
[style.color]="color"
|
||||||
|
[src]="icon"
|
||||||
|
></tui-svg>
|
||||||
|
<div tuiTitle>
|
||||||
|
<div tuiSubtitle><ng-content></ng-content></div>
|
||||||
|
<div [style.color]="color">
|
||||||
|
{{ notification.title }}
|
||||||
|
</div>
|
||||||
|
<tui-line-clamp
|
||||||
|
tuiSubtitle
|
||||||
|
style="pointer-events: none"
|
||||||
|
[linesLimit]="4"
|
||||||
|
[lineHeight]="16"
|
||||||
|
[content]="notification.message"
|
||||||
|
(overflownChange)="overflow = $event"
|
||||||
|
/>
|
||||||
|
<div style="display: flex; gap: 0.5rem; padding-top: 0.5rem;">
|
||||||
|
<button
|
||||||
|
*ngIf="notification.code === 1"
|
||||||
|
tuiButton
|
||||||
|
appearance="secondary"
|
||||||
|
size="xs"
|
||||||
|
(click)="service.viewReport(notification)"
|
||||||
|
>
|
||||||
|
View Report
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
*ngIf="overflow"
|
||||||
|
tuiButton
|
||||||
|
appearance="secondary"
|
||||||
|
size="xs"
|
||||||
|
(click)="service.viewFull(notification)"
|
||||||
|
>
|
||||||
|
View full
|
||||||
|
</button>
|
||||||
|
<ng-content select="a"></ng-content>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-content select="button"></ng-content>
|
||||||
|
`,
|
||||||
|
styles: [':host { box-shadow: 0 1px var(--tui-clear); }'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
TuiSvgModule,
|
||||||
|
TuiTitleModule,
|
||||||
|
TuiButtonModule,
|
||||||
|
TuiLineClampModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class HeaderNotificationComponent<T extends number> {
|
||||||
|
readonly service = inject(NotificationService)
|
||||||
|
|
||||||
|
@Input({ required: true }) notification!: ServerNotification<T>
|
||||||
|
|
||||||
|
overflow = false
|
||||||
|
|
||||||
|
get color(): string {
|
||||||
|
return this.service.getColor(this.notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
get icon(): string {
|
||||||
|
return this.service.getIcon(this.notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Output,
|
||||||
|
inject,
|
||||||
|
EventEmitter,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { TuiForModule } from '@taiga-ui/cdk'
|
||||||
|
import { TuiScrollbarModule } from '@taiga-ui/core'
|
||||||
|
import {
|
||||||
|
TuiAvatarStackModule,
|
||||||
|
TuiButtonModule,
|
||||||
|
TuiCellModule,
|
||||||
|
TuiTitleModule,
|
||||||
|
} from '@taiga-ui/experimental'
|
||||||
|
import { PatchDB } from 'patch-db-client'
|
||||||
|
import { Subject, first, tap } from 'rxjs'
|
||||||
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { HeaderNotificationComponent } from './header-notification.component'
|
||||||
|
import { toRouterLink } from '../../utils/to-router-link'
|
||||||
|
import {
|
||||||
|
ServerNotification,
|
||||||
|
ServerNotifications,
|
||||||
|
} from 'src/app/services/api/api.types'
|
||||||
|
import { NotificationService } from '../../services/notification.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'header-notifications',
|
||||||
|
template: `
|
||||||
|
<ng-container *ngIf="notifications$ | async as notifications">
|
||||||
|
<h3 class="g-title" style="padding: 0 1rem">
|
||||||
|
Notifications
|
||||||
|
<a
|
||||||
|
*ngIf="notifications.length"
|
||||||
|
style="margin-left: auto; text-transform: none; font-size: 0.9rem; font-weight: 600;"
|
||||||
|
(click)="markAllSeen(notifications[0].id)"
|
||||||
|
>
|
||||||
|
Mark All Seen
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<tui-scrollbar *ngIf="packageData$ | async as packageData">
|
||||||
|
<header-notification
|
||||||
|
*ngFor="let not of notifications; let i = index"
|
||||||
|
tuiCell
|
||||||
|
[notification]="not"
|
||||||
|
>
|
||||||
|
<ng-container *ngIf="not['package-id'] as pkgId">
|
||||||
|
{{ $any(packageData[pkgId])?.manifest.title || pkgId }}
|
||||||
|
</ng-container>
|
||||||
|
<button
|
||||||
|
style="align-self: flex-start; flex-shrink: 0;"
|
||||||
|
tuiIconButton
|
||||||
|
appearance="icon"
|
||||||
|
iconLeft="tuiIconMinusCircle"
|
||||||
|
(click)="markSeen(notifications, not)"
|
||||||
|
></button>
|
||||||
|
<a
|
||||||
|
*ngIf="not['package-id'] && packageData[not['package-id']]"
|
||||||
|
tuiButton
|
||||||
|
size="xs"
|
||||||
|
appearance="secondary"
|
||||||
|
[routerLink]="getLink(not['package-id'] || '')"
|
||||||
|
>
|
||||||
|
View Service
|
||||||
|
</a>
|
||||||
|
</header-notification>
|
||||||
|
</tui-scrollbar>
|
||||||
|
<a
|
||||||
|
style="margin: 2rem; text-align: center; font-size: 0.9rem; font-weight: 600;"
|
||||||
|
[routerLink]="'/portal/system/notifications'"
|
||||||
|
>
|
||||||
|
View All
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
`,
|
||||||
|
styles: [
|
||||||
|
`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 22rem;
|
||||||
|
max-width: 80vw;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
RouterLink,
|
||||||
|
TuiForModule,
|
||||||
|
TuiScrollbarModule,
|
||||||
|
TuiButtonModule,
|
||||||
|
HeaderNotificationComponent,
|
||||||
|
TuiCellModule,
|
||||||
|
TuiAvatarStackModule,
|
||||||
|
TuiTitleModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class HeaderNotificationsComponent {
|
||||||
|
private readonly patch = inject(PatchDB<DataModel>)
|
||||||
|
private readonly service = inject(NotificationService)
|
||||||
|
|
||||||
|
readonly packageData$ = this.patch.watch$('package-data').pipe(first())
|
||||||
|
|
||||||
|
readonly notifications$ = new Subject<ServerNotifications>()
|
||||||
|
|
||||||
|
@Output() onEmpty = new EventEmitter()
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.patch
|
||||||
|
.watch$('server-info', 'unreadNotifications', 'recent')
|
||||||
|
.pipe(
|
||||||
|
tap(recent => this.notifications$.next(recent)),
|
||||||
|
first(),
|
||||||
|
)
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
markSeen(
|
||||||
|
current: ServerNotifications,
|
||||||
|
notification: ServerNotification<number>,
|
||||||
|
) {
|
||||||
|
this.notifications$.next(current.filter(c => c.id !== notification.id))
|
||||||
|
|
||||||
|
if (current.length === 1) this.onEmpty.emit()
|
||||||
|
|
||||||
|
this.service.markSeen([notification])
|
||||||
|
}
|
||||||
|
|
||||||
|
markAllSeen(latestId: number) {
|
||||||
|
this.notifications$.next([])
|
||||||
|
|
||||||
|
this.service.markSeenAll(latestId)
|
||||||
|
|
||||||
|
this.onEmpty.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
getLink(id: string) {
|
||||||
|
return toRouterLink(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<ng-content></ng-content>
|
|
||||||
<div class="toolbar">
|
|
||||||
<button tuiIconButton iconLeft="tuiIconCloudLarge" appearance="success">
|
|
||||||
Connection
|
|
||||||
</button>
|
|
||||||
<tui-badged-content [style.--tui-radius.%]="50">
|
|
||||||
<tui-badge-notification tuiSlot="bottom" size="s">4</tui-badge-notification>
|
|
||||||
<button tuiIconButton iconLeft="tuiIconBellLarge" appearance="warning">
|
|
||||||
Notifications
|
|
||||||
</button>
|
|
||||||
</tui-badged-content>
|
|
||||||
<header-menu></header-menu>
|
|
||||||
</div>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 4.5rem;
|
|
||||||
padding: 0 1rem 0 2rem;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
// TODO: Theme
|
|
||||||
background: rgb(51 51 51 / 84%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,15 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Router } from '@angular/router'
|
||||||
|
import { TuiSidebarModule } from '@taiga-ui/addon-mobile'
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
HostListener,
|
||||||
|
inject,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { tuiContainsOrAfter, tuiIsElement, TuiLetModule } from '@taiga-ui/cdk'
|
||||||
import {
|
import {
|
||||||
TuiDataListModule,
|
TuiDataListModule,
|
||||||
TuiHostedDropdownModule,
|
TuiHostedDropdownModule,
|
||||||
@@ -9,22 +20,105 @@ import {
|
|||||||
TuiBadgeNotificationModule,
|
TuiBadgeNotificationModule,
|
||||||
TuiButtonModule,
|
TuiButtonModule,
|
||||||
} from '@taiga-ui/experimental'
|
} from '@taiga-ui/experimental'
|
||||||
import { HeaderMenuComponent } from './header-menu/header-menu.component'
|
import { Subject } from 'rxjs'
|
||||||
|
import { SidebarDirective } from '../../../../app/sidebar-host.component'
|
||||||
|
import { HeaderMenuComponent } from './header-menu.component'
|
||||||
|
import { HeaderNotificationsComponent } from './header-notifications.component'
|
||||||
|
import { NotificationService } from '../../services/notification.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'header[appHeader]',
|
selector: 'header[appHeader]',
|
||||||
templateUrl: 'header.component.html',
|
template: `
|
||||||
styleUrls: ['header.component.scss'],
|
<ng-content></ng-content>
|
||||||
|
<button
|
||||||
|
tuiIconButton
|
||||||
|
iconLeft="tuiIconCloudLarge"
|
||||||
|
appearance="success"
|
||||||
|
[style.margin-left]="'auto'"
|
||||||
|
>
|
||||||
|
Connection
|
||||||
|
</button>
|
||||||
|
<tui-badged-content
|
||||||
|
*tuiLet="notificationService.unreadCount$ | async as unread"
|
||||||
|
[style.--tui-radius.%]="50"
|
||||||
|
>
|
||||||
|
<tui-badge-notification *ngIf="unread" tuiSlot="bottom" size="s">
|
||||||
|
{{ unread }}
|
||||||
|
</tui-badge-notification>
|
||||||
|
<button
|
||||||
|
tuiIconButton
|
||||||
|
iconLeft="tuiIconBellLarge"
|
||||||
|
appearance="warning"
|
||||||
|
(click)="handleNotificationsClick(unread || 0)"
|
||||||
|
>
|
||||||
|
Notifications
|
||||||
|
</button>
|
||||||
|
</tui-badged-content>
|
||||||
|
<header-menu></header-menu>
|
||||||
|
<header-notifications
|
||||||
|
(onEmpty)="this.open$.next(false)"
|
||||||
|
*tuiSidebar="!!(open$ | async); direction: 'right'; autoWidth: true"
|
||||||
|
/>
|
||||||
|
`,
|
||||||
|
styles: [
|
||||||
|
`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 4.5rem;
|
||||||
|
padding: 0 1rem 0 2rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
// TODO: Theme
|
||||||
|
background: rgb(51 51 51 / 84%);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [
|
imports: [
|
||||||
|
CommonModule,
|
||||||
TuiBadgedContentModule,
|
TuiBadgedContentModule,
|
||||||
TuiBadgeNotificationModule,
|
TuiBadgeNotificationModule,
|
||||||
TuiButtonModule,
|
TuiButtonModule,
|
||||||
TuiHostedDropdownModule,
|
TuiHostedDropdownModule,
|
||||||
TuiDataListModule,
|
TuiDataListModule,
|
||||||
TuiSvgModule,
|
TuiSvgModule,
|
||||||
|
TuiSidebarModule,
|
||||||
|
SidebarDirective,
|
||||||
HeaderMenuComponent,
|
HeaderMenuComponent,
|
||||||
|
HeaderNotificationsComponent,
|
||||||
|
TuiLetModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class HeaderComponent {}
|
export class HeaderComponent {
|
||||||
|
private readonly router = inject(Router)
|
||||||
|
readonly notificationService = inject(NotificationService)
|
||||||
|
|
||||||
|
@ViewChild(HeaderNotificationsComponent, { read: ElementRef })
|
||||||
|
private readonly panel?: ElementRef<HTMLElement>
|
||||||
|
|
||||||
|
private readonly _ = this.router.events.subscribe(() => {
|
||||||
|
this.open$.next(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
readonly open$ = new Subject<boolean>()
|
||||||
|
|
||||||
|
@HostListener('document:click.capture', ['$event.target'])
|
||||||
|
onClick(target: EventTarget | null) {
|
||||||
|
if (
|
||||||
|
tuiIsElement(target) &&
|
||||||
|
this.panel?.nativeElement &&
|
||||||
|
!tuiContainsOrAfter(this.panel.nativeElement, target)
|
||||||
|
) {
|
||||||
|
this.open$.next(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNotificationsClick(unread: number) {
|
||||||
|
if (unread) {
|
||||||
|
this.open$.next(true)
|
||||||
|
} else {
|
||||||
|
this.router.navigateByUrl('/portal/system/notifications')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,4 +16,8 @@ export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
|
|||||||
icon: 'assets/img/icon_transparent.png',
|
icon: 'assets/img/icon_transparent.png',
|
||||||
title: 'Snek',
|
title: 'Snek',
|
||||||
},
|
},
|
||||||
|
'/portal/system/notifications': {
|
||||||
|
icon: 'tuiIconBellLarge',
|
||||||
|
title: 'Notifications',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
15
web/projects/ui/src/app/apps/portal/pipes/to-badge.ts
Normal file
15
web/projects/ui/src/app/apps/portal/pipes/to-badge.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { inject, Pipe, PipeTransform } from '@angular/core'
|
||||||
|
import { BadgeService } from '../services/badge.service'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'toBadge',
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class ToBadgePipe implements PipeTransform {
|
||||||
|
readonly badge = inject(BadgeService)
|
||||||
|
|
||||||
|
transform(id: string): Observable<number> {
|
||||||
|
return this.badge.getCount(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { inject, Pipe, PipeTransform } from '@angular/core'
|
|
||||||
import { NotificationsService } from '../services/notifications.service'
|
|
||||||
import { Observable } from 'rxjs'
|
|
||||||
|
|
||||||
@Pipe({
|
|
||||||
name: 'toNotifications',
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
export class ToNotificationsPipe implements PipeTransform {
|
|
||||||
readonly notifications = inject(NotificationsService)
|
|
||||||
|
|
||||||
transform(id: string): Observable<number> {
|
|
||||||
return this.notifications.getNotifications(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
appCard
|
appCard
|
||||||
@tuiFadeIn
|
@tuiFadeIn
|
||||||
[id]="item"
|
[id]="item"
|
||||||
[badge]="item | toNotifications | async"
|
[badge]="item | toBadge | async"
|
||||||
[title]="navigationItem.title"
|
[title]="navigationItem.title"
|
||||||
[icon]="navigationItem.icon"
|
[icon]="navigationItem.icon"
|
||||||
[routerLink]="navigationItem.routerLink"
|
[routerLink]="navigationItem.routerLink"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { TuiTilesModule } from '@taiga-ui/kit'
|
|||||||
import { DesktopComponent } from './desktop.component'
|
import { DesktopComponent } from './desktop.component'
|
||||||
import { CardComponent } from '../../components/card/card.component'
|
import { CardComponent } from '../../components/card/card.component'
|
||||||
import { ToNavigationItemPipe } from '../../pipes/to-navigation-item'
|
import { ToNavigationItemPipe } from '../../pipes/to-navigation-item'
|
||||||
import { ToNotificationsPipe } from '../../pipes/to-notifications'
|
import { ToBadgePipe } from '../../pipes/to-badge'
|
||||||
import { DesktopItemDirective } from './desktop-item.directive'
|
import { DesktopItemDirective } from './desktop-item.directive'
|
||||||
|
|
||||||
const ROUTES: Routes = [
|
const ROUTES: Routes = [
|
||||||
@@ -30,7 +30,7 @@ const ROUTES: Routes = [
|
|||||||
RouterModule.forChild(ROUTES),
|
RouterModule.forChild(ROUTES),
|
||||||
TuiFadeModule,
|
TuiFadeModule,
|
||||||
DragScrollerDirective,
|
DragScrollerDirective,
|
||||||
ToNotificationsPipe,
|
ToBadgePipe,
|
||||||
],
|
],
|
||||||
declarations: [DesktopComponent],
|
declarations: [DesktopComponent],
|
||||||
exports: [DesktopComponent],
|
exports: [DesktopComponent],
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import { TuiCheckboxModule } from '@taiga-ui/kit'
|
|||||||
import { BehaviorSubject } from 'rxjs'
|
import { BehaviorSubject } from 'rxjs'
|
||||||
import { BackupRun } from 'src/app/services/api/api.types'
|
import { BackupRun } from 'src/app/services/api/api.types'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
|
import { REPORT } from 'src/app/apps/portal/modals/report.component'
|
||||||
import { DurationPipe } from '../pipes/duration.pipe'
|
import { DurationPipe } from '../pipes/duration.pipe'
|
||||||
import { HasErrorPipe } from '../pipes/has-error.pipe'
|
import { HasErrorPipe } from '../pipes/has-error.pipe'
|
||||||
import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
|
import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
|
||||||
import { REPORT } from './report.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
inject,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { Manifest } from '@start9labs/marketplace'
|
||||||
|
import { tuiPure } from '@taiga-ui/cdk'
|
||||||
|
import { TuiSvgModule } from '@taiga-ui/core'
|
||||||
|
import { TuiLineClampModule } from '@taiga-ui/kit'
|
||||||
|
import { PatchDB } from 'patch-db-client'
|
||||||
|
import { Observable, first } from 'rxjs'
|
||||||
|
import { ServerNotification } from 'src/app/services/api/api.types'
|
||||||
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { NotificationService } from '../../../services/notification.service'
|
||||||
|
import { toRouterLink } from '../../../utils/to-router-link'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: '[notificationItem]',
|
||||||
|
template: `
|
||||||
|
<td><ng-content /></td>
|
||||||
|
<td>{{ notificationItem['created-at'] | date : 'MMM d, y, h:mm a' }}</td>
|
||||||
|
<td [style.color]="color">
|
||||||
|
<tui-svg [style.color]="color" [src]="icon"></tui-svg>
|
||||||
|
{{ notificationItem.title }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
*ngIf="manifest$ | async as manifest; else na"
|
||||||
|
[routerLink]="getLink(manifest.id)"
|
||||||
|
>
|
||||||
|
{{ manifest.title }}
|
||||||
|
</a>
|
||||||
|
<ng-template #na>N/A</ng-template>
|
||||||
|
</td>
|
||||||
|
<td [style.padding-bottom.rem]="0.5">
|
||||||
|
<tui-line-clamp
|
||||||
|
style="pointer-events: none"
|
||||||
|
[linesLimit]="4"
|
||||||
|
[lineHeight]="21"
|
||||||
|
[content]="notificationItem.message"
|
||||||
|
(overflownChange)="overflow = $event"
|
||||||
|
/>
|
||||||
|
<a *ngIf="overflow" (click)="service.viewFull(notificationItem)">
|
||||||
|
View Full
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
*ngIf="notificationItem.code === 1"
|
||||||
|
(click)="service.viewReport(notificationItem)"
|
||||||
|
>
|
||||||
|
View Report
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, RouterLink, TuiLineClampModule, TuiSvgModule],
|
||||||
|
})
|
||||||
|
export class NotificationItemComponent {
|
||||||
|
private readonly patch = inject(PatchDB<DataModel>)
|
||||||
|
readonly service = inject(NotificationService)
|
||||||
|
|
||||||
|
@Input({ required: true }) notificationItem!: ServerNotification<number>
|
||||||
|
|
||||||
|
overflow = false
|
||||||
|
|
||||||
|
@tuiPure
|
||||||
|
get manifest$(): Observable<Manifest> {
|
||||||
|
return this.patch
|
||||||
|
.watch$(
|
||||||
|
'package-data',
|
||||||
|
this.notificationItem['package-id'] || '',
|
||||||
|
'manifest',
|
||||||
|
)
|
||||||
|
.pipe(first())
|
||||||
|
}
|
||||||
|
|
||||||
|
get color(): string {
|
||||||
|
return this.service.getColor(this.notificationItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
get icon(): string {
|
||||||
|
return this.service.getIcon(this.notificationItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
getLink(id: string) {
|
||||||
|
return toRouterLink(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
|
import { Subject } from 'rxjs'
|
||||||
|
import { RR, ServerNotifications } from 'src/app/services/api/api.types'
|
||||||
|
import { NotificationService } from '../../../services/notification.service'
|
||||||
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
|
import { ErrorService } from '@start9labs/shared'
|
||||||
|
import { TuiLetModule } from '@taiga-ui/cdk'
|
||||||
|
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||||
|
import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core'
|
||||||
|
import { NotificationsTableComponent } from './table.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<ng-container *tuiLet="notifications$ | async as notifications">
|
||||||
|
<h3 class="g-title">
|
||||||
|
Notifications
|
||||||
|
<ng-container *ngIf="table.selected$ | async as selected">
|
||||||
|
<tui-hosted-dropdown
|
||||||
|
tuiDropdownAlign="right"
|
||||||
|
[content]="dropdown"
|
||||||
|
[sided]="true"
|
||||||
|
[(open)]="open"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
appearance="primary"
|
||||||
|
iconRight="tuiIconChevronDown"
|
||||||
|
tuiButton
|
||||||
|
size="xs"
|
||||||
|
type="button"
|
||||||
|
[disabled]="!selected.length"
|
||||||
|
>
|
||||||
|
Batch Action
|
||||||
|
</button>
|
||||||
|
</tui-hosted-dropdown>
|
||||||
|
<ng-template #dropdown>
|
||||||
|
<tui-data-list>
|
||||||
|
<button tuiOption (click)="markSeen(notifications!, selected)">
|
||||||
|
Mark seen
|
||||||
|
</button>
|
||||||
|
<button tuiOption (click)="markUnseen(notifications!, selected)">
|
||||||
|
Mark unseen
|
||||||
|
</button>
|
||||||
|
<button tuiOption (click)="remove(notifications!, selected)">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</tui-data-list>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
</h3>
|
||||||
|
<table #table class="g-table" [notifications]="notifications"></table>
|
||||||
|
</ng-container>
|
||||||
|
`,
|
||||||
|
host: { class: 'g-page' },
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
TuiHostedDropdownModule,
|
||||||
|
TuiButtonModule,
|
||||||
|
TuiDataListModule,
|
||||||
|
NotificationsTableComponent,
|
||||||
|
TuiLetModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class NotificationsComponent {
|
||||||
|
readonly service = inject(NotificationService)
|
||||||
|
readonly api = inject(ApiService)
|
||||||
|
readonly errorService = inject(ErrorService)
|
||||||
|
|
||||||
|
readonly notifications$ = new Subject<ServerNotifications | null>()
|
||||||
|
|
||||||
|
open = false
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.getMore({})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMore(params: RR.GetNotificationsReq) {
|
||||||
|
try {
|
||||||
|
this.notifications$.next(null)
|
||||||
|
this.notifications$.next(await this.api.getNotifications(params))
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
markSeen(current: ServerNotifications, toUpdate: ServerNotifications) {
|
||||||
|
this.open = false
|
||||||
|
|
||||||
|
this.notifications$.next(
|
||||||
|
current.map(c => ({
|
||||||
|
...c,
|
||||||
|
read: toUpdate.some(n => n.id === c.id) || c.read,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
this.service.markSeen(toUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
markUnseen(current: ServerNotifications, toUpdate: ServerNotifications) {
|
||||||
|
this.open = false
|
||||||
|
|
||||||
|
this.notifications$.next(
|
||||||
|
current.map(c => ({
|
||||||
|
...c,
|
||||||
|
read: c.read && !toUpdate.some(n => n.id === c.id),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
this.service.markUnseen(toUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(current: ServerNotifications, toDelete: ServerNotifications) {
|
||||||
|
this.open = false
|
||||||
|
|
||||||
|
this.notifications$.next(
|
||||||
|
current.filter(c => !toDelete.some(n => n.id === c.id)),
|
||||||
|
)
|
||||||
|
|
||||||
|
this.service.remove(toDelete)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
} from '@angular/core'
|
||||||
|
import {
|
||||||
|
ServerNotification,
|
||||||
|
ServerNotifications,
|
||||||
|
} from 'src/app/services/api/api.types'
|
||||||
|
import { TuiForModule } from '@taiga-ui/cdk'
|
||||||
|
import { BehaviorSubject } from 'rxjs'
|
||||||
|
import { TuiLineClampModule } from '@taiga-ui/kit'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
|
import { NotificationItemComponent } from './item.component'
|
||||||
|
import { TuiCheckboxModule } from '@taiga-ui/experimental'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'table[notifications]',
|
||||||
|
template: `
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th [style.width.rem]="1.5">
|
||||||
|
<input
|
||||||
|
tuiCheckbox
|
||||||
|
size="s"
|
||||||
|
type="checkbox"
|
||||||
|
[style.display]="'block'"
|
||||||
|
[disabled]="!notifications?.length"
|
||||||
|
[ngModel]="all"
|
||||||
|
(ngModelChange)="onAll($event)"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th [style.min-width.rem]="12">Date</th>
|
||||||
|
<th [style.min-width.rem]="12">Title</th>
|
||||||
|
<th [style.min-width.rem]="8">Service</th>
|
||||||
|
<th>Message</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
*ngFor="let notification of notifications; else: loading; empty: blank"
|
||||||
|
[notificationItem]="notification"
|
||||||
|
[style.font-weight]="notification.read ? 'normal' : 'bold'"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
tuiCheckbox
|
||||||
|
size="s"
|
||||||
|
type="checkbox"
|
||||||
|
[style.display]="'block'"
|
||||||
|
[ngModel]="selected$.value.includes(notification)"
|
||||||
|
(ngModelChange)="handleToggle(notification)"
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
<ng-template #blank>
|
||||||
|
<tr>
|
||||||
|
<td colspan="5">You have no notifications</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #loading>
|
||||||
|
<tr *ngFor="let row of ['', '']">
|
||||||
|
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</tbody>
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
TuiForModule,
|
||||||
|
TuiCheckboxModule,
|
||||||
|
FormsModule,
|
||||||
|
TuiLineClampModule,
|
||||||
|
NotificationItemComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class NotificationsTableComponent implements OnChanges {
|
||||||
|
@Input() notifications: ServerNotifications | null = null
|
||||||
|
|
||||||
|
get all(): boolean | null {
|
||||||
|
if (!this.notifications?.length || !this.selected$.value.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.notifications?.length === this.selected$.value.length) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly selected$ = new BehaviorSubject<ServerNotifications>([])
|
||||||
|
|
||||||
|
ngOnChanges() {
|
||||||
|
this.selected$.next([])
|
||||||
|
}
|
||||||
|
|
||||||
|
onAll(selected: boolean) {
|
||||||
|
this.selected$.next((selected && this.notifications) || [])
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToggle(notification: ServerNotification<number>) {
|
||||||
|
const selected = this.selected$.value
|
||||||
|
|
||||||
|
if (selected.some(s => s.id === notification.id)) {
|
||||||
|
this.selected$.next(selected.filter(s => s.id !== notification.id))
|
||||||
|
} else {
|
||||||
|
this.selected$.next([...selected, notification])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,15 @@ const ROUTES: Routes = [
|
|||||||
import('./backups/backups.component').then(m => m.BackupsComponent),
|
import('./backups/backups.component').then(m => m.BackupsComponent),
|
||||||
data: toNavigationItem('/portal/system/backups'),
|
data: toNavigationItem('/portal/system/backups'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: systemTabResolver,
|
||||||
|
path: 'notifications',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./notifications/notifications.component').then(
|
||||||
|
m => m.NotificationsComponent,
|
||||||
|
),
|
||||||
|
data: toNavigationItem('/portal/system/notifications'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: systemTabResolver,
|
title: systemTabResolver,
|
||||||
path: 'sideload',
|
path: 'sideload',
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { ConnectionService } from 'src/app/services/connection.service'
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class NotificationsService {
|
export class BadgeService {
|
||||||
private readonly emver = inject(Emver)
|
private readonly emver = inject(Emver)
|
||||||
private readonly patch = inject(PatchDB<DataModel>)
|
private readonly patch = inject(PatchDB<DataModel>)
|
||||||
private readonly marketplace = inject(
|
private readonly marketplace = inject(
|
||||||
@@ -47,7 +47,7 @@ export class NotificationsService {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
private readonly updates$ = combineLatest([
|
private readonly updateCount$ = combineLatest([
|
||||||
this.marketplace.getMarketplace$(true),
|
this.marketplace.getMarketplace$(true),
|
||||||
this.local$,
|
this.local$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
@@ -67,10 +67,10 @@ export class NotificationsService {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
getNotifications(id: string): Observable<number> {
|
getCount(id: string): Observable<number> {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case '/portal/system/updates':
|
case '/portal/system/updates':
|
||||||
return this.updates$
|
return this.updateCount$
|
||||||
default:
|
default:
|
||||||
return EMPTY
|
return EMPTY
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { inject, Injectable } from '@angular/core'
|
||||||
|
import { ErrorService } from '@start9labs/shared'
|
||||||
|
import { TuiDialogService } from '@taiga-ui/core'
|
||||||
|
import {
|
||||||
|
NotificationLevel,
|
||||||
|
ServerNotification,
|
||||||
|
ServerNotifications,
|
||||||
|
} from 'src/app/services/api/api.types'
|
||||||
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
|
import { REPORT } from '../modals/report.component'
|
||||||
|
import { firstValueFrom, merge, shareReplay, Subject } from 'rxjs'
|
||||||
|
import { PatchDB } from 'patch-db-client'
|
||||||
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class NotificationService {
|
||||||
|
private readonly patch = inject(PatchDB<DataModel>)
|
||||||
|
private readonly errorService = inject(ErrorService)
|
||||||
|
private readonly api = inject(ApiService)
|
||||||
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
|
private readonly localUnreadCount$ = new Subject<number>()
|
||||||
|
|
||||||
|
readonly unreadCount$ = merge(
|
||||||
|
this.patch.watch$('server-info', 'unreadNotifications', 'count'),
|
||||||
|
this.localUnreadCount$,
|
||||||
|
).pipe(shareReplay(1))
|
||||||
|
|
||||||
|
async markSeen(notifications: ServerNotifications) {
|
||||||
|
const ids = notifications.filter(n => !n.read).map(n => n.id)
|
||||||
|
|
||||||
|
this.updateCount(-ids.length)
|
||||||
|
|
||||||
|
this.api
|
||||||
|
.markSeenNotifications({ ids })
|
||||||
|
.catch(e => this.errorService.handleError(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
async markSeenAll(latestId: number) {
|
||||||
|
this.localUnreadCount$.next(0)
|
||||||
|
|
||||||
|
this.api
|
||||||
|
.markSeenAllNotifications({ before: latestId })
|
||||||
|
.catch(e => this.errorService.handleError(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
async markUnseen(notifications: ServerNotifications) {
|
||||||
|
const ids = notifications.filter(n => n.read).map(n => n.id)
|
||||||
|
|
||||||
|
this.updateCount(ids.length)
|
||||||
|
|
||||||
|
this.api
|
||||||
|
.markUnseenNotifications({ ids })
|
||||||
|
.catch(e => this.errorService.handleError(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(notifications: ServerNotifications): Promise<void> {
|
||||||
|
this.updateCount(-notifications.filter(n => !n.read).length)
|
||||||
|
|
||||||
|
this.api
|
||||||
|
.deleteNotifications({ ids: notifications.map(n => n.id) })
|
||||||
|
.catch(e => this.errorService.handleError(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
getColor(notification: ServerNotification<number>): string {
|
||||||
|
switch (notification.level) {
|
||||||
|
case NotificationLevel.Info:
|
||||||
|
return 'var(--tui-info-fill)'
|
||||||
|
case NotificationLevel.Success:
|
||||||
|
return 'var(--tui-success-fill)'
|
||||||
|
case NotificationLevel.Warning:
|
||||||
|
return 'var(--tui-warning-fill)'
|
||||||
|
case NotificationLevel.Error:
|
||||||
|
return 'var(--tui-error-fill)'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getIcon(notification: ServerNotification<number>): string {
|
||||||
|
switch (notification.level) {
|
||||||
|
case NotificationLevel.Info:
|
||||||
|
return 'tuiIconInfo'
|
||||||
|
case NotificationLevel.Success:
|
||||||
|
return 'tuiIconCheckCircle'
|
||||||
|
case NotificationLevel.Warning:
|
||||||
|
case NotificationLevel.Error:
|
||||||
|
return 'tuiIconAlertCircle'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewFull(notification: ServerNotification<number>) {
|
||||||
|
this.dialogs
|
||||||
|
.open(notification.message, { label: notification.title })
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewReport(notification: ServerNotification<number>) {
|
||||||
|
this.dialogs
|
||||||
|
.open(REPORT, {
|
||||||
|
label: 'Backup Report',
|
||||||
|
data: {
|
||||||
|
report: notification.data,
|
||||||
|
timestamp: notification['created-at'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateCount(toAdjust: number) {
|
||||||
|
const currentCount = await firstValueFrom(this.unreadCount$)
|
||||||
|
|
||||||
|
this.localUnreadCount$.next(Math.max(currentCount + toAdjust, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,7 +73,7 @@ export class NotificationsPage {
|
|||||||
const loader = this.loader.open('Deleting...').subscribe()
|
const loader = this.loader.open('Deleting...').subscribe()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.embassyApi.deleteNotification({ id })
|
// await this.embassyApi.deleteNotification({ id })
|
||||||
this.notifications.splice(index, 1)
|
this.notifications.splice(index, 1)
|
||||||
this.beforeCursor = this.notifications[this.notifications.length - 1]?.id
|
this.beforeCursor = this.notifications[this.notifications.length - 1]?.id
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -98,7 +98,7 @@ export class NotificationsPage {
|
|||||||
.subscribe(() => this.deleteAll())
|
.subscribe(() => this.deleteAll())
|
||||||
}
|
}
|
||||||
|
|
||||||
async viewBackupReport(notification: ServerNotification<1>) {
|
async viewBackupReport(notification: ServerNotification<number>) {
|
||||||
this.dialogs
|
this.dialogs
|
||||||
.open(new PolymorpheusComponent(BackupReportComponent), {
|
.open(new PolymorpheusComponent(BackupReportComponent), {
|
||||||
label: 'Backup Report',
|
label: 'Backup Report',
|
||||||
@@ -137,9 +137,9 @@ export class NotificationsPage {
|
|||||||
const loader = this.loader.open('Deleting...').subscribe()
|
const loader = this.loader.open('Deleting...').subscribe()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.embassyApi.deleteAllNotifications({
|
// await this.embassyApi.deleteAllNotifications({
|
||||||
before: this.notifications[0].id + 1,
|
// before: this.notifications[0].id + 1,
|
||||||
})
|
// })
|
||||||
this.notifications = []
|
this.notifications = []
|
||||||
this.beforeCursor = undefined
|
this.beforeCursor = undefined
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class ServerMetricsPage {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly api: ApiService,
|
private readonly api: ApiService,
|
||||||
readonly timeService: TimeService,
|
private readonly timeService: TimeService,
|
||||||
private readonly connectionService: ConnectionService,
|
private readonly connectionService: ConnectionService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ const { enableWidgets } =
|
|||||||
export class BadgeMenuComponent {
|
export class BadgeMenuComponent {
|
||||||
readonly unreadCount$ = this.patch.watch$(
|
readonly unreadCount$ = this.patch.watch$(
|
||||||
'server-info',
|
'server-info',
|
||||||
'unread-notification-count',
|
'unreadNotifications',
|
||||||
|
'count',
|
||||||
)
|
)
|
||||||
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
|
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||||
readonly widgetDrawer$ = this.clientStorageService.widgetDrawer$
|
readonly widgetDrawer$ = this.clientStorageService.widgetDrawer$
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import {
|
|||||||
} from 'rxjs'
|
} from 'rxjs'
|
||||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||||
import {
|
import {
|
||||||
LogsRes,
|
FetchLogsReq,
|
||||||
ServerLogsReq,
|
FetchLogsRes,
|
||||||
toLocalIsoString,
|
toLocalIsoString,
|
||||||
Log,
|
Log,
|
||||||
DownloadHTMLService,
|
DownloadHTMLService,
|
||||||
@@ -50,8 +50,8 @@ export class LogsComponent {
|
|||||||
params: RR.FollowServerLogsReq,
|
params: RR.FollowServerLogsReq,
|
||||||
) => Promise<RR.FollowServerLogsRes>
|
) => Promise<RR.FollowServerLogsRes>
|
||||||
@Input({ required: true }) fetchLogs!: (
|
@Input({ required: true }) fetchLogs!: (
|
||||||
params: ServerLogsReq,
|
params: FetchLogsReq,
|
||||||
) => Promise<LogsRes>
|
) => Promise<FetchLogsRes>
|
||||||
@Input({ required: true }) context!: string
|
@Input({ required: true }) context!: string
|
||||||
@Input() defaultBack = ''
|
@Input() defaultBack = ''
|
||||||
@Input() pageTitle = ''
|
@Input() pageTitle = ''
|
||||||
@@ -205,7 +205,7 @@ export class LogsComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private processRes(res: LogsRes) {
|
private processRes(res: FetchLogsRes) {
|
||||||
const { entries, 'start-cursor': startCursor } = res
|
const { entries, 'start-cursor': startCursor } = res
|
||||||
|
|
||||||
if (!entries.length) return
|
if (!entries.length) return
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
|||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class NotificationsToastService extends Observable<boolean> {
|
export class NotificationsToastService extends Observable<boolean> {
|
||||||
private readonly stream$ = this.patch
|
private readonly stream$ = this.patch
|
||||||
.watch$('server-info', 'unread-notification-count')
|
.watch$('server-info', 'unreadNotifications', 'count')
|
||||||
.pipe(
|
.pipe(
|
||||||
pairwise(),
|
pairwise(),
|
||||||
map(([prev, cur]) => cur > prev),
|
map(([prev, cur]) => cur > prev),
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ export module Mock {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
read: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -349,6 +350,7 @@ export module Mock {
|
|||||||
title: 'SSH Key Added',
|
title: 'SSH Key Added',
|
||||||
message: 'A new SSH key was added. If you did not do this, shit is bad.',
|
message: 'A new SSH key was added. If you did not do this, shit is bad.',
|
||||||
data: null,
|
data: null,
|
||||||
|
read: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -359,6 +361,7 @@ export module Mock {
|
|||||||
title: 'SSH Key Removed',
|
title: 'SSH Key Removed',
|
||||||
message: 'A SSH key was removed.',
|
message: 'A SSH key was removed.',
|
||||||
data: null,
|
data: null,
|
||||||
|
read: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
@@ -367,7 +370,7 @@ export module Mock {
|
|||||||
code: 4,
|
code: 4,
|
||||||
level: NotificationLevel.Error,
|
level: NotificationLevel.Error,
|
||||||
title: 'Service Crashed',
|
title: 'Service Crashed',
|
||||||
message: new Array(40)
|
message: new Array(3)
|
||||||
.fill(
|
.fill(
|
||||||
`2021-11-27T18:36:30.451064Z 2021-11-27T18:36:30Z tor: Thread interrupt
|
`2021-11-27T18:36:30.451064Z 2021-11-27T18:36:30Z tor: Thread interrupt
|
||||||
2021-11-27T18:36:30.452833Z 2021-11-27T18:36:30Z Shutdown: In progress...
|
2021-11-27T18:36:30.452833Z 2021-11-27T18:36:30Z Shutdown: In progress...
|
||||||
@@ -376,6 +379,7 @@ export module Mock {
|
|||||||
)
|
)
|
||||||
.join(''),
|
.join(''),
|
||||||
data: null,
|
data: null,
|
||||||
|
read: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ import {
|
|||||||
ServiceOutboundProxy,
|
ServiceOutboundProxy,
|
||||||
HealthCheckResult,
|
HealthCheckResult,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
|
import {
|
||||||
|
StartOSDiskInfo,
|
||||||
|
FetchLogsReq,
|
||||||
|
FetchLogsRes,
|
||||||
|
FollowLogsRes,
|
||||||
|
FollowLogsReq,
|
||||||
|
} from '@start9labs/shared'
|
||||||
import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants'
|
import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants'
|
||||||
|
|
||||||
export module RR {
|
export module RR {
|
||||||
@@ -50,14 +56,11 @@ export module RR {
|
|||||||
uptime: number // seconds
|
uptime: number // seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetServerLogsReq = ServerLogsReq // server.logs & server.kernel-logs
|
export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs & server.tor-logs
|
||||||
export type GetServerLogsRes = LogsRes
|
export type GetServerLogsRes = FetchLogsRes
|
||||||
|
|
||||||
export type FollowServerLogsReq = { limit?: number } // server.logs.follow & server.kernel-logs.follow
|
export type FollowServerLogsReq = FollowLogsReq & { limit?: number } // server.logs.follow & server.kernel-logs.follow & server.tor-logs.follow
|
||||||
export type FollowServerLogsRes = {
|
export type FollowServerLogsRes = FollowLogsRes
|
||||||
'start-cursor': string
|
|
||||||
guid: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GetServerMetricsReq = {} // server.metrics
|
export type GetServerMetricsReq = {} // server.metrics
|
||||||
export type GetServerMetricsRes = {
|
export type GetServerMetricsRes = {
|
||||||
@@ -109,17 +112,29 @@ export module RR {
|
|||||||
|
|
||||||
// notification
|
// notification
|
||||||
|
|
||||||
|
export type FollowNotificationsReq = {}
|
||||||
|
export type FollowNotificationsRes = {
|
||||||
|
notifications: ServerNotifications
|
||||||
|
guid: string
|
||||||
|
}
|
||||||
|
|
||||||
export type GetNotificationsReq = {
|
export type GetNotificationsReq = {
|
||||||
before?: number
|
before?: number
|
||||||
limit?: number
|
limit?: number
|
||||||
} // notification.list
|
} // notification.list
|
||||||
export type GetNotificationsRes = ServerNotification<number>[]
|
export type GetNotificationsRes = ServerNotification<number>[]
|
||||||
|
|
||||||
export type DeleteNotificationReq = { id: number } // notification.delete
|
export type DeleteNotificationReq = { ids: number[] } // notification.delete
|
||||||
export type DeleteNotificationRes = null
|
export type DeleteNotificationRes = null
|
||||||
|
|
||||||
export type DeleteAllNotificationsReq = { before: number } // notification.delete-before
|
export type MarkSeenNotificationReq = DeleteNotificationReq // notification.mark-seen
|
||||||
export type DeleteAllNotificationsRes = null
|
export type MarkSeenNotificationRes = null
|
||||||
|
|
||||||
|
export type MarkSeenAllNotificationsReq = { before: number } // notification.mark-seen-before
|
||||||
|
export type MarkSeenAllNotificationsRes = null
|
||||||
|
|
||||||
|
export type MarkUnseenNotificationReq = DeleteNotificationReq // notification.mark-unseen
|
||||||
|
export type MarkUnseenNotificationRes = null
|
||||||
|
|
||||||
// network
|
// network
|
||||||
|
|
||||||
@@ -298,8 +313,8 @@ export module RR {
|
|||||||
export type GetPackageCredentialsReq = { id: string } // package.credentials
|
export type GetPackageCredentialsReq = { id: string } // package.credentials
|
||||||
export type GetPackageCredentialsRes = Record<string, string>
|
export type GetPackageCredentialsRes = Record<string, string>
|
||||||
|
|
||||||
export type GetPackageLogsReq = ServerLogsReq & { id: string } // package.logs
|
export type GetPackageLogsReq = FetchLogsReq & { id: string } // package.logs
|
||||||
export type GetPackageLogsRes = LogsRes
|
export type GetPackageLogsRes = FetchLogsRes
|
||||||
|
|
||||||
export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow
|
export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow
|
||||||
export type FollowPackageLogsRes = FollowServerLogsRes
|
export type FollowPackageLogsRes = FollowServerLogsRes
|
||||||
@@ -562,7 +577,7 @@ export interface SSHKey {
|
|||||||
fingerprint: string
|
fingerprint: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerNotifications = ServerNotification<any>[]
|
export type ServerNotifications = ServerNotification<number>[]
|
||||||
|
|
||||||
export interface ServerNotification<T extends number> {
|
export interface ServerNotification<T extends number> {
|
||||||
id: number
|
id: number
|
||||||
@@ -573,6 +588,7 @@ export interface ServerNotification<T extends number> {
|
|||||||
title: string
|
title: string
|
||||||
message: string
|
message: string
|
||||||
data: NotificationData<T>
|
data: NotificationData<T>
|
||||||
|
read: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NotificationLevel {
|
export enum NotificationLevel {
|
||||||
|
|||||||
@@ -125,13 +125,21 @@ export abstract class ApiService {
|
|||||||
params: RR.GetNotificationsReq,
|
params: RR.GetNotificationsReq,
|
||||||
): Promise<RR.GetNotificationsRes>
|
): Promise<RR.GetNotificationsRes>
|
||||||
|
|
||||||
abstract deleteNotification(
|
abstract markSeenNotifications(
|
||||||
|
params: RR.MarkSeenNotificationReq,
|
||||||
|
): Promise<RR.MarkSeenNotificationRes>
|
||||||
|
|
||||||
|
abstract markSeenAllNotifications(
|
||||||
|
params: RR.MarkSeenAllNotificationsReq,
|
||||||
|
): Promise<RR.MarkSeenAllNotificationsRes>
|
||||||
|
|
||||||
|
abstract markUnseenNotifications(
|
||||||
params: RR.DeleteNotificationReq,
|
params: RR.DeleteNotificationReq,
|
||||||
): Promise<RR.DeleteNotificationRes>
|
): Promise<RR.DeleteNotificationRes>
|
||||||
|
|
||||||
abstract deleteAllNotifications(
|
abstract deleteNotifications(
|
||||||
params: RR.DeleteAllNotificationsReq,
|
params: RR.DeleteNotificationReq,
|
||||||
): Promise<RR.DeleteAllNotificationsRes>
|
): Promise<RR.DeleteNotificationRes>
|
||||||
|
|
||||||
// network
|
// network
|
||||||
|
|
||||||
@@ -308,8 +316,6 @@ export abstract class ApiService {
|
|||||||
|
|
||||||
abstract getSetupStatus(): Promise<SetupStatus | null>
|
abstract getSetupStatus(): Promise<SetupStatus | null>
|
||||||
|
|
||||||
abstract followLogs(): Promise<string>
|
|
||||||
|
|
||||||
abstract setInterfaceClearnetAddress(
|
abstract setInterfaceClearnetAddress(
|
||||||
params: RR.SetInterfaceClearnetAddressReq,
|
params: RR.SetInterfaceClearnetAddressReq,
|
||||||
): Promise<RR.SetInterfaceClearnetAddressRes>
|
): Promise<RR.SetInterfaceClearnetAddressRes>
|
||||||
|
|||||||
@@ -117,10 +117,6 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.openWebsocket(config)
|
return this.openWebsocket(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
async followLogs(): Promise<string> {
|
|
||||||
return this.rpcRequest({ method: 'setup.logs.follow', params: {} })
|
|
||||||
}
|
|
||||||
|
|
||||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||||
return this.openWebsocket(config)
|
return this.openWebsocket(config)
|
||||||
}
|
}
|
||||||
@@ -259,21 +255,33 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.rpcRequest({ method: 'notification.list', params })
|
return this.rpcRequest({ method: 'notification.list', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteNotification(
|
async deleteNotifications(
|
||||||
params: RR.DeleteNotificationReq,
|
params: RR.DeleteNotificationReq,
|
||||||
): Promise<RR.DeleteNotificationRes> {
|
): Promise<RR.DeleteNotificationRes> {
|
||||||
return this.rpcRequest({ method: 'notification.delete', params })
|
return this.rpcRequest({ method: 'notification.delete', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAllNotifications(
|
async markSeenNotifications(
|
||||||
params: RR.DeleteAllNotificationsReq,
|
params: RR.MarkSeenNotificationReq,
|
||||||
): Promise<RR.DeleteAllNotificationsRes> {
|
): Promise<RR.MarkSeenNotificationRes> {
|
||||||
|
return this.rpcRequest({ method: 'notification.mark-seen', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
async markSeenAllNotifications(
|
||||||
|
params: RR.MarkSeenAllNotificationsReq,
|
||||||
|
): Promise<RR.MarkSeenAllNotificationsRes> {
|
||||||
return this.rpcRequest({
|
return this.rpcRequest({
|
||||||
method: 'notification.delete-before',
|
method: 'notification.mark-seen-before',
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async markUnseenNotifications(
|
||||||
|
params: RR.MarkUnseenNotificationReq,
|
||||||
|
): Promise<RR.MarkUnseenNotificationRes> {
|
||||||
|
return this.rpcRequest({ method: 'notification.mark-unseen', params })
|
||||||
|
}
|
||||||
|
|
||||||
// network
|
// network
|
||||||
|
|
||||||
async addProxy(params: RR.AddProxyReq): Promise<RR.AddProxyRes> {
|
async addProxy(params: RR.AddProxyReq): Promise<RR.AddProxyRes> {
|
||||||
|
|||||||
@@ -473,28 +473,34 @@ export class MockApiService extends ApiService {
|
|||||||
params: RR.GetNotificationsReq,
|
params: RR.GetNotificationsReq,
|
||||||
): Promise<RR.GetNotificationsRes> {
|
): Promise<RR.GetNotificationsRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
const patch = [
|
|
||||||
{
|
|
||||||
op: PatchOp.REPLACE,
|
|
||||||
path: '/server-info/unread-notification-count',
|
|
||||||
value: 0,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
this.mockRevision(patch)
|
|
||||||
|
|
||||||
return Mock.Notifications
|
return Mock.Notifications
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteNotification(
|
async deleteNotifications(
|
||||||
params: RR.DeleteNotificationReq,
|
params: RR.DeleteNotificationReq,
|
||||||
): Promise<RR.DeleteNotificationRes> {
|
): Promise<RR.DeleteNotificationRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAllNotifications(
|
async markSeenNotifications(
|
||||||
params: RR.DeleteAllNotificationsReq,
|
params: RR.MarkSeenNotificationReq,
|
||||||
): Promise<RR.DeleteAllNotificationsRes> {
|
): Promise<RR.MarkSeenNotificationRes> {
|
||||||
|
await pauseFor(2000)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async markSeenAllNotifications(
|
||||||
|
params: RR.MarkSeenAllNotificationsReq,
|
||||||
|
): Promise<RR.MarkSeenAllNotificationsRes> {
|
||||||
|
await pauseFor(2000)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async markUnseenNotifications(
|
||||||
|
params: RR.MarkUnseenNotificationReq,
|
||||||
|
): Promise<RR.MarkUnseenNotificationRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -1244,11 +1250,6 @@ export class MockApiService extends ApiService {
|
|||||||
return getSetupStatusMock()
|
return getSetupStatusMock()
|
||||||
}
|
}
|
||||||
|
|
||||||
async followLogs(): Promise<string> {
|
|
||||||
await pauseFor(1000)
|
|
||||||
return 'fake-guid'
|
|
||||||
}
|
|
||||||
|
|
||||||
async setInterfaceClearnetAddress(
|
async setInterfaceClearnetAddress(
|
||||||
params: RR.SetInterfaceClearnetAddressReq,
|
params: RR.SetInterfaceClearnetAddressReq,
|
||||||
): Promise<RR.SetInterfaceClearnetAddressRes> {
|
): Promise<RR.SetInterfaceClearnetAddressRes> {
|
||||||
|
|||||||
@@ -88,7 +88,10 @@ export const mockPatchData: DataModel = {
|
|||||||
outboundProxy: null,
|
outboundProxy: null,
|
||||||
},
|
},
|
||||||
'last-backup': new Date(new Date().valueOf() - 604800001).toISOString(),
|
'last-backup': new Date(new Date().valueOf() - 604800001).toISOString(),
|
||||||
'unread-notification-count': 4,
|
unreadNotifications: {
|
||||||
|
count: 4,
|
||||||
|
recent: Mock.Notifications,
|
||||||
|
},
|
||||||
'eos-version-compat': '>=0.3.0 <=0.3.0.1',
|
'eos-version-compat': '>=0.3.0 <=0.3.0.1',
|
||||||
'status-info': {
|
'status-info': {
|
||||||
'current-backup': null,
|
'current-backup': null,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes'
|
import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes'
|
||||||
import { Url } from '@start9labs/shared'
|
import { Url } from '@start9labs/shared'
|
||||||
import { Manifest } from '@start9labs/marketplace'
|
import { Manifest } from '@start9labs/marketplace'
|
||||||
import { BackupJob } from '../api/api.types'
|
import { BackupJob, ServerNotifications } from '../api/api.types'
|
||||||
import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants'
|
import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants'
|
||||||
import { NetworkInterfaceType } from '@start9labs/start-sdk/lib/util/utils'
|
import { NetworkInterfaceType } from '@start9labs/start-sdk/lib/util/utils'
|
||||||
import { DependencyInfo } from 'src/app/apps/portal/routes/service/types/dependency-info'
|
import { DependencyInfo } from 'src/app/apps/portal/routes/service/types/dependency-info'
|
||||||
@@ -61,7 +61,10 @@ export interface ServerInfo {
|
|||||||
ui: AddressInfo
|
ui: AddressInfo
|
||||||
network: NetworkInfo
|
network: NetworkInfo
|
||||||
'last-backup': string | null
|
'last-backup': string | null
|
||||||
'unread-notification-count': number
|
unreadNotifications: {
|
||||||
|
count: number
|
||||||
|
recent: ServerNotifications
|
||||||
|
}
|
||||||
'status-info': ServerStatusInfo
|
'status-info': ServerStatusInfo
|
||||||
'eos-version-compat': string
|
'eos-version-compat': string
|
||||||
pubkey: string
|
pubkey: string
|
||||||
|
|||||||
@@ -411,6 +411,7 @@ ul {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
margin: 2rem 0 1rem;
|
margin: 2rem 0 1rem;
|
||||||
color: var(--tui-text-02);
|
color: var(--tui-text-02);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user