Feat/domains

update FE types and unify sideload page with marketplace show

begin popover for UI launch select

update node version for github workflows

fix type errors

eager load more components

fix mocks for types

recalculate updates bad on pkg uninstall

chore: break form-object file structure

files for config

finish file upload API and implement for config

chore: break down form-object by type, part 1

remove NEW from config

comment entire setTimeout for new

generic form options

chore: break down form-object by type, part 2

headers for enums and unions

implement select and multiselect for config

update union types and camel case for specs

implement textarea config value

inputspec and required instead of nullable

remove subtype from list spec

update start-sdk

bump start-sdk

feat: use Taiga UI for config modal (#2250)

* feat: use Taiga UI for config modal

* chore: finish remaining changes

* chore: address comments

* bump sdk version

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

update package lock

update to sdk 20 and fix types

chore: update Taiga UI and migrate some more forms (#2252)

update form to latest sdk

validate length for textarea too

chore: accommodate new changes to the specs (#2254)

* chore: accommodate new changes to the specs

* chore: fix error

* chore: fix error

feat: add input color (#2257)

* feat: add input color

* patterns will always be there

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

chore: properly type pattern error

update to latest sdk

Add sans-serif font fallback (#2263)

* Add sans-serif font fallback

* Update frontend readme start scripts

feat: add datetime spec support (#2264)

Wifi optional (#2249)

* begin work

* allow enable and disable wifi

* nice styling

* done except for popover not dismissing

* update wifi.ts

* address comments

Feat/automated backups (#2142)

* initial restructuring

* very cool

* new structure in place

* delete unnecessary T

* down the rabbit hole

* getting better

* dont like it

* nice

* very nice

* sessions select all

* nice

* backup runs

* fix targets and more

* small improvements

* mostly working

* address PR comments

* fix error

* delete issue with merge

* fix checkboxes and add API for deleting backup runs

* better styling for checkboxes

* small button in ssh kpage too

* complete multiple UI launcher

* fix actions

* present error toast too

* fix target forms

Add logs window to setup wizard loading screen (#2076)

* add logs window to setup wizard loading screen

* fix type error

* Update frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts

Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>

---------

Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>

statically type server metrics and use websocket (#2124)

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

Feat/external-smtp (#1791)

* UI for EOS smtp, missing API layer

* implement api

* fix errors

* switch to external smtp creds

* fix things up

* fix types

* update types for new forms

* feat: add new form to emails and marketplace (#2268)

* import tuilet module

* feat: get rid of old form completely (#2270)

* move to builder spec and delete developer menu

* update sdk

* tiny

* getting better

* working

* done

* feat: add step to number config

* chore: small fixes

* update SDK and step for numbers

---------

Co-authored-by: Alex Inkin <alexander@inkin.ru>

latest sdk, fix build

update SDK for better disabled props

feat: implement `disabled`, `immutable` and `generate` (#2280)

* feat: implement `disabled`, `immutable` and `generate`

* chore: remove unnecessary code

* chore: add generate to textarea and implement immutable

* no generate for textarea

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

update lockfile

refactor: extract loading status to shared library (#2282)

* refactor: extract loading status to shared library

* chore: remove inline style

refactor: break routing down to apps level (#2285)

closes #2212 and closes #2214

Feat/credentials (#2290)

add credentials and remove properties

refactor: break ui up further down (#2292)

* refactor: break ui up further down

* permit loading even when authed

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

update patchdb for package compatability fixes

fix file structure

WIP

finish rebase

mvp complete

port forwards mvp

looking good

cleaner system page

move experimental features

manual port overrides

better info headers for jobs pages

refactor: move diagnostic-ui app under ui route (#2306)

* refactor: move diagnostic-ui app under ui route

* chore: hide navigation

* chore: remove ionic from diagnostic

* fix navbar showing on login

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

chore: partially remove ionic modals and loaders (#2308)

* chore: partially remove ionic modals and loaders

* change to snake

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

better session data fetching

abstract store icon component to shared marketplace project (#2311)

* abstract store icon component to shared marketplace project

* better than using a pipe

* minor cleanup

* chore: fix missing node types in libraries

* typo

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
Co-authored-by: waterplea <alexander@inkin.ru>

refactor: continue to get rid of ionic infrastructure (#2325)

refactor: finish removing ionic entities: (#2333)

* refactor: finish removing ionic entities:

ToastController
ErrorToastService
ModalController
AlertController
LoadingController

* chore: rollback testing code

* chore: fix comments

* minor form change

* chore: fix comments

* update clearnet address parts

* move around patchDB

* chore: fix comments

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

fixup after rebase
This commit is contained in:
Matt Hill
2023-03-07 14:37:14 -07:00
committed by Aiden McClelland
parent c03778ec8b
commit 38c2c47789
268 changed files with 4746 additions and 4784 deletions

View File

@@ -10,6 +10,8 @@
"@ng-web-apis/resize-observer": ">=2.0.0",
"@start9labs/emver": "^0.1.5",
"@taiga-ui/cdk": ">=3.0.0",
"@taiga-ui/core": ">=3.0.0",
"@tinkoff/ng-dompurify": ">=4.0.0",
"ansi-to-html": "^0.7.2"
},
"exports": {

View File

@@ -1,29 +0,0 @@
import { Directive, ElementRef, Input } from '@angular/core'
import { AlertButton } from '@ionic/angular'
@Directive({
selector: `button[alertButton], a[alertButton]`,
})
export class AlertButtonDirective implements AlertButton {
@Input()
icon?: string
@Input()
role?: 'cancel' | 'destructive' | string
handler = () => {
this.elementRef.nativeElement.click()
return false
}
constructor(private readonly elementRef: ElementRef<HTMLElement>) {}
get text(): string {
return this.elementRef.nativeElement.textContent?.trim() || ''
}
get cssClass(): string[] {
return Array.from(this.elementRef.nativeElement.classList)
}
}

View File

@@ -1,27 +0,0 @@
import { Directive, ElementRef, Input } from '@angular/core'
import { AlertInput } from '@ionic/angular'
@Directive({
selector: `input[alertInput], textarea[alertInput]`,
})
export class AlertInputDirective<T> implements AlertInput {
@Input()
value?: T
@Input()
label?: string
constructor(private readonly elementRef: ElementRef<HTMLInputElement>) {}
get checked(): boolean {
return this.elementRef.nativeElement.checked
}
get name(): string {
return this.elementRef.nativeElement.name
}
get type(): AlertInput['type'] {
return this.elementRef.nativeElement.type as AlertInput['type']
}
}

View File

@@ -1,99 +0,0 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
ContentChildren,
ElementRef,
EventEmitter,
Input,
OnDestroy,
Output,
QueryList,
ViewChild,
} from '@angular/core'
import { AlertController, AlertOptions, IonicSafeString } from '@ionic/angular'
import { OverlayEventDetail } from '@ionic/core'
import { AlertButtonDirective } from './alert-button.directive'
import { AlertInputDirective } from './alert-input.directive'
@Component({
selector: 'alert',
template: `
<div #message><ng-content></ng-content></div>
<ng-content select="[alertInput]"></ng-content>
<ng-content select="[alertButton]"></ng-content>
`,
styles: [':host { display: none !important; }'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertComponent<T> implements AfterViewInit, OnDestroy {
@Output()
readonly dismiss = new EventEmitter<OverlayEventDetail<T>>()
@Input()
header = ''
@Input()
subHeader = ''
@Input()
backdropDismiss = true
@ViewChild('message', { static: true })
private readonly content?: ElementRef<HTMLElement>
@ContentChildren(AlertButtonDirective)
private readonly buttons: QueryList<AlertButtonDirective> = new QueryList()
@ContentChildren(AlertInputDirective)
private readonly inputs: QueryList<AlertInputDirective<any>> = new QueryList()
private alert?: HTMLIonAlertElement
constructor(
private readonly elementRef: ElementRef<HTMLElement>,
private readonly controller: AlertController,
) {}
get cssClass(): string[] {
return Array.from(this.elementRef.nativeElement.classList)
}
get message(): IonicSafeString {
return new IonicSafeString(this.content?.nativeElement.innerHTML || '')
}
async ngAfterViewInit() {
this.alert = await this.controller.create(this.getOptions())
this.alert.onDidDismiss().then(event => {
this.dismiss.emit(event)
})
await this.alert.present()
}
async ngOnDestroy() {
await this.alert?.dismiss()
}
private getOptions(): AlertOptions {
const {
header,
subHeader,
message,
cssClass,
buttons,
inputs,
backdropDismiss,
} = this
return {
header,
subHeader,
message,
cssClass,
backdropDismiss,
buttons: buttons.toArray(),
inputs: inputs.toArray(),
}
}
}

View File

@@ -1,10 +0,0 @@
import { NgModule } from '@angular/core'
import { AlertComponent } from './alert.component'
import { AlertButtonDirective } from './alert-button.directive'
import { AlertInputDirective } from './alert-input.directive'
@NgModule({
declarations: [AlertComponent, AlertButtonDirective, AlertInputDirective],
exports: [AlertComponent, AlertButtonDirective, AlertInputDirective],
})
export class AlertModule {}

View File

@@ -0,0 +1,18 @@
ion-card-title {
font-size: 42px;
}
.progress {
max-width: 700px;
padding-bottom: 20px;
margin: auto auto 40px;
}
.logs-container {
margin-top: 24px;
height: 280px;
text-align: left;
overflow: hidden;
border-radius: 31px;
margin-inline: 10px;
}

View File

@@ -0,0 +1,35 @@
import { Component, inject, Input, Output } from '@angular/core'
import { delay, filter } from 'rxjs'
import { SetupService } from '../../services/setup.service'
@Component({
selector: 'app-initializing',
templateUrl: 'initializing.component.html',
styleUrls: ['initializing.component.scss'],
})
export class InitializingComponent {
readonly progress$ = inject(SetupService)
@Input()
setupType?: 'fresh' | 'restore' | 'attach' | 'transfer'
@Output()
readonly finished = this.progress$.pipe(
filter(progress => progress === 1),
delay(500),
)
getMessage(progress: number | null): string {
if (['fresh', 'attach'].includes(this.setupType || '')) {
return 'Setting up your server'
}
if (!progress) {
return 'Preparing data. This can take a while'
} else if (progress < 1) {
return 'Copying data'
} else {
return 'Finalizing'
}
}
}

View File

@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { TuiLetModule } from '@taiga-ui/cdk'
import { LogsWindowComponent } from './logs-window/logs-window.component'
import { InitializingComponent } from './initializing.component'
@NgModule({
imports: [CommonModule, IonicModule, TuiLetModule],
declarations: [InitializingComponent, LogsWindowComponent],
exports: [InitializingComponent],
})
export class InitializingModule {}

View File

@@ -6,8 +6,8 @@ import { SetupLogsService } from '../../../services/setup-logs.service'
import { Log } from '../../../types/api'
import { toLocalIsoString } from '../../../util/to-local-iso-string'
var Convert = require('ansi-to-html')
var convert = new Convert({
const Convert = require('ansi-to-html')
const convert = new Convert({
bg: 'transparent',
})

View File

@@ -1,18 +1,20 @@
ion-card-title {
font-size: 42px;
@import '@taiga-ui/core/styles/taiga-ui-local';
:host {
@include shadow(3);
display: flex;
align-items: center;
max-width: 80%;
margin: auto;
padding: 1.5rem;
background: var(--tui-elevation-01);
border-radius: var(--tui-radius-m);
--tui-primary: var(--tui-warning-fill);
}
.progress {
max-width: 700px;
padding-bottom: 20px;
margin: auto auto 40px;
}
.logs-container {
margin-top: 24px;
height: 280px;
text-align: left;
overflow: hidden;
border-radius: 31px;
margin-inline: 10px;
tui-loader {
flex-shrink: 0;
min-width: 2rem;
}

View File

@@ -1,35 +1,17 @@
import { Component, inject, Input, Output } from '@angular/core'
import { delay, filter } from 'rxjs'
import { SetupService } from '../../services/setup.service'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import {
POLYMORPHEUS_CONTEXT,
PolymorpheusContent,
} from '@tinkoff/ng-polymorpheus'
@Component({
selector: 'app-loading',
templateUrl: 'loading.component.html',
styleUrls: ['loading.component.scss'],
template: `
<tui-loader [textContent]="content"></tui-loader>
`,
styleUrls: ['./loading.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LoadingComponent {
readonly progress$ = inject(SetupService)
@Input()
setupType?: 'fresh' | 'restore' | 'attach' | 'transfer'
@Output()
readonly finished = this.progress$.pipe(
filter(progress => progress === 1),
delay(500),
)
getMessage(progress: number | null): string {
if (['fresh', 'attach'].includes(this.setupType || '')) {
return 'Setting up your server'
}
if (!progress) {
return 'Preparing data. This can take a while'
} else if (progress < 1) {
return 'Copying data'
} else {
return 'Finalizing'
}
}
readonly content: PolymorpheusContent =
inject(POLYMORPHEUS_CONTEXT)['content']
}

View File

@@ -1,14 +1,13 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { TuiLetModule } from '@taiga-ui/cdk'
import { LogsWindowComponent } from './logs-window/logs-window.component'
import { TuiLoaderModule } from '@taiga-ui/core'
import { tuiAsDialog } from '@taiga-ui/cdk'
import { LoadingComponent } from './loading.component'
import { LoadingService } from './loading.service'
@NgModule({
imports: [CommonModule, IonicModule, TuiLetModule],
declarations: [LoadingComponent, LogsWindowComponent],
imports: [TuiLoaderModule],
declarations: [LoadingComponent],
exports: [LoadingComponent],
providers: [tuiAsDialog(LoadingService)],
})
export class LoadingModule {}

View File

@@ -0,0 +1,10 @@
import { Injectable } from '@angular/core'
import { AbstractTuiDialogService } from '@taiga-ui/cdk'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
import { LoadingComponent } from './loading.component'
@Injectable({ providedIn: `root` })
export class LoadingService extends AbstractTuiDialogService<unknown> {
protected readonly component = new PolymorpheusComponent(LoadingComponent)
protected readonly defaultOptions = {}
}

View File

@@ -1,29 +1,16 @@
<ion-header>
<ion-toolbar>
<ion-title>{{ title | titlecase }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismiss()">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-item *ngIf="error$ | async as error">
<ion-label>
<ion-text safeLinks color="danger">{{ error }}</ion-text>
</ion-label>
</ion-item>
<ion-content>
<ion-item *ngIf="error$ | async as error">
<ion-label>
<ion-text safeLinks color="danger">{{ error }}</ion-text>
</ion-label>
</ion-item>
<div
*ngIf="content$ | async as result; else loading"
safeLinks
class="content-padding"
[innerHTML]="result | markdown | dompurify"
></div>
<div
*ngIf="content$ | async as result; else loading"
safeLinks
class="content-padding"
[innerHTML]="result | markdown"
></div>
<ng-template #loading>
<text-spinner [text]="'Loading ' + title | titlecase"></text-spinner>
</ng-template>
</ion-content>
<ng-template #loading>
<text-spinner [text]="'Loading ' + title | titlecase"></text-spinner>
</ng-template>

View File

@@ -1,6 +1,7 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { MarkdownPipeModule } from '../../pipes/markdown/markdown.module'
import { SafeLinksModule } from '../../directives/safe-links/safe-links.module'
@@ -15,6 +16,7 @@ import { MarkdownComponent } from './markdown.component'
MarkdownPipeModule,
TextSpinnerComponentModule,
SafeLinksModule,
NgDompurifyModule,
],
exports: [MarkdownComponent],
})

View File

@@ -1,5 +1,6 @@
import { Component, Input } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { Component, Inject } from '@angular/core'
import { TuiDialogContext } from '@taiga-ui/core'
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
import {
catchError,
ignoreElements,
@@ -10,7 +11,7 @@ import {
of,
} from 'rxjs'
import { getErrorMessage } from '../../services/error-toast.service'
import { getErrorMessage } from '../../services/error.service'
@Component({
selector: 'markdown',
@@ -18,11 +19,10 @@ import { getErrorMessage } from '../../services/error-toast.service'
styleUrls: ['./markdown.component.scss'],
})
export class MarkdownComponent {
@Input() content!: string | Observable<string>
@Input() title!: string
readonly content$ = defer(() =>
isObservable(this.content) ? this.content : of(this.content),
isObservable(this.context.data.content)
? this.context.data.content
: of(this.context.data.content),
).pipe(share())
readonly error$ = this.content$.pipe(
@@ -30,9 +30,15 @@ export class MarkdownComponent {
catchError(e => of(getErrorMessage(e))),
)
constructor(private readonly modalCtrl: ModalController) {}
constructor(
@Inject(POLYMORPHEUS_CONTEXT)
private readonly context: TuiDialogContext<
void,
{ content: string | Observable<string> }
>,
) {}
async dismiss() {
return this.modalCtrl.dismiss(true)
get title(): string {
return this.context.label || ''
}
}

View File

@@ -1,32 +0,0 @@
import { Directive, ElementRef, Input } from '@angular/core'
import { ToastButton } from '@ionic/angular'
@Directive({
selector: `button[toastButton], a[toastButton]`,
})
export class ToastButtonDirective implements ToastButton {
@Input()
icon?: string
@Input()
side?: 'start' | 'end'
@Input()
role?: 'cancel' | string
handler = () => {
this.elementRef.nativeElement.click()
return false
}
constructor(private readonly elementRef: ElementRef<HTMLElement>) {}
get text(): string | undefined {
return this.elementRef.nativeElement.textContent?.trim() || undefined
}
get cssClass(): string[] {
return Array.from(this.elementRef.nativeElement.classList)
}
}

View File

@@ -1,85 +0,0 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
ContentChildren,
ElementRef,
EventEmitter,
Input,
OnDestroy,
Output,
QueryList,
ViewChild,
} from '@angular/core'
import { IonicSafeString, ToastController, ToastOptions } from '@ionic/angular'
import { OverlayEventDetail } from '@ionic/core'
import { ToastButtonDirective } from './toast-button.directive'
@Component({
selector: 'toast',
template: `
<div #message><ng-content></ng-content></div>
<ng-content select="[toastButton]"></ng-content>
`,
styles: [':host { display: none !important; }'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ToastComponent<T> implements AfterViewInit, OnDestroy {
@Output()
readonly dismiss = new EventEmitter<OverlayEventDetail<T>>()
@Input()
header = ''
@Input()
duration = 0
@Input()
position: 'top' | 'bottom' | 'middle' = 'bottom'
@ViewChild('message', { static: true })
private readonly content?: ElementRef<HTMLElement>
@ContentChildren(ToastButtonDirective)
private readonly buttons: QueryList<ToastButtonDirective> = new QueryList()
private toast?: HTMLIonToastElement
constructor(
private readonly elementRef: ElementRef<HTMLElement>,
private readonly controller: ToastController,
) {}
get cssClass(): string[] {
return Array.from(this.elementRef.nativeElement.classList)
}
get message(): IonicSafeString {
return new IonicSafeString(this.content?.nativeElement.innerHTML || '')
}
async ngAfterViewInit() {
this.toast = await this.controller.create(this.getOptions())
this.toast.onDidDismiss().then(event => {
this.dismiss.emit(event)
})
await this.toast.present()
}
async ngOnDestroy() {
await this.toast?.dismiss()
}
private getOptions(): ToastOptions {
const { header, message, duration, position, cssClass, buttons } = this
return {
header,
message,
duration,
position,
cssClass,
buttons: buttons.toArray(),
}
}
}

View File

@@ -1,9 +0,0 @@
import { NgModule } from '@angular/core'
import { ToastComponent } from './toast.component'
import { ToastButtonDirective } from './toast-button.directive'
@NgModule({
declarations: [ToastComponent, ToastButtonDirective],
exports: [ToastComponent, ToastButtonDirective],
})
export class ToastModule {}

View File

@@ -0,0 +1,22 @@
import { Directive } from '@angular/core'
import {
AbstractTuiDialogDirective,
AbstractTuiDialogService,
} from '@taiga-ui/cdk'
import { TuiAlertOptions, TuiAlertService } from '@taiga-ui/core'
// TODO: Move to Taiga UI
@Directive({
selector: 'ng-template[tuiAlert]',
providers: [
{
provide: AbstractTuiDialogService,
useExisting: TuiAlertService,
},
],
inputs: ['options: tuiAlertOptions', 'open: tuiAlert'],
outputs: ['openChange: tuiAlertChange'],
})
export class TuiAlertDirective<T> extends AbstractTuiDialogDirective<
TuiAlertOptions<T>
> {}

View File

@@ -0,0 +1,8 @@
import { NgModule } from '@angular/core'
import { TuiAlertDirective } from './alert.directive'
@NgModule({
declarations: [TuiAlertDirective],
exports: [TuiAlertDirective],
})
export class TuiAlertModule {}

View File

@@ -1,28 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core'
import { marked } from 'marked'
import * as DOMPurify from 'dompurify'
@Pipe({
name: 'markdown',
})
export class MarkdownPipe implements PipeTransform {
transform(value: string): string {
if (value && value.length > 0) {
// convert markdown to html
const html = marked(value)
// sanitize html
const sanitized = DOMPurify.sanitize(html)
// parse html to find all links
let parser = new DOMParser()
const doc = parser.parseFromString(sanitized, 'text/html')
const links = Array.from(doc.getElementsByTagName('a'))
// add target="_blank" to every link
links.forEach(link => {
link.setAttribute('target', '_blank')
})
// return new html string
return doc.documentElement.innerHTML
}
return value
return value?.length ? marked(value) : ''
}
}

View File

@@ -5,23 +5,21 @@
export * from './classes/http-error'
export * from './classes/rpc-error'
export * from './components/alert/alert.component'
export * from './components/alert/alert.module'
export * from './components/alert/alert-button.directive'
export * from './components/alert/alert-input.directive'
export * from './components/loading/logs-window/logs-window.component'
export * from './components/loading/loading.module'
export * from './components/initializing/logs-window/logs-window.component'
export * from './components/initializing/initializing.module'
export * from './components/initializing/initializing.component'
export * from './components/loading/loading.component'
export * from './components/loading/loading.module'
export * from './components/loading/loading.service'
export * from './components/markdown/markdown.component'
export * from './components/markdown/markdown.component.module'
export * from './components/text-spinner/text-spinner.component'
export * from './components/text-spinner/text-spinner.component.module'
export * from './components/ticker/ticker.component'
export * from './components/ticker/ticker.module'
export * from './components/toast/toast.component'
export * from './components/toast/toast.module'
export * from './components/toast/toast-button.directive'
export * from './directives/alert/alert.directive'
export * from './directives/alert/alert.module'
export * from './directives/responsive-col/responsive-col.directive'
export * from './directives/responsive-col/responsive-col.module'
export * from './directives/responsive-col/responsive-col-viewport.directive'
@@ -43,10 +41,10 @@ export * from './pipes/shared/trust.pipe'
export * from './pipes/unit-conversion/unit-conversion.module'
export * from './pipes/unit-conversion/unit-conversion.pipe'
export * from './services/copy.service'
export * from './services/download-html.service'
export * from './services/emver.service'
export * from './services/error.service'
export * from './services/error-toast.service'
export * from './services/http.service'
export * from './services/setup.service'
export * from './services/setup-logs.service'

View File

@@ -0,0 +1,16 @@
import { inject, Injectable } from '@angular/core'
import { TuiAlertService } from '@taiga-ui/core'
import { copyToClipboard } from '../util/copy-to-clipboard'
@Injectable({ providedIn: 'root' })
export class CopyService {
private readonly alerts = inject(TuiAlertService)
async copy(text: string) {
const success = await copyToClipboard(text)
this.alerts
.open(success ? 'Copied to clipboard!' : 'Failed to copy to clipboard.')
.subscribe()
}
}

View File

@@ -1,70 +0,0 @@
import { Injectable } from '@angular/core'
import { IonicSafeString, ToastController } from '@ionic/angular'
import { HttpError } from '../classes/http-error'
@Injectable({
providedIn: 'root',
})
export class ErrorToastService {
private toast?: HTMLIonToastElement
constructor(private readonly toastCtrl: ToastController) {}
async present(e: HttpError | string, link?: string): Promise<void> {
console.error(e)
if (this.toast) return
this.toast = await this.toastCtrl.create({
header: 'Error',
message: getErrorMessage(e, link),
duration: 0,
position: 'top',
cssClass: 'error-toast',
buttons: [
{
side: 'end',
icon: 'close',
handler: () => {
this.dismiss()
},
},
],
})
await this.toast.present()
}
async dismiss(): Promise<void> {
if (this.toast) {
await this.toast.dismiss()
this.toast = undefined
}
}
}
export function getErrorMessage(
e: HttpError | string,
link?: string,
): string | IonicSafeString {
let message = ''
if (typeof e === 'string') {
message = e
} else if (e.code === 0) {
message =
'Request Error. Your browser blocked the request. This is usually caused by a corrupt browser cache or an overly aggressive ad blocker. Please clear your browser cache and/or adjust your ad blocker and try again'
} else if (!e.message) {
message = 'Unknown Error'
link = 'https://docs.start9.com/latest/support/faq'
} else {
message = e.message
}
if (link) {
return new IonicSafeString(
`${message}<br /><br /><a href=${link} target="_blank" rel="noreferrer" style="color: white;">Get Help</a>`,
)
}
return message
}

View File

@@ -22,7 +22,7 @@ export class ErrorService extends ErrorHandler {
}
}
function getErrorMessage(e: HttpError | string, link?: string): string {
export function getErrorMessage(e: HttpError | string, link?: string): string {
let message = ''
if (typeof e === 'string') {

View File

@@ -1,4 +1,4 @@
import { inject, StaticClassProvider, Type } from '@angular/core'
import { inject, StaticClassProvider } from '@angular/core'
import {
catchError,
EMPTY,
@@ -12,8 +12,8 @@ import {
takeWhile,
} from 'rxjs'
import { SetupStatus } from '../types/api'
import { ErrorToastService } from './error-toast.service'
import { Constructor } from '../types/constructor'
import { ErrorService } from './error.service'
export function provideSetupService(
api: Constructor<ConstructorParameters<typeof SetupService>[0]>,
@@ -26,12 +26,12 @@ export function provideSetupService(
}
export class SetupService extends Observable<number> {
private readonly errorToastService = inject(ErrorToastService)
private readonly errorService = inject(ErrorService)
private readonly progress$ = interval(500).pipe(
exhaustMap(() =>
from(this.api.getSetupStatus()).pipe(
catchError(e => {
this.errorToastService.present(e)
this.errorService.handleError(e)
return EMPTY
}),

View File

@@ -4,19 +4,21 @@ export type WorkspaceConfig = {
gitHash: string
useMocks: boolean
enableWidgets: boolean
// each key corresponds to a project and values adjust settings for that project, eg: ui, install-wizard, setup-wizard, diagnostic-ui
// each key corresponds to a project and values adjust settings for that project, eg: ui, install-wizard, setup-wizard
ui: {
api: {
url: string
version: string
}
marketplace: {
start9: 'https://registry.start9.com/'
community: 'https://community-registry.start9.com/'
}
marketplace: MarketplaceConfig
mocks: {
maskAs: 'tor' | 'lan'
skipStartupAlerts: boolean
}
}
}
export interface MarketplaceConfig {
start9: 'https://registry.start9.com/'
community: 'https://community-registry.start9.com/'
}

View File

@@ -160,3 +160,10 @@ a {
color: aqua;
text-decoration: none;
}
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 16px;
margin-top: 24px;
}

View File

@@ -0,0 +1,49 @@
@import '@taiga-ui/core/styles/taiga-ui-local';
/* stylelint-disable order/order */
[tuiWrapper][data-appearance='secondary-warning'] {
background: var(--tui-warning-bg);
color: var(--tui-warning-fill);
&[data-mode='onDark'] {
background: var(--tui-warning-bg-night);
color: var(--tui-warning-fill-night);
@include wrapper-hover {
background: var(--tui-warning-bg-night-hover);
}
@include wrapper-active {
background: var(--tui-warning-bg-night-hover);
}
}
@include wrapper-hover {
background: var(--tui-warning-bg-hover);
}
@include wrapper-active {
background: var(--tui-warning-bg-hover);
}
}
tui-dialog {
transform: translate3d(0, 0, 0);
}
tui-opt-group[data-label^='⚠️']:before {
color: var(--tui-warning-fill);
}
tui-hint[data-appearance='onDark'] {
background: white !important;
color: #222 !important;
}
[tuiLink] {
color: var(--tui-link) !important;
&:hover {
color: var(--tui-link-hover) !important;
}
}