mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
feat: redesign service route (#2835)
* feat: redesign service route * chore: more changes * remove automated backups and fix interface addresses * fix rpc methods and slightly better mocks --------- Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
@@ -83,7 +83,7 @@
|
||||
|
||||
<ng-template #trusted>
|
||||
<div tuiCardLarge tuiSurface="floating" class="card">
|
||||
<tui-icon icon="@tui.shield" class="g-success" [style.font-size.rem]="4" />
|
||||
<tui-icon icon="@tui.shield" class="g-positive" [style.font-size.rem]="4" />
|
||||
<h1>Root CA Trusted!</h1>
|
||||
<p>
|
||||
You have successfully trusted your server's Root CA and may now log in
|
||||
|
||||
@@ -13,30 +13,29 @@ import { AddressDetails, AddressesService } from './interface.utils'
|
||||
selector: 'app-address-group',
|
||||
template: `
|
||||
<div>
|
||||
@if (addresses.length) {
|
||||
@if (addresses.length && !service.static) {
|
||||
<button
|
||||
class="icon-add-btn"
|
||||
tuiIconButton
|
||||
appearance="secondary"
|
||||
iconStart="@tui.plus"
|
||||
(click)="service.add()"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
></button>
|
||||
}
|
||||
<ng-content />
|
||||
</div>
|
||||
@for (address of addresses; track $index) {
|
||||
<app-address-item [label]="address.label" [address]="address.url" />
|
||||
} @empty {
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.align-self]="'flex-start'"
|
||||
(click)="service.add()"
|
||||
>
|
||||
Add Address
|
||||
</button>
|
||||
@if (!service.static) {
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.align-self]="'flex-start'"
|
||||
(click)="service.add()"
|
||||
>
|
||||
Add Address
|
||||
</button>
|
||||
}
|
||||
}
|
||||
`,
|
||||
imports: [AddressItemComponent, TuiButton],
|
||||
|
||||
@@ -7,15 +7,16 @@ import {
|
||||
FormComponent,
|
||||
FormContext,
|
||||
} from 'src/app/routes/portal/components/form.component'
|
||||
import { getClearnetSpec } from 'src/app/routes/portal/components/interfaces/interface.utils'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { NetworkInfo } from 'src/app/services/patch-db/data-model'
|
||||
import { InterfaceComponent } from '../interface.component'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
import { toAcmeName } from 'src/app/utils/acme'
|
||||
|
||||
type ClearnetForm = {
|
||||
domain: string
|
||||
subdomain: string | null
|
||||
acme: string
|
||||
}
|
||||
|
||||
@Directive({
|
||||
@@ -32,18 +33,40 @@ export class ClearnetAddressesDirective implements AddressesService {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly interface = inject(InterfaceComponent)
|
||||
|
||||
@Input({ required: true }) network!: NetworkInfo
|
||||
@Input({ required: true }) acme!: string[]
|
||||
|
||||
static = false
|
||||
|
||||
async add() {
|
||||
const options: Partial<TuiDialogOptions<FormContext<ClearnetForm>>> = {
|
||||
label: 'Select Domain/Subdomain',
|
||||
data: {
|
||||
spec: await getClearnetSpec(this.network),
|
||||
spec: await configBuilderToSpec(
|
||||
ISB.InputSpec.of({
|
||||
domain: ISB.Value.text({
|
||||
name: 'Domain',
|
||||
description: 'The domain or subdomain you want to use',
|
||||
placeholder: `e.g. 'mydomain.com' or 'sub.mydomain.com'`,
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
}),
|
||||
acme: ISB.Value.select({
|
||||
name: 'ACME Provider',
|
||||
description:
|
||||
'Select which ACME provider to use for obtaining your SSL certificate. Add new ACME providers in the System tab. Optionally use your system Root CA. Note: only devices that have trusted your Root CA will be able to access the domain without security warnings.',
|
||||
values: this.acme.reduce(
|
||||
(obj, url) => ({
|
||||
...obj,
|
||||
[url]: toAcmeName(url),
|
||||
}),
|
||||
{ none: 'None (use system Root CA)' } as Record<string, string>,
|
||||
),
|
||||
default: '',
|
||||
}),
|
||||
}),
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Manage domains',
|
||||
link: 'portal/settings/domains',
|
||||
},
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async value => this.save(value),
|
||||
@@ -59,14 +82,23 @@ export class ClearnetAddressesDirective implements AddressesService {
|
||||
private async save(domainInfo: ClearnetForm): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
const { domain, acme } = domainInfo
|
||||
|
||||
const params = {
|
||||
domain,
|
||||
acme: acme === 'none' ? null : acme,
|
||||
private: false,
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.interface.packageContext) {
|
||||
await this.api.setInterfaceClearnetAddress({
|
||||
...this.interface.packageContext,
|
||||
domainInfo,
|
||||
if (this.interface.packageId) {
|
||||
await this.api.pkgAddDomain({
|
||||
...params,
|
||||
package: this.interface.packageId,
|
||||
host: this.interface.serviceInterface.addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.setServerClearnetAddress({ domainInfo })
|
||||
await this.api.serverAddDomain(params)
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AddressesService } from '../interface.utils'
|
||||
],
|
||||
})
|
||||
export class LocalAddressesDirective implements AddressesService {
|
||||
static = true
|
||||
async add() {}
|
||||
async remove() {}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { Directive } from '@angular/core'
|
||||
import { Directive, inject } from '@angular/core'
|
||||
import { AddressesService } from '../interface.utils'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { TuiDialogOptions } from '@taiga-ui/core'
|
||||
import { FormComponent, FormContext } from '../../form.component'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { InterfaceComponent } from '../interface.component'
|
||||
|
||||
type OnionForm = {
|
||||
key: string
|
||||
}
|
||||
|
||||
@Directive({
|
||||
standalone: true,
|
||||
@@ -9,6 +21,67 @@ import { AddressesService } from '../interface.utils'
|
||||
],
|
||||
})
|
||||
export class TorAddressesDirective implements AddressesService {
|
||||
async add() {}
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly interface = inject(InterfaceComponent)
|
||||
|
||||
static = false
|
||||
|
||||
async add() {
|
||||
const options: Partial<TuiDialogOptions<FormContext<OnionForm>>> = {
|
||||
label: 'Select Domain/Subdomain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(
|
||||
ISB.InputSpec.of({
|
||||
key: ISB.Value.text({
|
||||
name: 'Private Key (optional)',
|
||||
description:
|
||||
'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) address. If not provided, a random key will be generated and used.',
|
||||
required: false,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.base64],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async value => this.save(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
this.formDialog.open(FormComponent, options)
|
||||
}
|
||||
|
||||
async remove() {}
|
||||
|
||||
private async save(form: OnionForm): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
try {
|
||||
let onion = form.key
|
||||
? await this.api.addTorKey({ key: form.key })
|
||||
: await this.api.generateTorKey({})
|
||||
onion = `${onion}.onion`
|
||||
|
||||
if (this.interface.packageId) {
|
||||
await this.api.pkgAddOnion({
|
||||
onion,
|
||||
package: this.interface.packageId,
|
||||
host: this.interface.serviceInterface.addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverAddOnion({ onion })
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@ import { PatchDB } from 'patch-db-client'
|
||||
import { AddressGroupComponent } from 'src/app/routes/portal/components/interfaces/address-group.component'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { ClearnetAddressesDirective } from './directives/clearnet.directive'
|
||||
import { LocalAddressesDirective } from './directives/local.directive'
|
||||
import { TorAddressesDirective } from './directives/tor.directive'
|
||||
import { LocalAddressesDirective } from './directives/local.directive'
|
||||
import { AddressDetails } from './interface.utils'
|
||||
import { map } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -22,11 +23,11 @@ import { AddressDetails } from './interface.utils'
|
||||
template: `
|
||||
<h3 class="g-title">Clearnet</h3>
|
||||
<app-address-group
|
||||
*ngIf="network$ | async as network"
|
||||
*ngIf="acme$ | async as acme"
|
||||
clearnetAddresses
|
||||
tuiCardLarge="compact"
|
||||
tuiSurface="floating"
|
||||
[network]="network"
|
||||
[acme]="acme"
|
||||
[addresses]="serviceInterface.addresses.clearnet"
|
||||
>
|
||||
<em>
|
||||
@@ -70,9 +71,8 @@ import { AddressDetails } from './interface.utils'
|
||||
[addresses]="serviceInterface.addresses.local"
|
||||
>
|
||||
<em>
|
||||
Add a local address to expose this interface on your Local Area Network
|
||||
(LAN). Local addresses can only be accessed by devices connected to the
|
||||
same LAN as your server, either directly or using a VPN.
|
||||
Local addresses can only be accessed by devices connected to the same
|
||||
LAN as your server, either directly or using a VPN.
|
||||
<a
|
||||
href="https://docs.start9.com/latest/user-manual/interface-addresses#local"
|
||||
target="_blank"
|
||||
@@ -95,15 +95,11 @@ import { AddressDetails } from './interface.utils'
|
||||
],
|
||||
})
|
||||
export class InterfaceComponent {
|
||||
readonly network$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
|
||||
'serverInfo',
|
||||
'network',
|
||||
)
|
||||
readonly acme$ = inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('serverInfo', 'acme')
|
||||
.pipe(map(acme => Object.keys(acme)))
|
||||
|
||||
@Input() packageContext?: {
|
||||
packageId: string
|
||||
interfaceId: string
|
||||
}
|
||||
@Input() packageId?: string
|
||||
@Input({ required: true }) serviceInterface!: MappedServiceInterface
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { ISB, IST, T, utils } from '@start9labs/start-sdk'
|
||||
import { T, utils } from '@start9labs/start-sdk'
|
||||
import { TuiDialogOptions } from '@taiga-ui/core'
|
||||
import { TuiConfirmData } from '@taiga-ui/kit'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { NetworkInfo } from 'src/app/services/patch-db/data-model'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
|
||||
export abstract class AddressesService {
|
||||
abstract static: boolean
|
||||
abstract add(): Promise<void>
|
||||
abstract remove(): Promise<void>
|
||||
}
|
||||
@@ -20,37 +19,7 @@ export const REMOVE: Partial<TuiDialogOptions<TuiConfirmData>> = {
|
||||
},
|
||||
}
|
||||
|
||||
export function getClearnetSpec({
|
||||
domains,
|
||||
start9To,
|
||||
}: NetworkInfo): Promise<IST.InputSpec> {
|
||||
const start9ToDomain = `${start9To?.subdomain}.start9.to`
|
||||
const base = start9To ? { [start9ToDomain]: start9ToDomain } : {}
|
||||
|
||||
const values = Object.keys(domains).reduce((prev, curr) => {
|
||||
return {
|
||||
[curr]: curr,
|
||||
...prev,
|
||||
}
|
||||
}, base)
|
||||
|
||||
return configBuilderToSpec(
|
||||
ISB.InputSpec.of({
|
||||
domain: ISB.Value.select({
|
||||
name: 'Domain',
|
||||
default: '',
|
||||
values,
|
||||
}),
|
||||
subdomain: ISB.Value.text({
|
||||
name: 'Subdomain',
|
||||
required: false,
|
||||
default: '',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// @TODO Aiden audit
|
||||
// @TODO 040 Aiden audit
|
||||
export function getAddresses(
|
||||
serviceInterface: T.ServiceInterface,
|
||||
host: T.Host,
|
||||
@@ -143,8 +112,6 @@ export function getAddresses(
|
||||
// )
|
||||
}
|
||||
|
||||
function getLabel(name: string, url: string, multiple: boolean) {}
|
||||
|
||||
export type AddressDetails = {
|
||||
label: string
|
||||
url: string
|
||||
|
||||
@@ -17,17 +17,15 @@ const ROUTES: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'services',
|
||||
loadChildren: () =>
|
||||
import('./routes/services/services.module').then(
|
||||
m => m.ServicesModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: systemTabResolver,
|
||||
path: 'backups',
|
||||
loadComponent: () => import('./routes/backups/backups.component'),
|
||||
data: toNavigationItem('/portal/backups'),
|
||||
loadChildren: () => import('./routes/services/services.routes'),
|
||||
},
|
||||
// @TODO 041
|
||||
// {
|
||||
// title: systemTabResolver,
|
||||
// path: 'backups',
|
||||
// loadComponent: () => import('./routes/backups/backups.component'),
|
||||
// data: toNavigationItem('/portal/backups'),
|
||||
// },
|
||||
{
|
||||
title: systemTabResolver,
|
||||
path: 'logs',
|
||||
|
||||
@@ -41,7 +41,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
|
||||
<td class="available">
|
||||
<tui-icon
|
||||
[icon]="target.value.mountable ? '@tui.check' : '@tui.x'"
|
||||
[class]="target.value.mountable ? 'g-success' : 'g-error'"
|
||||
[class]="target.value.mountable ? 'g-positive' : 'g-negative'"
|
||||
/>
|
||||
</td>
|
||||
<td class="path">{{ target.value.path }}</td>
|
||||
|
||||
@@ -71,9 +71,9 @@ import { HasErrorPipe } from '../pipes/has-error.pipe'
|
||||
<td class="title">{{ run.job.name || 'No job' }}</td>
|
||||
<td class="result">
|
||||
@if (run.report | hasError) {
|
||||
<tui-icon icon="@tui.x" class="g-error" />
|
||||
<tui-icon icon="@tui.x" class="g-negative" />
|
||||
} @else {
|
||||
<tui-icon icon="@tui.check" class="g-success" />
|
||||
<tui-icon icon="@tui.check" class="g-positive" />
|
||||
}
|
||||
<button tuiLink (click)="showReport(run)">Report</button>
|
||||
</td>
|
||||
|
||||
@@ -6,92 +6,64 @@ import {
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiFade } from '@taiga-ui/kit'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { getDepDetails } from 'src/app/utils/dep-info'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
export type ActionRequest = T.ActionRequest & {
|
||||
actionName: string
|
||||
dependency: {
|
||||
title: string
|
||||
icon: string
|
||||
} | null
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'button[actionRequest]',
|
||||
template: `
|
||||
<tui-icon class="g-warning" [icon]="icon" />
|
||||
<span tuiTitle>
|
||||
<strong>{{ actionRequest.actionName }}</strong>
|
||||
@if (actionRequest.dependency) {
|
||||
<span tuiSubtitle>
|
||||
<strong>Service:</strong>
|
||||
<img
|
||||
alt=""
|
||||
[src]="actionRequest.dependency.icon"
|
||||
[style.width.rem]="1"
|
||||
/>
|
||||
{{ actionRequest.dependency.title }}
|
||||
</span>
|
||||
}
|
||||
<strong tuiFade><ng-content /></strong>
|
||||
<span tuiSubtitle>
|
||||
{{ actionRequest.reason || 'no reason provided' }}
|
||||
{{ actionRequest.reason || 'No reason provided' }}
|
||||
</span>
|
||||
</span>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
width: 100%;
|
||||
margin: 0 -1rem;
|
||||
}
|
||||
|
||||
strong {
|
||||
white-space: nowrap;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiIcon, TuiTitle],
|
||||
imports: [TuiTitle, TuiFade],
|
||||
hostDirectives: [TuiCell],
|
||||
})
|
||||
export class ServiceActionRequestComponent {
|
||||
private readonly actionService = inject(ActionService)
|
||||
|
||||
@Input({ required: true })
|
||||
actionRequest!: ActionRequest
|
||||
actionRequest!: T.ActionRequest
|
||||
|
||||
@Input({ required: true })
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
@Input({ required: true })
|
||||
allPkgs!: Record<string, PackageDataEntry>
|
||||
|
||||
get icon(): string {
|
||||
return this.actionRequest.severity === 'critical'
|
||||
? '@tui.triangle-alert'
|
||||
: '@tui.play'
|
||||
}
|
||||
|
||||
@HostListener('click')
|
||||
async handleAction() {
|
||||
const { id, title } = getManifest(this.pkg)
|
||||
const { title } = getManifest(this.pkg)
|
||||
const { actionId, packageId } = this.actionRequest
|
||||
const details = getDepDetails(this.pkg, this.allPkgs, packageId)
|
||||
const self = packageId === id
|
||||
|
||||
this.actionService.present({
|
||||
pkgInfo: {
|
||||
id: packageId,
|
||||
title: self ? title : details.title,
|
||||
mainStatus: self
|
||||
? this.pkg.status.main
|
||||
: this.allPkgs[packageId].status.main,
|
||||
icon: self ? this.pkg.icon : details.icon,
|
||||
title,
|
||||
mainStatus: this.pkg.status.main,
|
||||
icon: this.pkg.icon,
|
||||
},
|
||||
actionInfo: {
|
||||
id: actionId,
|
||||
metadata: self
|
||||
? this.pkg.actions[actionId]
|
||||
: this.allPkgs[packageId].actions[actionId],
|
||||
},
|
||||
requestInfo: {
|
||||
request: this.actionRequest,
|
||||
dependentId: self ? undefined : id,
|
||||
metadata: this.pkg.actions[actionId],
|
||||
},
|
||||
requestInfo: this.actionRequest,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { ServiceActionRequestComponent } from './action-request.component'
|
||||
|
||||
type ActionRequest = T.ActionRequest & {
|
||||
actionName: string
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'service-action-requests',
|
||||
template: `
|
||||
@for (request of requests().critical; track $index) {
|
||||
<button [actionRequest]="request" [pkg]="pkg()">
|
||||
{{ request.actionName }}
|
||||
<small class="g-warning">Required</small>
|
||||
</button>
|
||||
}
|
||||
@for (request of requests().important; track $index) {
|
||||
<button [actionRequest]="request" [pkg]="pkg()">
|
||||
{{ request.actionName }}
|
||||
<small class="g-info">Requested</small>
|
||||
</button>
|
||||
}
|
||||
@if (requests().critical.length + requests().important.length === 0) {
|
||||
<blockquote>No pending tasks</blockquote>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
small {
|
||||
margin-inline-start: 0.25rem;
|
||||
padding-inline-start: 0.5rem;
|
||||
box-shadow: inset 1px 0 var(--tui-border-normal);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
text-align: center;
|
||||
font: var(--tui-font-text-l);
|
||||
color: var(--tui-text-tertiary);
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ServiceActionRequestComponent],
|
||||
})
|
||||
export class ServiceActionRequestsComponent {
|
||||
readonly pkg = input.required<PackageDataEntry>()
|
||||
readonly requests = computed(() => {
|
||||
const { id } = getManifest(this.pkg())
|
||||
const critical: ActionRequest[] = []
|
||||
const important: ActionRequest[] = []
|
||||
|
||||
Object.values(this.pkg().requestedActions)
|
||||
.filter(r => r.active && r.request.packageId === id)
|
||||
.forEach(r => {
|
||||
const action = {
|
||||
...r.request,
|
||||
actionName: this.pkg().actions[r.request.actionId].name,
|
||||
}
|
||||
|
||||
if (r.request.severity === 'critical') {
|
||||
critical.push(action)
|
||||
} else {
|
||||
important.push(action)
|
||||
}
|
||||
})
|
||||
|
||||
return { critical, important }
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
@@ -7,22 +6,20 @@ import {
|
||||
} from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
import { tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { DependencyInfo } from 'src/app/routes/portal/routes/services/types/dependency-info'
|
||||
import { ControlsService } from '../../../../../services/controls.service'
|
||||
import { ControlsService } from 'src/app/services/controls.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { PackageStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { PrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
const STOPPABLE = ['running', 'starting', 'restarting']
|
||||
|
||||
@Component({
|
||||
selector: 'service-actions',
|
||||
template: `
|
||||
@if (canStop) {
|
||||
@if (['running', 'starting', 'restarting'].includes(status)) {
|
||||
<button
|
||||
tuiButton
|
||||
appearance="primary-destructive"
|
||||
appearance="outline-destructive"
|
||||
iconStart="@tui.square"
|
||||
(click)="actions.stop(manifest)"
|
||||
>
|
||||
@@ -30,9 +27,10 @@ const STOPPABLE = ['running', 'starting', 'restarting']
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (canRestart) {
|
||||
@if (status === 'running') {
|
||||
<button
|
||||
tuiButton
|
||||
appearance="outline"
|
||||
iconStart="@tui.rotate-cw"
|
||||
(click)="actions.restart(manifest)"
|
||||
>
|
||||
@@ -40,11 +38,12 @@ const STOPPABLE = ['running', 'starting', 'restarting']
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (canStart) {
|
||||
@if (status === 'stopped') {
|
||||
<button
|
||||
tuiButton
|
||||
appearance="outline"
|
||||
iconStart="@tui.play"
|
||||
(click)="actions.start(manifest, hasUnmet(service.dependencies))"
|
||||
(click)="actions.start(manifest, hasUnmet(dependencies))"
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
@@ -53,42 +52,32 @@ const STOPPABLE = ['running', 'starting', 'restarting']
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding-bottom: 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
margin-block-start: 1rem;
|
||||
}
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TuiButton],
|
||||
providers: [tuiButtonOptionsProvider({ size: 's' })],
|
||||
})
|
||||
export class ServiceActionsComponent {
|
||||
@Input({ required: true })
|
||||
service!: {
|
||||
pkg: PackageDataEntry
|
||||
dependencies: readonly DependencyInfo[]
|
||||
status: PackageStatus
|
||||
}
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
@Input({ required: true })
|
||||
status!: PrimaryStatus
|
||||
|
||||
// TODO
|
||||
dependencies: readonly DependencyInfo[] = []
|
||||
|
||||
readonly actions = inject(ControlsService)
|
||||
|
||||
get manifest(): T.Manifest {
|
||||
return getManifest(this.service.pkg)
|
||||
}
|
||||
|
||||
get canStop(): boolean {
|
||||
return STOPPABLE.includes(this.service.status.primary)
|
||||
}
|
||||
|
||||
get canStart(): boolean {
|
||||
return this.service.status.primary === 'stopped'
|
||||
}
|
||||
|
||||
get canRestart(): boolean {
|
||||
return this.service.status.primary === 'running'
|
||||
return getManifest(this.pkg)
|
||||
}
|
||||
|
||||
@tuiPure
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiIcon } from '@taiga-ui/core'
|
||||
import {
|
||||
AdditionalItem,
|
||||
FALLBACK_URL,
|
||||
} from 'src/app/routes/portal/routes/services/pipes/to-additional.pipe'
|
||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
|
||||
export const FALLBACK_URL = 'Not provided'
|
||||
export interface AdditionalItem {
|
||||
name: string
|
||||
description: string
|
||||
icon?: string
|
||||
action?: () => void
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: '[additionalItem]',
|
||||
template: `
|
||||
<div [style.flex]="1">
|
||||
<span tuiTitle>
|
||||
<strong>{{ additionalItem.name }}</strong>
|
||||
<div>{{ additionalItem.description }}</div>
|
||||
</div>
|
||||
<span tuiSubtitle>{{ additionalItem.description }}</span>
|
||||
</span>
|
||||
@if (icon) {
|
||||
<tui-icon [icon]="icon" />
|
||||
}
|
||||
@@ -33,7 +37,7 @@ import {
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TuiIcon],
|
||||
imports: [TuiIcon, TuiTitle],
|
||||
})
|
||||
export class ServiceAdditionalItemComponent {
|
||||
@Input({ required: true })
|
||||
@@ -1,84 +0,0 @@
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { I18nPluralPipe } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
@Component({
|
||||
selector: 'service-backups',
|
||||
template: `
|
||||
<div [style.flex]="1">
|
||||
<small>Last backup</small>
|
||||
{{ previous() | i18nPlural: ago }}
|
||||
</div>
|
||||
<div [style.flex]="1">
|
||||
<small>Next backup</small>
|
||||
{{ next() | i18nPlural: in }}
|
||||
</div>
|
||||
<div [style.min-width.%]="100">
|
||||
<a
|
||||
tuiButton
|
||||
iconStart="@tui.square-plus"
|
||||
routerLink="/portal/backups"
|
||||
size="s"
|
||||
appearance="warning"
|
||||
>
|
||||
Manage
|
||||
</a>
|
||||
</div>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
white-space: nowrap;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
small {
|
||||
display: block;
|
||||
text-transform: uppercase;
|
||||
color: var(--tui-text-secondary);
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TuiButton, RouterLink, I18nPluralPipe],
|
||||
})
|
||||
export class ServiceBackupsComponent {
|
||||
pkg = input.required<T.PackageDataEntry>()
|
||||
|
||||
readonly previous = computed(() =>
|
||||
daysBetween(new Date(), new Date(this.pkg().lastBackup || new Date())),
|
||||
)
|
||||
|
||||
readonly next = computed(() =>
|
||||
// TODO @lucy add this back in when types fixed for PackageDataEntry ie. when next/minor merge resolved
|
||||
// daysBetween(new Date(), new Date(this.pkg().nextBackup || new Date())),
|
||||
daysBetween(new Date(), new Date(new Date())),
|
||||
)
|
||||
|
||||
readonly ago = {
|
||||
'=0': 'Never performed',
|
||||
'=1': 'day ago',
|
||||
other: '# days ago',
|
||||
}
|
||||
|
||||
readonly in = {
|
||||
'=0': 'Not scheduled',
|
||||
'=1': 'Tomorrow',
|
||||
other: 'In # days',
|
||||
}
|
||||
}
|
||||
|
||||
function daysBetween(one: Date, two: Date): number {
|
||||
return Math.abs(
|
||||
Math.round((one.valueOf() - two.valueOf()) / (1000 * 60 * 60 * 24)),
|
||||
)
|
||||
}
|
||||
@@ -1,22 +1,75 @@
|
||||
import { KeyValuePipe } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { DependencyInfo } from '../types/dependency-info'
|
||||
import { ServiceDependencyComponent } from './dependency.component'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiAvatar } from '@taiga-ui/kit'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { ServiceActionRequestsComponent } from './action-requests.component'
|
||||
|
||||
@Component({
|
||||
selector: 'service-dependencies',
|
||||
template: `
|
||||
@for (dep of dependencies; track $index) {
|
||||
<button [serviceDependency]="dep"></button>
|
||||
<header>Dependencies</header>
|
||||
@for (d of pkg.currentDependencies | keyvalue; track $index) {
|
||||
<a
|
||||
tuiCell
|
||||
[routerLink]="services[d.key] ? ['..', d.key] : ['/portal/marketplace']"
|
||||
[queryParams]="services[d.key] ? {} : { id: d.key }"
|
||||
>
|
||||
<tui-avatar><img alt="" [src]="d.value.icon" /></tui-avatar>
|
||||
<span tuiTitle>
|
||||
{{ d.value.title }}
|
||||
<span tuiSubtitle>{{ d.value.versionRange }}</span>
|
||||
</span>
|
||||
<tui-icon icon="@tui.arrow-right" />
|
||||
</a>
|
||||
@if (services[d.key]; as service) {
|
||||
<service-action-requests [pkg]="service" />
|
||||
}
|
||||
} @empty {
|
||||
No dependencies
|
||||
<blockquote>No dependencies</blockquote>
|
||||
}
|
||||
`,
|
||||
styles: ':host { display: block; min-height: var(--tui-height-s) }',
|
||||
styles: `
|
||||
a {
|
||||
margin: 0 -1rem;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
service-action-requests {
|
||||
display: block;
|
||||
padding: 1rem 0 0 2.375rem;
|
||||
margin: -1rem 0 1rem 1.125rem;
|
||||
box-shadow: inset 0.125rem 0 var(--tui-border-normal);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
text-align: center;
|
||||
font: var(--tui-font-text-l);
|
||||
color: var(--tui-text-tertiary);
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [ServiceDependencyComponent],
|
||||
imports: [
|
||||
KeyValuePipe,
|
||||
TuiCell,
|
||||
TuiAvatar,
|
||||
TuiTitle,
|
||||
ServiceActionRequestsComponent,
|
||||
RouterLink,
|
||||
TuiIcon,
|
||||
],
|
||||
})
|
||||
export class ServiceDependenciesComponent {
|
||||
@Input({ required: true })
|
||||
dependencies: readonly DependencyInfo[] = []
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
@Input({ required: true })
|
||||
services: Record<string, PackageDataEntry> = {}
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { DependencyInfo } from '../types/dependency-info'
|
||||
|
||||
@Component({
|
||||
selector: '[serviceDependency]',
|
||||
template: `
|
||||
<img [src]="dep.icon" alt="" />
|
||||
<span tuiTitle>
|
||||
<strong>
|
||||
@if (dep.errorText) {
|
||||
<tui-icon icon="@tui.triangle-alert" [style.color]="color" />
|
||||
}
|
||||
{{ dep.title }}
|
||||
</strong>
|
||||
<span tuiSubtitle>{{ dep.version }}</span>
|
||||
<span tuiSubtitle="" [style.color]="color">
|
||||
{{ dep.errorText || 'Satisfied' }}
|
||||
</span>
|
||||
</span>
|
||||
@if (dep.actionText) {
|
||||
<span>
|
||||
{{ dep.actionText }}
|
||||
<tui-icon icon="@tui.arrow-right" />
|
||||
</span>
|
||||
}
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
img {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
tui-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
host: {
|
||||
'(click)': 'dep.action()',
|
||||
},
|
||||
imports: [TuiIcon, TuiTitle],
|
||||
hostDirectives: [TuiCell],
|
||||
})
|
||||
export class ServiceDependencyComponent {
|
||||
@Input({ required: true, alias: 'serviceDependency' })
|
||||
dep!: DependencyInfo
|
||||
|
||||
get color(): string {
|
||||
return this.dep.errorText
|
||||
? 'var(--tui-status-warning)'
|
||||
: 'var(--tui-status-positive)'
|
||||
}
|
||||
}
|
||||
@@ -20,17 +20,15 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
standalone: true,
|
||||
selector: 'service-error',
|
||||
template: `
|
||||
<header>Error</header>
|
||||
<tui-line-clamp
|
||||
style="pointer-events: none; margin: 1rem 0 -1rem; color: var(--tui-status-negative);"
|
||||
[linesLimit]="2"
|
||||
[content]="error?.message"
|
||||
(overflownChange)="overflow = $event"
|
||||
/>
|
||||
<h4 class="g-title">
|
||||
<span [style.display]="'flex'">
|
||||
Actions
|
||||
<tui-icon [style.margin-left.rem]="0.25" [tuiTooltip]="hint" />
|
||||
</span>
|
||||
<h4>
|
||||
Actions
|
||||
<tui-icon [tuiTooltip]="hint" />
|
||||
</h4>
|
||||
<ng-template #hint>
|
||||
<div>
|
||||
@@ -54,6 +52,32 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
}
|
||||
</p>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
header {
|
||||
--tui-background-neutral-1: var(--tui-status-negative-pale);
|
||||
}
|
||||
|
||||
tui-line-clamp {
|
||||
pointer-events: none;
|
||||
margin: 1rem 0;
|
||||
color: var(--tui-status-negative);
|
||||
}
|
||||
|
||||
h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font: var(--tui-font-text-m);
|
||||
font-weight: bold;
|
||||
color: var(--tui-text-secondary);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButton, TuiIcon, TuiTooltip, TuiLineClamp],
|
||||
})
|
||||
|
||||
@@ -10,22 +10,28 @@ import { ServiceHealthCheckComponent } from 'src/app/routes/portal/routes/servic
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'service-health-checks',
|
||||
template: `
|
||||
<header>Health Checks</header>
|
||||
@for (check of checks; track $index) {
|
||||
<service-health-check
|
||||
[check]="check"
|
||||
[connected]="!!(connected$ | async)"
|
||||
/>
|
||||
}
|
||||
|
||||
@if (!checks.length) {
|
||||
No health checks
|
||||
} @empty {
|
||||
<blockquote>No health checks</blockquote>
|
||||
}
|
||||
`,
|
||||
styles: ':host { display: block; min-height: var(--tui-height-s) }',
|
||||
styles: `
|
||||
blockquote {
|
||||
text-align: center;
|
||||
font: var(--tui-font-text-l);
|
||||
color: var(--tui-text-tertiary);
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [AsyncPipe, ServiceHealthCheckComponent],
|
||||
})
|
||||
export class ServiceHealthChecksComponent {
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { TuiLoader, TuiIcon, TuiButton, TuiTitle } from '@taiga-ui/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { map, timer } from 'rxjs'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { ExtendedInterfaceInfo } from '../pipes/interface-info.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'a[serviceInterfaceListItem]',
|
||||
template: `
|
||||
<ng-container *tuiLet="healthCheck$ | async as check">
|
||||
@if (check === null) {
|
||||
<tui-loader />
|
||||
} @else if (check === '') {
|
||||
<tui-icon [icon]="info.icon" [style.color]="info.color" />
|
||||
} @else {
|
||||
<tui-icon icon="@tui.circle-x" class="g-error" />
|
||||
}
|
||||
<span tuiTitle>
|
||||
<strong>{{ info.name }}</strong>
|
||||
<span tuiSubtitle>{{ info.description }}</span>
|
||||
@if (check) {
|
||||
<span tuiSubtitle class="g-error">
|
||||
<span>
|
||||
<b>Health check failed:</b>
|
||||
{{ check }}
|
||||
</span>
|
||||
</span>
|
||||
} @else {
|
||||
<span tuiSubtitle [style.color]="info.color">
|
||||
{{ info.typeDetail }}
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
@if (info.type === 'ui') {
|
||||
<a
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.external-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Open"
|
||||
size="m"
|
||||
[style.border-radius.%]="100"
|
||||
[attr.href]="href"
|
||||
(click.stop)="(0)"
|
||||
></a>
|
||||
}
|
||||
</ng-container>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, TuiButton, TuiLet, TuiLoader, TuiIcon, TuiTitle],
|
||||
hostDirectives: [TuiCell],
|
||||
})
|
||||
export class ServiceInterfaceListItemComponent {
|
||||
private readonly config = inject(ConfigService)
|
||||
|
||||
@Input({ required: true })
|
||||
info!: ExtendedInterfaceInfo
|
||||
|
||||
@Input({ required: true })
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
@Input()
|
||||
disabled = false
|
||||
|
||||
// TODO: Implement real health check
|
||||
readonly healthCheck$ = timer(3000).pipe(
|
||||
map(() => (Math.random() > 0.5 ? '' : 'You done f***d it up...')),
|
||||
)
|
||||
|
||||
get href(): string | null {
|
||||
return this.disabled
|
||||
? 'null'
|
||||
: this.config.launchableAddress(this.info, this.pkg.hosts)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { PackageStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { InterfaceInfoPipe } from '../pipes/interface-info.pipe'
|
||||
import { ServiceInterfaceListItemComponent } from './interface-list-item.component'
|
||||
|
||||
@Component({
|
||||
selector: 'service-interface-list',
|
||||
template: `
|
||||
@for (info of pkg | interfaceInfo; track $index) {
|
||||
<a
|
||||
serviceInterfaceListItem
|
||||
[info]="info"
|
||||
[pkg]="pkg"
|
||||
[disabled]="status.primary !== 'running'"
|
||||
[routerLink]="info.routerLink"
|
||||
></a>
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [RouterLink, InterfaceInfoPipe, ServiceInterfaceListItemComponent],
|
||||
})
|
||||
export class ServiceInterfaceListComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
@Input({ required: true })
|
||||
status!: PackageStatus
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { TuiButton, TuiLink } from '@taiga-ui/core'
|
||||
import { TuiBadge } from '@taiga-ui/kit'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { getManifest } from '../../../../../utils/get-package-data'
|
||||
import { MappedInterface } from '../types/mapped-interface'
|
||||
|
||||
@Component({
|
||||
selector: 'tr[serviceInterface]',
|
||||
template: `
|
||||
<td>
|
||||
<a tuiLink [routerLink]="info.routerLink">
|
||||
<strong>{{ info.name }}</strong>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<tui-badge size="m" [appearance]="appearance">{{ info.type }}</tui-badge>
|
||||
</td>
|
||||
<td class="g-secondary">{{ info.description }}</td>
|
||||
<td class="hosting">
|
||||
@if (info.public) {
|
||||
<button
|
||||
tuiButton
|
||||
size="s"
|
||||
iconStart="@tui.globe"
|
||||
appearance="positive"
|
||||
(click)="toggle()"
|
||||
>
|
||||
Public
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
tuiButton
|
||||
size="s"
|
||||
iconStart="@tui.lock"
|
||||
appearance="negative"
|
||||
(click)="toggle()"
|
||||
>
|
||||
Private
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (info.type === 'ui') {
|
||||
<a
|
||||
tuiButton
|
||||
appearance="action"
|
||||
iconStart="@tui.external-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
size="s"
|
||||
[style.border-radius.%]="100"
|
||||
[attr.href]="href"
|
||||
(click.stop)="(0)"
|
||||
>
|
||||
Open
|
||||
</a>
|
||||
}
|
||||
</td>
|
||||
`,
|
||||
styles: `
|
||||
strong {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tui-badge {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hosting {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
display: block;
|
||||
padding: 0.5rem 0;
|
||||
|
||||
td {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.hosting {
|
||||
font-size: 0;
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TuiButton, TuiBadge, TuiLink, RouterLink],
|
||||
})
|
||||
export class ServiceInterfaceComponent {
|
||||
private readonly config = inject(ConfigService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
@Input({ required: true })
|
||||
info!: MappedInterface
|
||||
|
||||
@Input({ required: true })
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
@Input()
|
||||
disabled = false
|
||||
|
||||
get appearance(): string {
|
||||
switch (this.info.type) {
|
||||
case 'ui':
|
||||
return 'primary'
|
||||
case 'api':
|
||||
return 'accent'
|
||||
case 'p2p':
|
||||
return 'primary-grayscale'
|
||||
}
|
||||
}
|
||||
|
||||
get href(): string | null {
|
||||
return this.disabled
|
||||
? 'null'
|
||||
: this.config.launchableAddress(this.info, this.pkg.hosts)
|
||||
}
|
||||
|
||||
async toggle() {
|
||||
const loader = this.loader
|
||||
.open(`Making ${this.info.public ? 'private' : 'public'}`)
|
||||
.subscribe()
|
||||
|
||||
const params = {
|
||||
internalPort: this.info.addressInfo.internalPort,
|
||||
public: !this.info.public,
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.info.public) {
|
||||
await this.api.pkgBindingSetPubic({
|
||||
...params,
|
||||
host: this.info.addressInfo.hostId,
|
||||
package: getManifest(this.pkg).id,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverBindingSetPubic(params)
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { TuiTable } from '@taiga-ui/addon-table'
|
||||
import { tuiDefaultSort } from '@taiga-ui/cdk'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { getAddresses } from '../../../components/interfaces/interface.utils'
|
||||
import { ServiceInterfaceComponent } from './interface.component'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'service-interfaces',
|
||||
template: `
|
||||
<header>Interfaces</header>
|
||||
<table tuiTable class="g-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th tuiTh>Name</th>
|
||||
<th tuiTh>Type</th>
|
||||
<th tuiTh>Description</th>
|
||||
<th tuiTh>Hosting</th>
|
||||
<th tuiTh></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@for (info of interfaces(); track $index) {
|
||||
<tr
|
||||
serviceInterface
|
||||
[info]="info"
|
||||
[pkg]="pkg()"
|
||||
[disabled]="disabled()"
|
||||
></tr>
|
||||
}
|
||||
</table>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0 -0.5rem;
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ServiceInterfaceComponent, TuiTable],
|
||||
})
|
||||
export class ServiceInterfacesComponent {
|
||||
private readonly config = inject(ConfigService)
|
||||
|
||||
readonly pkg = input.required<PackageDataEntry>()
|
||||
readonly disabled = input(false)
|
||||
|
||||
readonly interfaces = computed(({ serviceInterfaces, hosts } = this.pkg()) =>
|
||||
Object.entries(serviceInterfaces)
|
||||
.sort((a, b) => tuiDefaultSort(a[1], b[1]))
|
||||
.map(([id, value]) => {
|
||||
const host = hosts[value.addressInfo.hostId]
|
||||
|
||||
return {
|
||||
...value,
|
||||
public: !!host?.bindings[value.addressInfo.internalPort].net.public,
|
||||
addresses: host ? getAddresses(value, host, this.config) : {},
|
||||
routerLink: `./interface/${id}`,
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { ServiceMenu } from '../pipes/to-menu.pipe'
|
||||
|
||||
@Component({
|
||||
selector: '[serviceMenuItem]',
|
||||
template: `
|
||||
<tui-icon [icon]="menu.icon" />
|
||||
<span tuiTitle [style.flex]="1">
|
||||
<strong>{{ menu.name }}</strong>
|
||||
<span tuiSubtitle>
|
||||
{{ menu.description }}
|
||||
</span>
|
||||
<ng-content />
|
||||
</span>
|
||||
<tui-icon icon="@tui.chevron-right" />
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TuiIcon, TuiTitle],
|
||||
hostDirectives: [TuiCell],
|
||||
})
|
||||
export class ServiceMenuItemComponent {
|
||||
@Input({ required: true, alias: 'serviceMenuItem' })
|
||||
menu!: ServiceMenu
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { ToMenuPipe } from '../pipes/to-menu.pipe'
|
||||
import { ServiceMenuItemComponent } from './menu-item.component'
|
||||
|
||||
@Component({
|
||||
selector: 'service-menu',
|
||||
template: `
|
||||
@for (menu of pkg | toMenu; track $index) {
|
||||
@if (menu.routerLink) {
|
||||
<a
|
||||
[serviceMenuItem]="menu"
|
||||
[routerLink]="menu.routerLink"
|
||||
[queryParams]="menu.params || {}"
|
||||
></a>
|
||||
} @else {
|
||||
<button [serviceMenuItem]="menu" (click)="menu.action?.()">
|
||||
@if (menu.name === 'Outbound Proxy') {
|
||||
<div tuiSubtitle [style.color]="color">
|
||||
{{ pkg.outboundProxy || 'None' }}
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [ToMenuPipe, ServiceMenuItemComponent, RouterLink],
|
||||
})
|
||||
export class ServiceMenuComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
get color(): string {
|
||||
return this.pkg.outboundProxy
|
||||
? 'var(--tui-status-positive)'
|
||||
: 'var(--tui-status-warning)'
|
||||
}
|
||||
}
|
||||
@@ -7,36 +7,49 @@ import {
|
||||
} from '@angular/core'
|
||||
import { TuiIcon, TuiLoader } from '@taiga-ui/core'
|
||||
import { InstallingInfo } from 'src/app/services/patch-db/data-model'
|
||||
import { StatusRendering } from 'src/app/services/pkg-status-rendering.service'
|
||||
import {
|
||||
PrimaryRendering,
|
||||
PrimaryStatus,
|
||||
StatusRendering,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'service-status',
|
||||
template: `
|
||||
@if (installingInfo) {
|
||||
<strong>
|
||||
<tui-loader size="s" [inheritColor]="true" />
|
||||
Installing
|
||||
<span class="loading-dots"></span>
|
||||
{{ installingInfo.progress.overall | installingProgressString }}
|
||||
</strong>
|
||||
} @else {
|
||||
<tui-icon [icon]="icon" [style.margin-bottom.rem]="0.25" />
|
||||
{{ connected ? rendering.display : 'Unknown' }}
|
||||
@if (rendering.showDots) {
|
||||
<span class="loading-dots"></span>
|
||||
<header>Status</header>
|
||||
<div [class]="class">
|
||||
@if (installingInfo) {
|
||||
<strong>
|
||||
<tui-loader size="s" [inheritColor]="true" />
|
||||
Installing
|
||||
<span class="loading-dots"></span>
|
||||
{{ installingInfo.progress.overall | installingProgressString }}
|
||||
</strong>
|
||||
} @else {
|
||||
<tui-icon [icon]="icon" [style.margin-bottom.rem]="0.25" />
|
||||
{{ connected ? rendering.display : 'Unknown' }}
|
||||
@if (rendering.showDots) {
|
||||
<span class="loading-dots"></span>
|
||||
}
|
||||
}
|
||||
}
|
||||
<ng-content />
|
||||
</div>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: block;
|
||||
font-size: x-large;
|
||||
white-space: nowrap;
|
||||
margin: auto 0;
|
||||
min-height: 2.75rem;
|
||||
color: var(--tui-text-secondary);
|
||||
display: grid;
|
||||
grid-template-rows: min-content 1fr;
|
||||
align-items: center;
|
||||
font: var(--tui-font-heading-6);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
status {
|
||||
display: grid;
|
||||
grid-template-rows: min-content 1fr 1fr;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
tui-loader {
|
||||
@@ -44,21 +57,16 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
||||
vertical-align: bottom;
|
||||
margin: 0 0.25rem -0.125rem 0;
|
||||
}
|
||||
|
||||
div {
|
||||
font-size: 1rem;
|
||||
color: var(--tui-text-secondary);
|
||||
margin: 1rem 0;
|
||||
}
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'g-card' },
|
||||
standalone: true,
|
||||
imports: [CommonModule, InstallingProgressDisplayPipe, TuiIcon, TuiLoader],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [InstallingProgressDisplayPipe, TuiIcon, TuiLoader],
|
||||
})
|
||||
export class ServiceStatusComponent {
|
||||
@Input({ required: true })
|
||||
rendering!: StatusRendering
|
||||
status!: PrimaryStatus
|
||||
|
||||
@Input()
|
||||
installingInfo?: InstallingInfo
|
||||
@@ -66,17 +74,16 @@ export class ServiceStatusComponent {
|
||||
@Input()
|
||||
connected = false
|
||||
|
||||
@HostBinding('class')
|
||||
get class(): string | null {
|
||||
if (!this.connected) return null
|
||||
|
||||
switch (this.rendering.color) {
|
||||
case 'danger':
|
||||
return 'g-error'
|
||||
return 'g-negative'
|
||||
case 'warning':
|
||||
return 'g-warning'
|
||||
case 'success':
|
||||
return 'g-success'
|
||||
return 'g-positive'
|
||||
case 'primary':
|
||||
return 'g-info'
|
||||
default:
|
||||
@@ -84,6 +91,10 @@ export class ServiceStatusComponent {
|
||||
}
|
||||
}
|
||||
|
||||
get rendering() {
|
||||
return PrimaryRendering[this.status]
|
||||
}
|
||||
|
||||
get icon(): string {
|
||||
if (!this.connected) return '@tui.circle'
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ServicesService } from './services.service'
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
<table tuiTable [(sorter)]="sorter">
|
||||
<table tuiTable class="g-table" [(sorter)]="sorter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th [style.width.rem]="3"></th>
|
||||
@@ -53,41 +53,12 @@ import { ServicesService } from './services.service'
|
||||
font-size: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
tr:not(:last-child) {
|
||||
box-shadow: inset 0 -1px var(--tui-background-neutral-1);
|
||||
}
|
||||
|
||||
th {
|
||||
text-transform: uppercase;
|
||||
color: var(--tui-text-secondary);
|
||||
background: none;
|
||||
border: none;
|
||||
font: var(--tui-font-text-s);
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
thead {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-page' },
|
||||
imports: [ServiceComponent, ToManifestPipe, TuiTable],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DashboardComponent {
|
||||
export default class DashboardComponent {
|
||||
readonly services = toSignal(inject(ServicesService))
|
||||
readonly errors = toSignal(inject(DepErrorService).depErrors$)
|
||||
|
||||
|
||||
@@ -63,10 +63,6 @@ import { StatusComponent } from './status.component'
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--tui-text-primary);
|
||||
font-weight: bold;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
||||
<tui-loader size="s" />
|
||||
} @else {
|
||||
@if (healthy) {
|
||||
<tui-icon icon="@tui.check" class="g-success" />
|
||||
<tui-icon icon="@tui.check" class="g-positive" />
|
||||
} @else {
|
||||
<tui-icon icon="@tui.triangle-alert" class="g-warning" />
|
||||
}
|
||||
|
||||
@@ -37,10 +37,7 @@ export type PackageActionData = {
|
||||
id: string
|
||||
metadata: T.ActionMetadata
|
||||
}
|
||||
requestInfo?: {
|
||||
dependentId?: string
|
||||
request: T.ActionRequest
|
||||
}
|
||||
requestInfo?: T.ActionRequest
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -153,12 +150,12 @@ export class ActionInputModal {
|
||||
return {
|
||||
spec: res.spec,
|
||||
originalValue,
|
||||
operations: this.requestInfo?.request.input
|
||||
operations: this.requestInfo?.input
|
||||
? compare(
|
||||
JSON.parse(JSON.stringify(originalValue)),
|
||||
utils.deepMerge(
|
||||
JSON.parse(JSON.stringify(originalValue)),
|
||||
this.requestInfo.request.input.value,
|
||||
this.requestInfo.input.value,
|
||||
) as object,
|
||||
)
|
||||
: null,
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { TuiDialogOptions } from '@taiga-ui/core'
|
||||
import { injectContext } from '@taiga-ui/polymorpheus'
|
||||
import { ToAdditionalPipe } from 'src/app/routes/portal/routes/services/pipes/to-additional.pipe'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { ServiceAdditionalItemComponent } from './additional-item.component'
|
||||
|
||||
@Component({
|
||||
selector: 'service-additional',
|
||||
template: `
|
||||
@for (additional of pkg | toAdditional; track $index) {
|
||||
@if (additional.description.startsWith('http')) {
|
||||
<a class="g-action" [additionalItem]="additional"></a>
|
||||
} @else {
|
||||
<button
|
||||
class="g-action"
|
||||
[style.pointer-events]="!additional.icon ? 'none' : null"
|
||||
[additionalItem]="additional"
|
||||
(click)="additional.action?.()"
|
||||
></button>
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [ToAdditionalPipe, ServiceAdditionalItemComponent],
|
||||
})
|
||||
export class ServiceAdditionalModal {
|
||||
readonly pkg = injectContext<TuiDialogOptions<PackageDataEntry>>().data
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
// TODO drop these pipes
|
||||
@Pipe({
|
||||
standalone: true,
|
||||
name: 'installingProgressString',
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
export interface ExtendedInterfaceInfo extends T.ServiceInterface {
|
||||
id: string
|
||||
icon: string
|
||||
color: string
|
||||
typeDetail: string
|
||||
routerLink: string
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'interfaceInfo',
|
||||
standalone: true,
|
||||
})
|
||||
export class InterfaceInfoPipe implements PipeTransform {
|
||||
transform(pkg: PackageDataEntry): ExtendedInterfaceInfo[] {
|
||||
return Object.entries(pkg.serviceInterfaces).map(([id, val]) => {
|
||||
let color: string
|
||||
let icon: string
|
||||
let typeDetail: string
|
||||
|
||||
switch (val.type) {
|
||||
case 'ui':
|
||||
color = 'var(--tui-text-action)'
|
||||
icon = '@tui.monitor'
|
||||
typeDetail = 'User Interface (UI)'
|
||||
break
|
||||
case 'p2p':
|
||||
color = 'var(--tui-background-accent-2)'
|
||||
icon = '@tui.users'
|
||||
typeDetail = 'Peer-To-Peer Interface (P2P)'
|
||||
break
|
||||
case 'api':
|
||||
color = 'var(--tui-status-info)'
|
||||
icon = '@tui.terminal'
|
||||
typeDetail = 'Application Program Interface (API)'
|
||||
break
|
||||
}
|
||||
|
||||
return {
|
||||
...val,
|
||||
id,
|
||||
color,
|
||||
icon,
|
||||
typeDetail,
|
||||
routerLink: `./interface/${id}`,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { getDepDetails } from 'src/app/utils/dep-info'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { ActionRequest } from '../components/action-request.component'
|
||||
|
||||
@Pipe({
|
||||
standalone: true,
|
||||
name: 'toActionRequests',
|
||||
})
|
||||
export class ToActionRequestsPipe implements PipeTransform {
|
||||
transform(pkg: PackageDataEntry, packages: Record<string, PackageDataEntry>) {
|
||||
const { id } = getManifest(pkg)
|
||||
const critical: ActionRequest[] = []
|
||||
const important: ActionRequest[] = []
|
||||
|
||||
Object.values(pkg.requestedActions)
|
||||
.filter(r => r.active)
|
||||
.forEach(r => {
|
||||
const self = r.request.packageId === id
|
||||
const toReturn = {
|
||||
...r.request,
|
||||
actionName: self
|
||||
? pkg.actions[r.request.actionId].name
|
||||
: packages[r.request.packageId]?.actions[r.request.actionId].name ||
|
||||
'Unknown Action',
|
||||
dependency: self
|
||||
? null
|
||||
: getDepDetails(pkg, packages, r.request.packageId),
|
||||
}
|
||||
|
||||
if (r.request.severity === 'critical') {
|
||||
critical.push(toReturn)
|
||||
} else {
|
||||
important.push(toReturn)
|
||||
}
|
||||
})
|
||||
|
||||
return { critical, important }
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { inject, Pipe, PipeTransform } from '@angular/core'
|
||||
import { CopyService, MARKDOWN } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { from } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
export const FALLBACK_URL = 'Not provided'
|
||||
|
||||
export interface AdditionalItem {
|
||||
name: string
|
||||
description: string
|
||||
icon?: string
|
||||
action?: () => void
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'toAdditional',
|
||||
standalone: true,
|
||||
})
|
||||
export class ToAdditionalPipe implements PipeTransform {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly copyService = inject(CopyService)
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
|
||||
transform(pkg: PackageDataEntry): AdditionalItem[] {
|
||||
const manifest = getManifest(pkg)
|
||||
return [
|
||||
{
|
||||
name: 'Installed',
|
||||
description: new Intl.DateTimeFormat('en-US', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'medium',
|
||||
}).format(new Date(pkg.installedAt || 0)),
|
||||
},
|
||||
{
|
||||
name: 'Git Hash',
|
||||
description: manifest.gitHash || 'Unknown',
|
||||
icon: manifest.gitHash ? '@tui.copy' : '',
|
||||
action: () =>
|
||||
manifest.gitHash && this.copyService.copy(manifest.gitHash),
|
||||
},
|
||||
{
|
||||
name: 'License',
|
||||
description: manifest.license,
|
||||
icon: '@tui.chevron-right',
|
||||
action: () => this.showLicense(manifest),
|
||||
},
|
||||
{
|
||||
name: 'Website',
|
||||
description: manifest.marketingSite || FALLBACK_URL,
|
||||
},
|
||||
{
|
||||
name: 'Source Repository',
|
||||
description: manifest.upstreamRepo,
|
||||
},
|
||||
{
|
||||
name: 'Support Site',
|
||||
description: manifest.supportSite || FALLBACK_URL,
|
||||
},
|
||||
{
|
||||
name: 'Donation Link',
|
||||
description: manifest.donationUrl || FALLBACK_URL,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
private showLicense({ id, version }: T.Manifest) {
|
||||
this.dialogs
|
||||
.open(MARKDOWN, {
|
||||
label: 'License',
|
||||
size: 'l',
|
||||
data: {
|
||||
content: from(this.api.getStaticInstalled(id, 'LICENSE.md')),
|
||||
},
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { inject, Pipe, PipeTransform } from '@angular/core'
|
||||
import { Params } from '@angular/router'
|
||||
import { MARKDOWN } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { from } from 'rxjs'
|
||||
import { ServiceAdditionalModal } from 'src/app/routes/portal/routes/services/modals/additional.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { ProxyService } from 'src/app/services/proxy.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
export interface ServiceMenu {
|
||||
icon: string
|
||||
name: string
|
||||
description: string
|
||||
action?: () => void
|
||||
routerLink?: string
|
||||
params?: Params
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'toMenu',
|
||||
standalone: true,
|
||||
})
|
||||
export class ToMenuPipe implements PipeTransform {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly proxyService = inject(ProxyService)
|
||||
|
||||
transform(pkg: PackageDataEntry): ServiceMenu[] {
|
||||
const manifest = getManifest(pkg)
|
||||
|
||||
return [
|
||||
{
|
||||
icon: '@tui.list',
|
||||
name: 'Instructions',
|
||||
description: `Understand how to use ${manifest.title}`,
|
||||
action: () => this.showInstructions(manifest),
|
||||
},
|
||||
{
|
||||
icon: '@tui.zap',
|
||||
name: 'Actions',
|
||||
description: `Uninstall and other commands specific to ${manifest.title}`,
|
||||
routerLink: `actions`,
|
||||
},
|
||||
{
|
||||
icon: '@tui.shield',
|
||||
name: 'Outbound Proxy',
|
||||
description: `Proxy all outbound traffic from ${manifest.title}`,
|
||||
action: () =>
|
||||
this.proxyService.presentModalSetOutboundProxy(
|
||||
pkg.outboundProxy,
|
||||
manifest.id,
|
||||
),
|
||||
},
|
||||
{
|
||||
icon: '@tui.file-text',
|
||||
name: 'Logs',
|
||||
description: `Raw, unfiltered logs`,
|
||||
routerLink: 'logs',
|
||||
},
|
||||
{
|
||||
icon: '@tui.info',
|
||||
name: 'Additional Info',
|
||||
description: `View package details`,
|
||||
action: () =>
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(ServiceAdditionalModal), {
|
||||
label: `Additional Info`,
|
||||
data: pkg,
|
||||
})
|
||||
.subscribe(),
|
||||
},
|
||||
pkg.registry
|
||||
? {
|
||||
icon: '@tui.shopping-bag',
|
||||
name: 'Marketplace Listing',
|
||||
description: `View ${manifest.title} on the Marketplace`,
|
||||
routerLink: `/portal/marketplace`,
|
||||
params: { url: pkg.registry, id: manifest.id },
|
||||
}
|
||||
: {
|
||||
icon: '@tui.shopping-bag',
|
||||
name: 'Marketplace Listing',
|
||||
description: `This package was not installed from the marketplace`,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
private showInstructions({ title, id }: T.Manifest) {
|
||||
this.api
|
||||
.setDbValue<boolean>(['ack-instructions', id], true)
|
||||
.catch(e => console.error('Failed to mark instructions as seen', e))
|
||||
|
||||
this.dialogs
|
||||
.open(MARKDOWN, {
|
||||
label: `${title} instructions`,
|
||||
size: 'l',
|
||||
data: {
|
||||
content: from(this.api.getStaticInstalled(id, 'instructions.md')),
|
||||
},
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
INJECTOR,
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { CopyService, getPkgId } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import {
|
||||
FALLBACK_URL,
|
||||
ServiceAdditionalItemComponent,
|
||||
} from '../components/additional-item.component'
|
||||
import ServiceMarkdownRoute from './markdown.component'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<section class="g-card">
|
||||
@for (additional of items(); track $index) {
|
||||
@if (additional.description.startsWith('http')) {
|
||||
<a tuiCell [additionalItem]="additional"></a>
|
||||
} @else {
|
||||
<button
|
||||
tuiCell
|
||||
[style.pointer-events]="!additional.icon ? 'none' : null"
|
||||
[additionalItem]="additional"
|
||||
(click)="additional.action?.()"
|
||||
></button>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 32rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
host: { class: 'g-subpage' },
|
||||
imports: [ServiceAdditionalItemComponent, TuiCell],
|
||||
})
|
||||
export default class ServiceAboutRoute {
|
||||
private readonly copyService = inject(CopyService)
|
||||
private readonly markdown = inject(TuiDialogService).open(
|
||||
new PolymorpheusComponent(ServiceMarkdownRoute, inject(INJECTOR)),
|
||||
{ label: 'License', size: 'l' },
|
||||
)
|
||||
|
||||
readonly items = toSignal(
|
||||
inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('packageData', getPkgId())
|
||||
.pipe(
|
||||
map(pkg => {
|
||||
const manifest = getManifest(pkg)
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'Version',
|
||||
description: manifest.version,
|
||||
},
|
||||
{
|
||||
name: 'Git Hash',
|
||||
description: manifest.gitHash || 'Unknown',
|
||||
icon: manifest.gitHash ? '@tui.copy' : '',
|
||||
action: () =>
|
||||
manifest.gitHash && this.copyService.copy(manifest.gitHash),
|
||||
},
|
||||
{
|
||||
name: 'License',
|
||||
description: manifest.license,
|
||||
icon: '@tui.chevron-right',
|
||||
action: () => this.markdown.subscribe(),
|
||||
},
|
||||
{
|
||||
name: 'Website',
|
||||
description: manifest.marketingSite || FALLBACK_URL,
|
||||
},
|
||||
{
|
||||
name: 'Donation Link',
|
||||
description: manifest.donationUrl || FALLBACK_URL,
|
||||
},
|
||||
{
|
||||
name: 'Source Repository',
|
||||
description: manifest.upstreamRepo,
|
||||
},
|
||||
{
|
||||
name: 'Support Site',
|
||||
description: manifest.supportSite || FALLBACK_URL,
|
||||
},
|
||||
{
|
||||
name: 'Registry',
|
||||
description: pkg.registry || FALLBACK_URL,
|
||||
},
|
||||
{
|
||||
name: 'Binary Source',
|
||||
description: manifest.wrapperRepo,
|
||||
},
|
||||
]
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,53 +1,75 @@
|
||||
import { KeyValuePipe } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, map } from 'rxjs'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { StandardActionsService } from 'src/app/services/standard-actions.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { ServiceActionComponent } from '../components/action.component'
|
||||
|
||||
const OTHER = 'Other Custom Actions'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@if (package(); as pkg) {
|
||||
<section>
|
||||
<h3 class="g-title">Standard Actions</h3>
|
||||
<section class="g-card">
|
||||
<header>Standard Actions</header>
|
||||
<button
|
||||
class="g-action"
|
||||
tuiCell
|
||||
[action]="rebuild"
|
||||
(click)="service.rebuild(pkg.manifest.id)"
|
||||
></button>
|
||||
<button
|
||||
class="g-action"
|
||||
tuiCell
|
||||
[action]="uninstall"
|
||||
(click)="service.uninstall(pkg.manifest)"
|
||||
></button>
|
||||
</section>
|
||||
@if (pkg.actions.length) {
|
||||
<h3 class="g-title">Actions for {{ pkg.manifest.title }}</h3>
|
||||
}
|
||||
@for (action of pkg.actions; track $index) {
|
||||
@if (action.visibility !== 'hidden') {
|
||||
<button
|
||||
class="g-action"
|
||||
[action]="action"
|
||||
(click)="
|
||||
handleAction(pkg.mainStatus, pkg.icon, pkg.manifest, action)
|
||||
"
|
||||
></button>
|
||||
@for (group of pkg.actions | keyvalue; track $index) {
|
||||
@if (group.value.length) {
|
||||
<section class="g-card">
|
||||
<header>{{ group.key }}</header>
|
||||
@for (a of group.value; track $index) {
|
||||
@if (a.visibility !== 'hidden') {
|
||||
<button
|
||||
tuiCell
|
||||
[action]="a"
|
||||
(click)="handle(pkg.mainStatus, pkg.icon, pkg.manifest, a)"
|
||||
></button>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
section {
|
||||
max-width: 54rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
[tuiCell] {
|
||||
margin: 0 -1rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: -0.75rem;
|
||||
}
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-subpage' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [ServiceActionComponent],
|
||||
imports: [ServiceActionComponent, TuiCell, KeyValuePipe],
|
||||
})
|
||||
export class ServiceActionsRoute {
|
||||
export default class ServiceActionsRoute {
|
||||
private readonly actions = inject(ActionService)
|
||||
|
||||
readonly service = inject(StandardActionsService)
|
||||
@@ -60,10 +82,18 @@ export class ServiceActionsRoute {
|
||||
mainStatus: pkg.status.main,
|
||||
icon: pkg.icon,
|
||||
manifest: getManifest(pkg),
|
||||
actions: Object.keys(pkg.actions).map(id => ({
|
||||
id,
|
||||
...pkg.actions[id],
|
||||
})),
|
||||
actions: Object.keys(pkg.actions).reduce<
|
||||
Record<string, ReadonlyArray<T.ActionMetadata & { id: string }>>
|
||||
>(
|
||||
(acc, id) => {
|
||||
const action = { id, ...pkg.actions[id] }
|
||||
const group = pkg.actions[id].group || OTHER
|
||||
const current = acc[group] || []
|
||||
|
||||
return { ...acc, [group]: current.concat(action) }
|
||||
},
|
||||
{ [OTHER]: [] },
|
||||
),
|
||||
})),
|
||||
),
|
||||
)
|
||||
@@ -71,14 +101,14 @@ export class ServiceActionsRoute {
|
||||
readonly rebuild = REBUILD
|
||||
readonly uninstall = UNINSTALL
|
||||
|
||||
handleAction(
|
||||
handle(
|
||||
mainStatus: T.MainStatus['main'],
|
||||
icon: string,
|
||||
manifest: T.Manifest,
|
||||
{ id, title }: T.Manifest,
|
||||
action: T.ActionMetadata & { id: string },
|
||||
) {
|
||||
this.actions.present({
|
||||
pkgInfo: { id: manifest.id, title: manifest.title, icon, mainStatus },
|
||||
pkgInfo: { id, title, icon, mainStatus },
|
||||
actionInfo: { id: action.id, metadata: action },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,15 +13,16 @@ import { ConfigService } from 'src/app/services/config.service'
|
||||
template: `
|
||||
<app-interface
|
||||
*ngIf="interfacesWithAddresses$ | async as serviceInterface"
|
||||
[packageContext]="context"
|
||||
[packageId]="context.packageId"
|
||||
[serviceInterface]="serviceInterface"
|
||||
/>
|
||||
`,
|
||||
host: { class: 'g-subpage' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, InterfaceComponent],
|
||||
})
|
||||
export class ServiceInterfaceRoute {
|
||||
export default class ServiceInterfaceRoute {
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly config = inject(ConfigService)
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
template: '<logs [fetchLogs]="fetch" [followLogs]="follow" [context]="id" />',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
styles: [':host { height: 100%}'],
|
||||
host: { class: 'g-subpage' },
|
||||
imports: [LogsComponent],
|
||||
})
|
||||
export class ServiceLogsRoute {
|
||||
export default class ServiceLogsRoute {
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
readonly id = getPkgId()
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import {
|
||||
getErrorMessage,
|
||||
MarkdownPipeModule,
|
||||
SafeLinksDirective,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiLoader, TuiNotification } from '@taiga-ui/core'
|
||||
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
||||
import { catchError, ignoreElements, of } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@if (error()) {
|
||||
<tui-notification appearance="negative" safeLinks>
|
||||
{{ error() }}
|
||||
</tui-notification>
|
||||
}
|
||||
|
||||
@if (content(); as result) {
|
||||
<div safeLinks [innerHTML]="result | markdown | dompurify"></div>
|
||||
} @else {
|
||||
<tui-loader textContent="Loading" />
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
host: { class: 'g-subpage' },
|
||||
imports: [
|
||||
TuiNotification,
|
||||
TuiLoader,
|
||||
MarkdownPipeModule,
|
||||
NgDompurifyModule,
|
||||
SafeLinksDirective,
|
||||
],
|
||||
})
|
||||
export default class ServiceMarkdownRoute {
|
||||
private readonly data = inject(ActivatedRoute).snapshot.data
|
||||
|
||||
readonly content = toSignal<string>(this.data['content'])
|
||||
readonly error = toSignal(
|
||||
this.data['content'].pipe(
|
||||
ignoreElements(),
|
||||
catchError(e => of(getErrorMessage(e))),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,35 +1,170 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router'
|
||||
import { TuiAppearance, TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiAvatar, TuiFade } from '@taiga-ui/kit'
|
||||
import { TuiCell, tuiCellOptionsProvider } from '@taiga-ui/layout'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
const ICONS = {
|
||||
dashboard: '@tui.layout-dashboard',
|
||||
actions: '@tui.clapperboard',
|
||||
instructions: '@tui.book-open-text',
|
||||
logs: '@tui.logs',
|
||||
about: '@tui.info',
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-container *ngIf="service$ | async" />
|
||||
@if (service()) {
|
||||
<aside>
|
||||
<header tuiCell>
|
||||
<tui-avatar><img alt="" [src]="service()?.icon" /></tui-avatar>
|
||||
<span tuiTitle>
|
||||
<strong tuiFade>{{ manifest()?.title }}</strong>
|
||||
<span tuiSubtitle>{{ manifest()?.version }}</span>
|
||||
</span>
|
||||
</header>
|
||||
<nav>
|
||||
@for (item of nav; track $index) {
|
||||
<a
|
||||
tuiCell
|
||||
tuiAppearance="action-grayscale"
|
||||
routerLinkActive="active"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
[routerLink]="item === 'dashboard' ? './' : item"
|
||||
>
|
||||
<tui-icon [icon]="icons[item]" />
|
||||
<span tuiTitle>{{ item }}</span>
|
||||
</a>
|
||||
}
|
||||
</nav>
|
||||
</aside>
|
||||
}
|
||||
<router-outlet />
|
||||
`,
|
||||
host: { class: 'g-page' },
|
||||
styles: `
|
||||
:host {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
aside {
|
||||
position: sticky;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
margin: 1px;
|
||||
width: 16rem;
|
||||
padding: 0.5rem;
|
||||
text-transform: capitalize;
|
||||
box-shadow: 1px 0 var(--tui-border-normal);
|
||||
backdrop-filter: blur(1rem);
|
||||
background-color: color-mix(
|
||||
in hsl,
|
||||
var(--tui-background-base) 90%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
header {
|
||||
margin: 0 -0.5rem;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: var(--tui-text-primary);
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
|
||||
aside {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
z-index: 1;
|
||||
box-shadow: inset 0 1px 0 1px var(--tui-background-neutral-1);
|
||||
|
||||
header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
a {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
border-radius: 0;
|
||||
background: var(--tui-background-neutral-1);
|
||||
|
||||
&.active {
|
||||
background: none;
|
||||
}
|
||||
|
||||
[tuiTitle] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterOutlet],
|
||||
imports: [
|
||||
RouterModule,
|
||||
TuiCell,
|
||||
TuiAvatar,
|
||||
TuiTitle,
|
||||
TuiAppearance,
|
||||
TuiIcon,
|
||||
TuiFade,
|
||||
],
|
||||
providers: [tuiCellOptionsProvider({ height: 'spacious' })],
|
||||
})
|
||||
export class ServiceOutletComponent {
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly route = inject(ActivatedRoute)
|
||||
private readonly router = inject(Router)
|
||||
private readonly params = inject(ActivatedRoute).paramMap
|
||||
|
||||
readonly service$ = this.router.events.pipe(
|
||||
map(() => this.route.firstChild?.snapshot.paramMap?.get('pkgId')),
|
||||
filter(Boolean),
|
||||
distinctUntilChanged(),
|
||||
switchMap(id => this.patch.watch$('packageData', id)),
|
||||
tap(pkg => {
|
||||
// if package disappears, navigate to list page
|
||||
if (!pkg) {
|
||||
this.router.navigate(['./portal/services'])
|
||||
}
|
||||
}),
|
||||
protected readonly icons = ICONS
|
||||
protected readonly nav = [
|
||||
'dashboard',
|
||||
'actions',
|
||||
'instructions',
|
||||
'logs',
|
||||
'about',
|
||||
] as const
|
||||
|
||||
protected readonly service = toSignal(
|
||||
this.router.events.pipe(
|
||||
switchMap(() => this.params),
|
||||
map(params => params.get('pkgId')),
|
||||
filter(Boolean),
|
||||
distinctUntilChanged(),
|
||||
switchMap(id => this.patch.watch$('packageData', id)),
|
||||
tap(pkg => {
|
||||
// if package disappears, navigate to list page
|
||||
if (!pkg) {
|
||||
this.router.navigate(['./portal/services'])
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
protected readonly manifest = computed(
|
||||
(pkg = this.service()) => pkg && getManifest(pkg),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,197 +1,89 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { isEmptyObject } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, map, switchMap } from 'rxjs'
|
||||
import { ServiceBackupsComponent } from 'src/app/routes/portal/routes/services/components/backups.component'
|
||||
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe'
|
||||
import { map } from 'rxjs'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import {
|
||||
DepErrorService,
|
||||
PkgDependencyErrors,
|
||||
} from 'src/app/services/dep-error.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PackageStatus,
|
||||
PrimaryRendering,
|
||||
renderPkgStatus,
|
||||
StatusRendering,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { ServiceActionRequestComponent } from '../components/action-request.component'
|
||||
import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { ServiceActionRequestsComponent } from '../components/action-requests.component'
|
||||
import { ServiceActionsComponent } from '../components/actions.component'
|
||||
import { ServiceDependenciesComponent } from '../components/dependencies.component'
|
||||
import { ServiceErrorComponent } from '../components/error.component'
|
||||
import { ServiceHealthChecksComponent } from '../components/health-checks.component'
|
||||
import { ServiceInterfaceListComponent } from '../components/interface-list.component'
|
||||
import { ServiceMenuComponent } from '../components/menu.component'
|
||||
import { ServiceInterfacesComponent } from '../components/interfaces.component'
|
||||
import { ServiceProgressComponent } from '../components/progress.component'
|
||||
import { ServiceStatusComponent } from '../components/status.component'
|
||||
import { ToActionRequestsPipe } from '../pipes/to-action-requests.pipe'
|
||||
import { DependencyInfo } from '../types/dependency-info'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@if (service$ | async; as service) {
|
||||
<section [style.grid-column]="'span 3'">
|
||||
<h3>Status</h3>
|
||||
<service-status
|
||||
[connected]="!!(connected$ | async)"
|
||||
[installingInfo]="service.pkg.stateInfo.installingInfo"
|
||||
[rendering]="getRendering(service.status)"
|
||||
/>
|
||||
<service-status
|
||||
[connected]="!!connected()"
|
||||
[installingInfo]="pkg().stateInfo.installingInfo"
|
||||
[status]="status()"
|
||||
>
|
||||
@if (installed() && connected()) {
|
||||
<service-actions [pkg]="pkg()" [status]="status()" />
|
||||
}
|
||||
</service-status>
|
||||
|
||||
@if (isInstalled(service) && (connected$ | async)) {
|
||||
<service-actions [service]="service" />
|
||||
}
|
||||
</section>
|
||||
|
||||
@if (isInstalled(service)) {
|
||||
<section [style.grid-column]="'span 3'">
|
||||
<h3>Backups</h3>
|
||||
<service-backups [pkg]="service.pkg" />
|
||||
</section>
|
||||
|
||||
@if (service.pkg.status.main === 'error') {
|
||||
<section class="error">
|
||||
<h3>Error</h3>
|
||||
<service-error [pkg]="service.pkg" />
|
||||
</section>
|
||||
} @else {
|
||||
<section [style.grid-column]="'span 6'">
|
||||
<h3>Metrics</h3>
|
||||
TODO
|
||||
</section>
|
||||
}
|
||||
|
||||
<section [style.grid-column]="'span 4'" [style.align-self]="'start'">
|
||||
<h3>Menu</h3>
|
||||
<service-menu [pkg]="service.pkg" />
|
||||
</section>
|
||||
|
||||
<div>
|
||||
@if (service.pkg | toActionRequests: service.allPkgs; as requests) {
|
||||
@if (requests.critical.length) {
|
||||
<section>
|
||||
<h3>Required Actions</h3>
|
||||
@for (request of requests.critical; track $index) {
|
||||
<button
|
||||
[actionRequest]="request"
|
||||
[pkg]="service.pkg"
|
||||
[allPkgs]="service.allPkgs"
|
||||
></button>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (requests.important.length) {
|
||||
<section>
|
||||
<h3>Requested Actions</h3>
|
||||
@for (request of requests.important; track $index) {
|
||||
<button
|
||||
[actionRequest]="request"
|
||||
[pkg]="service.pkg"
|
||||
[allPkgs]="service.allPkgs"
|
||||
></button>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
<section>
|
||||
<h3>Health Checks</h3>
|
||||
<service-health-checks [checks]="(health$ | async) || []" />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Dependencies</h3>
|
||||
<service-dependencies [dependencies]="service.dependencies" />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section [style.grid-column]="'span 4'" [style.align-self]="'start'">
|
||||
<h3>Service Interfaces</h3>
|
||||
<service-interface-list
|
||||
[pkg]="service.pkg"
|
||||
[status]="service.status"
|
||||
/>
|
||||
</section>
|
||||
@if (installed()) {
|
||||
@if (pkg().status.main === 'error') {
|
||||
<service-error [pkg]="pkg()" />
|
||||
}
|
||||
|
||||
@if (isInstalling(service.pkg.stateInfo.state)) {
|
||||
@for (
|
||||
item of service.pkg.stateInfo.installingInfo?.progress?.phases;
|
||||
track $index
|
||||
) {
|
||||
<p [progress]="item.progress">{{ item.name }}</p>
|
||||
}
|
||||
<service-interfaces [pkg]="pkg()" [disabled]="status() !== 'running'" />
|
||||
<service-dependencies [pkg]="pkg()" [services]="services()" />
|
||||
<service-health-checks [checks]="health()" />
|
||||
|
||||
<section class="g-card">
|
||||
<header>Tasks</header>
|
||||
<service-action-requests [pkg]="pkg()" />
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (installing()) {
|
||||
@for (
|
||||
item of pkg().stateInfo.installingInfo?.progress?.phases;
|
||||
track $index
|
||||
) {
|
||||
<p [progress]="item.progress">{{ item.name }}</p>
|
||||
}
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
flex-direction: column;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-auto-rows: max-content;
|
||||
gap: 1rem;
|
||||
margin: 1rem -1rem 0;
|
||||
}
|
||||
|
||||
small {
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
display: flex;
|
||||
}
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 1rem 1.5rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
background: var(--tui-background-neutral-1);
|
||||
box-shadow: inset 0 7rem 0 -4rem var(--tui-background-neutral-1);
|
||||
clip-path: polygon(0 1.5rem, 1.5rem 0, 100% 0, 100% 100%, 0 100%);
|
||||
|
||||
&.error {
|
||||
box-shadow: inset 0 7rem 0 -4rem var(--tui-status-negative-pale);
|
||||
grid-column: span 6;
|
||||
|
||||
h3 {
|
||||
color: var(--tui-status-negative);
|
||||
}
|
||||
> * {
|
||||
grid-column: span 1 !important;
|
||||
}
|
||||
|
||||
::ng-deep [tuiCell] {
|
||||
width: stretch;
|
||||
margin: 0 -1rem;
|
||||
|
||||
&:not(:last-child) {
|
||||
box-shadow: 0 0.51rem 0 -0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: inherit;
|
||||
grid-column: span 4;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
margin: 0;
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-subpage' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
@@ -199,193 +91,51 @@ import { DependencyInfo } from '../types/dependency-info'
|
||||
ServiceProgressComponent,
|
||||
ServiceStatusComponent,
|
||||
ServiceActionsComponent,
|
||||
ServiceInterfaceListComponent,
|
||||
ServiceInterfacesComponent,
|
||||
ServiceHealthChecksComponent,
|
||||
ServiceDependenciesComponent,
|
||||
ServiceMenuComponent,
|
||||
ServiceBackupsComponent,
|
||||
ServiceActionRequestComponent,
|
||||
ServiceErrorComponent,
|
||||
ToActionRequestsPipe,
|
||||
InstallingProgressPipe,
|
||||
ServiceActionRequestsComponent,
|
||||
],
|
||||
})
|
||||
export class ServiceRoute {
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly pkgId$ = inject(ActivatedRoute).paramMap.pipe(
|
||||
map(params => params.get('pkgId')!),
|
||||
)
|
||||
private readonly depErrorService = inject(DepErrorService)
|
||||
private readonly router = inject(Router)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
readonly connected$ = inject(ConnectionService)
|
||||
protected readonly connected = toSignal(inject(ConnectionService))
|
||||
|
||||
readonly service$ = this.pkgId$.pipe(
|
||||
switchMap(pkgId =>
|
||||
combineLatest([
|
||||
this.patch.watch$('packageData'),
|
||||
this.depErrorService.getPkgDepErrors$(pkgId),
|
||||
]).pipe(
|
||||
map(([allPkgs, depErrors]) => {
|
||||
const pkg = allPkgs[pkgId]
|
||||
return {
|
||||
allPkgs,
|
||||
pkg,
|
||||
dependencies: this.getDepInfo(pkg, depErrors),
|
||||
status: renderPkgStatus(pkg, depErrors),
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
protected readonly id = toSignal(
|
||||
inject(ActivatedRoute).paramMap.pipe(map(params => params.get('pkgId'))),
|
||||
)
|
||||
|
||||
readonly health$ = this.pkgId$.pipe(
|
||||
switchMap(pkgId => this.patch.watch$('packageData', pkgId, 'status')),
|
||||
map(toHealthCheck),
|
||||
protected readonly services = toSignal(
|
||||
inject<PatchDB<DataModel>>(PatchDB).watch$('packageData'),
|
||||
{ initialValue: {} as Record<string, PackageDataEntry> },
|
||||
)
|
||||
|
||||
isInstalling(state: string): boolean {
|
||||
return (
|
||||
state === 'installing' || state === 'updating' || state === 'restoring'
|
||||
)
|
||||
}
|
||||
protected readonly pkg = computed(() => this.services()[this.id() || ''])
|
||||
|
||||
isInstalled({ pkg, status }: any): boolean {
|
||||
return pkg.stateInfo.state === 'installed' && status.primary !== 'backingUp'
|
||||
}
|
||||
protected readonly health = computed(() =>
|
||||
this.pkg() ? toHealthCheck(this.pkg().status) : [],
|
||||
)
|
||||
|
||||
getRendering({ primary }: PackageStatus): StatusRendering {
|
||||
return PrimaryRendering[primary]
|
||||
}
|
||||
protected readonly status = computed((pkg = this.pkg()) =>
|
||||
pkg?.stateInfo.state === 'installed'
|
||||
? getInstalledPrimaryStatus(pkg)
|
||||
: pkg?.stateInfo.state,
|
||||
)
|
||||
|
||||
private getDepInfo(
|
||||
pkg: PackageDataEntry,
|
||||
depErrors: PkgDependencyErrors,
|
||||
): DependencyInfo[] {
|
||||
const manifest = getManifest(pkg)
|
||||
protected readonly installed = computed(
|
||||
() =>
|
||||
this.pkg()?.stateInfo.state === 'installed' &&
|
||||
this.status() !== 'backingUp',
|
||||
)
|
||||
|
||||
return Object.keys(pkg.currentDependencies).map(id =>
|
||||
this.getDepValues(pkg, manifest, id, depErrors),
|
||||
)
|
||||
}
|
||||
|
||||
private getDepValues(
|
||||
pkg: PackageDataEntry,
|
||||
pkgManifest: T.Manifest,
|
||||
depId: string,
|
||||
depErrors: PkgDependencyErrors,
|
||||
): DependencyInfo {
|
||||
const { errorText, fixText, fixAction } = this.getDepErrors(
|
||||
pkg,
|
||||
pkgManifest,
|
||||
depId,
|
||||
depErrors,
|
||||
)
|
||||
|
||||
const { title, icon, versionRange } = pkg.currentDependencies[depId]
|
||||
|
||||
return {
|
||||
id: depId,
|
||||
version: versionRange,
|
||||
title,
|
||||
icon,
|
||||
errorText: errorText
|
||||
? `${errorText}. ${pkgManifest.title} will not work as expected.`
|
||||
: '',
|
||||
actionText: fixText || 'View',
|
||||
action:
|
||||
fixAction ||
|
||||
(() => {
|
||||
this.router.navigate([`portal`, `service`, depId])
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
private getDepErrors(
|
||||
pkg: PackageDataEntry,
|
||||
pkgManifest: T.Manifest,
|
||||
depId: string,
|
||||
depErrors: PkgDependencyErrors,
|
||||
) {
|
||||
const depError = depErrors[depId]
|
||||
|
||||
let errorText: string | null = null
|
||||
let fixText: string | null = null
|
||||
let fixAction: (() => any) | null = null
|
||||
|
||||
if (depError) {
|
||||
if (depError.type === 'notInstalled') {
|
||||
errorText = 'Not installed'
|
||||
fixText = 'Install'
|
||||
fixAction = () => this.fixDep(pkg, pkgManifest, 'install', depId)
|
||||
} else if (depError.type === 'incorrectVersion') {
|
||||
errorText = 'Incorrect version'
|
||||
fixText = 'Update'
|
||||
fixAction = () => this.fixDep(pkg, pkgManifest, 'update', depId)
|
||||
} else if (depError.type === 'actionRequired') {
|
||||
errorText = 'Action Required (see above)'
|
||||
} else if (depError.type === 'notRunning') {
|
||||
errorText = 'Not running'
|
||||
fixText = 'Start'
|
||||
} else if (depError.type === 'healthChecksFailed') {
|
||||
errorText = 'Required health check not passing'
|
||||
} else if (depError.type === 'transitive') {
|
||||
errorText = 'Dependency has a dependency issue'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
errorText,
|
||||
fixText,
|
||||
fixAction,
|
||||
}
|
||||
}
|
||||
|
||||
async fixDep(
|
||||
pkg: PackageDataEntry,
|
||||
pkgManifest: T.Manifest,
|
||||
action: 'install' | 'update' | 'configure',
|
||||
depId: string,
|
||||
): Promise<void> {
|
||||
switch (action) {
|
||||
case 'install':
|
||||
case 'update':
|
||||
return this.installDep(pkg, pkgManifest, depId)
|
||||
case 'configure':
|
||||
// return this.formDialog.open<PackageConfigData>(ConfigModal, {
|
||||
// label: `${pkg.currentDependencies[depId].title} config`,
|
||||
// data: {
|
||||
// pkgId: depId,
|
||||
// dependentInfo: pkgManifest,
|
||||
// },
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
private async installDep(
|
||||
pkg: PackageDataEntry,
|
||||
manifest: T.Manifest,
|
||||
depId: string,
|
||||
): Promise<void> {
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: manifest.id,
|
||||
title: manifest.title,
|
||||
version: pkg.currentDependencies[depId].versionRange,
|
||||
}
|
||||
const navigationExtras: NavigationExtras = {
|
||||
// @TODO state not being used by marketplace component. Maybe it is not important to use.
|
||||
state: { dependentInfo },
|
||||
queryParams: { id: depId },
|
||||
}
|
||||
|
||||
await this.router.navigate(['portal', 'marketplace'], navigationExtras)
|
||||
}
|
||||
protected readonly installing = computed(
|
||||
(state = this.status()) =>
|
||||
state === 'installing' || state === 'updating' || state === 'restoring',
|
||||
)
|
||||
}
|
||||
|
||||
function toHealthCheck(
|
||||
status: T.MainStatus,
|
||||
): T.NamedHealthCheckResult[] | null {
|
||||
function toHealthCheck(status: T.MainStatus): T.NamedHealthCheckResult[] {
|
||||
return status.main !== 'running' || isEmptyObject(status.health)
|
||||
? null
|
||||
? []
|
||||
: Object.values(status.health)
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
|
||||
import { ServiceOutletComponent } from './routes/outlet.component'
|
||||
import { ServiceRoute } from './routes/service.component'
|
||||
|
||||
const ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ServiceOutletComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':pkgId',
|
||||
component: ServiceRoute,
|
||||
},
|
||||
{
|
||||
path: ':pkgId/actions',
|
||||
loadComponent: () =>
|
||||
import('./routes/actions.component').then(m => m.ServiceActionsRoute),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/interface/:interfaceId',
|
||||
loadComponent: () =>
|
||||
import('./routes/interface.component').then(
|
||||
m => m.ServiceInterfaceRoute,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/logs',
|
||||
loadComponent: () =>
|
||||
import('./routes/logs.component').then(m => m.ServiceLogsRoute),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadComponent: () =>
|
||||
import('./dashboard/dashboard.component').then(
|
||||
m => m.DashboardComponent,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({ imports: [RouterModule.forChild(ROUTES)] })
|
||||
export class ServicesModule {}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { inject } from '@angular/core'
|
||||
import { ActivatedRouteSnapshot, ResolveFn, Routes } from '@angular/router'
|
||||
import { defer, map, Observable, of } from 'rxjs'
|
||||
import { share } from 'rxjs/operators'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
import { ServiceOutletComponent } from './routes/outlet.component'
|
||||
import { ServiceRoute } from './routes/service.component'
|
||||
|
||||
export const ROUTES: Routes = [
|
||||
{
|
||||
path: ':pkgId',
|
||||
component: ServiceOutletComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: ServiceRoute,
|
||||
},
|
||||
{
|
||||
path: 'actions',
|
||||
loadComponent: () => import('./routes/actions.component'),
|
||||
},
|
||||
{
|
||||
path: 'instructions',
|
||||
loadComponent: () => import('./routes/markdown.component'),
|
||||
resolve: { content: getStatic('instructions.md') },
|
||||
canActivate: [
|
||||
({ paramMap }: ActivatedRouteSnapshot) => {
|
||||
inject(ApiService)
|
||||
.setDbValue(['ack-instructions', paramMap.get('pkgId')!], true)
|
||||
.catch(e => console.error('Failed to mark as seen', e))
|
||||
|
||||
return true
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'interface/:interfaceId',
|
||||
loadComponent: () => import('./routes/interface.component'),
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
loadComponent: () => import('./routes/logs.component'),
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
loadComponent: () => import('./routes/about.component'),
|
||||
resolve: { content: getStatic('LICENSE.md') },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadComponent: () => import('./dashboard/dashboard.component'),
|
||||
},
|
||||
]
|
||||
|
||||
function getStatic(
|
||||
path: 'LICENSE.md' | 'instructions.md',
|
||||
): ResolveFn<Observable<string>> {
|
||||
return ({ paramMap }: ActivatedRouteSnapshot) =>
|
||||
of(inject(ApiService)).pipe(
|
||||
map(api =>
|
||||
defer(() => api.getStaticInstalled(paramMap.get('pkgId')!, path)).pipe(
|
||||
share(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export default ROUTES
|
||||
@@ -0,0 +1,16 @@
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
export type MappedInterface = T.ServiceInterface & {
|
||||
public: boolean
|
||||
// TODO implement addresses
|
||||
addresses: any
|
||||
routerLink: string
|
||||
}
|
||||
|
||||
export type MappedAddress = {
|
||||
name: string
|
||||
url: string
|
||||
isDomain: boolean
|
||||
isOnion: boolean
|
||||
acme: string | null
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { SettingsUpdateComponent } from './update.component'
|
||||
/>
|
||||
<ng-container *ngFor="let btn of cat.value">
|
||||
<settings-button [button]="btn">
|
||||
<!-- // @TODO 041
|
||||
<div
|
||||
*ngIf="btn.title === 'Outbound Proxy'"
|
||||
tuiSubtitle
|
||||
@@ -32,7 +33,7 @@ import { SettingsUpdateComponent } from './update.component'
|
||||
"
|
||||
>
|
||||
{{ server.network.outboundProxy || 'None' }}
|
||||
</div>
|
||||
</div> -->
|
||||
</settings-button>
|
||||
</ng-container>
|
||||
</section>
|
||||
|
||||
@@ -19,7 +19,7 @@ const iface: T.ServiceInterface = {
|
||||
type: 'ui' as const,
|
||||
masked: false,
|
||||
addressInfo: {
|
||||
hostId: '',
|
||||
hostId: 'startos-ui',
|
||||
internalPort: 80,
|
||||
scheme: 'http',
|
||||
sslScheme: 'https',
|
||||
|
||||
@@ -21,7 +21,6 @@ import { switchMap } from 'rxjs/operators'
|
||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||
import { PROMPT } from 'src/app/routes/portal/modals/prompt.component'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { ProxyService } from 'src/app/services/proxy.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { getServerInfo } from 'src/app/utils/get-server-info'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
@@ -35,7 +34,6 @@ import { ConfigService } from 'src/app/services/config.service'
|
||||
export class SettingsService {
|
||||
private readonly alerts = inject(TuiAlertService)
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly proxyService = inject(ProxyService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
|
||||
@@ -45,7 +45,7 @@ import { parseS9pk } from './sideload.utils'
|
||||
@if (error()) {
|
||||
<div>
|
||||
<tui-avatar appearance="secondary" src="@tui.circle-x" />
|
||||
<p class="g-error">{{ error() }}</p>
|
||||
<p class="g-negative">{{ error() }}</p>
|
||||
<button tuiButton>Try again</button>
|
||||
</div>
|
||||
} @else {
|
||||
@@ -53,7 +53,7 @@ import { parseS9pk } from './sideload.utils'
|
||||
<tui-avatar appearance="secondary" src="@tui.cloud-upload" />
|
||||
<p>Upload .s9pk package file</p>
|
||||
@if (isTor) {
|
||||
<p class="g-success">Tip: switch to LAN for faster uploads</p>
|
||||
<p class="g-positive">Tip: switch to LAN for faster uploads</p>
|
||||
}
|
||||
<button tuiButton>Upload</button>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
// </div>
|
||||
// @if (localPkg.stateInfo.state === 'updating') {
|
||||
// <tui-progress-circle
|
||||
// class="g-success"
|
||||
// class="g-positive"
|
||||
// size="s"
|
||||
// [max]="1"
|
||||
// [value]="
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
// {{ host.name }}
|
||||
// </h3>
|
||||
// @if (data.errors.includes(host.url)) {
|
||||
// <p class="g-error">Request Failed</p>
|
||||
// <p class="g-negative">Request Failed</p>
|
||||
// }
|
||||
// @if (data.mp[host.url]?.packages | filterUpdates: data.local; as pkgs) {
|
||||
// @for (pkg of pkgs; track pkg) {
|
||||
|
||||
@@ -37,6 +37,7 @@ const routes: Routes = [
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
scrollPositionRestoration: 'enabled',
|
||||
paramsInheritanceStrategy: 'always',
|
||||
preloadingStrategy: PreloadAllModules,
|
||||
initialNavigation: 'disabled',
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
InstalledState,
|
||||
PackageDataEntry,
|
||||
ServerStatusInfo,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { RR, ServerMetrics, ServerNotifications } from './api.types'
|
||||
import { BTC_ICON, LND_ICON, PROXY_ICON, REGISTRY_ICON } from './api-icons'
|
||||
@@ -22,14 +21,15 @@ const mockDescription = {
|
||||
long: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
|
||||
}
|
||||
|
||||
export module Mock {
|
||||
export const ServerUpdated: ServerStatusInfo = {
|
||||
currentBackup: null,
|
||||
export namespace Mock {
|
||||
export const ServerUpdated: T.ServerStatus = {
|
||||
backupProgress: null,
|
||||
updateProgress: null,
|
||||
updated: true,
|
||||
restarting: false,
|
||||
shuttingDown: false,
|
||||
}
|
||||
|
||||
export const MarketplaceEos: RR.CheckOSUpdateRes = {
|
||||
version: '0.3.6',
|
||||
headline: 'Our biggest release ever.',
|
||||
@@ -1012,131 +1012,192 @@ export module Mock {
|
||||
}
|
||||
|
||||
export const BackupTargets: RR.GetBackupTargetsRes = {
|
||||
unknownDisks: [
|
||||
{
|
||||
logicalname: 'sbc4',
|
||||
label: 'My Backup Drive',
|
||||
capacity: 2000000000000,
|
||||
used: 100000000000,
|
||||
model: 'T7',
|
||||
vendor: 'Samsung',
|
||||
startOs: {},
|
||||
},
|
||||
],
|
||||
saved: {
|
||||
hsbdjhasbasda: {
|
||||
type: 'cifs',
|
||||
name: 'Embassy Backups',
|
||||
hostname: 'smb://192.169.10.0',
|
||||
path: '/Desktop/embassy-backups',
|
||||
username: 'TestUser',
|
||||
mountable: true,
|
||||
startOs: {
|
||||
abcdefgh: {
|
||||
hostname: 'adjective-noun.local',
|
||||
version: '0.3.6',
|
||||
timestamp: new Date().toISOString(),
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: '',
|
||||
},
|
||||
hsbdjhasbasda: {
|
||||
type: 'cifs',
|
||||
hostname: 'smb://192.169.10.0',
|
||||
path: '/Desktop/startos-backups',
|
||||
username: 'TestUser',
|
||||
mountable: false,
|
||||
startOs: {
|
||||
'1234-5678-9876-5432': {
|
||||
hostname: 'adjective-noun',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '0.3.6',
|
||||
passwordHash:
|
||||
// password is asdfasdf
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: '',
|
||||
},
|
||||
},
|
||||
ftcvewdnkemfksdm: {
|
||||
type: 'cloud',
|
||||
name: 'Dropbox 1',
|
||||
provider: 'dropbox',
|
||||
path: '/Home/backups',
|
||||
mountable: false,
|
||||
startOs: {},
|
||||
},
|
||||
csgashbdjkasnd: {
|
||||
type: 'cifs',
|
||||
name: 'Network Folder 2',
|
||||
hostname: 'smb://192.169.10.0',
|
||||
path: '/Desktop/embassy-backups-2',
|
||||
username: 'TestUser',
|
||||
mountable: true,
|
||||
startOs: {},
|
||||
},
|
||||
powjefhjbnwhdva: {
|
||||
type: 'disk',
|
||||
name: 'Physical Drive 1',
|
||||
logicalname: 'sdba1',
|
||||
label: 'Another Drive',
|
||||
capacity: 2000000000000,
|
||||
used: 100000000000,
|
||||
model: null,
|
||||
vendor: 'SSK',
|
||||
mountable: true,
|
||||
path: '/HomeFolder/Documents',
|
||||
startOs: {
|
||||
'different-server': {
|
||||
hostname: 'different-server.local',
|
||||
version: '0.3.6',
|
||||
timestamp: new Date().toISOString(),
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: '',
|
||||
},
|
||||
},
|
||||
// 'ftcvewdnkemfksdm': {
|
||||
// type: 'disk',
|
||||
// logicalname: 'sdba1',
|
||||
// label: 'Matt Stuff',
|
||||
// capacity: 1000000000000,
|
||||
// used: 0,
|
||||
// model: 'Evo SATA 2.5',
|
||||
// vendor: 'Samsung',
|
||||
// startOs: {},
|
||||
// },
|
||||
csgashbdjkasnd: {
|
||||
type: 'cifs',
|
||||
hostname: 'smb://192.169.10.0',
|
||||
path: '/Desktop/startos-backups-2',
|
||||
username: 'TestUser',
|
||||
mountable: true,
|
||||
startOs: {},
|
||||
},
|
||||
powjefhjbnwhdva: {
|
||||
type: 'disk',
|
||||
logicalname: 'sdba1',
|
||||
label: 'Another Drive',
|
||||
capacity: 2000000000000,
|
||||
used: 100000000000,
|
||||
model: null,
|
||||
vendor: 'SSK',
|
||||
startOs: {
|
||||
'1234-5678-9876-5432': {
|
||||
hostname: 'adjective-noun',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '0.3.6',
|
||||
passwordHash:
|
||||
// password is asdfasdf
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const BackupJobs: RR.GetBackupJobsRes = [
|
||||
{
|
||||
id: 'lalalalalala-babababababa',
|
||||
name: 'My Backup Job',
|
||||
targetId: Object.keys(BackupTargets.saved)[0],
|
||||
cron: '0 3 * * *',
|
||||
packageIds: ['bitcoind', 'lnd'],
|
||||
},
|
||||
{
|
||||
id: 'hahahahaha-mwmwmwmwmwmw',
|
||||
name: 'Another Backup Job',
|
||||
targetId: Object.keys(BackupTargets.saved)[1],
|
||||
cron: '0 * * * *',
|
||||
packageIds: ['lnd'],
|
||||
},
|
||||
]
|
||||
// @TODO 041
|
||||
|
||||
export const BackupRuns: RR.GetBackupRunsRes = [
|
||||
{
|
||||
id: 'kladhbfweubdsk',
|
||||
startedAt: new Date().toISOString(),
|
||||
completedAt: new Date(new Date().valueOf() + 10000).toISOString(),
|
||||
packageIds: ['bitcoind', 'lnd'],
|
||||
job: BackupJobs[0],
|
||||
report: {
|
||||
server: {
|
||||
attempted: true,
|
||||
error: null,
|
||||
},
|
||||
packages: {
|
||||
bitcoind: { error: null },
|
||||
lnd: { error: null },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'kladhbfwhrfeubdsk',
|
||||
startedAt: new Date().toISOString(),
|
||||
completedAt: new Date(new Date().valueOf() + 10000).toISOString(),
|
||||
packageIds: ['bitcoind', 'lnd'],
|
||||
job: BackupJobs[0],
|
||||
report: {
|
||||
server: {
|
||||
attempted: true,
|
||||
error: null,
|
||||
},
|
||||
packages: {
|
||||
bitcoind: { error: null },
|
||||
lnd: { error: null },
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
// export const BackupTargets: RR.GetBackupTargetsRes = {
|
||||
// unknownDisks: [
|
||||
// {
|
||||
// logicalname: 'sbc4',
|
||||
// label: 'My Backup Drive',
|
||||
// capacity: 2000000000000,
|
||||
// used: 100000000000,
|
||||
// model: 'T7',
|
||||
// vendor: 'Samsung',
|
||||
// startOs: {},
|
||||
// },
|
||||
// ],
|
||||
// saved: {
|
||||
// hsbdjhasbasda: {
|
||||
// type: 'cifs',
|
||||
// name: 'Embassy Backups',
|
||||
// hostname: 'smb://192.169.10.0',
|
||||
// path: '/Desktop/embassy-backups',
|
||||
// username: 'TestUser',
|
||||
// mountable: true,
|
||||
// startOs: {
|
||||
// abcdefgh: {
|
||||
// hostname: 'adjective-noun.local',
|
||||
// version: '0.3.6',
|
||||
// timestamp: new Date().toISOString(),
|
||||
// passwordHash:
|
||||
// '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
// wrappedKey: '',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// ftcvewdnkemfksdm: {
|
||||
// type: 'cloud',
|
||||
// name: 'Dropbox 1',
|
||||
// provider: 'dropbox',
|
||||
// path: '/Home/backups',
|
||||
// mountable: false,
|
||||
// startOs: {},
|
||||
// },
|
||||
// csgashbdjkasnd: {
|
||||
// type: 'cifs',
|
||||
// name: 'Network Folder 2',
|
||||
// hostname: 'smb://192.169.10.0',
|
||||
// path: '/Desktop/embassy-backups-2',
|
||||
// username: 'TestUser',
|
||||
// mountable: true,
|
||||
// startOs: {},
|
||||
// },
|
||||
// powjefhjbnwhdva: {
|
||||
// type: 'disk',
|
||||
// name: 'Physical Drive 1',
|
||||
// logicalname: 'sdba1',
|
||||
// label: 'Another Drive',
|
||||
// capacity: 2000000000000,
|
||||
// used: 100000000000,
|
||||
// model: null,
|
||||
// vendor: 'SSK',
|
||||
// mountable: true,
|
||||
// path: '/HomeFolder/Documents',
|
||||
// startOs: {
|
||||
// 'different-server': {
|
||||
// hostname: 'different-server.local',
|
||||
// version: '0.3.6',
|
||||
// timestamp: new Date().toISOString(),
|
||||
// passwordHash:
|
||||
// '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
// wrappedKey: '',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
|
||||
// export const BackupJobs: RR.GetBackupJobsRes = [
|
||||
// {
|
||||
// id: 'lalalalalala-babababababa',
|
||||
// name: 'My Backup Job',
|
||||
// targetId: Object.keys(BackupTargets.saved)[0],
|
||||
// cron: '0 3 * * *',
|
||||
// packageIds: ['bitcoind', 'lnd'],
|
||||
// },
|
||||
// {
|
||||
// id: 'hahahahaha-mwmwmwmwmwmw',
|
||||
// name: 'Another Backup Job',
|
||||
// targetId: Object.keys(BackupTargets.saved)[1],
|
||||
// cron: '0 * * * *',
|
||||
// packageIds: ['lnd'],
|
||||
// },
|
||||
// ]
|
||||
|
||||
// export const BackupRuns: RR.GetBackupRunsRes = [
|
||||
// {
|
||||
// id: 'kladhbfweubdsk',
|
||||
// startedAt: new Date().toISOString(),
|
||||
// completedAt: new Date(new Date().valueOf() + 10000).toISOString(),
|
||||
// packageIds: ['bitcoind', 'lnd'],
|
||||
// job: BackupJobs[0],
|
||||
// report: {
|
||||
// server: {
|
||||
// attempted: true,
|
||||
// error: null,
|
||||
// },
|
||||
// packages: {
|
||||
// bitcoind: { error: null },
|
||||
// lnd: { error: null },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// id: 'kladhbfwhrfeubdsk',
|
||||
// startedAt: new Date().toISOString(),
|
||||
// completedAt: new Date(new Date().valueOf() + 10000).toISOString(),
|
||||
// packageIds: ['bitcoind', 'lnd'],
|
||||
// job: BackupJobs[0],
|
||||
// report: {
|
||||
// server: {
|
||||
// attempted: true,
|
||||
// error: null,
|
||||
// },
|
||||
// packages: {
|
||||
// bitcoind: { error: null },
|
||||
// lnd: { error: null },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
|
||||
export const BackupInfo: RR.GetBackupInfoRes = {
|
||||
version: '0.3.6',
|
||||
@@ -1819,9 +1880,7 @@ export module Mock {
|
||||
},
|
||||
dataVersion: MockManifestBitcoind.version,
|
||||
icon: '/assets/img/service-icons/bitcoind.svg',
|
||||
installedAt: new Date().toISOString(),
|
||||
lastBackup: null,
|
||||
nextBackup: null,
|
||||
status: {
|
||||
main: 'running',
|
||||
started: new Date().toISOString(),
|
||||
@@ -2065,7 +2124,6 @@ export module Mock {
|
||||
storeExposedDependents: [],
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
outboundProxy: null,
|
||||
requestedActions: {
|
||||
'bitcoind-config': {
|
||||
request: {
|
||||
@@ -2096,9 +2154,7 @@ export module Mock {
|
||||
},
|
||||
dataVersion: MockManifestBitcoinProxy.version,
|
||||
icon: '/assets/img/service-icons/btc-rpc-proxy.png',
|
||||
installedAt: new Date().toISOString(),
|
||||
lastBackup: null,
|
||||
nextBackup: null,
|
||||
status: {
|
||||
main: 'stopped',
|
||||
},
|
||||
@@ -2133,7 +2189,6 @@ export module Mock {
|
||||
storeExposedDependents: [],
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
outboundProxy: null,
|
||||
requestedActions: {},
|
||||
}
|
||||
|
||||
@@ -2144,9 +2199,7 @@ export module Mock {
|
||||
},
|
||||
dataVersion: MockManifestLnd.version,
|
||||
icon: '/assets/img/service-icons/lnd.png',
|
||||
installedAt: new Date().toISOString(),
|
||||
lastBackup: null,
|
||||
nextBackup: null,
|
||||
status: {
|
||||
main: 'stopped',
|
||||
},
|
||||
@@ -2239,7 +2292,6 @@ export module Mock {
|
||||
storeExposedDependents: [],
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
outboundProxy: null,
|
||||
requestedActions: {
|
||||
config: {
|
||||
active: true,
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { DomainInfo } from 'src/app/services/patch-db/data-model'
|
||||
import { FetchLogsReq, FetchLogsRes } from '@start9labs/shared'
|
||||
import { Dump } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { StartOSDiskInfo } from '@start9labs/shared'
|
||||
import { StartOSDiskInfo, FetchLogsReq, FetchLogsRes } from '@start9labs/shared'
|
||||
import { IST, T } from '@start9labs/start-sdk'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
|
||||
export module RR {
|
||||
export namespace RR {
|
||||
// websocket
|
||||
|
||||
export type WebsocketConfig<T> = Omit<WebSocketSubjectConfig<T>, 'url'>
|
||||
@@ -70,7 +68,7 @@ export module RR {
|
||||
uptime: number // seconds
|
||||
}
|
||||
|
||||
export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs & server.tor-logs
|
||||
export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs
|
||||
export type GetServerLogsRes = FetchLogsRes
|
||||
|
||||
export type FollowServerLogsReq = {
|
||||
@@ -83,6 +81,7 @@ export module RR {
|
||||
guid: string
|
||||
}
|
||||
|
||||
// @TODO 040 implement websocket
|
||||
export type FollowServerMetricsReq = {} // server.metrics.follow
|
||||
export type FollowServerMetricsRes = {
|
||||
guid: string
|
||||
@@ -92,9 +91,6 @@ export module RR {
|
||||
export type UpdateServerReq = { registry: string } // server.update
|
||||
export type UpdateServerRes = 'updating' | 'no-updates'
|
||||
|
||||
export type SetServerClearnetAddressReq = { domainInfo: DomainInfo | null } // server.set-clearnet
|
||||
export type SetServerClearnetAddressRes = null
|
||||
|
||||
export type RestartServerReq = {} // server.restart
|
||||
export type RestartServerRes = null
|
||||
|
||||
@@ -110,11 +106,6 @@ export module RR {
|
||||
} // net.tor.reset
|
||||
export type ResetTorRes = null
|
||||
|
||||
export type SetOsOutboundProxyReq = {
|
||||
proxy: string | null
|
||||
} // server.proxy.set-outbound
|
||||
export type SetOsOutboundProxyRes = null
|
||||
|
||||
// smtp
|
||||
|
||||
export type SetSMTPReq = T.SmtpValue // server.set-smtp
|
||||
@@ -139,18 +130,13 @@ export module RR {
|
||||
|
||||
// notification
|
||||
|
||||
export type FollowNotificationsReq = {}
|
||||
export type FollowNotificationsRes = {
|
||||
notifications: ServerNotifications
|
||||
guid: string
|
||||
}
|
||||
|
||||
export type GetNotificationsReq = {
|
||||
before?: number
|
||||
limit?: number
|
||||
} // notification.list
|
||||
export type GetNotificationsRes = ServerNotification<number>[]
|
||||
|
||||
// @TODO 040 all these notification endpoints need updating
|
||||
export type DeleteNotificationReq = { ids: number[] } // notification.delete
|
||||
export type DeleteNotificationRes = null
|
||||
|
||||
@@ -163,51 +149,12 @@ export module RR {
|
||||
export type MarkUnseenNotificationReq = DeleteNotificationReq // notification.mark-unseen
|
||||
export type MarkUnseenNotificationRes = null
|
||||
|
||||
// network
|
||||
|
||||
export type AddProxyReq = {
|
||||
name: string
|
||||
config: string
|
||||
} // net.proxy.add
|
||||
export type AddProxyRes = null
|
||||
|
||||
export type UpdateProxyReq = {
|
||||
name: string
|
||||
} // net.proxy.update
|
||||
export type UpdateProxyRes = null
|
||||
|
||||
export type DeleteProxyReq = { id: string } // net.proxy.delete
|
||||
export type DeleteProxyRes = null
|
||||
|
||||
// domains
|
||||
|
||||
export type ClaimStart9ToReq = { networkInterfaceId: string } // net.domain.me.claim
|
||||
export type ClaimStart9ToRes = null
|
||||
|
||||
export type DeleteStart9ToReq = {} // net.domain.me.delete
|
||||
export type DeleteStart9ToRes = null
|
||||
|
||||
export type AddDomainReq = {
|
||||
hostname: string
|
||||
provider: {
|
||||
name: string
|
||||
username: string | null
|
||||
password: string | null
|
||||
}
|
||||
networkInterfaceId: string
|
||||
} // net.domain.add
|
||||
export type AddDomainRes = null
|
||||
|
||||
export type DeleteDomainReq = { hostname: string } // net.domain.delete
|
||||
export type DeleteDomainRes = null
|
||||
|
||||
// port forwards
|
||||
|
||||
export type OverridePortReq = { target: number; port: number } // net.port-forwards.override
|
||||
export type OverridePortRes = null
|
||||
|
||||
// wifi
|
||||
|
||||
// @TODO remove for 040, set at server scope
|
||||
// export type SetWifiCountryReq = { country: string }
|
||||
// export type SetWifiCountryRes = null
|
||||
|
||||
export type GetWifiReq = {}
|
||||
export type GetWifiRes = {
|
||||
ssids: {
|
||||
@@ -228,23 +175,16 @@ export module RR {
|
||||
}
|
||||
export type AddWifiRes = null
|
||||
|
||||
// @TODO 040
|
||||
export type EnableWifiReq = { enable: boolean } // wifi.enable
|
||||
export type EnableWifiRes = null
|
||||
|
||||
export type ConnectWifiReq = { ssid: string } // wifi.connect
|
||||
export type ConnectWifiRes = null
|
||||
|
||||
export type DeleteWifiReq = { ssid: string } // wifi.delete
|
||||
export type DeleteWifiReq = { ssid: string } // wifi.remove
|
||||
export type DeleteWifiRes = null
|
||||
|
||||
// email
|
||||
|
||||
export type ConfigureEmailReq = T.SmtpValue // email.configure
|
||||
export type ConfigureEmailRes = null
|
||||
|
||||
export type TestEmailReq = ConfigureEmailReq & { to: string } // email.test
|
||||
export type TestEmailRes = null
|
||||
|
||||
// ssh
|
||||
|
||||
export type GetSSHKeysReq = {} // ssh.list
|
||||
@@ -253,84 +193,44 @@ export module RR {
|
||||
export type AddSSHKeyReq = { key: string } // ssh.add
|
||||
export type AddSSHKeyRes = SSHKey
|
||||
|
||||
export type DeleteSSHKeyReq = { fingerprint: string } // ssh.delete
|
||||
export type DeleteSSHKeyReq = { fingerprint: string } // ssh.remove
|
||||
export type DeleteSSHKeyRes = null
|
||||
|
||||
// backup
|
||||
|
||||
export type GetBackupTargetsReq = {} // backup.target.list
|
||||
export type GetBackupTargetsRes = {
|
||||
unknownDisks: UnknownDisk[]
|
||||
saved: Record<string, BackupTarget>
|
||||
}
|
||||
export type GetBackupTargetsRes = { [id: string]: BackupTarget }
|
||||
|
||||
export type AddCifsBackupTargetReq = {
|
||||
name: string
|
||||
path: string
|
||||
export type AddBackupTargetReq = {
|
||||
// backup.target.cifs.add
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
password?: string
|
||||
} // backup.target.cifs.add
|
||||
export type AddCloudBackupTargetReq = {
|
||||
name: string
|
||||
path: string
|
||||
provider: CloudProvider
|
||||
[params: string]: any
|
||||
} // backup.target.cloud.add
|
||||
export type AddDiskBackupTargetReq = {
|
||||
logicalname: string
|
||||
name: string
|
||||
path: string
|
||||
} // backup.target.disk.add
|
||||
export type AddBackupTargetRes = Record<string, BackupTarget>
|
||||
password: string | null
|
||||
}
|
||||
export type AddBackupTargetRes = { [id: string]: CifsBackupTarget }
|
||||
|
||||
export type UpdateCifsBackupTargetReq = AddCifsBackupTargetReq & {
|
||||
id: string
|
||||
} // backup.target.cifs.update
|
||||
export type UpdateCloudBackupTargetReq = AddCloudBackupTargetReq & {
|
||||
id: string
|
||||
} // backup.target.cloud.update
|
||||
export type UpdateDiskBackupTargetReq = Omit<
|
||||
AddDiskBackupTargetReq,
|
||||
'logicalname'
|
||||
> & {
|
||||
id: string
|
||||
} // backup.target.disk.update
|
||||
export type UpdateBackupTargetReq = AddBackupTargetReq & { id: string } // backup.target.cifs.update
|
||||
export type UpdateBackupTargetRes = AddBackupTargetRes
|
||||
|
||||
export type RemoveBackupTargetReq = { id: string } // backup.target.remove
|
||||
export type RemoveBackupTargetReq = { id: string } // backup.target.cifs.remove
|
||||
export type RemoveBackupTargetRes = null
|
||||
|
||||
export type GetBackupJobsReq = {} // backup.job.list
|
||||
export type GetBackupJobsRes = BackupJob[]
|
||||
|
||||
export type CreateBackupJobReq = {
|
||||
name: string
|
||||
export type GetBackupInfoReq = {
|
||||
// backup.target.info
|
||||
targetId: string
|
||||
cron: string
|
||||
packageIds: string[]
|
||||
now: boolean
|
||||
} // backup.job.create
|
||||
export type CreateBackupJobRes = BackupJob
|
||||
|
||||
export type UpdateBackupJobReq = Omit<CreateBackupJobReq, 'now'> & {
|
||||
id: string
|
||||
} // backup.job.update
|
||||
export type UpdateBackupJobRes = CreateBackupJobRes
|
||||
|
||||
export type DeleteBackupJobReq = { id: string } // backup.job.delete
|
||||
export type DeleteBackupJobRes = null
|
||||
|
||||
export type GetBackupRunsReq = {} // backup.runs
|
||||
export type GetBackupRunsRes = BackupRun[]
|
||||
|
||||
export type DeleteBackupRunsReq = { ids: string[] } // backup.runs.delete
|
||||
export type DeleteBackupRunsRes = null
|
||||
|
||||
export type GetBackupInfoReq = { targetId: string; password: string } // backup.target.info
|
||||
serverId: string
|
||||
password: string
|
||||
}
|
||||
export type GetBackupInfoRes = BackupInfo
|
||||
|
||||
export type CreateBackupReq = { targetId: string; packageIds: string[] } // backup.create
|
||||
export type CreateBackupReq = {
|
||||
// backup.create
|
||||
targetId: string
|
||||
packageIds: string[]
|
||||
oldPassword: string | null
|
||||
password: string
|
||||
}
|
||||
export type CreateBackupRes = null
|
||||
|
||||
// package
|
||||
@@ -375,7 +275,7 @@ export module RR {
|
||||
private: boolean
|
||||
acme: string | null // "letsencrypt" | "letsencrypt-staging" | Url | null
|
||||
}
|
||||
export type ServerAddDomainRes = null
|
||||
export type AddDomainRes = null
|
||||
|
||||
export type ServerRemoveDomainReq = {
|
||||
// server.host.address.domain.remove
|
||||
@@ -409,8 +309,8 @@ export module RR {
|
||||
host: T.HostId // string
|
||||
}
|
||||
|
||||
export type GetPackageLogsReq = GetServerLogsReq & { id: string } // package.logs
|
||||
export type GetPackageLogsRes = GetServerLogsRes
|
||||
export type GetPackageLogsReq = FetchLogsReq & { id: string } // package.logs
|
||||
export type GetPackageLogsRes = FetchLogsRes
|
||||
|
||||
export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow
|
||||
export type FollowPackageLogsRes = FollowServerLogsRes
|
||||
@@ -458,25 +358,12 @@ export module RR {
|
||||
export type SideloadPackageReq = {
|
||||
manifest: T.Manifest
|
||||
icon: string // base64
|
||||
size: number // bytes
|
||||
}
|
||||
export type SideloadPackageRes = {
|
||||
upload: string
|
||||
progress: string
|
||||
upload: string // guid
|
||||
progress: string // guid
|
||||
}
|
||||
|
||||
export type SetInterfaceClearnetAddressReq = SetServerClearnetAddressReq & {
|
||||
packageId: string
|
||||
interfaceId: string
|
||||
} // package.interface.set-clearnet
|
||||
export type SetInterfaceClearnetAddressRes = null
|
||||
|
||||
export type SetServiceOutboundProxyReq = {
|
||||
packageId: string
|
||||
proxy: string | null
|
||||
} // package.proxy.set-outbound
|
||||
export type SetServiceOutboundProxyRes = null
|
||||
|
||||
// registry
|
||||
|
||||
/** these are returned in ASCENDING order. the newest available version will be the LAST in the object */
|
||||
@@ -534,20 +421,6 @@ export type ServerMetrics = {
|
||||
}
|
||||
}
|
||||
|
||||
export type AppMetrics = {
|
||||
memory: {
|
||||
percentageUsed: MetricData
|
||||
used: MetricData
|
||||
}
|
||||
cpu: {
|
||||
percentageUsed: MetricData
|
||||
}
|
||||
disk: {
|
||||
percentageUsed: MetricData
|
||||
used: MetricData
|
||||
}
|
||||
}
|
||||
|
||||
export type Session = {
|
||||
loggedIn: string
|
||||
lastActive: string
|
||||
@@ -576,59 +449,41 @@ export type PlatformType =
|
||||
| 'desktop'
|
||||
| 'hybrid'
|
||||
|
||||
export type RemoteBackupTarget = CifsBackupTarget | CloudBackupTarget
|
||||
export type BackupTarget = RemoteBackupTarget | DiskBackupTarget
|
||||
export type BackupTarget = DiskBackupTarget | CifsBackupTarget
|
||||
|
||||
export type BackupTargetType = 'disk' | 'cifs' | 'cloud'
|
||||
|
||||
export interface UnknownDisk {
|
||||
logicalname: string
|
||||
export interface DiskBackupTarget {
|
||||
type: 'disk'
|
||||
vendor: string | null
|
||||
model: string | null
|
||||
logicalname: string | null
|
||||
label: string | null
|
||||
capacity: number
|
||||
used: number | null
|
||||
startOs: Record<string, StartOSDiskInfo>
|
||||
}
|
||||
|
||||
export interface BaseBackupTarget {
|
||||
type: BackupTargetType
|
||||
name: string
|
||||
mountable: boolean
|
||||
export interface CifsBackupTarget {
|
||||
type: 'cifs'
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
mountable: boolean
|
||||
startOs: Record<string, StartOSDiskInfo>
|
||||
}
|
||||
|
||||
export interface DiskBackupTarget extends UnknownDisk, BaseBackupTarget {
|
||||
export type RecoverySource = DiskRecoverySource | CifsRecoverySource
|
||||
|
||||
export interface DiskRecoverySource {
|
||||
type: 'disk'
|
||||
logicalname: string // partition logicalname
|
||||
}
|
||||
|
||||
export interface CifsBackupTarget extends BaseBackupTarget {
|
||||
export interface CifsRecoverySource {
|
||||
type: 'cifs'
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface CloudBackupTarget extends BaseBackupTarget {
|
||||
type: 'cloud'
|
||||
provider: 'dropbox' | 'google-drive'
|
||||
}
|
||||
|
||||
export type BackupRun = {
|
||||
id: string
|
||||
startedAt: string
|
||||
completedAt: string
|
||||
packageIds: string[]
|
||||
job: BackupJob
|
||||
report: BackupReport
|
||||
}
|
||||
|
||||
export type BackupJob = {
|
||||
id: string
|
||||
name: string
|
||||
targetId: string
|
||||
cron: string // '* * * * * *' https://cloud.google.com/scheduler/docs/configuring/cron-job-schedules
|
||||
packageIds: string[]
|
||||
password: string
|
||||
}
|
||||
|
||||
export type BackupInfo = {
|
||||
@@ -664,13 +519,16 @@ export type ServerNotification<T extends number> = {
|
||||
packageId: string | null
|
||||
createdAt: string
|
||||
code: T
|
||||
level: 'success' | 'info' | 'warning' | 'error'
|
||||
level: NotificationLevel
|
||||
title: string
|
||||
message: string
|
||||
data: NotificationData<T>
|
||||
// @TODO 040
|
||||
read: boolean
|
||||
}
|
||||
|
||||
export type NotificationLevel = 'success' | 'info' | 'warning' | 'error'
|
||||
|
||||
export type NotificationData<T> = T extends 0
|
||||
? null
|
||||
: T extends 1
|
||||
@@ -716,8 +574,6 @@ export type Encrypted = {
|
||||
encrypted: string
|
||||
}
|
||||
|
||||
export type CloudProvider = 'dropbox' | 'google-drive'
|
||||
|
||||
export type DependencyError =
|
||||
| DependencyErrorNotInstalled
|
||||
| DependencyErrorNotRunning
|
||||
@@ -752,3 +608,213 @@ export type DependencyErrorHealthChecksFailed = {
|
||||
export type DependencyErrorTransitive = {
|
||||
type: 'transitive'
|
||||
}
|
||||
|
||||
// **** @TODO 041 ****
|
||||
|
||||
// export namespace RR041 {
|
||||
// // ** domains **
|
||||
|
||||
// export type ClaimStart9ToReq = { networkInterfaceId: string } // net.domain.me.claim
|
||||
// export type ClaimStart9ToRes = null
|
||||
|
||||
// export type DeleteStart9ToReq = {} // net.domain.me.delete
|
||||
// export type DeleteStart9ToRes = null
|
||||
|
||||
// export type AddDomainReq = {
|
||||
// hostname: string
|
||||
// provider: {
|
||||
// name: string
|
||||
// username: string | null
|
||||
// password: string | null
|
||||
// }
|
||||
// networkInterfaceId: string
|
||||
// } // net.domain.add
|
||||
// export type AddDomainRes = null
|
||||
|
||||
// export type DeleteDomainReq = { hostname: string } // net.domain.delete
|
||||
// export type DeleteDomainRes = null
|
||||
|
||||
// // port forwards
|
||||
|
||||
// export type OverridePortReq = { target: number; port: number } // net.port-forwards.override
|
||||
// export type OverridePortRes = null
|
||||
|
||||
// // ** proxies **
|
||||
|
||||
// export type AddProxyReq = {
|
||||
// name: string
|
||||
// config: string
|
||||
// } // net.proxy.add
|
||||
// export type AddProxyRes = null
|
||||
|
||||
// export type UpdateProxyReq = {
|
||||
// name: string
|
||||
// } // net.proxy.update
|
||||
// export type UpdateProxyRes = null
|
||||
|
||||
// export type DeleteProxyReq = { id: string } // net.proxy.delete
|
||||
// export type DeleteProxyRes = null
|
||||
|
||||
// // ** set outbound proxies **
|
||||
|
||||
// export type SetOsOutboundProxyReq = {
|
||||
// proxy: string | null
|
||||
// } // server.proxy.set-outbound
|
||||
// export type SetOsOutboundProxyRes = null
|
||||
|
||||
// export type SetServiceOutboundProxyReq = {
|
||||
// packageId: string
|
||||
// proxy: string | null
|
||||
// } // package.proxy.set-outbound
|
||||
// export type SetServiceOutboundProxyRes = null
|
||||
|
||||
// // ** automated backups **
|
||||
|
||||
// export type GetBackupTargetsReq = {} // backup.target.list
|
||||
// export type GetBackupTargetsRes = {
|
||||
// unknownDisks: UnknownDisk[]
|
||||
// saved: Record<string, BackupTarget>
|
||||
// }
|
||||
|
||||
// export type AddCifsBackupTargetReq = {
|
||||
// name: string
|
||||
// path: string
|
||||
// hostname: string
|
||||
// username: string
|
||||
// password?: string
|
||||
// } // backup.target.cifs.add
|
||||
// export type AddCloudBackupTargetReq = {
|
||||
// name: string
|
||||
// path: string
|
||||
// provider: CloudProvider
|
||||
// [params: string]: any
|
||||
// } // backup.target.cloud.add
|
||||
// export type AddDiskBackupTargetReq = {
|
||||
// logicalname: string
|
||||
// name: string
|
||||
// path: string
|
||||
// } // backup.target.disk.add
|
||||
// export type AddBackupTargetRes = Record<string, BackupTarget>
|
||||
|
||||
// export type UpdateCifsBackupTargetReq = AddCifsBackupTargetReq & {
|
||||
// id: string
|
||||
// } // backup.target.cifs.update
|
||||
// export type UpdateCloudBackupTargetReq = AddCloudBackupTargetReq & {
|
||||
// id: string
|
||||
// } // backup.target.cloud.update
|
||||
// export type UpdateDiskBackupTargetReq = Omit<
|
||||
// AddDiskBackupTargetReq,
|
||||
// 'logicalname'
|
||||
// > & {
|
||||
// id: string
|
||||
// } // backup.target.disk.update
|
||||
// export type UpdateBackupTargetRes = AddBackupTargetRes
|
||||
|
||||
// export type RemoveBackupTargetReq = { id: string } // backup.target.remove
|
||||
// export type RemoveBackupTargetRes = null
|
||||
|
||||
// export type GetBackupJobsReq = {} // backup.job.list
|
||||
// export type GetBackupJobsRes = BackupJob[]
|
||||
|
||||
// export type CreateBackupJobReq = {
|
||||
// name: string
|
||||
// targetId: string
|
||||
// cron: string
|
||||
// packageIds: string[]
|
||||
// now: boolean
|
||||
// } // backup.job.create
|
||||
// export type CreateBackupJobRes = BackupJob
|
||||
|
||||
// export type UpdateBackupJobReq = Omit<CreateBackupJobReq, 'now'> & {
|
||||
// id: string
|
||||
// } // backup.job.update
|
||||
// export type UpdateBackupJobRes = CreateBackupJobRes
|
||||
|
||||
// export type DeleteBackupJobReq = { id: string } // backup.job.delete
|
||||
// export type DeleteBackupJobRes = null
|
||||
|
||||
// export type GetBackupRunsReq = {} // backup.runs
|
||||
// export type GetBackupRunsRes = BackupRun[]
|
||||
|
||||
// export type DeleteBackupRunsReq = { ids: string[] } // backup.runs.delete
|
||||
// export type DeleteBackupRunsRes = null
|
||||
|
||||
// export type GetBackupInfoReq = { targetId: string; password: string } // backup.target.info
|
||||
// export type GetBackupInfoRes = BackupInfo
|
||||
|
||||
// export type CreateBackupReq = { targetId: string; packageIds: string[] } // backup.create
|
||||
// export type CreateBackupRes = null
|
||||
// }
|
||||
|
||||
// @TODO 041 types
|
||||
|
||||
// export type AppMetrics = {
|
||||
// memory: {
|
||||
// percentageUsed: MetricData
|
||||
// used: MetricData
|
||||
// }
|
||||
// cpu: {
|
||||
// percentageUsed: MetricData
|
||||
// }
|
||||
// disk: {
|
||||
// percentageUsed: MetricData
|
||||
// used: MetricData
|
||||
// }
|
||||
// }
|
||||
|
||||
// export type RemoteBackupTarget = CifsBackupTarget | CloudBackupTarget
|
||||
// export type BackupTarget = RemoteBackupTarget | DiskBackupTarget
|
||||
|
||||
// export type BackupTargetType = 'disk' | 'cifs' | 'cloud'
|
||||
|
||||
// export interface UnknownDisk {
|
||||
// logicalname: string
|
||||
// vendor: string | null
|
||||
// model: string | null
|
||||
// label: string | null
|
||||
// capacity: number
|
||||
// used: number | null
|
||||
// startOs: Record<string, StartOSDiskInfo>
|
||||
// }
|
||||
|
||||
// export interface BaseBackupTarget {
|
||||
// type: BackupTargetType
|
||||
// name: string
|
||||
// mountable: boolean
|
||||
// path: string
|
||||
// startOs: Record<string, StartOSDiskInfo>
|
||||
// }
|
||||
|
||||
// export interface DiskBackupTarget extends UnknownDisk, BaseBackupTarget {
|
||||
// type: 'disk'
|
||||
// }
|
||||
|
||||
// export interface CifsBackupTarget extends BaseBackupTarget {
|
||||
// type: 'cifs'
|
||||
// hostname: string
|
||||
// username: string
|
||||
// }
|
||||
|
||||
// export interface CloudBackupTarget extends BaseBackupTarget {
|
||||
// type: 'cloud'
|
||||
// provider: 'dropbox' | 'google-drive'
|
||||
// }
|
||||
|
||||
// export type BackupRun = {
|
||||
// id: string
|
||||
// startedAt: string
|
||||
// completedAt: string
|
||||
// packageIds: string[]
|
||||
// job: BackupJob
|
||||
// report: BackupReport
|
||||
// }
|
||||
|
||||
// export type BackupJob = {
|
||||
// id: string
|
||||
// name: string
|
||||
// targetId: string
|
||||
// cron: string // '* * * * * *' https://cloud.google.com/scheduler/docs/configuring/cron-job-schedules
|
||||
// packageIds: string[]
|
||||
// }
|
||||
|
||||
// export type CloudProvider = 'dropbox' | 'google-drive'
|
||||
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
} from '@start9labs/marketplace'
|
||||
import { RPCOptions } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { Observable } from 'rxjs'
|
||||
import { BackupTargetType, RR } from './api.types'
|
||||
import { RR } from './api.types'
|
||||
import { WebSocketSubject } from 'rxjs/webSocket'
|
||||
|
||||
export abstract class ApiService {
|
||||
@@ -15,8 +14,6 @@ export abstract class ApiService {
|
||||
// for sideloading packages
|
||||
abstract uploadPackage(guid: string, body: Blob): Promise<void>
|
||||
|
||||
abstract uploadFile(body: Blob): Promise<string>
|
||||
|
||||
// for getting static files: ex icons, instructions, licenses
|
||||
abstract getStaticProxy(
|
||||
pkg: MarketplacePkg,
|
||||
@@ -118,10 +115,6 @@ export abstract class ApiService {
|
||||
|
||||
abstract updateServer(url?: string): Promise<RR.UpdateServerRes>
|
||||
|
||||
abstract setServerClearnetAddress(
|
||||
params: RR.SetServerClearnetAddressReq,
|
||||
): Promise<RR.SetServerClearnetAddressRes>
|
||||
|
||||
abstract restartServer(
|
||||
params: RR.RestartServerReq,
|
||||
): Promise<RR.RestartServerRes>
|
||||
@@ -134,9 +127,13 @@ export abstract class ApiService {
|
||||
|
||||
abstract resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes>
|
||||
|
||||
abstract setOsOutboundProxy(
|
||||
params: RR.SetOsOutboundProxyReq,
|
||||
): Promise<RR.SetOsOutboundProxyRes>
|
||||
// @TODO 041
|
||||
|
||||
// ** server outbound proxy **
|
||||
|
||||
// abstract setOsOutboundProxy(
|
||||
// params: RR.SetOsOutboundProxyReq,
|
||||
// ): Promise<RR.SetOsOutboundProxyRes>
|
||||
|
||||
// smtp
|
||||
|
||||
@@ -187,33 +184,39 @@ export abstract class ApiService {
|
||||
params: RR.DeleteNotificationReq,
|
||||
): Promise<RR.DeleteNotificationRes>
|
||||
|
||||
// network
|
||||
// ** proxies **
|
||||
|
||||
abstract addProxy(params: RR.AddProxyReq): Promise<RR.AddProxyRes>
|
||||
// @TODO 041
|
||||
|
||||
abstract updateProxy(params: RR.UpdateProxyReq): Promise<RR.UpdateProxyRes>
|
||||
// abstract addProxy(params: RR.AddProxyReq): Promise<RR.AddProxyRes>
|
||||
|
||||
abstract deleteProxy(params: RR.DeleteProxyReq): Promise<RR.DeleteProxyRes>
|
||||
// abstract updateProxy(params: RR.UpdateProxyReq): Promise<RR.UpdateProxyRes>
|
||||
|
||||
// domains
|
||||
// abstract deleteProxy(params: RR.DeleteProxyReq): Promise<RR.DeleteProxyRes>
|
||||
|
||||
abstract claimStart9ToDomain(
|
||||
params: RR.ClaimStart9ToReq,
|
||||
): Promise<RR.ClaimStart9ToRes>
|
||||
// ** domains **
|
||||
|
||||
abstract deleteStart9ToDomain(
|
||||
params: RR.DeleteStart9ToReq,
|
||||
): Promise<RR.DeleteStart9ToRes>
|
||||
// @TODO 041
|
||||
|
||||
abstract addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes>
|
||||
// abstract claimStart9ToDomain(
|
||||
// params: RR.ClaimStart9ToReq,
|
||||
// ): Promise<RR.ClaimStart9ToRes>
|
||||
|
||||
abstract deleteDomain(params: RR.DeleteDomainReq): Promise<RR.DeleteDomainRes>
|
||||
// abstract deleteStart9ToDomain(
|
||||
// params: RR.DeleteStart9ToReq,
|
||||
// ): Promise<RR.DeleteStart9ToRes>
|
||||
|
||||
// port forwards
|
||||
// abstract addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes>
|
||||
|
||||
abstract overridePortForward(
|
||||
params: RR.OverridePortReq,
|
||||
): Promise<RR.OverridePortRes>
|
||||
// abstract deleteDomain(params: RR.DeleteDomainReq): Promise<RR.DeleteDomainRes>
|
||||
|
||||
// ** port forwards **
|
||||
|
||||
// @TODO 041
|
||||
|
||||
// abstract overridePortForward(
|
||||
// params: RR.OverridePortReq,
|
||||
// ): Promise<RR.OverridePortRes>
|
||||
|
||||
// wifi
|
||||
|
||||
@@ -245,55 +248,71 @@ export abstract class ApiService {
|
||||
): Promise<RR.GetBackupTargetsRes>
|
||||
|
||||
abstract addBackupTarget(
|
||||
type: BackupTargetType,
|
||||
params:
|
||||
| RR.AddCifsBackupTargetReq
|
||||
| RR.AddCloudBackupTargetReq
|
||||
| RR.AddDiskBackupTargetReq,
|
||||
params: RR.AddBackupTargetReq,
|
||||
): Promise<RR.AddBackupTargetRes>
|
||||
|
||||
abstract updateBackupTarget(
|
||||
type: BackupTargetType,
|
||||
params:
|
||||
| RR.UpdateCifsBackupTargetReq
|
||||
| RR.UpdateCloudBackupTargetReq
|
||||
| RR.UpdateDiskBackupTargetReq,
|
||||
params: RR.UpdateBackupTargetReq,
|
||||
): Promise<RR.UpdateBackupTargetRes>
|
||||
|
||||
abstract removeBackupTarget(
|
||||
params: RR.RemoveBackupTargetReq,
|
||||
): Promise<RR.RemoveBackupTargetRes>
|
||||
|
||||
abstract getBackupJobs(
|
||||
params: RR.GetBackupJobsReq,
|
||||
): Promise<RR.GetBackupJobsRes>
|
||||
|
||||
abstract createBackupJob(
|
||||
params: RR.CreateBackupJobReq,
|
||||
): Promise<RR.CreateBackupJobRes>
|
||||
|
||||
abstract updateBackupJob(
|
||||
params: RR.UpdateBackupJobReq,
|
||||
): Promise<RR.UpdateBackupJobRes>
|
||||
|
||||
abstract deleteBackupJob(
|
||||
params: RR.DeleteBackupJobReq,
|
||||
): Promise<RR.DeleteBackupJobRes>
|
||||
|
||||
abstract getBackupRuns(
|
||||
params: RR.GetBackupRunsReq,
|
||||
): Promise<RR.GetBackupRunsRes>
|
||||
|
||||
abstract deleteBackupRuns(
|
||||
params: RR.DeleteBackupRunsReq,
|
||||
): Promise<RR.DeleteBackupRunsRes>
|
||||
|
||||
abstract getBackupInfo(
|
||||
params: RR.GetBackupInfoReq,
|
||||
): Promise<RR.GetBackupInfoRes>
|
||||
|
||||
abstract createBackup(params: RR.CreateBackupReq): Promise<RR.CreateBackupRes>
|
||||
|
||||
// @TODO 041
|
||||
|
||||
// ** automated backups **
|
||||
|
||||
// abstract addBackupTarget(
|
||||
// type: BackupTargetType,
|
||||
// params:
|
||||
// | RR.AddCifsBackupTargetReq
|
||||
// | RR.AddCloudBackupTargetReq
|
||||
// | RR.AddDiskBackupTargetReq,
|
||||
// ): Promise<RR.AddBackupTargetRes>
|
||||
|
||||
// abstract updateBackupTarget(
|
||||
// type: BackupTargetType,
|
||||
// params:
|
||||
// | RR.UpdateCifsBackupTargetReq
|
||||
// | RR.UpdateCloudBackupTargetReq
|
||||
// | RR.UpdateDiskBackupTargetReq,
|
||||
// ): Promise<RR.UpdateBackupTargetRes>
|
||||
|
||||
// abstract removeBackupTarget(
|
||||
// params: RR.RemoveBackupTargetReq,
|
||||
// ): Promise<RR.RemoveBackupTargetRes>
|
||||
|
||||
// abstract getBackupJobs(
|
||||
// params: RR.GetBackupJobsReq,
|
||||
// ): Promise<RR.GetBackupJobsRes>
|
||||
|
||||
// abstract createBackupJob(
|
||||
// params: RR.CreateBackupJobReq,
|
||||
// ): Promise<RR.CreateBackupJobRes>
|
||||
|
||||
// abstract updateBackupJob(
|
||||
// params: RR.UpdateBackupJobReq,
|
||||
// ): Promise<RR.UpdateBackupJobRes>
|
||||
|
||||
// abstract deleteBackupJob(
|
||||
// params: RR.DeleteBackupJobReq,
|
||||
// ): Promise<RR.DeleteBackupJobRes>
|
||||
|
||||
// abstract getBackupRuns(
|
||||
// params: RR.GetBackupRunsReq,
|
||||
// ): Promise<RR.GetBackupRunsRes>
|
||||
|
||||
// abstract deleteBackupRuns(
|
||||
// params: RR.DeleteBackupRunsReq,
|
||||
// ): Promise<RR.DeleteBackupRunsRes>
|
||||
|
||||
// package
|
||||
|
||||
abstract getPackageLogs(
|
||||
@@ -336,13 +355,13 @@ export abstract class ApiService {
|
||||
|
||||
abstract sideloadPackage(): Promise<RR.SideloadPackageRes>
|
||||
|
||||
abstract setInterfaceClearnetAddress(
|
||||
params: RR.SetInterfaceClearnetAddressReq,
|
||||
): Promise<RR.SetInterfaceClearnetAddressRes>
|
||||
// @TODO 041
|
||||
|
||||
abstract setServiceOutboundProxy(
|
||||
params: RR.SetServiceOutboundProxyReq,
|
||||
): Promise<RR.SetServiceOutboundProxyRes>
|
||||
// ** service outbound proxy **
|
||||
|
||||
// abstract setServiceOutboundProxy(
|
||||
// params: RR.SetServiceOutboundProxyReq,
|
||||
// ): Promise<RR.SetServiceOutboundProxyRes>
|
||||
|
||||
abstract initAcme(params: RR.InitAcmeReq): Promise<RR.InitAcmeRes>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from '@start9labs/shared'
|
||||
import { PATCH_CACHE } from 'src/app/services/patch-db/patch-db-source'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import { BackupTargetType, RR } from './api.types'
|
||||
import { RR } from './api.types'
|
||||
import { ConfigService } from '../config.service'
|
||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'
|
||||
import { Observable, filter, firstValueFrom } from 'rxjs'
|
||||
@@ -52,14 +52,6 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async uploadFile(body: Blob): Promise<string> {
|
||||
return this.httpRequest({
|
||||
method: Method.POST,
|
||||
body,
|
||||
url: `/rest/upload`,
|
||||
})
|
||||
}
|
||||
|
||||
// for getting static files: ex. instructions, licenses
|
||||
|
||||
async getStaticProxy(
|
||||
@@ -253,7 +245,8 @@ export class LiveApiService extends ApiService {
|
||||
async followServerMetrics(
|
||||
params: RR.FollowServerMetricsReq,
|
||||
): Promise<RR.FollowServerMetricsRes> {
|
||||
return this.rpcRequest({ method: 'server.metrics', params })
|
||||
// @TODO 040 implement .follow
|
||||
return this.rpcRequest({ method: 'server.metrics.follow', params })
|
||||
}
|
||||
|
||||
async updateServer(url?: string): Promise<RR.UpdateServerRes> {
|
||||
@@ -263,12 +256,6 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'server.update', params })
|
||||
}
|
||||
|
||||
async setServerClearnetAddress(
|
||||
params: RR.SetServerClearnetAddressReq,
|
||||
): Promise<RR.SetServerClearnetAddressRes> {
|
||||
return this.rpcRequest({ method: 'server.set-clearnet', params })
|
||||
}
|
||||
|
||||
async restartServer(
|
||||
params: RR.RestartServerReq,
|
||||
): Promise<RR.RestartServerRes> {
|
||||
@@ -289,11 +276,11 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'net.tor.reset', params })
|
||||
}
|
||||
|
||||
async setOsOutboundProxy(
|
||||
params: RR.SetOsOutboundProxyReq,
|
||||
): Promise<RR.SetOsOutboundProxyRes> {
|
||||
return this.rpcRequest({ method: 'server.proxy.set-outbound', params })
|
||||
}
|
||||
// async setOsOutboundProxy(
|
||||
// params: RR.SetOsOutboundProxyReq,
|
||||
// ): Promise<RR.SetOsOutboundProxyRes> {
|
||||
// return this.rpcRequest({ method: 'server.proxy.set-outbound', params })
|
||||
// }
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
@@ -389,49 +376,49 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'notification.mark-unseen', params })
|
||||
}
|
||||
|
||||
// network
|
||||
// proxies
|
||||
|
||||
async addProxy(params: RR.AddProxyReq): Promise<RR.AddProxyRes> {
|
||||
return this.rpcRequest({ method: 'net.proxy.add', params })
|
||||
}
|
||||
// async addProxy(params: RR.AddProxyReq): Promise<RR.AddProxyRes> {
|
||||
// return this.rpcRequest({ method: 'net.proxy.add', params })
|
||||
// }
|
||||
|
||||
async updateProxy(params: RR.UpdateProxyReq): Promise<RR.UpdateProxyRes> {
|
||||
return this.rpcRequest({ method: 'net.proxy.update', params })
|
||||
}
|
||||
// async updateProxy(params: RR.UpdateProxyReq): Promise<RR.UpdateProxyRes> {
|
||||
// return this.rpcRequest({ method: 'net.proxy.update', params })
|
||||
// }
|
||||
|
||||
async deleteProxy(params: RR.DeleteProxyReq): Promise<RR.DeleteProxyRes> {
|
||||
return this.rpcRequest({ method: 'net.proxy.delete', params })
|
||||
}
|
||||
// async deleteProxy(params: RR.DeleteProxyReq): Promise<RR.DeleteProxyRes> {
|
||||
// return this.rpcRequest({ method: 'net.proxy.delete', params })
|
||||
// }
|
||||
|
||||
// domains
|
||||
|
||||
async claimStart9ToDomain(
|
||||
params: RR.ClaimStart9ToReq,
|
||||
): Promise<RR.ClaimStart9ToRes> {
|
||||
return this.rpcRequest({ method: 'net.domain.me.claim', params })
|
||||
}
|
||||
// async claimStart9ToDomain(
|
||||
// params: RR.ClaimStart9ToReq,
|
||||
// ): Promise<RR.ClaimStart9ToRes> {
|
||||
// return this.rpcRequest({ method: 'net.domain.me.claim', params })
|
||||
// }
|
||||
|
||||
async deleteStart9ToDomain(
|
||||
params: RR.DeleteStart9ToReq,
|
||||
): Promise<RR.DeleteStart9ToRes> {
|
||||
return this.rpcRequest({ method: 'net.domain.me.delete', params })
|
||||
}
|
||||
// async deleteStart9ToDomain(
|
||||
// params: RR.DeleteStart9ToReq,
|
||||
// ): Promise<RR.DeleteStart9ToRes> {
|
||||
// return this.rpcRequest({ method: 'net.domain.me.delete', params })
|
||||
// }
|
||||
|
||||
async addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes> {
|
||||
return this.rpcRequest({ method: 'net.domain.add', params })
|
||||
}
|
||||
// async addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes> {
|
||||
// return this.rpcRequest({ method: 'net.domain.add', params })
|
||||
// }
|
||||
|
||||
async deleteDomain(params: RR.DeleteDomainReq): Promise<RR.DeleteDomainRes> {
|
||||
return this.rpcRequest({ method: 'net.domain.delete', params })
|
||||
}
|
||||
// async deleteDomain(params: RR.DeleteDomainReq): Promise<RR.DeleteDomainRes> {
|
||||
// return this.rpcRequest({ method: 'net.domain.delete', params })
|
||||
// }
|
||||
|
||||
// port forwards
|
||||
|
||||
async overridePortForward(
|
||||
params: RR.OverridePortReq,
|
||||
): Promise<RR.OverridePortRes> {
|
||||
return this.rpcRequest({ method: 'net.port-forwards.override', params })
|
||||
}
|
||||
// async overridePortForward(
|
||||
// params: RR.OverridePortReq,
|
||||
// ): Promise<RR.OverridePortRes> {
|
||||
// return this.rpcRequest({ method: 'net.port-forwards.override', params })
|
||||
// }
|
||||
|
||||
// wifi
|
||||
|
||||
@@ -455,7 +442,7 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
|
||||
async deleteWifi(params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes> {
|
||||
return this.rpcRequest({ method: 'wifi.delete', params })
|
||||
return this.rpcRequest({ method: 'wifi.remove', params })
|
||||
}
|
||||
|
||||
// smtp
|
||||
@@ -483,7 +470,7 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
|
||||
async deleteSshKey(params: RR.DeleteSSHKeyReq): Promise<RR.DeleteSSHKeyRes> {
|
||||
return this.rpcRequest({ method: 'ssh.delete', params })
|
||||
return this.rpcRequest({ method: 'ssh.remove', params })
|
||||
}
|
||||
|
||||
// backup
|
||||
@@ -495,60 +482,22 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
|
||||
async addBackupTarget(
|
||||
type: BackupTargetType,
|
||||
params: RR.AddCifsBackupTargetReq | RR.AddCloudBackupTargetReq,
|
||||
params: RR.AddBackupTargetReq,
|
||||
): Promise<RR.AddBackupTargetRes> {
|
||||
params.path = params.path.replace('/\\/g', '/')
|
||||
return this.rpcRequest({ method: `backup.target.${type}.add`, params })
|
||||
return this.rpcRequest({ method: 'backup.target.cifs.add', params })
|
||||
}
|
||||
|
||||
async updateBackupTarget(
|
||||
type: BackupTargetType,
|
||||
params: RR.UpdateCifsBackupTargetReq | RR.UpdateCloudBackupTargetReq,
|
||||
params: RR.UpdateBackupTargetReq,
|
||||
): Promise<RR.UpdateBackupTargetRes> {
|
||||
return this.rpcRequest({ method: `backup.target.${type}.update`, params })
|
||||
return this.rpcRequest({ method: 'backup.target.cifs.update', params })
|
||||
}
|
||||
|
||||
async removeBackupTarget(
|
||||
params: RR.RemoveBackupTargetReq,
|
||||
): Promise<RR.RemoveBackupTargetRes> {
|
||||
return this.rpcRequest({ method: 'backup.target.remove', params })
|
||||
}
|
||||
|
||||
async getBackupJobs(
|
||||
params: RR.GetBackupJobsReq,
|
||||
): Promise<RR.GetBackupJobsRes> {
|
||||
return this.rpcRequest({ method: 'backup.job.list', params })
|
||||
}
|
||||
|
||||
async createBackupJob(
|
||||
params: RR.CreateBackupJobReq,
|
||||
): Promise<RR.CreateBackupJobRes> {
|
||||
return this.rpcRequest({ method: 'backup.job.create', params })
|
||||
}
|
||||
|
||||
async updateBackupJob(
|
||||
params: RR.UpdateBackupJobReq,
|
||||
): Promise<RR.UpdateBackupJobRes> {
|
||||
return this.rpcRequest({ method: 'backup.job.update', params })
|
||||
}
|
||||
|
||||
async deleteBackupJob(
|
||||
params: RR.DeleteBackupJobReq,
|
||||
): Promise<RR.DeleteBackupJobRes> {
|
||||
return this.rpcRequest({ method: 'backup.job.delete', params })
|
||||
}
|
||||
|
||||
async getBackupRuns(
|
||||
params: RR.GetBackupRunsReq,
|
||||
): Promise<RR.GetBackupRunsRes> {
|
||||
return this.rpcRequest({ method: 'backup.runs.list', params })
|
||||
}
|
||||
|
||||
async deleteBackupRuns(
|
||||
params: RR.DeleteBackupRunsReq,
|
||||
): Promise<RR.DeleteBackupRunsRes> {
|
||||
return this.rpcRequest({ method: 'backup.runs.delete', params })
|
||||
return this.rpcRequest({ method: 'backup.target.cifs.remove', params })
|
||||
}
|
||||
|
||||
async getBackupInfo(
|
||||
@@ -561,6 +510,63 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'backup.create', params })
|
||||
}
|
||||
|
||||
// async addBackupTarget(
|
||||
// type: BackupTargetType,
|
||||
// params: RR.AddCifsBackupTargetReq | RR.AddCloudBackupTargetReq,
|
||||
// ): Promise<RR.AddBackupTargetRes> {
|
||||
// params.path = params.path.replace('/\\/g', '/')
|
||||
// return this.rpcRequest({ method: `backup.target.${type}.add`, params })
|
||||
// }
|
||||
|
||||
// async updateBackupTarget(
|
||||
// type: BackupTargetType,
|
||||
// params: RR.UpdateCifsBackupTargetReq | RR.UpdateCloudBackupTargetReq,
|
||||
// ): Promise<RR.UpdateBackupTargetRes> {
|
||||
// return this.rpcRequest({ method: `backup.target.${type}.update`, params })
|
||||
// }
|
||||
|
||||
// async removeBackupTarget(
|
||||
// params: RR.RemoveBackupTargetReq,
|
||||
// ): Promise<RR.RemoveBackupTargetRes> {
|
||||
// return this.rpcRequest({ method: 'backup.target.remove', params })
|
||||
// }
|
||||
|
||||
// async getBackupJobs(
|
||||
// params: RR.GetBackupJobsReq,
|
||||
// ): Promise<RR.GetBackupJobsRes> {
|
||||
// return this.rpcRequest({ method: 'backup.job.list', params })
|
||||
// }
|
||||
|
||||
// async createBackupJob(
|
||||
// params: RR.CreateBackupJobReq,
|
||||
// ): Promise<RR.CreateBackupJobRes> {
|
||||
// return this.rpcRequest({ method: 'backup.job.create', params })
|
||||
// }
|
||||
|
||||
// async updateBackupJob(
|
||||
// params: RR.UpdateBackupJobReq,
|
||||
// ): Promise<RR.UpdateBackupJobRes> {
|
||||
// return this.rpcRequest({ method: 'backup.job.update', params })
|
||||
// }
|
||||
|
||||
// async deleteBackupJob(
|
||||
// params: RR.DeleteBackupJobReq,
|
||||
// ): Promise<RR.DeleteBackupJobRes> {
|
||||
// return this.rpcRequest({ method: 'backup.job.delete', params })
|
||||
// }
|
||||
|
||||
// async getBackupRuns(
|
||||
// params: RR.GetBackupRunsReq,
|
||||
// ): Promise<RR.GetBackupRunsRes> {
|
||||
// return this.rpcRequest({ method: 'backup.runs.list', params })
|
||||
// }
|
||||
|
||||
// async deleteBackupRuns(
|
||||
// params: RR.DeleteBackupRunsReq,
|
||||
// ): Promise<RR.DeleteBackupRunsRes> {
|
||||
// return this.rpcRequest({ method: 'backup.runs.delete', params })
|
||||
// }
|
||||
|
||||
// package
|
||||
|
||||
async getPackageLogs(
|
||||
@@ -630,21 +636,15 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async setInterfaceClearnetAddress(
|
||||
params: RR.SetInterfaceClearnetAddressReq,
|
||||
): Promise<RR.SetInterfaceClearnetAddressRes> {
|
||||
return this.rpcRequest({ method: 'package.interface.set-clearnet', params })
|
||||
}
|
||||
|
||||
async setServiceOutboundProxy(
|
||||
params: RR.SetServiceOutboundProxyReq,
|
||||
): Promise<RR.SetServiceOutboundProxyRes> {
|
||||
return this.rpcRequest({ method: 'package.proxy.set-outbound', params })
|
||||
}
|
||||
// async setServiceOutboundProxy(
|
||||
// params: RR.SetServiceOutboundProxyReq,
|
||||
// ): Promise<RR.SetServiceOutboundProxyRes> {
|
||||
// return this.rpcRequest({ method: 'package.proxy.set-outbound', params })
|
||||
// }
|
||||
|
||||
async removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'net.acme.delete',
|
||||
method: 'net.acme.remove',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
StateInfo,
|
||||
UpdatingState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { BackupTargetType, RR } from './api.types'
|
||||
import { CifsBackupTarget, RR } from './api.types'
|
||||
import { Mock } from './api.fixures'
|
||||
import { from, interval, map, shareReplay, startWith, Subject, tap } from 'rxjs'
|
||||
import { mockPatchData } from './mock-patch'
|
||||
@@ -79,11 +79,6 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
}
|
||||
|
||||
async uploadFile(body: Blob): Promise<string> {
|
||||
await pauseFor(2000)
|
||||
return 'returnedhash'
|
||||
}
|
||||
|
||||
async getStaticProxy(
|
||||
pkg: MarketplacePkg,
|
||||
path: 'LICENSE.md' | 'instructions.md',
|
||||
@@ -391,23 +386,6 @@ export class MockApiService extends ApiService {
|
||||
return 'updating'
|
||||
}
|
||||
|
||||
async setServerClearnetAddress(
|
||||
params: RR.SetServerClearnetAddressReq,
|
||||
): Promise<RR.SetServerClearnetAddressRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/ui/domainInfo',
|
||||
value: params.domainInfo,
|
||||
},
|
||||
]
|
||||
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async restartServer(
|
||||
params: RR.RestartServerReq,
|
||||
): Promise<RR.RestartServerRes> {
|
||||
@@ -474,22 +452,22 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async setOsOutboundProxy(
|
||||
params: RR.SetOsOutboundProxyReq,
|
||||
): Promise<RR.SetOsOutboundProxyRes> {
|
||||
await pauseFor(2000)
|
||||
// async setOsOutboundProxy(
|
||||
// params: RR.SetOsOutboundProxyReq,
|
||||
// ): Promise<RR.SetOsOutboundProxyRes> {
|
||||
// await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/network/outboundProxy',
|
||||
value: params.proxy,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
// const patch = [
|
||||
// {
|
||||
// op: PatchOp.REPLACE,
|
||||
// path: '/serverInfo/network/outboundProxy',
|
||||
// value: params.proxy,
|
||||
// },
|
||||
// ]
|
||||
// this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
// return null
|
||||
// }
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
@@ -570,151 +548,151 @@ export class MockApiService extends ApiService {
|
||||
|
||||
// network
|
||||
|
||||
async addProxy(params: RR.AddProxyReq): Promise<RR.AddProxyRes> {
|
||||
await pauseFor(2000)
|
||||
// async addProxy(params: RR.AddProxyReq): Promise<RR.AddProxyRes> {
|
||||
// await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/network/networkInterfaces/wga1`,
|
||||
value: {
|
||||
inbound: true,
|
||||
outbound: true,
|
||||
ipInfo: {
|
||||
name: params.name,
|
||||
scopeId: 3,
|
||||
deviceType: 'wireguard',
|
||||
subnets: [],
|
||||
wanIp: '1.1.1.1',
|
||||
ntpServers: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
// const patch = [
|
||||
// {
|
||||
// op: PatchOp.ADD,
|
||||
// path: `/serverInfo/network/networkInterfaces/wga1`,
|
||||
// value: {
|
||||
// inbound: true,
|
||||
// outbound: true,
|
||||
// ipInfo: {
|
||||
// name: params.name,
|
||||
// scopeId: 3,
|
||||
// deviceType: 'wireguard',
|
||||
// subnets: [],
|
||||
// wanIp: '1.1.1.1',
|
||||
// ntpServers: [],
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
// this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
// return null
|
||||
// }
|
||||
|
||||
async updateProxy(params: RR.UpdateProxyReq): Promise<RR.UpdateProxyRes> {
|
||||
await pauseFor(2000)
|
||||
// async updateProxy(params: RR.UpdateProxyReq): Promise<RR.UpdateProxyRes> {
|
||||
// await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/serverInfo/network/proxies/0/name`,
|
||||
value: params.name,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
// const patch = [
|
||||
// {
|
||||
// op: PatchOp.REPLACE,
|
||||
// path: `/serverInfo/network/proxies/0/name`,
|
||||
// value: params.name,
|
||||
// },
|
||||
// ]
|
||||
// this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
// return null
|
||||
// }
|
||||
|
||||
async deleteProxy(params: RR.DeleteProxyReq): Promise<RR.DeleteProxyRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/network/proxies',
|
||||
value: [],
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
// async deleteProxy(params: RR.DeleteProxyReq): Promise<RR.DeleteProxyRes> {
|
||||
// await pauseFor(2000)
|
||||
// const patch = [
|
||||
// {
|
||||
// op: PatchOp.REPLACE,
|
||||
// path: '/serverInfo/network/proxies',
|
||||
// value: [],
|
||||
// },
|
||||
// ]
|
||||
// this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
// return null
|
||||
// }
|
||||
|
||||
// domains
|
||||
|
||||
async claimStart9ToDomain(
|
||||
params: RR.ClaimStart9ToReq,
|
||||
): Promise<RR.ClaimStart9ToRes> {
|
||||
await pauseFor(2000)
|
||||
// async claimStart9ToDomain(
|
||||
// params: RR.ClaimStart9ToReq,
|
||||
// ): Promise<RR.ClaimStart9ToRes> {
|
||||
// await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/network/start9To',
|
||||
value: {
|
||||
subdomain: 'xyz',
|
||||
networkInterfaceId: params.networkInterfaceId,
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
// const patch = [
|
||||
// {
|
||||
// op: PatchOp.REPLACE,
|
||||
// path: '/serverInfo/network/start9To',
|
||||
// value: {
|
||||
// subdomain: 'xyz',
|
||||
// networkInterfaceId: params.networkInterfaceId,
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
// this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
// return null
|
||||
// }
|
||||
|
||||
async deleteStart9ToDomain(
|
||||
params: RR.DeleteStart9ToReq,
|
||||
): Promise<RR.DeleteStart9ToRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/network/start9To',
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
// async deleteStart9ToDomain(
|
||||
// params: RR.DeleteStart9ToReq,
|
||||
// ): Promise<RR.DeleteStart9ToRes> {
|
||||
// await pauseFor(2000)
|
||||
// const patch = [
|
||||
// {
|
||||
// op: PatchOp.REPLACE,
|
||||
// path: '/serverInfo/network/start9To',
|
||||
// value: null,
|
||||
// },
|
||||
// ]
|
||||
// this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
// return null
|
||||
// }
|
||||
|
||||
async addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes> {
|
||||
await pauseFor(2000)
|
||||
// async addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes> {
|
||||
// await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/serverInfo/network/domains`,
|
||||
value: {
|
||||
[params.hostname]: {
|
||||
networkInterfaceId: params.networkInterfaceId,
|
||||
provider: params.provider.name,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
// const patch = [
|
||||
// {
|
||||
// op: PatchOp.REPLACE,
|
||||
// path: `/serverInfo/network/domains`,
|
||||
// value: {
|
||||
// [params.hostname]: {
|
||||
// networkInterfaceId: params.networkInterfaceId,
|
||||
// provider: params.provider.name,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
// this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
// return null
|
||||
// }
|
||||
|
||||
async deleteDomain(params: RR.DeleteDomainReq): Promise<RR.DeleteDomainRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/network/domains',
|
||||
value: {},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
// async deleteDomain(params: RR.DeleteDomainReq): Promise<RR.DeleteDomainRes> {
|
||||
// await pauseFor(2000)
|
||||
// const patch = [
|
||||
// {
|
||||
// op: PatchOp.REPLACE,
|
||||
// path: '/serverInfo/network/domains',
|
||||
// value: {},
|
||||
// },
|
||||
// ]
|
||||
// this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
// return null
|
||||
// }
|
||||
|
||||
// port forwards
|
||||
|
||||
async overridePortForward(
|
||||
params: RR.OverridePortReq,
|
||||
): Promise<RR.OverridePortRes> {
|
||||
await pauseFor(2000)
|
||||
// async overridePortForward(
|
||||
// params: RR.OverridePortReq,
|
||||
// ): Promise<RR.OverridePortRes> {
|
||||
// await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/network/wanConfig/forwards/0/override',
|
||||
value: params.port,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
// const patch = [
|
||||
// {
|
||||
// op: PatchOp.REPLACE,
|
||||
// path: '/serverInfo/network/wanConfig/forwards/0/override',
|
||||
// value: params.port,
|
||||
// },
|
||||
// ]
|
||||
// this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
// return null
|
||||
// }
|
||||
|
||||
// wifi
|
||||
|
||||
@@ -814,21 +792,16 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
|
||||
async addBackupTarget(
|
||||
type: BackupTargetType,
|
||||
params:
|
||||
| RR.AddCifsBackupTargetReq
|
||||
| RR.AddCloudBackupTargetReq
|
||||
| RR.AddDiskBackupTargetReq,
|
||||
params: RR.AddBackupTargetReq,
|
||||
): Promise<RR.AddBackupTargetRes> {
|
||||
await pauseFor(2000)
|
||||
const { path, name } = params
|
||||
const { hostname, path, username } = params
|
||||
return {
|
||||
latfgvwdbhjsndmk: {
|
||||
name,
|
||||
type: 'cifs',
|
||||
hostname: 'mockhotname',
|
||||
hostname,
|
||||
path: path.replace(/\\/g, '/'),
|
||||
username: 'mockusername',
|
||||
username,
|
||||
mountable: true,
|
||||
startOs: {},
|
||||
},
|
||||
@@ -836,11 +809,18 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
|
||||
async updateBackupTarget(
|
||||
type: BackupTargetType,
|
||||
params: RR.UpdateCifsBackupTargetReq | RR.UpdateCloudBackupTargetReq,
|
||||
params: RR.UpdateBackupTargetReq,
|
||||
): Promise<RR.UpdateBackupTargetRes> {
|
||||
await pauseFor(2000)
|
||||
return { [params.id]: Mock.BackupTargets.saved[params.id] }
|
||||
const { id, hostname, path, username } = params
|
||||
return {
|
||||
[id]: {
|
||||
...(Mock.BackupTargets[id] as CifsBackupTarget),
|
||||
hostname,
|
||||
path,
|
||||
username,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async removeBackupTarget(
|
||||
@@ -850,57 +830,6 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async getBackupJobs(
|
||||
params: RR.GetBackupJobsReq,
|
||||
): Promise<RR.GetBackupJobsRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.BackupJobs
|
||||
}
|
||||
|
||||
async createBackupJob(
|
||||
params: RR.CreateBackupJobReq,
|
||||
): Promise<RR.CreateBackupJobRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
id: 'hjdfbjsahdbn',
|
||||
name: params.name,
|
||||
targetId: Object.keys(Mock.BackupTargets.saved)[0],
|
||||
cron: params.cron,
|
||||
packageIds: params.packageIds,
|
||||
}
|
||||
}
|
||||
|
||||
async updateBackupJob(
|
||||
params: RR.UpdateBackupJobReq,
|
||||
): Promise<RR.UpdateBackupJobRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
...Mock.BackupJobs[0],
|
||||
...params,
|
||||
}
|
||||
}
|
||||
|
||||
async deleteBackupJob(
|
||||
params: RR.DeleteBackupJobReq,
|
||||
): Promise<RR.DeleteBackupJobRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
async getBackupRuns(
|
||||
params: RR.GetBackupRunsReq,
|
||||
): Promise<RR.GetBackupRunsRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.BackupRuns
|
||||
}
|
||||
|
||||
async deleteBackupRuns(
|
||||
params: RR.DeleteBackupRunsReq,
|
||||
): Promise<RR.DeleteBackupRunsRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
async getBackupInfo(
|
||||
params: RR.GetBackupInfoReq,
|
||||
): Promise<RR.GetBackupInfoRes> {
|
||||
@@ -977,6 +906,94 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
// async addBackupTarget(
|
||||
// type: BackupTargetType,
|
||||
// params:
|
||||
// | RR.AddCifsBackupTargetReq
|
||||
// | RR.AddCloudBackupTargetReq
|
||||
// | RR.AddDiskBackupTargetReq,
|
||||
// ): Promise<RR.AddBackupTargetRes> {
|
||||
// await pauseFor(2000)
|
||||
// const { path, name } = params
|
||||
// return {
|
||||
// latfgvwdbhjsndmk: {
|
||||
// name,
|
||||
// type: 'cifs',
|
||||
// hostname: 'mockhotname',
|
||||
// path: path.replace(/\\/g, '/'),
|
||||
// username: 'mockusername',
|
||||
// mountable: true,
|
||||
// startOs: {},
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
// async updateBackupTarget(
|
||||
// type: BackupTargetType,
|
||||
// params: RR.UpdateCifsBackupTargetReq | RR.UpdateCloudBackupTargetReq,
|
||||
// ): Promise<RR.UpdateBackupTargetRes> {
|
||||
// await pauseFor(2000)
|
||||
// return { [params.id]: Mock.BackupTargets.saved[params.id] }
|
||||
// }
|
||||
|
||||
// async removeBackupTarget(
|
||||
// params: RR.RemoveBackupTargetReq,
|
||||
// ): Promise<RR.RemoveBackupTargetRes> {
|
||||
// await pauseFor(2000)
|
||||
// return null
|
||||
// }
|
||||
|
||||
// async getBackupJobs(
|
||||
// params: RR.GetBackupJobsReq,
|
||||
// ): Promise<RR.GetBackupJobsRes> {
|
||||
// await pauseFor(2000)
|
||||
// return Mock.BackupJobs
|
||||
// }
|
||||
|
||||
// async createBackupJob(
|
||||
// params: RR.CreateBackupJobReq,
|
||||
// ): Promise<RR.CreateBackupJobRes> {
|
||||
// await pauseFor(2000)
|
||||
// return {
|
||||
// id: 'hjdfbjsahdbn',
|
||||
// name: params.name,
|
||||
// targetId: Object.keys(Mock.BackupTargets.saved)[0],
|
||||
// cron: params.cron,
|
||||
// packageIds: params.packageIds,
|
||||
// }
|
||||
// }
|
||||
|
||||
// async updateBackupJob(
|
||||
// params: RR.UpdateBackupJobReq,
|
||||
// ): Promise<RR.UpdateBackupJobRes> {
|
||||
// await pauseFor(2000)
|
||||
// return {
|
||||
// ...Mock.BackupJobs[0],
|
||||
// ...params,
|
||||
// }
|
||||
// }
|
||||
|
||||
// async deleteBackupJob(
|
||||
// params: RR.DeleteBackupJobReq,
|
||||
// ): Promise<RR.DeleteBackupJobRes> {
|
||||
// await pauseFor(2000)
|
||||
// return null
|
||||
// }
|
||||
|
||||
// async getBackupRuns(
|
||||
// params: RR.GetBackupRunsReq,
|
||||
// ): Promise<RR.GetBackupRunsRes> {
|
||||
// await pauseFor(2000)
|
||||
// return Mock.BackupRuns
|
||||
// }
|
||||
|
||||
// async deleteBackupRuns(
|
||||
// params: RR.DeleteBackupRunsReq,
|
||||
// ): Promise<RR.DeleteBackupRunsRes> {
|
||||
// await pauseFor(2000)
|
||||
// return null
|
||||
// }
|
||||
|
||||
// package
|
||||
|
||||
async getPackageLogs(
|
||||
@@ -1308,37 +1325,21 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async setInterfaceClearnetAddress(
|
||||
params: RR.SetInterfaceClearnetAddressReq,
|
||||
): Promise<RR.SetInterfaceClearnetAddressRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/packageData/${params.packageId}/serviceInterfaces/${params.interfaceId}/addressInfo/domainInfo`,
|
||||
value: params.domainInfo,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
// async setServiceOutboundProxy(
|
||||
// params: RR.SetServiceOutboundProxyReq,
|
||||
// ): Promise<RR.SetServiceOutboundProxyRes> {
|
||||
// await pauseFor(2000)
|
||||
// const patch = [
|
||||
// {
|
||||
// op: PatchOp.REPLACE,
|
||||
// path: `/packageData/${params.packageId}/outboundProxy`,
|
||||
// value: params.proxy,
|
||||
// },
|
||||
// ]
|
||||
// this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async setServiceOutboundProxy(
|
||||
params: RR.SetServiceOutboundProxyReq,
|
||||
): Promise<RR.SetServiceOutboundProxyRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/packageData/${params.packageId}/outboundProxy`,
|
||||
value: params.proxy,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
// return null
|
||||
// }
|
||||
|
||||
async initAcme(params: RR.InitAcmeReq): Promise<RR.InitAcmeRes> {
|
||||
await pauseFor(2000)
|
||||
@@ -1820,6 +1821,16 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch3)
|
||||
// quickly revert server to "running" for continued testing
|
||||
await pauseFor(100)
|
||||
const patch4 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/status',
|
||||
value: 'running',
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch4)
|
||||
// set patch indicating update is complete
|
||||
await pauseFor(100)
|
||||
const patch6 = [
|
||||
|
||||
@@ -39,13 +39,6 @@ export const mockPatchData: DataModel = {
|
||||
selected: null,
|
||||
lastRegion: null,
|
||||
},
|
||||
start9To: null,
|
||||
domains: {},
|
||||
wanConfig: {
|
||||
upnp: true,
|
||||
forwards: [],
|
||||
},
|
||||
outboundProxy: null,
|
||||
host: {
|
||||
bindings: {
|
||||
80: {
|
||||
@@ -225,9 +218,7 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
dataVersion: '0.20.0:0',
|
||||
icon: '/assets/img/service-icons/bitcoind.svg',
|
||||
installedAt: new Date().toISOString(),
|
||||
lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(),
|
||||
nextBackup: new Date(new Date().valueOf() + 100000000).toISOString(),
|
||||
status: {
|
||||
main: 'stopped',
|
||||
},
|
||||
@@ -475,18 +466,17 @@ export const mockPatchData: DataModel = {
|
||||
storeExposedDependents: [],
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
outboundProxy: null,
|
||||
requestedActions: {
|
||||
'bitcoind-config': {
|
||||
request: {
|
||||
packageId: 'bitcoind',
|
||||
actionId: 'config',
|
||||
severity: 'critical',
|
||||
reason:
|
||||
'You must run Config before starting Bitcoin for the first time',
|
||||
},
|
||||
active: true,
|
||||
},
|
||||
// 'bitcoind-config': {
|
||||
// request: {
|
||||
// packageId: 'bitcoind',
|
||||
// actionId: 'config',
|
||||
// severity: 'critical',
|
||||
// reason:
|
||||
// 'You must run Config before starting Bitcoin for the first time',
|
||||
// },
|
||||
// active: true,
|
||||
// },
|
||||
'bitcoind-properties': {
|
||||
request: {
|
||||
packageId: 'bitcoind',
|
||||
@@ -508,9 +498,7 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
dataVersion: '0.11.0:0.0.1',
|
||||
icon: '/assets/img/service-icons/lnd.png',
|
||||
installedAt: new Date().toISOString(),
|
||||
lastBackup: null,
|
||||
nextBackup: null,
|
||||
status: {
|
||||
main: 'stopped',
|
||||
},
|
||||
@@ -604,7 +592,6 @@ export const mockPatchData: DataModel = {
|
||||
storeExposedDependents: [],
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
outboundProxy: null,
|
||||
requestedActions: {
|
||||
config: {
|
||||
active: true,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { BackupJob } from '../api/api.types'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
export type DataModel = {
|
||||
export type DataModel = Omit<T.Public, 'serverInfo'> & {
|
||||
ui: UIData
|
||||
// @TODO 040
|
||||
serverInfo: Omit<
|
||||
T.Public['serverInfo'],
|
||||
'wifi' | 'networkInterfaces' | 'host'
|
||||
@@ -51,54 +51,24 @@ export type NetworkInfo = {
|
||||
| null
|
||||
}
|
||||
}
|
||||
start9To: {
|
||||
subdomain: string
|
||||
networkInterfaceId: string
|
||||
} | null
|
||||
domains: {
|
||||
[key: string]: Domain
|
||||
}
|
||||
wanConfig: {
|
||||
upnp: boolean
|
||||
forwards: PortForward[]
|
||||
}
|
||||
outboundProxy: string | null
|
||||
}
|
||||
|
||||
export type DomainInfo = {
|
||||
domain: string
|
||||
subdomain: string | null
|
||||
}
|
||||
|
||||
export type PortForward = {
|
||||
assigned: number
|
||||
override: number | null
|
||||
target: number
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export type Domain = {
|
||||
provider: string
|
||||
networkInterfaceId: string
|
||||
}
|
||||
|
||||
export interface ServerStatusInfo {
|
||||
currentBackup: null | {
|
||||
job: BackupJob
|
||||
backupProgress: Record<string, boolean>
|
||||
}
|
||||
updated: boolean
|
||||
updateProgress: { size: number | null; downloaded: number } | null
|
||||
restarting: boolean
|
||||
shuttingDown: boolean
|
||||
// @TODO 041
|
||||
// start9To: {
|
||||
// subdomain: string
|
||||
// networkInterfaceId: string
|
||||
// } | null
|
||||
// domains: {
|
||||
// [key: string]: Domain
|
||||
// }
|
||||
// wanConfig: {
|
||||
// upnp: boolean
|
||||
// forwards: PortForward[]
|
||||
// }
|
||||
// outboundProxy: string | null
|
||||
}
|
||||
|
||||
export type PackageDataEntry<T extends StateInfo = StateInfo> =
|
||||
T.PackageDataEntry & {
|
||||
stateInfo: T
|
||||
installedAt: string
|
||||
outboundProxy: string | null
|
||||
nextBackup: string | null
|
||||
}
|
||||
|
||||
export type AllPackageData = NonNullable<
|
||||
@@ -129,3 +99,11 @@ export type InstallingInfo = {
|
||||
progress: T.FullProgress
|
||||
newManifest: T.Manifest
|
||||
}
|
||||
|
||||
// @TODO 041
|
||||
// export type ServerStatusInfo = Omit<T.ServerStatus, 'backupProgress'> & {
|
||||
// currentBackup: null | {
|
||||
// job: BackupJob
|
||||
// backupProgress: Record<string, boolean>
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,89 +1,91 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogOptions } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { firstValueFrom } from 'rxjs'
|
||||
import {
|
||||
FormComponent,
|
||||
FormContext,
|
||||
} from 'src/app/routes/portal/components/form.component'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { ApiService } from './api/embassy-api.service'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
import { ISB } from '@start9labs/start-sdk'
|
||||
// @TODO 041
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ProxyService {
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly formDialog: FormDialogService,
|
||||
private readonly api: ApiService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly errorService: ErrorService,
|
||||
) {}
|
||||
// import { Injectable } from '@angular/core'
|
||||
// import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
// import { TuiDialogOptions } from '@taiga-ui/core'
|
||||
// import { PatchDB } from 'patch-db-client'
|
||||
// import { firstValueFrom } from 'rxjs'
|
||||
// import {
|
||||
// FormComponent,
|
||||
// FormContext,
|
||||
// } from 'src/app/routes/portal/components/form.component'
|
||||
// import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
// import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
// import { ApiService } from './api/embassy-api.service'
|
||||
// import { DataModel } from './patch-db/data-model'
|
||||
// import { ISB } from '@start9labs/start-sdk'
|
||||
|
||||
async presentModalSetOutboundProxy(current: string | null, pkgId?: string) {
|
||||
const networkInterfaces = await firstValueFrom(
|
||||
this.patch.watch$('serverInfo', 'network', 'networkInterfaces'),
|
||||
)
|
||||
const config = ISB.InputSpec.of({
|
||||
proxyId: ISB.Value.select({
|
||||
name: 'Select Proxy',
|
||||
default: current || '',
|
||||
values: Object.entries(networkInterfaces)
|
||||
.filter(
|
||||
([_, n]) => n.outbound && n.ipInfo?.deviceType === 'wireguard',
|
||||
)
|
||||
.reduce<Record<string, string>>(
|
||||
(prev, [id, n]) => ({
|
||||
[id]: n.ipInfo!.name,
|
||||
...prev,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}),
|
||||
})
|
||||
// @Injectable({
|
||||
// providedIn: 'root',
|
||||
// })
|
||||
// export class ProxyService {
|
||||
// constructor(
|
||||
// private readonly patch: PatchDB<DataModel>,
|
||||
// private readonly formDialog: FormDialogService,
|
||||
// private readonly api: ApiService,
|
||||
// private readonly loader: LoadingService,
|
||||
// private readonly errorService: ErrorService,
|
||||
// ) {}
|
||||
|
||||
const options: Partial<
|
||||
TuiDialogOptions<FormContext<typeof config.validator._TYPE>>
|
||||
> = {
|
||||
label: 'Outbound Proxy',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(config),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Manage proxies',
|
||||
link: '/portal/settings/proxies',
|
||||
},
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async value => {
|
||||
await this.saveOutboundProxy(value.proxyId, pkgId)
|
||||
return true
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
this.formDialog.open(FormComponent, options)
|
||||
}
|
||||
// async presentModalSetOutboundProxy(current: string | null, pkgId?: string) {
|
||||
// const networkInterfaces = await firstValueFrom(
|
||||
// this.patch.watch$('serverInfo', 'network', 'networkInterfaces'),
|
||||
// )
|
||||
// const config = ISB.InputSpec.of({
|
||||
// proxyId: ISB.Value.select({
|
||||
// name: 'Select Proxy',
|
||||
// default: current || '',
|
||||
// values: Object.entries(networkInterfaces)
|
||||
// .filter(
|
||||
// ([_, n]) => n.outbound && n.ipInfo?.deviceType === 'wireguard',
|
||||
// )
|
||||
// .reduce<Record<string, string>>(
|
||||
// (prev, [id, n]) => ({
|
||||
// [id]: n.ipInfo!.name,
|
||||
// ...prev,
|
||||
// }),
|
||||
// {},
|
||||
// ),
|
||||
// }),
|
||||
// })
|
||||
|
||||
private async saveOutboundProxy(proxy: string | null, packageId?: string) {
|
||||
const loader = this.loader.open(`Saving`).subscribe()
|
||||
// const options: Partial<
|
||||
// TuiDialogOptions<FormContext<typeof config.validator._TYPE>>
|
||||
// > = {
|
||||
// label: 'Outbound Proxy',
|
||||
// data: {
|
||||
// spec: await configBuilderToSpec(config),
|
||||
// buttons: [
|
||||
// {
|
||||
// text: 'Manage proxies',
|
||||
// link: '/portal/settings/proxies',
|
||||
// },
|
||||
// {
|
||||
// text: 'Save',
|
||||
// handler: async value => {
|
||||
// await this.saveOutboundProxy(value.proxyId, pkgId)
|
||||
// return true
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// }
|
||||
// this.formDialog.open(FormComponent, options)
|
||||
// }
|
||||
|
||||
try {
|
||||
if (packageId) {
|
||||
await this.api.setServiceOutboundProxy({ packageId, proxy })
|
||||
} else {
|
||||
await this.api.setOsOutboundProxy({ proxy })
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
// private async saveOutboundProxy(proxy: string | null, packageId?: string) {
|
||||
// const loader = this.loader.open(`Saving`).subscribe()
|
||||
|
||||
// try {
|
||||
// if (packageId) {
|
||||
// await this.api.setServiceOutboundProxy({ packageId, proxy })
|
||||
// } else {
|
||||
// await this.api.setOsOutboundProxy({ proxy })
|
||||
// }
|
||||
// } catch (e: any) {
|
||||
// this.errorService.handleError(e)
|
||||
// } finally {
|
||||
// loader.unsubscribe()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import {
|
||||
AllPackageData,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
|
||||
export function getDepDetails(
|
||||
pkg: PackageDataEntry,
|
||||
allPkgs: AllPackageData,
|
||||
depId: string,
|
||||
) {
|
||||
const { title, icon, versionRange } = pkg.currentDependencies[depId] || {}
|
||||
|
||||
if (
|
||||
allPkgs[depId] &&
|
||||
(allPkgs[depId].stateInfo.state === 'installed' ||
|
||||
allPkgs[depId].stateInfo.state === 'updating')
|
||||
) {
|
||||
return {
|
||||
title: allPkgs[depId].stateInfo.manifest!.title,
|
||||
icon: allPkgs[depId].icon,
|
||||
versionRange,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
title: title || depId,
|
||||
icon: icon || 'assets/img/service-icons/fallback.png',
|
||||
versionRange,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,11 @@ export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
|
||||
// icon: '@tui.globe',
|
||||
// title: 'Updates',
|
||||
// },
|
||||
'/portal/backups': {
|
||||
icon: '@tui.save',
|
||||
title: 'Backups',
|
||||
},
|
||||
// @TODO 041
|
||||
// '/portal/backups': {
|
||||
// icon: '@tui.save',
|
||||
// title: 'Backups',
|
||||
// },
|
||||
'/portal/metrics': {
|
||||
icon: '@tui.activity',
|
||||
title: 'Metrics',
|
||||
|
||||
@@ -65,8 +65,76 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
.g-table {
|
||||
width: 100%;
|
||||
.g-subpage {
|
||||
height: 100%;
|
||||
min-height: fit-content;
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
|
||||
tui-root._mobile & {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.g-card {
|
||||
transition: all 300ms ease-in-out;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
background-color: color-mix(in hsl, var(--start9-base-1) 50%, transparent);
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 0.15),
|
||||
transparent
|
||||
),
|
||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.15), transparent);
|
||||
background-size: 1px 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position:
|
||||
top left,
|
||||
top right;
|
||||
box-sizing: border-box;
|
||||
box-shadow:
|
||||
0 0.25rem 0.125rem rgba(0, 0, 0, 0.25),
|
||||
0 -0.125rem 0.25rem rgba(55, 155, 255, 0.08),
|
||||
0 0 0.5rem rgba(0, 0, 0, 0.3),
|
||||
inset 0 -0.125rem rgba(255, 255, 255, 0.03),
|
||||
inset 0 2px rgba(255, 255, 255, 0.1),
|
||||
inset 0 1px rgba(255, 255, 255, 0.15),
|
||||
inset 0 0 1rem rgba(0, 0, 0, 0.25);
|
||||
|
||||
&:hover {
|
||||
box-shadow:
|
||||
0 0.375rem 0.5rem rgba(0, 0, 0, 0.25),
|
||||
0 -0.125rem 0.25rem rgba(55, 155, 255, 0.08),
|
||||
0 0 0.5rem rgba(0, 0, 0, 0.3),
|
||||
inset 0 -0.125rem rgba(255, 255, 255, 0.03),
|
||||
inset 0 2px rgba(255, 255, 255, 0.1),
|
||||
inset 0 1px rgba(255, 255, 255, 0.15),
|
||||
inset 0 0 1rem rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
> [tuiCell]:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
height: 1px;
|
||||
background: var(--tui-border-normal);
|
||||
}
|
||||
|
||||
> header {
|
||||
padding-bottom: 0.75rem;
|
||||
margin: -0.5rem 0 0.5rem;
|
||||
background: var(--tui-background-neutral-1);
|
||||
box-shadow: 0 -10rem 0 10rem var(--tui-background-neutral-1);
|
||||
font: var(--tui-font-heading-6);
|
||||
}
|
||||
}
|
||||
|
||||
.g-table:not([tuiTable]) {
|
||||
width: stretch;
|
||||
min-width: 40rem;
|
||||
border-spacing: 0;
|
||||
|
||||
@@ -127,6 +195,38 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
.g-table[tuiTable] {
|
||||
width: stretch;
|
||||
|
||||
tr:not(:last-child) {
|
||||
box-shadow: inset 0 -1px var(--tui-background-neutral-1);
|
||||
}
|
||||
|
||||
th {
|
||||
text-transform: uppercase;
|
||||
color: var(--tui-text-secondary);
|
||||
background: none;
|
||||
border: none;
|
||||
font: var(--tui-font-text-s);
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
td:only-child {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
tui-root._mobile & thead {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.g-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -196,7 +296,7 @@ button.g-action {
|
||||
}
|
||||
}
|
||||
|
||||
.g-success {
|
||||
.g-positive {
|
||||
color: var(--tui-status-positive) !important;
|
||||
}
|
||||
|
||||
@@ -204,7 +304,7 @@ button.g-action {
|
||||
color: var(--tui-status-warning) !important;
|
||||
}
|
||||
|
||||
.g-error.g-error {
|
||||
.g-negative.g-negative {
|
||||
color: var(--tui-status-negative) !important;
|
||||
}
|
||||
|
||||
@@ -212,6 +312,10 @@ button.g-action {
|
||||
color: var(--tui-status-info) !important;
|
||||
}
|
||||
|
||||
.g-secondary {
|
||||
color: var(--tui-text-secondary) !important;
|
||||
}
|
||||
|
||||
ng-component {
|
||||
display: block;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user