mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
chore: refactor settings (#2846)
* small type changes and clear todos * handle notifications and metrics * wip * fixes * migration * dedup all urls * better handling of clearnet ips * add rfkill dependency * chore: refactor settings --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
@@ -103,8 +103,6 @@ tui-hint[data-appearance='onDark'] {
|
|||||||
tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
|
tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
|
||||||
border: 0;
|
border: 0;
|
||||||
backdrop-filter: blur(0.25rem);
|
backdrop-filter: blur(0.25rem);
|
||||||
border-radius: 0.325rem;
|
|
||||||
// TODO: Replace --tui-background-elevation-2 when Taiga UI is updated
|
|
||||||
background-color: color-mix(
|
background-color: color-mix(
|
||||||
in hsl,
|
in hsl,
|
||||||
var(--tui-background-elevation-3) 75%,
|
var(--tui-background-elevation-3) 75%,
|
||||||
@@ -129,30 +127,6 @@ tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
|
|||||||
inset 0 1px rgba(255, 255, 255, 0.15),
|
inset 0 1px rgba(255, 255, 255, 0.15),
|
||||||
inset 0 0 1rem rgba(0, 0, 0, 0.25),
|
inset 0 0 1rem rgba(0, 0, 0, 0.25),
|
||||||
var(--tui-shadow-medium);
|
var(--tui-shadow-medium);
|
||||||
|
|
||||||
tui-opt-group {
|
|
||||||
&::before {
|
|
||||||
background: var(--tui-background-neutral-1);
|
|
||||||
height: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[tuiOption] {
|
|
||||||
border-radius: 0.1875rem !important;
|
|
||||||
transition-property: background, box-shadow;
|
|
||||||
|
|
||||||
&:focus,
|
|
||||||
&._with-dropdown {
|
|
||||||
box-shadow:
|
|
||||||
inset 0 -1px rgba(0, 0, 0, 0.3),
|
|
||||||
inset 0 1px rgba(255, 255, 255, 0.1),
|
|
||||||
inset 0 -3rem 4rem -2rem rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[tuiSidebar] > div.t-wrapper {
|
[tuiSidebar] > div.t-wrapper {
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
<ion-item *ngIf="iFace">
|
|
||||||
<ion-icon
|
|
||||||
slot="start"
|
|
||||||
size="large"
|
|
||||||
[name]="
|
|
||||||
iFace.type === 'ui'
|
|
||||||
? 'desktop-outline'
|
|
||||||
: iFace.type === 'api'
|
|
||||||
? 'terminal-outline'
|
|
||||||
: 'people-outline'
|
|
||||||
"
|
|
||||||
></ion-icon>
|
|
||||||
<ion-label>
|
|
||||||
<h1>{{ iFace.name }}</h1>
|
|
||||||
<h2>{{ iFace.description }}</h2>
|
|
||||||
<ion-button style="margin-right: 8px" (click)="presentDomainForm()">
|
|
||||||
Add Domain
|
|
||||||
</ion-button>
|
|
||||||
<ion-button
|
|
||||||
[color]="iFace.public ? 'danger' : 'success'"
|
|
||||||
(click)="togglePublic()"
|
|
||||||
>
|
|
||||||
Make {{ iFace.public ? 'Private' : 'Public' }}
|
|
||||||
</ion-button>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<div *ngIf="iFace" style="padding-left: 64px">
|
|
||||||
<ion-item *ngFor="let address of iFace.addresses">
|
|
||||||
<ion-label>
|
|
||||||
<h2>{{ address.name }}</h2>
|
|
||||||
<p>{{ address.url }}</p>
|
|
||||||
<ion-button
|
|
||||||
*ngIf="address.isDomain"
|
|
||||||
color="danger"
|
|
||||||
(click)="removeStandard(address.url)"
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</ion-button>
|
|
||||||
<ion-button
|
|
||||||
*ngIf="address.isOnion"
|
|
||||||
color="danger"
|
|
||||||
(click)="removeOnion(address.url)"
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</ion-button>
|
|
||||||
</ion-label>
|
|
||||||
<ion-buttons slot="end">
|
|
||||||
<ion-button *ngIf="address.isDomain" (click)="showAcme(address.acme)">
|
|
||||||
<ion-icon name="finger-print"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-button *ngIf="iFace.type === 'ui'" (click)="launch(address.url)">
|
|
||||||
<ion-icon name="open-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-button (click)="showQR(address.url)">
|
|
||||||
<ion-icon name="qr-code-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-button (click)="copy(address.url)">
|
|
||||||
<ion-icon name="copy-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-buttons>
|
|
||||||
</ion-item>
|
|
||||||
</div>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
p {
|
|
||||||
font-family: 'Courier New';
|
|
||||||
}
|
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
import { Component, Inject, Input } from '@angular/core'
|
|
||||||
import { WINDOW } from '@ng-web-apis/common'
|
|
||||||
import {
|
|
||||||
AlertController,
|
|
||||||
ModalController,
|
|
||||||
ToastController,
|
|
||||||
} from '@ionic/angular'
|
|
||||||
import {
|
|
||||||
copyToClipboard,
|
|
||||||
ErrorService,
|
|
||||||
LoadingService,
|
|
||||||
} from '@start9labs/shared'
|
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
|
||||||
import { PatchDB } from 'patch-db-client'
|
|
||||||
import { QRComponent } from 'src/app/components/qr/qr.component'
|
|
||||||
import { firstValueFrom } from 'rxjs'
|
|
||||||
import { ISB, T, utils } from '@start9labs/start-sdk'
|
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
|
||||||
import { FormComponent } from 'src/app/components/form.component'
|
|
||||||
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
|
||||||
import { toAcmeName } from 'src/app/util/acme'
|
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
|
||||||
|
|
||||||
export type MappedInterface = T.ServiceInterface & {
|
|
||||||
addresses: MappedAddress[]
|
|
||||||
public: boolean
|
|
||||||
}
|
|
||||||
export type MappedAddress = {
|
|
||||||
name: string
|
|
||||||
url: string
|
|
||||||
isDomain: boolean
|
|
||||||
isOnion: boolean
|
|
||||||
acme: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'interface-info',
|
|
||||||
templateUrl: './interface-info.component.html',
|
|
||||||
styleUrls: ['./interface-info.component.scss'],
|
|
||||||
})
|
|
||||||
export class InterfaceInfoComponent {
|
|
||||||
@Input() pkgId?: string
|
|
||||||
@Input() iFace!: MappedInterface
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly toastCtrl: ToastController,
|
|
||||||
private readonly modalCtrl: ModalController,
|
|
||||||
private readonly errorService: ErrorService,
|
|
||||||
private readonly loader: LoadingService,
|
|
||||||
private readonly api: ApiService,
|
|
||||||
private readonly formDialog: FormDialogService,
|
|
||||||
private readonly alertCtrl: AlertController,
|
|
||||||
private readonly patch: PatchDB<DataModel>,
|
|
||||||
private readonly config: ConfigService,
|
|
||||||
@Inject(WINDOW) private readonly windowRef: Window,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
launch(url: string): void {
|
|
||||||
this.windowRef.open(url, '_blank', 'noreferrer')
|
|
||||||
}
|
|
||||||
|
|
||||||
async togglePublic() {
|
|
||||||
const loader = this.loader
|
|
||||||
.open(`Making ${this.iFace.public ? 'private' : 'public'}`)
|
|
||||||
.subscribe()
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
internalPort: this.iFace.addressInfo.internalPort,
|
|
||||||
public: !this.iFace.public,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.pkgId) {
|
|
||||||
await this.api.pkgBindingSetPubic({
|
|
||||||
...params,
|
|
||||||
host: this.iFace.addressInfo.hostId,
|
|
||||||
package: this.pkgId,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await this.api.serverBindingSetPubic(params)
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async presentDomainForm() {
|
|
||||||
const acme = await firstValueFrom(this.patch.watch$('serverInfo', 'acme'))
|
|
||||||
|
|
||||||
const spec = getDomainSpec(Object.keys(acme))
|
|
||||||
|
|
||||||
this.formDialog.open(FormComponent, {
|
|
||||||
label: 'Add Domain',
|
|
||||||
data: {
|
|
||||||
spec: await configBuilderToSpec(spec),
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
text: 'Save',
|
|
||||||
handler: async (val: typeof spec._TYPE) => {
|
|
||||||
if (val.type.selection === 'standard') {
|
|
||||||
return this.saveStandard(
|
|
||||||
val.type.value.domain,
|
|
||||||
val.type.value.acme,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return this.saveTor(val.type.value.key)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeStandard(url: string) {
|
|
||||||
const loader = this.loader.open('Removing').subscribe()
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
domain: new URL(url).hostname,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.pkgId) {
|
|
||||||
await this.api.pkgRemoveDomain({
|
|
||||||
...params,
|
|
||||||
package: this.pkgId,
|
|
||||||
host: this.iFace.addressInfo.hostId,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await this.api.serverRemoveDomain(params)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeOnion(url: string) {
|
|
||||||
const loader = this.loader.open('Removing').subscribe()
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
onion: new URL(url).hostname,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.pkgId) {
|
|
||||||
await this.api.pkgRemoveOnion({
|
|
||||||
...params,
|
|
||||||
package: this.pkgId,
|
|
||||||
host: this.iFace.addressInfo.hostId,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await this.api.serverRemoveOnion(params)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async showAcme(url: string | null): Promise<void> {
|
|
||||||
const alert = await this.alertCtrl.create({
|
|
||||||
header: 'ACME Provider',
|
|
||||||
message: toAcmeName(url),
|
|
||||||
})
|
|
||||||
await alert.present()
|
|
||||||
}
|
|
||||||
|
|
||||||
async showQR(text: string): Promise<void> {
|
|
||||||
const modal = await this.modalCtrl.create({
|
|
||||||
component: QRComponent,
|
|
||||||
componentProps: {
|
|
||||||
text,
|
|
||||||
},
|
|
||||||
cssClass: 'qr-modal',
|
|
||||||
})
|
|
||||||
await modal.present()
|
|
||||||
}
|
|
||||||
|
|
||||||
async copy(address: string): Promise<void> {
|
|
||||||
let message = ''
|
|
||||||
await copyToClipboard(address || '').then(success => {
|
|
||||||
message = success
|
|
||||||
? 'Copied to clipboard!'
|
|
||||||
: 'Failed to copy to clipboard.'
|
|
||||||
})
|
|
||||||
|
|
||||||
const toast = await this.toastCtrl.create({
|
|
||||||
header: message,
|
|
||||||
position: 'bottom',
|
|
||||||
duration: 1000,
|
|
||||||
})
|
|
||||||
await toast.present()
|
|
||||||
}
|
|
||||||
|
|
||||||
private async saveStandard(domain: string, acme: string) {
|
|
||||||
const loader = this.loader.open('Saving').subscribe()
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
domain,
|
|
||||||
acme: acme === 'none' ? null : acme,
|
|
||||||
private: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.pkgId) {
|
|
||||||
await this.api.pkgAddDomain({
|
|
||||||
...params,
|
|
||||||
package: this.pkgId,
|
|
||||||
host: this.iFace.addressInfo.hostId,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await this.api.serverAddDomain(params)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async saveTor(key: string | null) {
|
|
||||||
const loader = this.loader.open('Creating onion address').subscribe()
|
|
||||||
|
|
||||||
try {
|
|
||||||
let onion = key
|
|
||||||
? await this.api.addTorKey({ key })
|
|
||||||
: await this.api.generateTorKey({})
|
|
||||||
onion = `${onion}.onion`
|
|
||||||
|
|
||||||
if (this.pkgId) {
|
|
||||||
await this.api.pkgAddOnion({
|
|
||||||
onion,
|
|
||||||
package: this.pkgId,
|
|
||||||
host: this.iFace.addressInfo.hostId,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await this.api.serverAddOnion({ onion })
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDomainSpec(acme: string[]) {
|
|
||||||
return ISB.InputSpec.of({
|
|
||||||
type: ISB.Value.union(
|
|
||||||
{ name: 'Type', default: 'standard' },
|
|
||||||
ISB.Variants.of({
|
|
||||||
standard: {
|
|
||||||
name: 'Standard',
|
|
||||||
spec: 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: acme.reduce(
|
|
||||||
(obj, url) => ({
|
|
||||||
...obj,
|
|
||||||
[url]: toAcmeName(url),
|
|
||||||
}),
|
|
||||||
{ none: 'None (use system Root CA)' } as Record<string, string>,
|
|
||||||
),
|
|
||||||
default: '',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
onion: {
|
|
||||||
name: 'Onion',
|
|
||||||
spec: 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],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAddresses(
|
|
||||||
serviceInterface: T.ServiceInterface,
|
|
||||||
host: T.Host,
|
|
||||||
config: ConfigService,
|
|
||||||
): MappedAddress[] {
|
|
||||||
const addressInfo = serviceInterface.addressInfo
|
|
||||||
|
|
||||||
let hostnames = host.hostnameInfo[addressInfo.internalPort]
|
|
||||||
|
|
||||||
hostnames = hostnames.filter(
|
|
||||||
h =>
|
|
||||||
config.isLocalhost() ||
|
|
||||||
h.kind !== 'ip' ||
|
|
||||||
h.hostname.kind !== 'ipv6' ||
|
|
||||||
!h.hostname.value.startsWith('fe80::'),
|
|
||||||
)
|
|
||||||
if (config.isLocalhost()) {
|
|
||||||
const local = hostnames.find(
|
|
||||||
h => h.kind === 'ip' && h.hostname.kind === 'local',
|
|
||||||
)
|
|
||||||
if (local) {
|
|
||||||
hostnames.unshift({
|
|
||||||
kind: 'ip',
|
|
||||||
networkInterfaceId: 'lo',
|
|
||||||
public: false,
|
|
||||||
hostname: {
|
|
||||||
kind: 'local',
|
|
||||||
port: local.hostname.port,
|
|
||||||
sslPort: local.hostname.sslPort,
|
|
||||||
value: 'localhost',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const mappedAddresses = hostnames.flatMap(h => {
|
|
||||||
let name = ''
|
|
||||||
let isDomain = false
|
|
||||||
let isOnion = false
|
|
||||||
let acme: string | null = null
|
|
||||||
|
|
||||||
if (h.kind === 'onion') {
|
|
||||||
name = `Tor`
|
|
||||||
isOnion = true
|
|
||||||
} else {
|
|
||||||
const hostnameKind = h.hostname.kind
|
|
||||||
|
|
||||||
if (hostnameKind === 'domain') {
|
|
||||||
name = 'Domain'
|
|
||||||
isDomain = true
|
|
||||||
acme = host.domains[h.hostname.domain]?.acme
|
|
||||||
} else {
|
|
||||||
name =
|
|
||||||
hostnameKind === 'local'
|
|
||||||
? 'Local'
|
|
||||||
: `${h.networkInterfaceId} (${hostnameKind})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addresses = utils.addressHostToUrl(addressInfo, h)
|
|
||||||
if (addresses.length > 1) {
|
|
||||||
return addresses.map(url => ({
|
|
||||||
name: `${name} (${new URL(url).protocol
|
|
||||||
.replace(':', '')
|
|
||||||
.toUpperCase()})`,
|
|
||||||
url,
|
|
||||||
isDomain,
|
|
||||||
isOnion,
|
|
||||||
acme,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
return addresses.map(url => ({
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
isDomain,
|
|
||||||
isOnion,
|
|
||||||
acme,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return mappedAddresses.filter(
|
|
||||||
(value, index, self) => index === self.findIndex(t => t.url === value.url),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { IonicModule } from '@ionic/angular'
|
|
||||||
import { InterfaceInfoComponent } from './interface-info.component'
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [InterfaceInfoComponent],
|
|
||||||
imports: [CommonModule, IonicModule],
|
|
||||||
exports: [InterfaceInfoComponent],
|
|
||||||
})
|
|
||||||
export class InterfaceInfoModule {}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { TuiDataList, TuiIcon } from '@taiga-ui/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
|
||||||
|
|
||||||
export interface Action {
|
|
||||||
icon: string
|
|
||||||
label: string
|
|
||||||
action: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-actions',
|
|
||||||
template: `
|
|
||||||
<tui-data-list>
|
|
||||||
<h3 class="title"><ng-content /></h3>
|
|
||||||
<tui-opt-group
|
|
||||||
*ngFor="let group of actions | keyvalue: asIsOrder"
|
|
||||||
[label]="group.key.toUpperCase()"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
*ngFor="let action of group.value"
|
|
||||||
tuiOption
|
|
||||||
class="item"
|
|
||||||
(click)="action.action()"
|
|
||||||
>
|
|
||||||
<tui-icon class="icon" [icon]="action.icon" />
|
|
||||||
{{ action.label }}
|
|
||||||
</button>
|
|
||||||
</tui-opt-group>
|
|
||||||
</tui-data-list>
|
|
||||||
`,
|
|
||||||
styles: [
|
|
||||||
`
|
|
||||||
.title {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 0.5rem 0.25rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
font: var(--tui-font-text-l);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
opacity: var(--tui-disabled-opacity);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
],
|
|
||||||
standalone: true,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [TuiDataList, CommonModule, TuiIcon],
|
|
||||||
})
|
|
||||||
export class ActionsComponent {
|
|
||||||
@Input()
|
|
||||||
actions: Record<string, readonly Action[]> = {}
|
|
||||||
|
|
||||||
asIsOrder(a: any, b: any) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,16 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { RouterLink } from '@angular/router'
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
||||||
import { TuiButton, TuiDataList, TuiDropdown, TuiIcon } from '@taiga-ui/core'
|
import {
|
||||||
|
TuiButton,
|
||||||
|
TuiDataList,
|
||||||
|
TuiDialogOptions,
|
||||||
|
TuiDropdown,
|
||||||
|
TuiIcon,
|
||||||
|
} from '@taiga-ui/core'
|
||||||
|
import { TUI_CONFIRM, TuiConfirmData } from '@taiga-ui/kit'
|
||||||
|
import { filter } from 'rxjs'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { AuthService } from 'src/app/services/auth.service'
|
import { AuthService } from 'src/app/services/auth.service'
|
||||||
import { STATUS } from 'src/app/services/status.service'
|
import { STATUS } from 'src/app/services/status.service'
|
||||||
@@ -28,34 +37,53 @@ import { ABOUT } from './about.component'
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<tui-data-list [style.width.rem]="13">
|
<tui-data-list [style.width.rem]="13">
|
||||||
<button tuiOption iconStart="@tui.info" (click)="about()">
|
<tui-opt-group>
|
||||||
About this server
|
<button tuiOption iconStart="@tui.info" (click)="about()">
|
||||||
</button>
|
About this server
|
||||||
<hr />
|
</button>
|
||||||
@for (link of links; track $index) {
|
</tui-opt-group>
|
||||||
|
<tui-opt-group label="">
|
||||||
|
@for (link of links; track $index) {
|
||||||
|
<a
|
||||||
|
tuiOption
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
[iconStart]="link.icon"
|
||||||
|
[href]="link.href"
|
||||||
|
>
|
||||||
|
{{ link.name }}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</tui-opt-group>
|
||||||
|
<tui-opt-group label="">
|
||||||
<a
|
<a
|
||||||
tuiOption
|
tuiOption
|
||||||
target="_blank"
|
iconStart="@tui.settings"
|
||||||
rel="noreferrer"
|
routerLink="/portal/system"
|
||||||
[iconStart]="link.icon"
|
(click)="open = false"
|
||||||
[href]="link.href"
|
|
||||||
>
|
>
|
||||||
{{ link.name }}
|
System Settings
|
||||||
</a>
|
</a>
|
||||||
}
|
</tui-opt-group>
|
||||||
<hr />
|
<tui-opt-group label="">
|
||||||
<a
|
<button
|
||||||
tuiOption
|
tuiOption
|
||||||
iconStart="@tui.wrench"
|
iconStart="@tui.refresh-cw"
|
||||||
routerLink="/portal/settings"
|
(click)="promptPower('Restart')"
|
||||||
(click)="open = false"
|
>
|
||||||
>
|
Restart
|
||||||
System Settings
|
</button>
|
||||||
</a>
|
<button
|
||||||
<hr />
|
tuiOption
|
||||||
<button tuiOption iconStart="@tui.log-out" (click)="logout()">
|
iconStart="@tui.power"
|
||||||
Logout
|
(click)="promptPower('Shutdown')"
|
||||||
</button>
|
>
|
||||||
|
Shutdown
|
||||||
|
</button>
|
||||||
|
<button tuiOption iconStart="@tui.log-out" (click)="logout()">
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</tui-opt-group>
|
||||||
</tui-data-list>
|
</tui-data-list>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
`,
|
`,
|
||||||
@@ -98,6 +126,8 @@ export class HeaderMenuComponent {
|
|||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly auth = inject(AuthService)
|
private readonly auth = inject(AuthService)
|
||||||
private readonly dialogs = inject(TuiResponsiveDialogService)
|
private readonly dialogs = inject(TuiResponsiveDialogService)
|
||||||
|
private readonly loader = inject(LoadingService)
|
||||||
|
private readonly errorService = inject(ErrorService)
|
||||||
|
|
||||||
open = false
|
open = false
|
||||||
|
|
||||||
@@ -108,8 +138,53 @@ export class HeaderMenuComponent {
|
|||||||
this.dialogs.open(ABOUT, { label: 'About this server' }).subscribe()
|
this.dialogs.open(ABOUT, { label: 'About this server' }).subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async promptPower(action: 'Restart' | 'Shutdown') {
|
||||||
|
this.dialogs
|
||||||
|
.open(TUI_CONFIRM, getOptions(action))
|
||||||
|
.pipe(filter(Boolean))
|
||||||
|
.subscribe(async () => {
|
||||||
|
const loader = this.loader.open(`Beginning ${action}...`).subscribe()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api[
|
||||||
|
action === 'Restart' ? 'restartServer' : 'shutdownServer'
|
||||||
|
]({})
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
} finally {
|
||||||
|
loader.unsubscribe()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
this.api.logout({}).catch(e => console.error('Failed to log out', e))
|
this.api.logout({}).catch(e => console.error('Failed to log out', e))
|
||||||
this.auth.setUnverified()
|
this.auth.setUnverified()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOptions(
|
||||||
|
operation: 'Restart' | 'Shutdown',
|
||||||
|
): Partial<TuiDialogOptions<TuiConfirmData>> {
|
||||||
|
return operation === 'Restart'
|
||||||
|
? {
|
||||||
|
label: 'Restart',
|
||||||
|
size: 's',
|
||||||
|
data: {
|
||||||
|
content:
|
||||||
|
'Are you sure you want to restart your server? It can take several minutes to come back online.',
|
||||||
|
yes: 'Restart',
|
||||||
|
no: 'Cancel',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
label: 'Warning',
|
||||||
|
size: 's',
|
||||||
|
data: {
|
||||||
|
content:
|
||||||
|
'Are you sure you want to power down your server? This can take several minutes, and your server will not come back online automatically. To power on again, You will need to physically unplug your server and plug it back in',
|
||||||
|
yes: 'Shutdown',
|
||||||
|
no: 'Cancel',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { TuiIcon } from '@taiga-ui/core'
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
selector: 'service-placeholder',
|
selector: 'app-placeholder',
|
||||||
template: '<tui-icon [icon]="icon()" /><ng-content/>',
|
template: '<tui-icon [icon]="icon()" /><ng-content/>',
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
:host {
|
||||||
@@ -26,6 +26,6 @@ import { TuiIcon } from '@taiga-ui/core'
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [TuiIcon],
|
imports: [TuiIcon],
|
||||||
})
|
})
|
||||||
export class ServicePlaceholderComponent {
|
export class PlaceholderComponent {
|
||||||
readonly icon = input.required<string>()
|
readonly icon = input.required<string>()
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ import { BadgeService } from 'src/app/services/badge.service'
|
|||||||
import { RESOURCES } from 'src/app/utils/resources'
|
import { RESOURCES } from 'src/app/utils/resources'
|
||||||
import { getMenu } from 'src/app/utils/system-utilities'
|
import { getMenu } from 'src/app/utils/system-utilities'
|
||||||
|
|
||||||
const FILTER = ['/portal/services', '/portal/settings', '/portal/marketplace']
|
const FILTER = ['/portal/services', '/portal/system', '/portal/marketplace']
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -43,12 +43,12 @@ const FILTER = ['/portal/services', '/portal/settings', '/portal/marketplace']
|
|||||||
<a
|
<a
|
||||||
tuiTabBarItem
|
tuiTabBarItem
|
||||||
icon="@tui.settings"
|
icon="@tui.settings"
|
||||||
routerLink="/portal/settings"
|
routerLink="/portal/system"
|
||||||
routerLinkActive
|
routerLinkActive
|
||||||
[badge]="badge()"
|
[badge]="badge()"
|
||||||
(isActiveChange)="update()"
|
(isActiveChange)="update()"
|
||||||
>
|
>
|
||||||
Settings
|
System
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
tuiTabBarItem
|
tuiTabBarItem
|
||||||
@@ -140,7 +140,7 @@ export class TabsComponent {
|
|||||||
|
|
||||||
readonly resources = RESOURCES
|
readonly resources = RESOURCES
|
||||||
readonly menu = getMenu().filter(item => !FILTER.includes(item.routerLink))
|
readonly menu = getMenu().filter(item => !FILTER.includes(item.routerLink))
|
||||||
readonly badge = toSignal(inject(BadgeService).getCount('/portal/settings'), {
|
readonly badge = toSignal(inject(BadgeService).getCount('/portal/system'), {
|
||||||
initialValue: 0,
|
initialValue: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ const ROUTES: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: systemTabResolver,
|
title: systemTabResolver,
|
||||||
path: 'settings',
|
path: 'system',
|
||||||
loadChildren: () => import('./routes/settings/settings.routes'),
|
loadChildren: () => import('./routes/system/system.routes'),
|
||||||
data: toNavigationItem('/portal/settings'),
|
data: toNavigationItem('/portal/system'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: systemTabResolver,
|
title: systemTabResolver,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
TuiIcon,
|
TuiIcon,
|
||||||
TuiButton,
|
TuiButton,
|
||||||
TuiNotification,
|
TuiNotification,
|
||||||
|
TuiLink,
|
||||||
} from '@taiga-ui/core'
|
} from '@taiga-ui/core'
|
||||||
import { TuiConfirmData, TUI_CONFIRM, TuiSkeleton } from '@taiga-ui/kit'
|
import { TuiConfirmData, TUI_CONFIRM, TuiSkeleton } from '@taiga-ui/kit'
|
||||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||||
@@ -25,6 +26,7 @@ import { EDIT } from './edit.component'
|
|||||||
data is safely backed up. StartOS will issue a notification whenever one
|
data is safely backed up. StartOS will issue a notification whenever one
|
||||||
of your scheduled backups succeeds or fails.
|
of your scheduled backups succeeds or fails.
|
||||||
<a
|
<a
|
||||||
|
tuiLink
|
||||||
href="https://docs.start9.com/latest/user-manual/backups/backup-jobs"
|
href="https://docs.start9.com/latest/user-manual/backups/backup-jobs"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
@@ -144,6 +146,7 @@ import { EDIT } from './edit.component'
|
|||||||
ToHumanCronPipe,
|
ToHumanCronPipe,
|
||||||
GetBackupIconPipe,
|
GetBackupIconPipe,
|
||||||
TuiSkeleton,
|
TuiSkeleton,
|
||||||
|
TuiLink,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class BackupsJobsModal implements OnInit {
|
export class BackupsJobsModal implements OnInit {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Component, inject, OnInit, signal } from '@angular/core'
|
import { Component, inject, OnInit, signal } from '@angular/core'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { TuiButton, TuiNotification } from '@taiga-ui/core'
|
import { TuiButton, TuiLink, TuiNotification } from '@taiga-ui/core'
|
||||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||||
import {
|
import {
|
||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
folders on your Local Area Network (LAN), or third party clouds such as
|
folders on your Local Area Network (LAN), or third party clouds such as
|
||||||
Dropbox or Google Drive.
|
Dropbox or Google Drive.
|
||||||
<a
|
<a
|
||||||
|
tuiLink
|
||||||
href="https://docs.start9.com/latest/user-manual/backups/backup-targets"
|
href="https://docs.start9.com/latest/user-manual/backups/backup-targets"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
@@ -75,6 +76,7 @@ import {
|
|||||||
TuiButton,
|
TuiButton,
|
||||||
BackupsPhysicalComponent,
|
BackupsPhysicalComponent,
|
||||||
BackupsTargetsComponent,
|
BackupsTargetsComponent,
|
||||||
|
TuiLink,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class BackupsTargetsModal implements OnInit {
|
export class BackupsTargetsModal implements OnInit {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import {
|
|||||||
input,
|
input,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { TuiTable } from '@taiga-ui/addon-table'
|
import { TuiTable } from '@taiga-ui/addon-table'
|
||||||
|
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
import { ServiceActionRequestComponent } from './action-request.component'
|
import { ServiceActionRequestComponent } from './action-request.component'
|
||||||
import { ServicePlaceholderComponent } from './placeholder.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -30,9 +30,9 @@ import { ServicePlaceholderComponent } from './placeholder.component'
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@if (!requests().length) {
|
@if (!requests().length) {
|
||||||
<service-placeholder icon="@tui.list-checks">
|
<app-placeholder icon="@tui.list-checks">
|
||||||
All tasks complete
|
All tasks complete
|
||||||
</service-placeholder>
|
</app-placeholder>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
@@ -42,11 +42,7 @@ import { ServicePlaceholderComponent } from './placeholder.component'
|
|||||||
`,
|
`,
|
||||||
host: { class: 'g-card' },
|
host: { class: 'g-card' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [
|
imports: [TuiTable, ServiceActionRequestComponent, PlaceholderComponent],
|
||||||
TuiTable,
|
|
||||||
ServiceActionRequestComponent,
|
|
||||||
ServicePlaceholderComponent,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class ServiceActionRequestsComponent {
|
export class ServiceActionRequestsComponent {
|
||||||
readonly pkg = input.required<PackageDataEntry>()
|
readonly pkg = input.required<PackageDataEntry>()
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { RouterLink } from '@angular/router'
|
|||||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||||
import { TuiAvatar } from '@taiga-ui/kit'
|
import { TuiAvatar } from '@taiga-ui/kit'
|
||||||
import { TuiCell } from '@taiga-ui/layout'
|
import { TuiCell } from '@taiga-ui/layout'
|
||||||
|
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
import { ServicePlaceholderComponent } from './placeholder.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'service-dependencies',
|
selector: 'service-dependencies',
|
||||||
@@ -25,9 +25,7 @@ import { ServicePlaceholderComponent } from './placeholder.component'
|
|||||||
<tui-icon icon="@tui.arrow-right" />
|
<tui-icon icon="@tui.arrow-right" />
|
||||||
</a>
|
</a>
|
||||||
} @empty {
|
} @empty {
|
||||||
<service-placeholder icon="@tui.boxes">
|
<app-placeholder icon="@tui.boxes">No dependencies</app-placeholder>
|
||||||
No dependencies
|
|
||||||
</service-placeholder>
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
@@ -45,7 +43,7 @@ import { ServicePlaceholderComponent } from './placeholder.component'
|
|||||||
TuiAvatar,
|
TuiAvatar,
|
||||||
TuiTitle,
|
TuiTitle,
|
||||||
TuiIcon,
|
TuiIcon,
|
||||||
ServicePlaceholderComponent,
|
PlaceholderComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ServiceDependenciesComponent {
|
export class ServiceDependenciesComponent {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { TuiTable } from '@taiga-ui/addon-table'
|
import { TuiTable } from '@taiga-ui/addon-table'
|
||||||
|
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||||
import { ServiceHealthCheckComponent } from './health-check.component'
|
import { ServiceHealthCheckComponent } from './health-check.component'
|
||||||
import { ServicePlaceholderComponent } from './placeholder.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -23,9 +23,9 @@ import { ServicePlaceholderComponent } from './placeholder.component'
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@if (!checks().length) {
|
@if (!checks().length) {
|
||||||
<service-placeholder icon="@tui.heart-pulse">
|
<app-placeholder icon="@tui.heart-pulse">
|
||||||
No health checks
|
No health checks
|
||||||
</service-placeholder>
|
</app-placeholder>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
@@ -35,7 +35,7 @@ import { ServicePlaceholderComponent } from './placeholder.component'
|
|||||||
`,
|
`,
|
||||||
host: { class: 'g-card' },
|
host: { class: 'g-card' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [ServiceHealthCheckComponent, ServicePlaceholderComponent, TuiTable],
|
imports: [ServiceHealthCheckComponent, PlaceholderComponent, TuiTable],
|
||||||
})
|
})
|
||||||
export class ServiceHealthChecksComponent {
|
export class ServiceHealthChecksComponent {
|
||||||
readonly checks = input.required<readonly T.NamedHealthCheckResult[]>()
|
readonly checks = input.required<readonly T.NamedHealthCheckResult[]>()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { toSignal } from '@angular/core/rxjs-interop'
|
|||||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router'
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router'
|
||||||
import { TuiAppearance, TuiButton, TuiIcon, TuiTitle } from '@taiga-ui/core'
|
import { TuiAppearance, TuiButton, TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||||
import { TuiAvatar, TuiFade } from '@taiga-ui/kit'
|
import { TuiAvatar, TuiFade } from '@taiga-ui/kit'
|
||||||
import { TuiCell, tuiCellOptionsProvider } from '@taiga-ui/layout'
|
import { TuiCell } from '@taiga-ui/layout'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs'
|
import { distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
@@ -155,7 +155,6 @@ const ICONS = {
|
|||||||
TitleDirective,
|
TitleDirective,
|
||||||
TuiButton,
|
TuiButton,
|
||||||
],
|
],
|
||||||
providers: [tuiCellOptionsProvider({ height: 'spacious' })],
|
|
||||||
})
|
})
|
||||||
export class ServiceOutletComponent {
|
export class ServiceOutletComponent {
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
|
||||||
import { RouterLink } from '@angular/router'
|
|
||||||
import { SettingBtn } from '../settings.types'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'settings-button',
|
|
||||||
template: `
|
|
||||||
<button *ngIf="button.action" class="g-action" (click)="button.action()">
|
|
||||||
<ng-container *ngTemplateOutlet="template" />
|
|
||||||
</button>
|
|
||||||
<a
|
|
||||||
*ngIf="button.routerLink"
|
|
||||||
class="g-action"
|
|
||||||
[routerLink]="button.routerLink"
|
|
||||||
>
|
|
||||||
<ng-container *ngTemplateOutlet="template" />
|
|
||||||
</a>
|
|
||||||
<ng-template #template>
|
|
||||||
<tui-icon [icon]="button.icon" />
|
|
||||||
<div tuiTitle [style.flex]="1">
|
|
||||||
<strong>{{ button.title }}</strong>
|
|
||||||
<div tuiSubtitle>{{ button.description }}</div>
|
|
||||||
<ng-content />
|
|
||||||
</div>
|
|
||||||
<tui-icon *ngIf="button.routerLink" icon="@tui.chevron-right" />
|
|
||||||
</ng-template>
|
|
||||||
`,
|
|
||||||
styles: `
|
|
||||||
:host:not(:last-child) {
|
|
||||||
display: block;
|
|
||||||
box-shadow: 0 1px var(--tui-background-neutral-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, TuiIcon, TuiTitle, RouterLink],
|
|
||||||
})
|
|
||||||
export class SettingsButtonComponent {
|
|
||||||
@Input({ required: true })
|
|
||||||
button!: SettingBtn
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
|
||||||
import { TuiAlertService, TuiLoader, TuiButton } from '@taiga-ui/core'
|
|
||||||
import { PatchDB } from 'patch-db-client'
|
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
|
||||||
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
|
||||||
import { SettingsService } from '../settings.service'
|
|
||||||
import { SettingsSyncComponent } from './sync.component'
|
|
||||||
import { SettingsButtonComponent } from './button.component'
|
|
||||||
import { SettingsUpdateComponent } from './update.component'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'settings-menu',
|
|
||||||
template: `
|
|
||||||
<ng-container *ngIf="server$ | async as server; else loading">
|
|
||||||
<settings-sync *ngIf="!server.ntpSynced" />
|
|
||||||
<section *ngFor="let cat of service.settings | keyvalue: asIsOrder">
|
|
||||||
<h3 class="g-title" (click)="addClick(cat.key)">{{ cat.key }}</h3>
|
|
||||||
<settings-update
|
|
||||||
*ngIf="cat.key === 'General'"
|
|
||||||
[updated]="server.statusInfo.updated"
|
|
||||||
/>
|
|
||||||
<ng-container *ngFor="let btn of cat.value">
|
|
||||||
<settings-button [button]="btn">
|
|
||||||
<!-- // @TODO 041
|
|
||||||
<div
|
|
||||||
*ngIf="btn.title === 'Outbound Proxy'"
|
|
||||||
tuiSubtitle
|
|
||||||
[style.color]="
|
|
||||||
!server.network.outboundProxy
|
|
||||||
? 'var(--tui-status-warning)'
|
|
||||||
: 'var(--tui-status-positive)'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ server.network.outboundProxy || 'None' }}
|
|
||||||
</div> -->
|
|
||||||
</settings-button>
|
|
||||||
</ng-container>
|
|
||||||
</section>
|
|
||||||
</ng-container>
|
|
||||||
<ng-template #loading>
|
|
||||||
<tui-loader
|
|
||||||
textContent="Connecting to server"
|
|
||||||
[style.margin-top.rem]="10"
|
|
||||||
/>
|
|
||||||
</ng-template>
|
|
||||||
`,
|
|
||||||
styles: [
|
|
||||||
`
|
|
||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
],
|
|
||||||
standalone: true,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
TuiLoader,
|
|
||||||
TuiButton,
|
|
||||||
SettingsSyncComponent,
|
|
||||||
SettingsButtonComponent,
|
|
||||||
SettingsUpdateComponent,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class SettingsMenuComponent {
|
|
||||||
private readonly clientStorageService = inject(ClientStorageService)
|
|
||||||
private readonly alerts = inject(TuiAlertService)
|
|
||||||
|
|
||||||
readonly server$ = inject<PatchDB<DataModel>>(PatchDB).watch$('serverInfo')
|
|
||||||
readonly service = inject(SettingsService)
|
|
||||||
|
|
||||||
manageClicks = 0
|
|
||||||
powerClicks = 0
|
|
||||||
|
|
||||||
addClick(title: string) {
|
|
||||||
switch (title) {
|
|
||||||
case 'Security':
|
|
||||||
this.addSecurityClick()
|
|
||||||
break
|
|
||||||
case 'Power':
|
|
||||||
this.addPowerClick()
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
asIsOrder() {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private addSecurityClick() {
|
|
||||||
this.manageClicks++
|
|
||||||
|
|
||||||
if (this.manageClicks === 5) {
|
|
||||||
this.manageClicks = 0
|
|
||||||
this.alerts
|
|
||||||
.open(
|
|
||||||
this.clientStorageService.toggleShowDevTools()
|
|
||||||
? 'Dev tools unlocked'
|
|
||||||
: 'Dev tools hidden',
|
|
||||||
)
|
|
||||||
.subscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private addPowerClick() {
|
|
||||||
this.powerClicks++
|
|
||||||
if (this.powerClicks === 5) {
|
|
||||||
this.powerClicks = 0
|
|
||||||
this.clientStorageService.toggleShowDiskRepair()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { SettingsComponent } from './settings.component'
|
|
||||||
|
|
||||||
export default [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: SettingsComponent,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'acme',
|
|
||||||
loadComponent: () =>
|
|
||||||
import('./routes/acme/acme.component').then(
|
|
||||||
m => m.SettingsACMEComponent,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'email',
|
|
||||||
loadComponent: () =>
|
|
||||||
import('./routes/email/email.component').then(
|
|
||||||
m => m.SettingsEmailComponent,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// path: 'domains',
|
|
||||||
// loadComponent: () =>
|
|
||||||
// import('./routes/domains/domains.component').then(
|
|
||||||
// m => m.SettingsDomainsComponent,
|
|
||||||
// ),
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: 'proxies',
|
|
||||||
// loadComponent: () =>
|
|
||||||
// import('./routes/proxies/proxies.component').then(
|
|
||||||
// m => m.SettingsProxiesComponent,
|
|
||||||
// ),
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: 'router',
|
|
||||||
// loadComponent: () =>
|
|
||||||
// import('./routes/router/router.component').then(
|
|
||||||
// m => m.SettingsRouterComponent,
|
|
||||||
// ),
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
path: 'wifi',
|
|
||||||
loadComponent: () =>
|
|
||||||
import('./routes/wifi/wifi.component').then(
|
|
||||||
m => m.SettingsWifiComponent,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'ui',
|
|
||||||
loadComponent: () =>
|
|
||||||
import('./routes/interfaces/ui.component').then(
|
|
||||||
m => m.StartOsUiComponent,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'ssh',
|
|
||||||
loadComponent: () =>
|
|
||||||
import('./routes/ssh/ssh.component').then(
|
|
||||||
m => m.SettingsSSHComponent,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'sessions',
|
|
||||||
loadComponent: () =>
|
|
||||||
import('./routes/sessions/sessions.component').then(
|
|
||||||
m => m.SettingsSessionsComponent,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { TuiCell } from '@taiga-ui/layout'
|
||||||
|
import { SettingBtn } from '../system.types'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'system-button',
|
||||||
|
template: `
|
||||||
|
@if (button.action) {
|
||||||
|
<button tuiCell (click)="button.action()">
|
||||||
|
<ng-container *ngTemplateOutlet="template" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (button.routerLink) {
|
||||||
|
<a tuiCell [routerLink]="button.routerLink">
|
||||||
|
<ng-container *ngTemplateOutlet="template" />
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
|
||||||
|
<ng-template #template>
|
||||||
|
<tui-icon [icon]="button.icon" />
|
||||||
|
<div tuiTitle>
|
||||||
|
<strong>{{ button.title }}</strong>
|
||||||
|
<div tuiSubtitle>{{ button.description }}</div>
|
||||||
|
<ng-content />
|
||||||
|
</div>
|
||||||
|
@if (button.routerLink) {
|
||||||
|
<tui-icon icon="@tui.chevron-right" />
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
`,
|
||||||
|
styles: `
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
box-shadow: 0 1px var(--tui-background-neutral-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, TuiIcon, TuiTitle, RouterLink, TuiCell],
|
||||||
|
})
|
||||||
|
export class SystemButtonComponent {
|
||||||
|
@Input({ required: true })
|
||||||
|
button!: SettingBtn
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
|
import { TuiAlertService, TuiLoader, TuiButton } from '@taiga-ui/core'
|
||||||
|
import { PatchDB } from 'patch-db-client'
|
||||||
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
||||||
|
import { SystemService } from '../system.service'
|
||||||
|
import { SystemSyncComponent } from './sync.component'
|
||||||
|
import { SystemButtonComponent } from './button.component'
|
||||||
|
import { SystemUpdateComponent } from './update.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'system-menu',
|
||||||
|
template: `
|
||||||
|
@if (data(); as server) {
|
||||||
|
@if (!server.ntpSynced) {
|
||||||
|
<system-sync />
|
||||||
|
}
|
||||||
|
|
||||||
|
@for (cat of service.settings | keyvalue: asIsOrder; track $index) {
|
||||||
|
<section class="g-card">
|
||||||
|
<header (click)="addClick(cat.key)">{{ cat.key }}</header>
|
||||||
|
@if (cat.key === 'General') {
|
||||||
|
<system-update [updated]="server.statusInfo.updated" />
|
||||||
|
}
|
||||||
|
|
||||||
|
@for (btn of cat.value; track $index) {
|
||||||
|
<system-button [button]="btn">
|
||||||
|
<!-- // @TODO 041
|
||||||
|
<div
|
||||||
|
*ngIf="btn.title === 'Outbound Proxy'"
|
||||||
|
tuiSubtitle
|
||||||
|
[style.color]="
|
||||||
|
!server.network.outboundProxy
|
||||||
|
? 'var(--tui-status-warning)'
|
||||||
|
: 'var(--tui-status-positive)'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ server.network.outboundProxy || 'None' }}
|
||||||
|
</div> -->
|
||||||
|
</system-button>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
<tui-loader
|
||||||
|
textContent="Connecting to server"
|
||||||
|
[style.margin-top.rem]="10"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
TuiLoader,
|
||||||
|
SystemSyncComponent,
|
||||||
|
SystemButtonComponent,
|
||||||
|
SystemUpdateComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class SystemMenuComponent {
|
||||||
|
private readonly clientStorageService = inject(ClientStorageService)
|
||||||
|
private readonly alerts = inject(TuiAlertService)
|
||||||
|
|
||||||
|
readonly service = inject(SystemService)
|
||||||
|
readonly data = toSignal(
|
||||||
|
inject<PatchDB<DataModel>>(PatchDB).watch$('serverInfo'),
|
||||||
|
)
|
||||||
|
|
||||||
|
manageClicks = 0
|
||||||
|
powerClicks = 0
|
||||||
|
|
||||||
|
addClick(title: string) {
|
||||||
|
switch (title) {
|
||||||
|
case 'Security':
|
||||||
|
this.addSecurityClick()
|
||||||
|
break
|
||||||
|
case 'Power':
|
||||||
|
this.addPowerClick()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
asIsOrder() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private addSecurityClick() {
|
||||||
|
this.manageClicks++
|
||||||
|
|
||||||
|
if (this.manageClicks === 5) {
|
||||||
|
this.manageClicks = 0
|
||||||
|
this.alerts
|
||||||
|
.open(
|
||||||
|
this.clientStorageService.toggleShowDevTools()
|
||||||
|
? 'Dev tools unlocked'
|
||||||
|
: 'Dev tools hidden',
|
||||||
|
)
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addPowerClick() {
|
||||||
|
this.powerClicks++
|
||||||
|
if (this.powerClicks === 5) {
|
||||||
|
this.powerClicks = 0
|
||||||
|
this.clientStorageService.toggleShowDiskRepair()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { TuiTitle, TuiButton, TuiNotification } from '@taiga-ui/core'
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'settings-sync',
|
selector: 'system-sync',
|
||||||
template: `
|
template: `
|
||||||
<tui-notification appearance="warning">
|
<tui-notification appearance="warning">
|
||||||
<div tuiCell [style.padding]="0">
|
<div tuiCell [style.padding]="0">
|
||||||
@@ -31,4 +31,4 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [TuiButton, TuiCell, TuiNotification, TuiTitle],
|
imports: [TuiButton, TuiCell, TuiNotification, TuiTitle],
|
||||||
})
|
})
|
||||||
export class SettingsSyncComponent {}
|
export class SystemSyncComponent {}
|
||||||
@@ -7,48 +7,46 @@ import {
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { TuiDialogService, TuiIcon, TuiTitle } from '@taiga-ui/core'
|
import { TuiDialogService, TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||||
|
import { TuiCell } from '@taiga-ui/layout'
|
||||||
import { EOSService } from 'src/app/services/eos.service'
|
import { EOSService } from 'src/app/services/eos.service'
|
||||||
import { UPDATE } from '../modals/update.component'
|
import { UPDATE } from '../modals/update.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'settings-update',
|
selector: 'system-update',
|
||||||
template: `
|
template: `
|
||||||
<button
|
<button
|
||||||
class="g-action"
|
tuiCell
|
||||||
[disabled]="service.updatingOrBackingUp$ | async"
|
[disabled]="service.updatingOrBackingUp$ | async"
|
||||||
(click)="onClick()"
|
(click)="onClick()"
|
||||||
>
|
>
|
||||||
<tui-icon icon="@tui.cloud-download"></tui-icon>
|
<tui-icon icon="@tui.cloud-download" />
|
||||||
<div tuiTitle>
|
<div tuiTitle>
|
||||||
<strong>Software Update</strong>
|
<strong>Software Update</strong>
|
||||||
<div tuiSubtitle>Get the latest version of StartOS</div>
|
<div tuiSubtitle>Get the latest version of StartOS</div>
|
||||||
<div
|
@if (updated) {
|
||||||
*ngIf="updated; else notUpdated"
|
<div tuiSubtitle class="g-warning">
|
||||||
tuiSubtitle
|
Update Complete. Restart to apply changes
|
||||||
[style.color]="'var(--tui-status-warning)'"
|
</div>
|
||||||
>
|
} @else {
|
||||||
Update Complete. Restart to apply changes
|
@if (service.showUpdate$ | async) {
|
||||||
</div>
|
<div tuiSubtitle class="g-positive">
|
||||||
<ng-template #notUpdated>
|
|
||||||
<ng-container *ngIf="service.showUpdate$ | async; else check">
|
|
||||||
<div tuiSubtitle [style.color]="'var(--tui-status-positive)'">
|
|
||||||
<tui-icon class="small" icon="@tui.zap" />
|
<tui-icon class="small" icon="@tui.zap" />
|
||||||
Update Available
|
Update Available
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
} @else {
|
||||||
<ng-template #check>
|
<div tuiSubtitle class="g-info">
|
||||||
<div tuiSubtitle [style.color]="'var(--tui-status-info)'">
|
|
||||||
<tui-icon class="small" icon="@tui.rotate-cw" />
|
<tui-icon class="small" icon="@tui.rotate-cw" />
|
||||||
Check for updates
|
Check for updates
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
}
|
||||||
</ng-template>
|
}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
box-shadow: 0 1px var(--tui-background-neutral-1);
|
box-shadow: 0 1px var(--tui-background-neutral-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,9 +60,9 @@ import { UPDATE } from '../modals/update.component'
|
|||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, TuiIcon, TuiTitle],
|
imports: [CommonModule, TuiIcon, TuiTitle, TuiCell],
|
||||||
})
|
})
|
||||||
export class SettingsUpdateComponent {
|
export class SystemUpdateComponent {
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
@@ -46,7 +46,7 @@ import { EOSService } from 'src/app/services/eos.service'
|
|||||||
TuiScrollbar,
|
TuiScrollbar,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsUpdateModal {
|
export class SystemUpdateModal {
|
||||||
readonly versions = Object.entries(this.eosService.osUpdate?.releaseNotes!)
|
readonly versions = Object.entries(this.eosService.osUpdate?.releaseNotes!)
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
.reverse()
|
.reverse()
|
||||||
@@ -77,4 +77,4 @@ export class SettingsUpdateModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UPDATE = new PolymorpheusComponent(SettingsUpdateModal)
|
export const UPDATE = new PolymorpheusComponent(SystemUpdateModal)
|
||||||
@@ -1,47 +1,93 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { PatchDB } from 'patch-db-client'
|
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|
||||||
import { ISB, utils } from '@start9labs/start-sdk'
|
import { ISB, utils } from '@start9labs/start-sdk'
|
||||||
import { knownACME, toAcmeName } from 'src/app/utils/acme'
|
import { TuiButton, TuiLoader, TuiTitle } from '@taiga-ui/core'
|
||||||
|
import { TuiCell } from '@taiga-ui/layout'
|
||||||
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { map } from 'rxjs'
|
import { map } from 'rxjs'
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
|
||||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
|
||||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||||
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { knownACME, toAcmeName } from 'src/app/utils/acme'
|
||||||
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
|
import { AcmeInfoComponent } from './info.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'acme',
|
template: `
|
||||||
template: ``,
|
<acme-info />
|
||||||
styles: [],
|
<section class="g-card">
|
||||||
|
<header>
|
||||||
|
Saved Providers
|
||||||
|
@if (acme(); as value) {
|
||||||
|
<button
|
||||||
|
tuiButton
|
||||||
|
size="xs"
|
||||||
|
iconStart="@tui.plus"
|
||||||
|
[style.margin-inline-start]="'auto'"
|
||||||
|
(click)="addAcme(value)"
|
||||||
|
>
|
||||||
|
Add Provider
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</header>
|
||||||
|
@if (acme(); as value) {
|
||||||
|
@for (provider of value; track $index) {
|
||||||
|
<div tuiCell>
|
||||||
|
<span tuiTitle>
|
||||||
|
<strong>{{ toAcmeName(provider.url) }}</strong>
|
||||||
|
<span tuiSubtitle>Contact: {{ provider.contactString }}</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
tuiIconButton
|
||||||
|
iconStart="@tui.pencil"
|
||||||
|
appearance="icon"
|
||||||
|
(click)="editAcme(provider.url, provider.contact)"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
tuiIconButton
|
||||||
|
iconStart="@tui.trash"
|
||||||
|
appearance="icon"
|
||||||
|
(click)="removeAcme(provider.url)"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
<tui-loader [style.height.rem]="5" />
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [TuiButton, TuiLoader, TuiCell, TuiTitle, AcmeInfoComponent],
|
||||||
})
|
})
|
||||||
export class SettingsACMEComponent {
|
export default class SystemAcmeComponent {
|
||||||
private readonly formDialog = inject(FormDialogService)
|
private readonly formDialog = inject(FormDialogService)
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
|
|
||||||
readonly docsUrl = 'https://docs.start9.com/0.3.6/user-manual/acme'
|
acme = toSignal(
|
||||||
|
this.patch.watch$('serverInfo', 'network', 'acme').pipe(
|
||||||
acme$ = this.patch.watch$('serverInfo', 'network', 'acme').pipe(
|
map(acme =>
|
||||||
map(acme => {
|
Object.keys(acme).map(url => {
|
||||||
const providerUrls = Object.keys(acme)
|
const contact = acme[url].contact.map(mailto =>
|
||||||
return providerUrls.map(url => {
|
mailto.replace('mailto:', ''),
|
||||||
const contact = acme[url].contact.map(mailto =>
|
)
|
||||||
mailto.replace('mailto:', ''),
|
return {
|
||||||
)
|
url,
|
||||||
return {
|
contact,
|
||||||
url,
|
contactString: contact.join(', '),
|
||||||
contact,
|
}
|
||||||
contactString: contact.join(', '),
|
}),
|
||||||
}
|
),
|
||||||
})
|
),
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
toAcmeName = toAcmeName
|
toAcmeName = toAcmeName
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
|
import { TuiLink, TuiNotification } from '@taiga-ui/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'acme-info',
|
||||||
|
template: `
|
||||||
|
<tui-notification>
|
||||||
|
Register with one or more ACME providers such as Let's Encrypt in order to
|
||||||
|
generate SSL (https) certificates on-demand for clearnet hosting.
|
||||||
|
<a
|
||||||
|
tuiLink
|
||||||
|
href="https://docs.start9.com/latest/user-manual/acme"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
View instructions
|
||||||
|
</a>
|
||||||
|
</tui-notification>
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true,
|
||||||
|
imports: [TuiNotification, TuiLink],
|
||||||
|
})
|
||||||
|
export class AcmeInfoComponent {}
|
||||||
@@ -60,7 +60,7 @@ import { DomainsTableComponent } from './table.component'
|
|||||||
DomainsInfoComponent,
|
DomainsInfoComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsDomainsComponent {
|
export default class SystemDomainsComponent {
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
private readonly formDialog = inject(FormDialogService)
|
private readonly formDialog = inject(FormDialogService)
|
||||||
@@ -103,7 +103,7 @@ export class SettingsDomainsComponent {
|
|||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: 'Manage proxies',
|
text: 'Manage proxies',
|
||||||
link: '/portal/settings/proxies',
|
link: '/portal/system/proxies',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Save',
|
text: 'Save',
|
||||||
@@ -128,7 +128,7 @@ export class SettingsDomainsComponent {
|
|||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: 'Manage proxies',
|
text: 'Manage proxies',
|
||||||
link: '/portal/settings/proxies',
|
link: '/portal/system/proxies',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Save',
|
text: 'Save',
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { TuiNotification } from '@taiga-ui/core'
|
import { TuiLink, TuiNotification } from '@taiga-ui/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'domains-info',
|
selector: 'domains-info',
|
||||||
@@ -7,6 +7,7 @@ import { TuiNotification } from '@taiga-ui/core'
|
|||||||
<tui-notification>
|
<tui-notification>
|
||||||
Adding domains permits accessing your server and services over clearnet.
|
Adding domains permits accessing your server and services over clearnet.
|
||||||
<a
|
<a
|
||||||
|
tuiLink
|
||||||
href="https://docs.start9.com/latest/user-manual/domains"
|
href="https://docs.start9.com/latest/user-manual/domains"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
@@ -17,6 +18,6 @@ import { TuiNotification } from '@taiga-ui/core'
|
|||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [TuiNotification],
|
imports: [TuiNotification, TuiLink],
|
||||||
})
|
})
|
||||||
export class DomainsInfoComponent {}
|
export class DomainsInfoComponent {}
|
||||||
@@ -1,17 +1,13 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import {
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
UntypedFormGroup,
|
|
||||||
} from '@angular/forms'
|
|
||||||
import { RouterLink } from '@angular/router'
|
import { RouterLink } from '@angular/router'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { IST, inputSpec } from '@start9labs/start-sdk'
|
import { IST, inputSpec } from '@start9labs/start-sdk'
|
||||||
import { TuiButton, TuiDialogService } from '@taiga-ui/core'
|
import { TuiButton, TuiDialogService } from '@taiga-ui/core'
|
||||||
import { TuiInputModule } from '@taiga-ui/legacy'
|
import { TuiInputModule } from '@taiga-ui/legacy'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { switchMap } from 'rxjs'
|
import { switchMap, tap } from 'rxjs'
|
||||||
import { FormModule } from 'src/app/routes/portal/components/form/form.module'
|
import { FormModule } from 'src/app/routes/portal/components/form/form.module'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { FormService } from 'src/app/services/form.service'
|
import { FormService } from 'src/app/services/form.service'
|
||||||
@@ -28,33 +24,33 @@ import { EmailInfoComponent } from './info.component'
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<email-info />
|
<email-info />
|
||||||
<ng-container *ngIf="form$ | async as form">
|
<ng-container *ngIf="form$ | async as form">
|
||||||
<form [formGroup]="form" [style.text-align]="'right'">
|
<form class="g-card" [formGroup]="form">
|
||||||
<h3 class="g-title">SMTP Credentials</h3>
|
<header>SMTP Credentials</header>
|
||||||
<form-group
|
<form-group
|
||||||
*ngIf="spec | async as resolved"
|
*ngIf="spec | async as resolved"
|
||||||
[spec]="resolved"
|
[spec]="resolved"
|
||||||
></form-group>
|
></form-group>
|
||||||
<button
|
<footer>
|
||||||
*ngIf="isSaved"
|
@if (isSaved) {
|
||||||
tuiButton
|
<button
|
||||||
appearance="secondary-destructive"
|
tuiButton
|
||||||
[style.margin-top.rem]="1"
|
appearance="secondary-destructive"
|
||||||
[style.margin-right.rem]="1"
|
(click)="save(null)"
|
||||||
(click)="save(null)"
|
>
|
||||||
>
|
Delete
|
||||||
Delete
|
</button>
|
||||||
</button>
|
}
|
||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
[style.margin-top.rem]="1"
|
[disabled]="form.invalid"
|
||||||
[disabled]="form.invalid"
|
(click)="save(form.value)"
|
||||||
(click)="save(form.value)"
|
>
|
||||||
>
|
Save
|
||||||
Save
|
</button>
|
||||||
</button>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
<form [style.text-align]="'right'">
|
<form class="g-card">
|
||||||
<h3 class="g-title">Test Email</h3>
|
<header>Send Test Email</header>
|
||||||
<tui-input
|
<tui-input
|
||||||
[(ngModel)]="testAddress"
|
[(ngModel)]="testAddress"
|
||||||
[ngModelOptions]="{ standalone: true }"
|
[ngModelOptions]="{ standalone: true }"
|
||||||
@@ -62,19 +58,19 @@ import { EmailInfoComponent } from './info.component'
|
|||||||
Firstname Lastname <email@example.com>
|
Firstname Lastname <email@example.com>
|
||||||
<input tuiTextfieldLegacy inputmode="email" />
|
<input tuiTextfieldLegacy inputmode="email" />
|
||||||
</tui-input>
|
</tui-input>
|
||||||
<button
|
<footer>
|
||||||
tuiButton
|
<button
|
||||||
appearance="secondary"
|
tuiButton
|
||||||
[style.margin-top.rem]="1"
|
appearance="secondary"
|
||||||
[disabled]="!testAddress || form.invalid"
|
[disabled]="!testAddress || form.invalid"
|
||||||
(click)="sendTestEmail(form)"
|
(click)="sendTestEmail(form.value)"
|
||||||
>
|
>
|
||||||
Send Test Email
|
Send
|
||||||
</button>
|
</button>
|
||||||
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
`,
|
`,
|
||||||
styles: ['form { margin: auto; max-width: 30rem; }'],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
@@ -89,7 +85,7 @@ import { EmailInfoComponent } from './info.component'
|
|||||||
TitleDirective,
|
TitleDirective,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsEmailComponent {
|
export default class SystemEmailComponent {
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
@@ -103,13 +99,12 @@ export class SettingsEmailComponent {
|
|||||||
readonly spec: Promise<IST.InputSpec> = configBuilderToSpec(
|
readonly spec: Promise<IST.InputSpec> = configBuilderToSpec(
|
||||||
inputSpec.constants.customSmtp,
|
inputSpec.constants.customSmtp,
|
||||||
)
|
)
|
||||||
readonly form$ = this.patch
|
readonly form$ = this.patch.watch$('serverInfo', 'smtp').pipe(
|
||||||
.watch$('serverInfo', 'smtp')
|
tap(value => (this.isSaved = !!value)),
|
||||||
.pipe(
|
switchMap(async value =>
|
||||||
switchMap(async value =>
|
this.formService.createForm(await this.spec, value),
|
||||||
this.formService.createForm(await this.spec, value),
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
|
|
||||||
async save(
|
async save(
|
||||||
value: typeof inputSpec.constants.customSmtp._TYPE | null,
|
value: typeof inputSpec.constants.customSmtp._TYPE | null,
|
||||||
@@ -131,13 +126,13 @@ export class SettingsEmailComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendTestEmail(form: UntypedFormGroup) {
|
async sendTestEmail(value: typeof inputSpec.constants.customSmtp._TYPE) {
|
||||||
const loader = this.loader.open('Sending...').subscribe()
|
const loader = this.loader.open('Sending email...').subscribe()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.testSmtp({
|
await this.api.testSmtp({
|
||||||
to: this.testAddress,
|
to: this.testAddress,
|
||||||
...form.value,
|
...value,
|
||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { TuiNotification } from '@taiga-ui/core'
|
import { TuiLink, TuiNotification } from '@taiga-ui/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'email-info',
|
selector: 'email-info',
|
||||||
@@ -8,6 +8,7 @@ import { TuiNotification } from '@taiga-ui/core'
|
|||||||
Adding SMTP credentials to StartOS enables StartOS and some services to
|
Adding SMTP credentials to StartOS enables StartOS and some services to
|
||||||
send you emails.
|
send you emails.
|
||||||
<a
|
<a
|
||||||
|
tuiLink
|
||||||
href="https://docs.start9.com/latest/user-manual/smtp"
|
href="https://docs.start9.com/latest/user-manual/smtp"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
@@ -18,6 +19,6 @@ import { TuiNotification } from '@taiga-ui/core'
|
|||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [TuiNotification],
|
imports: [TuiNotification, TuiLink],
|
||||||
})
|
})
|
||||||
export class EmailInfoComponent {}
|
export class EmailInfoComponent {}
|
||||||
@@ -53,7 +53,7 @@ const iface: T.ServiceInterface = {
|
|||||||
TitleDirective,
|
TitleDirective,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class StartOsUiComponent {
|
export default class StartOsUiComponent {
|
||||||
private readonly config = inject(ConfigService)
|
private readonly config = inject(ConfigService)
|
||||||
|
|
||||||
readonly ui$: Observable<MappedServiceInterface> = inject<PatchDB<DataModel>>(
|
readonly ui$: Observable<MappedServiceInterface> = inject<PatchDB<DataModel>>(
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { TuiNotification } from '@taiga-ui/core'
|
import { TuiLink, TuiNotification } from '@taiga-ui/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'proxies-info',
|
selector: 'proxies-info',
|
||||||
@@ -25,6 +25,7 @@ import { TuiNotification } from '@taiga-ui/core'
|
|||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<a
|
<a
|
||||||
|
tuiLink
|
||||||
href="https://docs.start9.com/latest/user-manual/vpns/"
|
href="https://docs.start9.com/latest/user-manual/vpns/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
@@ -35,6 +36,6 @@ import { TuiNotification } from '@taiga-ui/core'
|
|||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [TuiNotification],
|
imports: [TuiNotification, TuiLink],
|
||||||
})
|
})
|
||||||
export class ProxiesInfoComponent {}
|
export class ProxiesInfoComponent {}
|
||||||
@@ -35,7 +35,7 @@ import { wireguardSpec, WireguardSpec } from './constants'
|
|||||||
ProxiesTableComponent,
|
ProxiesTableComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsProxiesComponent {
|
export default class SystemProxiesComponent {
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
DELETE_OPTIONS,
|
DELETE_OPTIONS,
|
||||||
ProxyUpdate,
|
ProxyUpdate,
|
||||||
} from 'src/app/routes/portal/routes/settings/routes/proxies/constants'
|
} from 'src/app/routes/portal/routes/system/routes/proxies/constants'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
import { Proxy } from 'src/app/services/patch-db/data-model'
|
import { Proxy } from 'src/app/services/patch-db/data-model'
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
import { CommonModule } from '@angular/common'
|
import { TuiLink, TuiNotification } from '@taiga-ui/core'
|
||||||
import { TuiNotification } from '@taiga-ui/core'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'router-info',
|
selector: 'router-info',
|
||||||
template: `
|
template: `
|
||||||
<tui-notification [appearance]="enabled ? 'positive' : 'warning'">
|
<tui-notification [appearance]="enabled ? 'positive' : 'warning'">
|
||||||
<ng-container *ngIf="enabled; else disabled">
|
@if (enabled) {
|
||||||
<strong>UPnP Enabled!</strong>
|
<strong>UPnP Enabled!</strong>
|
||||||
<p>
|
<p>
|
||||||
The ports below have been
|
The ports below have been
|
||||||
@@ -16,14 +15,14 @@ import { TuiNotification } from '@taiga-ui/core'
|
|||||||
If you are running multiple servers, you may want to override specific
|
If you are running multiple servers, you may want to override specific
|
||||||
ports to suite your needs.
|
ports to suite your needs.
|
||||||
<a
|
<a
|
||||||
|
tuiLink
|
||||||
href="https://docs.start9.com/latest/user-manual/port-forwards/upnp#override"
|
href="https://docs.start9.com/latest/user-manual/port-forwards/upnp#override"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
View instructions
|
View instructions
|
||||||
</a>
|
</a>
|
||||||
</ng-container>
|
} @else {
|
||||||
<ng-template #disabled>
|
|
||||||
<strong>UPnP Disabled</strong>
|
<strong>UPnP Disabled</strong>
|
||||||
<p>
|
<p>
|
||||||
Below are a list of ports that must be
|
Below are a list of ports that must be
|
||||||
@@ -33,19 +32,20 @@ import { TuiNotification } from '@taiga-ui/core'
|
|||||||
Alternatively, you can enable UPnP in your router for automatic
|
Alternatively, you can enable UPnP in your router for automatic
|
||||||
configuration.
|
configuration.
|
||||||
<a
|
<a
|
||||||
|
tuiLink
|
||||||
href="https://docs.start9.com/latest/user-manual/port-forwards/manual"
|
href="https://docs.start9.com/latest/user-manual/port-forwards/manual"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
View instructions
|
View instructions
|
||||||
</a>
|
</a>
|
||||||
</ng-template>
|
}
|
||||||
</tui-notification>
|
</tui-notification>
|
||||||
`,
|
`,
|
||||||
styles: ['strong { font-size: 1rem }'],
|
styles: ['strong { font-size: 1rem }'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, TuiNotification],
|
imports: [TuiNotification, TuiLink],
|
||||||
})
|
})
|
||||||
export class RouterInfoComponent {
|
export class RouterInfoComponent {
|
||||||
@Input()
|
@Input()
|
||||||
@@ -64,6 +64,6 @@ import { RouterPortComponent } from './table.component'
|
|||||||
PrimaryIpPipe,
|
PrimaryIpPipe,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsRouterComponent {
|
export default class SystemRouterComponent {
|
||||||
readonly server$ = inject<PatchDB<DataModel>>(PatchDB).watch$('serverInfo')
|
readonly server$ = inject<PatchDB<DataModel>>(PatchDB).watch$('serverInfo')
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { RouterLink } from '@angular/router'
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { TuiTable } from '@taiga-ui/addon-table'
|
||||||
import { TuiLet } from '@taiga-ui/cdk'
|
import { TuiLet } from '@taiga-ui/cdk'
|
||||||
import { TuiButton } from '@taiga-ui/core'
|
import { TuiButton } from '@taiga-ui/core'
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
@@ -16,29 +17,34 @@ import { SSHTableComponent } from './table.component'
|
|||||||
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
|
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
|
||||||
Active Sessions
|
Active Sessions
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<h3 class="g-title">Current session</h3>
|
<section class="g-card">
|
||||||
<table
|
<header>Current session</header>
|
||||||
class="g-table"
|
<table
|
||||||
[single]="true"
|
tuiTable
|
||||||
[sessions]="current$ | async"
|
class="g-table"
|
||||||
></table>
|
[single]="true"
|
||||||
|
[sessions]="current$ | async"
|
||||||
|
></table>
|
||||||
|
</section>
|
||||||
|
|
||||||
<ng-container *tuiLet="other$ | async as others">
|
<section *tuiLet="other$ | async as others" class="g-card">
|
||||||
<h3 class="g-title">
|
<header>
|
||||||
Other sessions
|
Other sessions
|
||||||
<button
|
@if (table.selected$ | async; as selected) {
|
||||||
*ngIf="table.selected$ | async as selected"
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
size="xs"
|
size="xs"
|
||||||
appearance="error"
|
appearance="negative"
|
||||||
[disabled]="!selected.length"
|
[style.margin-inline-start]="'auto'"
|
||||||
(click)="terminate(selected, others || [])"
|
[disabled]="!selected.length"
|
||||||
>
|
(click)="terminate(selected, others || [])"
|
||||||
Terminate selected
|
>
|
||||||
</button>
|
Terminate selected
|
||||||
</h3>
|
</button>
|
||||||
<table #table class="g-table" [sessions]="others"></table>
|
}
|
||||||
</ng-container>
|
</header>
|
||||||
|
<table #table tuiTable class="g-table" [sessions]="others"></table>
|
||||||
|
</section>
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -49,9 +55,10 @@ import { SSHTableComponent } from './table.component'
|
|||||||
TuiLet,
|
TuiLet,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
TitleDirective,
|
TitleDirective,
|
||||||
|
TuiTable,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsSessionsComponent {
|
export default class SystemSessionsComponent {
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TuiCheckbox, TuiFade, TuiSkeleton } from '@taiga-ui/kit'
|
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@@ -7,7 +6,9 @@ import {
|
|||||||
OnChanges,
|
OnChanges,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { TuiIcon, TuiLink, TuiButton } from '@taiga-ui/core'
|
import { TuiTable } from '@taiga-ui/addon-table'
|
||||||
|
import { TuiIcon } from '@taiga-ui/core'
|
||||||
|
import { TuiCheckbox, TuiFade, TuiSkeleton } from '@taiga-ui/kit'
|
||||||
import { BehaviorSubject } from 'rxjs'
|
import { BehaviorSubject } from 'rxjs'
|
||||||
import { Session } from 'src/app/services/api/api.types'
|
import { Session } from 'src/app/services/api/api.types'
|
||||||
import { PlatformInfoPipe } from './platform-info.pipe'
|
import { PlatformInfoPipe } from './platform-info.pipe'
|
||||||
@@ -17,7 +18,11 @@ import { PlatformInfoPipe } from './platform-info.pipe'
|
|||||||
template: `
|
template: `
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th [style.width.%]="50" [style.padding-left.rem]="single ? null : 2">
|
<th
|
||||||
|
tuiTh
|
||||||
|
[style.width.%]="50"
|
||||||
|
[style.padding-left.rem]="single ? null : 2"
|
||||||
|
>
|
||||||
@if (!single) {
|
@if (!single) {
|
||||||
<input
|
<input
|
||||||
tuiCheckbox
|
tuiCheckbox
|
||||||
@@ -30,14 +35,14 @@ import { PlatformInfoPipe } from './platform-info.pipe'
|
|||||||
}
|
}
|
||||||
User Agent
|
User Agent
|
||||||
</th>
|
</th>
|
||||||
<th [style.width.%]="25">Platform</th>
|
<th tuiTh [style.width.%]="25">Platform</th>
|
||||||
<th [style.width.%]="25">Last Active</th>
|
<th tuiTh [style.width.%]="25">Last Active</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (session of sessions; track $index) {
|
@for (session of sessions; track $index) {
|
||||||
<tr>
|
<tr>
|
||||||
<td [style.padding-left.rem]="single ? null : 2">
|
<td [style.padding-left.rem]="single ? null : 2.25">
|
||||||
@if (!single) {
|
@if (!single) {
|
||||||
<input
|
<input
|
||||||
tuiCheckbox
|
tuiCheckbox
|
||||||
@@ -74,10 +79,14 @@ import { PlatformInfoPipe } from './platform-info.pipe'
|
|||||||
`
|
`
|
||||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||||
|
|
||||||
|
td {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 0.25rem;
|
left: 0.5rem;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,12 +127,11 @@ import { PlatformInfoPipe } from './platform-info.pipe'
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
PlatformInfoPipe,
|
PlatformInfoPipe,
|
||||||
TuiButton,
|
|
||||||
TuiLink,
|
|
||||||
TuiIcon,
|
TuiIcon,
|
||||||
TuiCheckbox,
|
TuiCheckbox,
|
||||||
TuiFade,
|
TuiFade,
|
||||||
TuiSkeleton,
|
TuiSkeleton,
|
||||||
|
TuiTable,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SSHTableComponent<T extends Session> implements OnChanges {
|
export class SSHTableComponent<T extends Session> implements OnChanges {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { TuiNotification } from '@taiga-ui/core'
|
import { TuiLink, TuiNotification } from '@taiga-ui/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ssh-info',
|
selector: 'ssh-info',
|
||||||
@@ -8,6 +8,7 @@ import { TuiNotification } from '@taiga-ui/core'
|
|||||||
Adding domains to StartOS enables you to access your server and service
|
Adding domains to StartOS enables you to access your server and service
|
||||||
interfaces over clearnet.
|
interfaces over clearnet.
|
||||||
<a
|
<a
|
||||||
|
tuiLink
|
||||||
href="https://docs.start9.com/0.3.5.x/user-manual/ssh"
|
href="https://docs.start9.com/0.3.5.x/user-manual/ssh"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
@@ -18,6 +19,6 @@ import { TuiNotification } from '@taiga-ui/core'
|
|||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [TuiNotification],
|
imports: [TuiNotification, TuiLink],
|
||||||
})
|
})
|
||||||
export class SSHInfoComponent {}
|
export class SSHInfoComponent {}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { RouterLink } from '@angular/router'
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { TuiTable } from '@taiga-ui/addon-table'
|
||||||
import { TuiButton } from '@taiga-ui/core'
|
import { TuiButton } from '@taiga-ui/core'
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
@@ -16,18 +17,21 @@ import { SSHTableComponent } from './table.component'
|
|||||||
SSH
|
SSH
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ssh-info />
|
<ssh-info />
|
||||||
<h3 class="g-title">
|
<section class="g-card">
|
||||||
Saved Keys
|
<header>
|
||||||
<button
|
Saved Keys
|
||||||
tuiButton
|
<button
|
||||||
size="xs"
|
tuiButton
|
||||||
iconStart="@tui.plus"
|
size="xs"
|
||||||
(click)="table.add.call(table)"
|
iconStart="@tui.plus"
|
||||||
>
|
[style.margin-inline-start]="'auto'"
|
||||||
Add Key
|
(click)="table.add.call(table)"
|
||||||
</button>
|
>
|
||||||
</h3>
|
Add Key
|
||||||
<table #table class="g-table" [keys]="keys$ | async"></table>
|
</button>
|
||||||
|
</header>
|
||||||
|
<table #table tuiTable class="g-table" [keys]="keys$ | async"></table>
|
||||||
|
</section>
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -38,9 +42,10 @@ import { SSHTableComponent } from './table.component'
|
|||||||
SSHInfoComponent,
|
SSHInfoComponent,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
TitleDirective,
|
TitleDirective,
|
||||||
|
TuiTable,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsSSHComponent {
|
export default class SystemSSHComponent {
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
|
import { TuiTable } from '@taiga-ui/addon-table'
|
||||||
import { TuiDialogOptions, TuiDialogService, TuiButton } from '@taiga-ui/core'
|
import { TuiDialogOptions, TuiDialogService, TuiButton } from '@taiga-ui/core'
|
||||||
import {
|
import {
|
||||||
TuiConfirmData,
|
TuiConfirmData,
|
||||||
@@ -24,11 +25,11 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|||||||
template: `
|
template: `
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Hostname</th>
|
<th tuiTh>Hostname</th>
|
||||||
<th>Created At</th>
|
<th tuiTh>Created At</th>
|
||||||
<th>Algorithm</th>
|
<th tuiTh>Algorithm</th>
|
||||||
<th>Fingerprint</th>
|
<th tuiTh>Fingerprint</th>
|
||||||
<th></th>
|
<th tuiTh></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -108,7 +109,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|||||||
`,
|
`,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [CommonModule, TuiButton, TuiFade, TuiSkeleton],
|
imports: [CommonModule, TuiButton, TuiFade, TuiSkeleton, TuiTable],
|
||||||
})
|
})
|
||||||
export class SSHTableComponent {
|
export class SSHTableComponent {
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { TuiNotification } from '@taiga-ui/core'
|
import { TuiLink, TuiNotification } from '@taiga-ui/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'wifi-info',
|
selector: 'wifi-info',
|
||||||
@@ -9,6 +9,7 @@ import { TuiNotification } from '@taiga-ui/core'
|
|||||||
and move the device anywhere you want. StartOS will automatically connect
|
and move the device anywhere you want. StartOS will automatically connect
|
||||||
to available networks.
|
to available networks.
|
||||||
<a
|
<a
|
||||||
|
tuiLink
|
||||||
href="https://docs.start9.com/latest/user-manual/wifi"
|
href="https://docs.start9.com/latest/user-manual/wifi"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
@@ -19,6 +20,6 @@ import { TuiNotification } from '@taiga-ui/core'
|
|||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [TuiNotification],
|
imports: [TuiNotification, TuiLink],
|
||||||
})
|
})
|
||||||
export class WifiInfoComponent {}
|
export class WifiInfoComponent {}
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { TuiButton, TuiDialogOptions, TuiIcon, TuiTitle } from '@taiga-ui/core'
|
import { TuiButton, TuiDialogOptions, TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||||
import { TuiBadge } from '@taiga-ui/kit'
|
import { TuiBadge, TuiFade } from '@taiga-ui/kit'
|
||||||
import { TuiCell } from '@taiga-ui/layout'
|
import { TuiCell } from '@taiga-ui/layout'
|
||||||
import {
|
import {
|
||||||
FormComponent,
|
FormComponent,
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
import { Wifi, WiFiForm, wifiSpec } from './utils'
|
import { Wifi, WiFiForm, wifiSpec } from './utils'
|
||||||
import { SettingsWifiComponent } from './wifi.component'
|
import SystemWifiComponent from './wifi.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: '[wifi]',
|
selector: '[wifi]',
|
||||||
@@ -26,7 +26,7 @@ import { SettingsWifiComponent } from './wifi.component'
|
|||||||
@if (network.ssid) {
|
@if (network.ssid) {
|
||||||
<div tuiCell [style.padding]="0">
|
<div tuiCell [style.padding]="0">
|
||||||
<div tuiTitle>
|
<div tuiTitle>
|
||||||
<strong>
|
<strong tuiFade>
|
||||||
{{ network.ssid }}
|
{{ network.ssid }}
|
||||||
@if (network.connected) {
|
@if (network.connected) {
|
||||||
<tui-badge appearance="success">Connected</tui-badge>
|
<tui-badge appearance="success">Connected</tui-badge>
|
||||||
@@ -34,12 +34,7 @@ import { SettingsWifiComponent } from './wifi.component'
|
|||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
@if (!network.connected) {
|
@if (!network.connected) {
|
||||||
<button
|
<button tuiButton size="xs" (click)="prompt(network)">
|
||||||
tuiButton
|
|
||||||
size="xs"
|
|
||||||
appearance="opposite"
|
|
||||||
(click)="prompt(network)"
|
|
||||||
>
|
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@@ -72,8 +67,12 @@ import { SettingsWifiComponent } from './wifi.component'
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
host: { style: 'align-items: stretch' },
|
|
||||||
styles: `
|
styles: `
|
||||||
|
:host {
|
||||||
|
align-items: stretch;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
tui-icon {
|
tui-icon {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
color: var(--tui-text-tertiary);
|
color: var(--tui-text-tertiary);
|
||||||
@@ -81,14 +80,22 @@ import { SettingsWifiComponent } from './wifi.component'
|
|||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, TuiCell, TuiTitle, TuiBadge, TuiButton, TuiIcon],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
TuiCell,
|
||||||
|
TuiTitle,
|
||||||
|
TuiBadge,
|
||||||
|
TuiButton,
|
||||||
|
TuiIcon,
|
||||||
|
TuiFade,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class WifiTableComponent {
|
export class WifiTableComponent {
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly formDialog = inject(FormDialogService)
|
private readonly formDialog = inject(FormDialogService)
|
||||||
private readonly component = inject(SettingsWifiComponent)
|
private readonly component = inject(SystemWifiComponent)
|
||||||
private readonly cdr = inject(ChangeDetectorRef)
|
private readonly cdr = inject(ChangeDetectorRef)
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
FormComponent,
|
FormComponent,
|
||||||
FormContext,
|
FormContext,
|
||||||
} from 'src/app/routes/portal/components/form.component'
|
} from 'src/app/routes/portal/components/form.component'
|
||||||
|
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
@@ -40,46 +41,51 @@ import { wifiSpec } from './wifi.const'
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<wifi-info />
|
<wifi-info />
|
||||||
@if (status()?.interface) {
|
@if (status()?.interface) {
|
||||||
<h3 class="g-title">
|
<section class="g-card">
|
||||||
Wi-Fi
|
<header>
|
||||||
<input
|
Wi-Fi
|
||||||
type="checkbox"
|
<input
|
||||||
tuiSwitch
|
type="checkbox"
|
||||||
[ngModel]="status()?.enabled"
|
tuiSwitch
|
||||||
(ngModelChange)="onToggle($event)"
|
[style.margin-inline-start]="'auto'"
|
||||||
/>
|
[ngModel]="status()?.enabled"
|
||||||
</h3>
|
(ngModelChange)="onToggle($event)"
|
||||||
|
/>
|
||||||
@if (status()?.enabled) {
|
</header>
|
||||||
@if (wifi(); as data) {
|
@if (status()?.enabled) {
|
||||||
@if (data.known.length) {
|
@if (wifi(); as data) {
|
||||||
<h3 class="g-title">Known Networks</h3>
|
@if (data.known.length) {
|
||||||
<div tuiCardLarge tuiAppearance="neutral" [wifi]="data.known"></div>
|
<p class="g-secondary">KNOWN NETWORKS</p>
|
||||||
|
<div
|
||||||
|
tuiCardLarge="compact"
|
||||||
|
tuiAppearance="neutral"
|
||||||
|
[wifi]="data.known"
|
||||||
|
[style.padding-block.rem]="0.5"
|
||||||
|
></div>
|
||||||
|
}
|
||||||
|
@if (data.available.length) {
|
||||||
|
<p class="g-secondary">OTHER NETWORKS</p>
|
||||||
|
<div
|
||||||
|
tuiCardLarge="compact"
|
||||||
|
tuiAppearance="neutral"
|
||||||
|
[wifi]="data.available"
|
||||||
|
[style.padding-block.rem]="0.5"
|
||||||
|
></div>
|
||||||
|
}
|
||||||
|
<p>
|
||||||
|
<button tuiButton size="s" (click)="other(data)">Add</button>
|
||||||
|
</p>
|
||||||
|
} @else {
|
||||||
|
<tui-loader [style.height.rem]="5" />
|
||||||
}
|
}
|
||||||
@if (data.available.length) {
|
|
||||||
<h3 class="g-title">Other Networks</h3>
|
|
||||||
<div
|
|
||||||
tuiCardLarge
|
|
||||||
tuiAppearance="neutral"
|
|
||||||
[wifi]="data.available"
|
|
||||||
></div>
|
|
||||||
}
|
|
||||||
<p>
|
|
||||||
<button
|
|
||||||
tuiButton
|
|
||||||
size="s"
|
|
||||||
appearance="opposite"
|
|
||||||
(click)="other(data)"
|
|
||||||
>
|
|
||||||
Other...
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
} @else {
|
} @else {
|
||||||
<tui-loader />
|
<app-placeholder icon="@tui.wifi">WiFi is disabled</app-placeholder>
|
||||||
}
|
}
|
||||||
}
|
</section>
|
||||||
} @else {
|
} @else {
|
||||||
<p>No wireless interface detected.</p>
|
<app-placeholder icon="@tui.wifi">
|
||||||
|
No wireless interface detected
|
||||||
|
</app-placeholder>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -95,9 +101,10 @@ import { wifiSpec } from './wifi.const'
|
|||||||
WifiTableComponent,
|
WifiTableComponent,
|
||||||
TitleDirective,
|
TitleDirective,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
|
PlaceholderComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsWifiComponent {
|
export default class SystemWifiComponent {
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
@@ -1,18 +1,12 @@
|
|||||||
import { TuiIcon } from '@taiga-ui/core'
|
|
||||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
import { TitleDirective } from 'src/app/services/title.service'
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { SettingsMenuComponent } from './components/menu.component'
|
import { SystemMenuComponent } from './components/menu.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<ng-container *title><span>Settings</span></ng-container>
|
<ng-container *title><span>System System</span></ng-container>
|
||||||
<a
|
<system-menu />
|
||||||
routerLink="/portal/settings"
|
|
||||||
routerLinkActive="_current"
|
|
||||||
[routerLinkActiveOptions]="{ exact: true }"
|
|
||||||
></a>
|
|
||||||
<settings-menu />
|
|
||||||
<router-outlet />
|
<router-outlet />
|
||||||
`,
|
`,
|
||||||
styles: [
|
styles: [
|
||||||
@@ -26,22 +20,24 @@ import { SettingsMenuComponent } from './components/menu.component'
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a,
|
|
||||||
span:not(:last-child),
|
span:not(:last-child),
|
||||||
settings-menu {
|
system-menu:not(:nth-last-child(2)) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
._current + settings-menu {
|
system-menu,
|
||||||
|
router-outlet + ::ng-deep * {
|
||||||
display: flex;
|
display: flex;
|
||||||
max-width: 30rem;
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
max-width: 45rem;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
host: { class: 'g-page' },
|
host: { class: 'g-page' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterModule, TuiIcon, SettingsMenuComponent, TitleDirective],
|
imports: [RouterModule, SystemMenuComponent, TitleDirective],
|
||||||
})
|
})
|
||||||
export class SettingsComponent {}
|
export class SystemComponent {}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { SystemComponent } from './system.component'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: SystemComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'acme',
|
||||||
|
loadComponent: () => import('./routes/acme/acme.component'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'email',
|
||||||
|
loadComponent: () => import('./routes/email/email.component'),
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// path: 'domains',
|
||||||
|
// loadComponent: () => import('./routes/domains/domains.component')
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// path: 'proxies',
|
||||||
|
// loadComponent: () => import('./routes/proxies/proxies.component')
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// path: 'router',
|
||||||
|
// loadComponent: () => import('./routes/router/router.component')
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
path: 'wifi',
|
||||||
|
loadComponent: () => import('./routes/wifi/wifi.component'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'ui',
|
||||||
|
loadComponent: () => import('./routes/interfaces/ui.component'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'ssh',
|
||||||
|
loadComponent: () => import('./routes/ssh/ssh.component'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sessions',
|
||||||
|
loadComponent: () => import('./routes/sessions/sessions.component'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -5,33 +5,32 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
|
import * as argon2 from '@start9labs/argon2'
|
||||||
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
TuiAlertService,
|
TuiAlertService,
|
||||||
TuiDialogOptions,
|
TuiDialogOptions,
|
||||||
TuiDialogService,
|
TuiDialogService,
|
||||||
TuiLabel,
|
TuiLabel,
|
||||||
} from '@taiga-ui/core'
|
} from '@taiga-ui/core'
|
||||||
import * as argon2 from '@start9labs/argon2'
|
import { TUI_CONFIRM, TuiCheckbox, TuiConfirmData } from '@taiga-ui/kit'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
|
||||||
import { TuiConfirmData, TUI_CONFIRM, TuiCheckbox } from '@taiga-ui/kit'
|
|
||||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { filter, firstValueFrom, from, take } from 'rxjs'
|
import { filter, from, take } from 'rxjs'
|
||||||
import { switchMap } from 'rxjs/operators'
|
import { switchMap } from 'rxjs/operators'
|
||||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||||
import { PROMPT } from 'src/app/routes/portal/modals/prompt.component'
|
import { PROMPT } from 'src/app/routes/portal/modals/prompt.component'
|
||||||
import { AuthService } from 'src/app/services/auth.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { getServerInfo } from 'src/app/utils/get-server-info'
|
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
|
import { getServerInfo } from 'src/app/utils/get-server-info'
|
||||||
|
|
||||||
import { passwordSpec, PasswordSpec, SettingBtn } from './settings.types'
|
import { passwordSpec, PasswordSpec, SettingBtn } from './system.types'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class SettingsService {
|
export class SystemService {
|
||||||
private readonly alerts = inject(TuiAlertService)
|
private readonly alerts = inject(TuiAlertService)
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
@@ -39,7 +38,6 @@ export class SettingsService {
|
|||||||
private readonly formDialog = inject(FormDialogService)
|
private readonly formDialog = inject(FormDialogService)
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly auth = inject(AuthService)
|
|
||||||
private readonly isTor = inject(ConfigService).isTor()
|
private readonly isTor = inject(ConfigService).isTor()
|
||||||
|
|
||||||
wipe = false
|
wipe = false
|
||||||
@@ -48,17 +46,10 @@ export class SettingsService {
|
|||||||
General: [
|
General: [
|
||||||
{
|
{
|
||||||
title: 'Email',
|
title: 'Email',
|
||||||
description:
|
description: 'Connect to an external SMTP server for sending emails',
|
||||||
'Connect to an external SMTP server to send yourself emails',
|
|
||||||
icon: '@tui.mail',
|
icon: '@tui.mail',
|
||||||
routerLink: 'email',
|
routerLink: 'email',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Change Master Password',
|
|
||||||
description: `Change your StartOS master password`,
|
|
||||||
icon: '@tui.key',
|
|
||||||
action: () => this.promptNewPassword(),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
Network: [
|
Network: [
|
||||||
// {
|
// {
|
||||||
@@ -79,6 +70,19 @@ export class SettingsService {
|
|||||||
// icon: '@tui.radio',
|
// icon: '@tui.radio',
|
||||||
// routerLink: 'router',
|
// routerLink: 'router',
|
||||||
// },
|
// },
|
||||||
|
{
|
||||||
|
title: 'User Interface Addresses',
|
||||||
|
description: 'View and manage your Start OS UI addresses',
|
||||||
|
icon: '@tui.monitor',
|
||||||
|
routerLink: 'ui',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ACME',
|
||||||
|
description:
|
||||||
|
'Add ACME providers to create SSL certificates for clearnet access',
|
||||||
|
icon: '@tui.award',
|
||||||
|
routerLink: 'acme',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'WiFi',
|
title: 'WiFi',
|
||||||
description: 'Add or remove WiFi networks',
|
description: 'Add or remove WiFi networks',
|
||||||
@@ -92,27 +96,27 @@ export class SettingsService {
|
|||||||
action: () => this.promptResetTor(),
|
action: () => this.promptResetTor(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'StartOS UI': [
|
Customize: [
|
||||||
{
|
{
|
||||||
title: 'Browser Tab Title',
|
title: 'Browser Tab Title',
|
||||||
description: `Customize the display name of your browser tab`,
|
description: `Customize the display name of your browser tab`,
|
||||||
icon: '@tui.tag',
|
icon: '@tui.tag',
|
||||||
action: () => this.setBrowserTab(),
|
action: () => this.setBrowserTab(),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Web Addresses',
|
|
||||||
description: 'View and manage web addresses for accessing this UI',
|
|
||||||
icon: '@tui.monitor',
|
|
||||||
routerLink: 'ui',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
'Privacy and Security': [
|
Security: [
|
||||||
// {
|
// {
|
||||||
// title: 'Outbound Proxy',
|
// title: 'Outbound Proxy',
|
||||||
// description: 'Proxy outbound traffic from the StartOS main process',
|
// description: 'Proxy outbound traffic from the StartOS main process',
|
||||||
// icon: '@tui.shield',
|
// icon: '@tui.shield',
|
||||||
// action: () => this.setOutboundProxy(),
|
// action: () => this.setOutboundProxy(),
|
||||||
// },
|
// },
|
||||||
|
{
|
||||||
|
title: 'Active Sessions',
|
||||||
|
description: 'View and manage device access',
|
||||||
|
icon: '@tui.clock',
|
||||||
|
routerLink: 'sessions',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'SSH',
|
title: 'SSH',
|
||||||
description:
|
description:
|
||||||
@@ -121,33 +125,10 @@ export class SettingsService {
|
|||||||
routerLink: 'ssh',
|
routerLink: 'ssh',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Active Sessions',
|
title: 'Change Password',
|
||||||
description: 'View and manage device access',
|
description: `Change your StartOS master password`,
|
||||||
icon: '@tui.clock',
|
icon: '@tui.key',
|
||||||
routerLink: 'sessions',
|
action: () => this.promptNewPassword(),
|
||||||
},
|
|
||||||
],
|
|
||||||
Power: [
|
|
||||||
{
|
|
||||||
title: 'Restart',
|
|
||||||
icon: '@tui.refresh-cw',
|
|
||||||
description: 'Restart Start OS server',
|
|
||||||
action: () => this.promptPower('Restart'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Shutdown',
|
|
||||||
icon: '@tui.power',
|
|
||||||
description: 'Turn Start OS server off',
|
|
||||||
action: () => this.promptPower('Shutdown'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Logout',
|
|
||||||
icon: '@tui.log-out',
|
|
||||||
description: 'Log off from Start OS',
|
|
||||||
action: () => {
|
|
||||||
this.api.logout({}).catch(e => console.error('Failed to log out', e))
|
|
||||||
this.auth.setUnverified()
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -174,25 +155,6 @@ export class SettingsService {
|
|||||||
.subscribe(() => this.resetTor(this.wipe))
|
.subscribe(() => this.resetTor(this.wipe))
|
||||||
}
|
}
|
||||||
|
|
||||||
private async promptPower(action: 'Restart' | 'Shutdown') {
|
|
||||||
this.dialogs
|
|
||||||
.open(TUI_CONFIRM, getOptions(action))
|
|
||||||
.pipe(filter(Boolean))
|
|
||||||
.subscribe(async () => {
|
|
||||||
const loader = this.loader.open(`Beginning ${action}...`).subscribe()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.api[
|
|
||||||
action === 'Restart' ? 'restartServer' : 'shutdownServer'
|
|
||||||
]({})
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private async resetTor(wipeState: boolean) {
|
private async resetTor(wipeState: boolean) {
|
||||||
const loader = this.loader.open('Resetting Tor...').subscribe()
|
const loader = this.loader.open('Resetting Tor...').subscribe()
|
||||||
|
|
||||||
@@ -340,31 +302,5 @@ export class SettingsService {
|
|||||||
})
|
})
|
||||||
class WipeComponent {
|
class WipeComponent {
|
||||||
readonly isTor = inject(ConfigService).isTor()
|
readonly isTor = inject(ConfigService).isTor()
|
||||||
readonly service = inject(SettingsService)
|
readonly service = inject(SystemService)
|
||||||
}
|
|
||||||
|
|
||||||
function getOptions(
|
|
||||||
operation: 'Restart' | 'Shutdown',
|
|
||||||
): Partial<TuiDialogOptions<TuiConfirmData>> {
|
|
||||||
return operation === 'Restart'
|
|
||||||
? {
|
|
||||||
label: 'Restart',
|
|
||||||
size: 's',
|
|
||||||
data: {
|
|
||||||
content:
|
|
||||||
'Are you sure you want to restart your server? It can take several minutes to come back online.',
|
|
||||||
yes: 'Restart',
|
|
||||||
no: 'Cancel',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
label: 'Warning',
|
|
||||||
size: 's',
|
|
||||||
data: {
|
|
||||||
content:
|
|
||||||
'Are you sure you want to power down your server? This can take several minutes, and your server will not come back online automatically. To power on again, You will need to physically unplug your server and plug it back in',
|
|
||||||
yes: 'Shutdown',
|
|
||||||
no: 'Cancel',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ export class BadgeService {
|
|||||||
private readonly notifications = inject(NotificationService)
|
private readonly notifications = inject(NotificationService)
|
||||||
private readonly exver = inject(Exver)
|
private readonly exver = inject(Exver)
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
private readonly settings$ = combineLatest([
|
private readonly system$ = combineLatest([
|
||||||
this.patch.watch$('serverInfo', 'ntpSynced'),
|
this.patch.watch$('serverInfo', 'ntpSynced'),
|
||||||
inject(EOSService).updateAvailable$,
|
inject(EOSService).updateAvailable$,
|
||||||
]).pipe(map(([synced, update]) => Number(!synced) + Number(update)))
|
]).pipe(map(([synced, update]) => Number(!synced) + Number(update)))
|
||||||
@@ -83,8 +83,8 @@ export class BadgeService {
|
|||||||
switch (id) {
|
switch (id) {
|
||||||
// case '/portal/updates':
|
// case '/portal/updates':
|
||||||
// return this.updates$
|
// return this.updates$
|
||||||
case '/portal/settings':
|
case '/portal/system':
|
||||||
return this.settings$
|
return this.system$
|
||||||
case '/portal/notifications':
|
case '/portal/notifications':
|
||||||
return this.notifications.unreadCount$
|
return this.notifications.unreadCount$
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -29,11 +29,7 @@ export class EOSService {
|
|||||||
readonly updatingOrBackingUp$ = combineLatest([
|
readonly updatingOrBackingUp$ = combineLatest([
|
||||||
this.updating$,
|
this.updating$,
|
||||||
this.backingUp$,
|
this.backingUp$,
|
||||||
]).pipe(
|
]).pipe(map(([updating, backingUp]) => updating || backingUp))
|
||||||
map(([updating, backingUp]) => {
|
|
||||||
return updating || backingUp
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
readonly showUpdate$ = combineLatest([
|
readonly showUpdate$ = combineLatest([
|
||||||
this.updateAvailable$,
|
this.updateAvailable$,
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
|
|||||||
icon: '@tui.file-text',
|
icon: '@tui.file-text',
|
||||||
title: 'Logs',
|
title: 'Logs',
|
||||||
},
|
},
|
||||||
'/portal/settings': {
|
'/portal/system': {
|
||||||
icon: '@tui.wrench',
|
icon: '@tui.settings',
|
||||||
title: 'Settings',
|
title: 'System',
|
||||||
},
|
},
|
||||||
'/portal/notifications': {
|
'/portal/notifications': {
|
||||||
icon: '@tui.bell',
|
icon: '@tui.bell',
|
||||||
|
|||||||
@@ -76,16 +76,16 @@ hr {
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 3.25rem 1rem 0.5rem;
|
padding: 3.125rem 1rem 0.5rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: color-mix(in hsl, var(--start9-base-1) 50%, transparent);
|
background-color: color-mix(in hsl, var(--start9-base-1) 50%, transparent);
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
rgba(255, 255, 255, 0.15),
|
var(--tui-background-neutral-2),
|
||||||
transparent
|
transparent
|
||||||
),
|
),
|
||||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.15), transparent);
|
linear-gradient(to bottom, var(--tui-background-neutral-2), transparent);
|
||||||
background-size: 1px 100%;
|
background-size: 1px 100%;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position:
|
background-position:
|
||||||
@@ -101,8 +101,13 @@ hr {
|
|||||||
inset 0 1px rgba(255, 255, 255, 0.15),
|
inset 0 1px rgba(255, 255, 255, 0.15),
|
||||||
inset 0 0 1rem rgba(0, 0, 0, 0.25);
|
inset 0 0 1rem rgba(0, 0, 0, 0.25);
|
||||||
|
|
||||||
> [tuiCell] {
|
&:is(form) {
|
||||||
margin: 0 -0.5rem;
|
padding-top: 3.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[tuiCell] {
|
||||||
|
margin: 0 -0.625rem;
|
||||||
|
border-radius: var(--tui-radius-s);
|
||||||
|
|
||||||
&:not(:last-child)::after {
|
&:not(:last-child)::after {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -111,7 +116,7 @@ hr {
|
|||||||
left: 1rem;
|
left: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: var(--tui-border-normal);
|
background: var(--tui-background-neutral-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,11 +134,20 @@ hr {
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
background: var(--tui-background-neutral-1);
|
background: var(--tui-background-neutral-1);
|
||||||
font: var(--tui-font-text-l);
|
font: var(--tui-font-text-l);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 1rem 0 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.g-table:not([tuiTable]) {
|
.g-table:not([tuiTable]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user