refactor: completely remove ionic

This commit is contained in:
waterplea
2024-04-05 12:06:02 +07:00
parent b2c8907635
commit 8594781780
291 changed files with 416 additions and 3365 deletions

View File

@@ -0,0 +1,140 @@
import { inject, Injectable } from '@angular/core'
import { ErrorService, LoadingService } from '@start9labs/shared'
import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
import { defaultIfEmpty, filter, firstValueFrom } from 'rxjs'
import {
PackageConfigData,
ConfigModal,
} from 'src/app/routes/portal/modals/config.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { hasCurrentDeps } from 'src/app/utils/has-deps'
import { getAllPackages } from 'src/app/utils/get-package-data'
import { PatchDB } from 'patch-db-client'
import { Manifest } from '../../../../../../core/startos/bindings/Manifest'
@Injectable({
providedIn: 'root',
})
export class ActionsService {
private readonly dialogs = inject(TuiDialogService)
private readonly errorService = inject(ErrorService)
private readonly loader = inject(LoadingService)
private readonly api = inject(ApiService)
private readonly formDialog = inject(FormDialogService)
private readonly patch = inject(PatchDB<DataModel>)
configure(manifest: Manifest): void {
this.formDialog.open<PackageConfigData>(ConfigModal, {
label: `${manifest.title} configuration`,
data: { pkgId: manifest.id },
})
}
async start(manifest: Manifest, unmet: boolean): Promise<void> {
const deps = `${manifest.title} has unmet dependencies. It will not work as expected.`
if (
(!unmet || (await this.alert(deps))) &&
(!manifest.alerts.start || (await this.alert(manifest.alerts.start)))
) {
this.doStart(manifest.id)
}
}
async stop({ id, title, alerts }: Manifest): Promise<void> {
let content = alerts.stop || ''
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
const depMessage = `Services that depend on ${title} will no longer work properly and may crash`
content = content ? `${content}.\n\n${depMessage}` : depMessage
}
if (content) {
this.dialogs
.open(TUI_PROMPT, getOptions(content, 'Stop'))
.pipe(filter(Boolean))
.subscribe(() => this.doStop(id))
} else {
this.doStop(id)
}
}
async restart({ id, title }: Manifest): Promise<void> {
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
this.dialogs
.open(
TUI_PROMPT,
getOptions(
`Services that depend on ${title} may temporarily experiences issues`,
'Restart',
),
)
.pipe(filter(Boolean))
.subscribe(() => this.doRestart(id))
} else {
this.doRestart(id)
}
}
private async doStart(id: string): Promise<void> {
const loader = this.loader.open(`Starting...`).subscribe()
try {
await this.api.startPackage({ id })
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
}
private async doStop(id: string): Promise<void> {
const loader = this.loader.open(`Stopping...`).subscribe()
try {
await this.api.stopPackage({ id })
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
}
private async doRestart(id: string): Promise<void> {
const loader = this.loader.open(`Restarting...`).subscribe()
try {
await this.api.restartPackage({ id })
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
}
private alert(content: string): Promise<boolean> {
return firstValueFrom(
this.dialogs
.open<boolean>(TUI_PROMPT, getOptions(content))
.pipe(defaultIfEmpty(false)),
)
}
}
function getOptions(
content: string,
yes = 'Continue',
): Partial<TuiDialogOptions<TuiPromptData>> {
return {
label: 'Warning',
size: 's',
data: {
content,
yes,
no: 'Cancel',
},
}
}

View File

@@ -11,7 +11,7 @@ import {
} from './api.types'
import { DependencyMetadata, MarketplacePkg } from '@start9labs/marketplace'
import { Log } from '@start9labs/shared'
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { CT, T, CB } from '@start9labs/start-sdk'
const BTC_ICON = '/assets/img/service-icons/bitcoind.svg'

View File

@@ -18,7 +18,7 @@ import { AuthService } from '../auth.service'
import { DOCUMENT } from '@angular/common'
import { DataModel } from '../patch-db/data-model'
import { PatchDB, pathFromArray, Update } from 'patch-db-client'
import { getServerInfo } from 'src/app/util/get-server-info'
import { getServerInfo } from 'src/app/utils/get-server-info'
@Injectable()
export class LiveApiService extends ApiService {

View File

@@ -0,0 +1,92 @@
import { inject, Injectable } from '@angular/core'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { Emver } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client'
import {
combineLatest,
EMPTY,
filter,
first,
map,
Observable,
pairwise,
shareReplay,
startWith,
switchMap,
} from 'rxjs'
import { EOSService } from 'src/app/services/eos.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { ConnectionService } from 'src/app/services/connection.service'
import { getManifest } from 'src/app/utils/get-package-data'
@Injectable({
providedIn: 'root',
})
export class BadgeService {
private readonly emver = inject(Emver)
private readonly patch = inject(PatchDB<DataModel>)
private readonly settings$ = combineLatest([
this.patch.watch$('serverInfo', 'ntpSynced'),
inject(EOSService).updateAvailable$,
]).pipe(map(([synced, update]) => Number(!synced) + Number(update)))
private readonly marketplace = inject(
AbstractMarketplaceService,
) as MarketplaceService
private readonly local$ = inject(ConnectionService).connected$.pipe(
filter(Boolean),
switchMap(() => this.patch.watch$('packageData').pipe(first())),
switchMap(outer =>
this.patch.watch$('packageData').pipe(
pairwise(),
filter(([prev, curr]) =>
Object.values(prev).some(p => {
const { id } = getManifest(p)
return (
!curr[id] ||
(p.stateInfo.installingInfo && !curr[id].stateInfo.installingInfo)
)
}),
),
map(([_, curr]) => curr),
startWith(outer),
),
),
)
private readonly updates$ = combineLatest([
this.marketplace.getMarketplace$(true),
this.local$,
]).pipe(
map(
([marketplace, local]) =>
Object.entries(marketplace).reduce(
(list, [_, store]) =>
store?.packages.reduce(
(result, { manifest: { id, version } }) =>
local[id] &&
this.emver.compare(version, getManifest(local[id]).version) ===
1
? result.add(id)
: result,
list,
) || list,
new Set<string>(),
).size,
),
shareReplay(1),
)
getCount(id: string): Observable<number> {
switch (id) {
case '/portal/system/updates':
return this.updates$
case '/portal/system/settings':
return this.settings$
default:
return EMPTY
}
}
}

View File

@@ -0,0 +1,88 @@
import { inject, Injectable } from '@angular/core'
import { PatchDB } from 'patch-db-client'
import { BehaviorSubject } from 'rxjs'
import {
DataModel,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { SYSTEM_UTILITIES } from 'src/app/utils/system-utilities'
import { toRouterLink } from 'src/app/utils/to-router-link'
import { getAllPackages, getManifest } from 'src/app/utils/get-package-data'
export interface Breadcrumb {
title: string
routerLink: string
subtitle?: string
icon?: string
}
@Injectable({
providedIn: 'root',
})
export class BreadcrumbsService extends BehaviorSubject<readonly Breadcrumb[]> {
private readonly patch = inject(PatchDB<DataModel>)
constructor() {
super([])
}
async update(page: string) {
const packages = await getAllPackages(this.patch)
try {
this.next(toBreadcrumbs(page.split('?')[0], packages))
} catch (e) {
this.next([])
}
}
}
function toBreadcrumbs(
id: string,
packages: Record<string, PackageDataEntry> = {},
): Breadcrumb[] {
const item = SYSTEM_UTILITIES[id]
const routerLink = toRouterLink(id)
if (id.startsWith('/portal/system/')) {
const [page, ...path] = id.replace('/portal/system/', '').split('/')
const service = `/portal/system/${page}`
const { icon, title } = SYSTEM_UTILITIES[service]
const breadcrumbs: Breadcrumb[] = [
{
icon,
title,
routerLink: toRouterLink(service),
},
]
if (path.length) {
breadcrumbs.push({
title: path.join(': '),
routerLink: breadcrumbs[0].routerLink + '/' + path.join('/'),
})
}
return breadcrumbs
}
const [service, ...path] = id.split('/')
const { title, version } = getManifest(packages[service])
const breadcrumbs: Breadcrumb[] = [
{
icon: packages[service].icon,
title,
subtitle: version,
routerLink: toRouterLink(service),
},
]
if (path.length) {
breadcrumbs.push({
title: path.join(': '),
routerLink: breadcrumbs[0].routerLink + '/' + path.join('/'),
})
}
return breadcrumbs
}

View File

@@ -9,7 +9,7 @@ import {
} from './patch-db/data-model'
import * as deepEqual from 'fast-deep-equal'
import { Observable } from 'rxjs'
import { isInstalled } from '../util/get-package-data'
import { isInstalled } from 'src/app/utils/get-package-data'
import { DependencyError } from './api/api.types'
export type AllDependencyErrors = Record<string, PkgDependencyErrors>

View File

@@ -4,7 +4,7 @@ import { PatchDB } from 'patch-db-client'
import { BehaviorSubject, distinctUntilChanged, map, combineLatest } from 'rxjs'
import { MarketplaceEOS } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { getServerInfo } from 'src/app/util/get-server-info'
import { getServerInfo } from 'src/app/utils/get-server-info'
import { DataModel } from './patch-db/data-model'
@Injectable({

View File

@@ -7,7 +7,7 @@ import {
ValidatorFn,
Validators,
} from '@angular/forms'
import { getDefaultString } from '../util/config-utilities'
import { getDefaultString } from 'src/app/utils/config-utilities'
import { CT } from '@start9labs/start-sdk'
const Mustache = require('mustache')

View File

@@ -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 'src/app/components/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$('serverInfo', '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.createdAt,
},
})
.subscribe()
}
private async updateCount(toAdjust: number) {
const currentCount = await firstValueFrom(this.unreadCount$)
this.localUnreadCount$.next(Math.max(currentCount + toAdjust, 0))
}
}

View File

@@ -5,7 +5,7 @@ import { filter, share, switchMap, take, tap, Observable } from 'rxjs'
import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { EOSService } from 'src/app/services/eos.service'
import { OSWelcomePage } from '../common/os-welcome/os-welcome.page'
import { WelcomeComponent } from 'src/app/components/welcome.component'
import { ConfigService } from 'src/app/services/config.service'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
@@ -54,7 +54,7 @@ export class PatchDataService extends Observable<DataModel> {
}
this.dialogs
.open(new PolymorpheusComponent(OSWelcomePage), {
.open(new PolymorpheusComponent(WelcomeComponent), {
label: 'Release Notes',
})
.subscribe({

View File

@@ -6,9 +6,9 @@ import { firstValueFrom } from 'rxjs'
import {
FormComponent,
FormContext,
} from 'src/app/apps/portal/components/form.component'
} from 'src/app/routes/portal/components/form.component'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { ApiService } from './api/embassy-api.service'
import { DataModel } from './patch-db/data-model'
import { CB } from '@start9labs/start-sdk'

View File

@@ -1,21 +0,0 @@
import { Injectable } from '@angular/core'
import { BehaviorSubject, Observable } from 'rxjs'
@Injectable({
providedIn: 'root',
})
export class SidebarService {
openMap: Record<string, BehaviorSubject<boolean>> = {}
setMap(ids: string[]) {
ids.map(i => (this.openMap[i] = new BehaviorSubject(false)))
}
getToggleState(pkgId: string): Observable<boolean> {
return this.openMap[pkgId]
}
toggleState(pkgId: string, open: boolean) {
this.openMap[pkgId].next(open)
}
}

View File

@@ -1,9 +0,0 @@
import { BehaviorSubject } from 'rxjs'
import { Injectable } from '@angular/core'
@Injectable({
providedIn: 'root',
})
export class SplitPaneTracker {
readonly sidebarOpen$ = new BehaviorSubject<boolean>(false)
}

View File

@@ -1,59 +0,0 @@
import { Injectable } from '@angular/core'
import { map, shareReplay, startWith, switchMap } from 'rxjs/operators'
import { PatchDB } from 'patch-db-client'
import { DataModel } from './patch-db/data-model'
import { ApiService } from './api/embassy-api.service'
import { combineLatest, interval, of } from 'rxjs'
@Injectable({
providedIn: 'root',
})
export class TimeService {
private readonly time$ = of({}).pipe(
switchMap(() => this.apiService.getSystemTime({})),
switchMap(({ now, uptime }) => {
const current = new Date(now).valueOf()
return interval(1000).pipe(
map(index => {
const incremented = index + 1
return {
now: current + 1000 * incremented,
uptime: uptime + incremented,
}
}),
startWith({
now: current,
uptime,
}),
)
}),
shareReplay({ bufferSize: 1, refCount: true }),
)
readonly now$ = combineLatest([
this.time$,
this.patch.watch$('serverInfo', 'ntpSynced'),
]).pipe(
map(([time, synced]) => ({
value: time.now,
synced,
})),
)
readonly uptime$ = this.time$.pipe(
map(({ uptime }) => {
const days = Math.floor(uptime / (24 * 60 * 60))
const daysSec = uptime % (24 * 60 * 60)
const hours = Math.floor(daysSec / (60 * 60))
const hoursSec = uptime % (60 * 60)
const minutes = Math.floor(hoursSec / 60)
const seconds = uptime % 60
return { days, hours, minutes, seconds }
}),
)
constructor(
private readonly patch: PatchDB<DataModel>,
private readonly apiService: ApiService,
) {}
}