Misc frontend fixes (#2974)

* fix dependency input warning and extra comma

* clean up buttons during install in marketplace preview

* chore: grayscale and closing action-bar

* fix prerelease precedence

* fix duplicate url for addSsl on ssl proto

* no warning for soft uninstall

* fix: stop logs from repeating disconnected status and add 1 second delay between reconnection attempts

* fix stop on reactivation of critical task

* fix: fix disconnected toast

* fix: updates styles

* fix: updates styles

* misc fixes

* beta.33

* fix updates badge and initialization of marketplace preview controls

---------

Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2025-07-08 12:08:27 -06:00
committed by GitHub
parent 340775a593
commit 7ba66c419a
32 changed files with 203 additions and 158 deletions

View File

@@ -46,6 +46,7 @@ import { ClientStorageService } from './services/client-storage.service'
import { DateTransformerService } from './services/date-transformer.service'
import { DatetimeTransformerService } from './services/datetime-transformer.service'
import { StorageService } from './services/storage.service'
import { FilterUpdatesPipe } from './routes/portal/routes/updates/filter-updates.pipe'
const {
useMocks,
@@ -56,6 +57,7 @@ export const APP_PROVIDERS = [
provideEventPlugins(),
I18N_PROVIDERS,
FilterPackagesPipe,
FilterUpdatesPipe,
UntypedFormBuilder,
tuiNumberFormatProvider({ decimalSeparator: '.', thousandSeparator: '' }),
tuiButtonOptionsProvider({ size: 'm' }),

View File

@@ -103,7 +103,6 @@ import { HeaderStatusComponent } from './status.component'
&:has([data-status='neutral']) {
--status: var(--tui-status-neutral);
filter: none;
}
&:has([data-status='success']) {

View File

@@ -18,18 +18,18 @@
@if (followLogs | logs | async; as logs) {
<section childList (waMutationObserver)="scrollToBottom()">
@for (log of logs; track log) {
@for (log of logs; track $index) {
<pre [innerHTML]="log | dompurify"></pre>
}
@if ((status$ | async) !== 'connected') {
<p class="loading-dots" [attr.data-status]="status$.value">
<div class="loading-dots" [attr.data-status]="status$.value">
{{
status$.value === 'reconnecting'
? ('Reconnecting' | i18n)
: ('Waiting for network connectivity' | i18n)
}}
</p>
</div>
}
</section>
} @else {

View File

@@ -48,4 +48,5 @@
pre {
overflow: visible;
white-space: normal;
margin: 0;
}

View File

@@ -8,16 +8,19 @@ import {
import {
bufferTime,
catchError,
concat,
defer,
delay,
EMPTY,
filter,
ignoreElements,
map,
merge,
Observable,
of,
repeat,
scan,
skipWhile,
startWith,
switchMap,
take,
tap,
@@ -62,12 +65,19 @@ export class LogsPipe implements PipeTransform {
),
).pipe(
catchError(() =>
this.connection.pipe(
tap(v => this.logs.status$.next(v ? 'reconnecting' : 'disconnected')),
filter(Boolean),
take(1),
ignoreElements(),
startWith(this.getMessage(false)),
concat(
this.logs.status$.value === 'connected'
? of(this.getMessage(false))
: EMPTY,
this.connection.pipe(
tap(v =>
this.logs.status$.next(v ? 'reconnecting' : 'disconnected'),
),
filter(Boolean),
delay(1000),
take(1),
ignoreElements(),
),
),
),
repeat(),
@@ -76,11 +86,11 @@ export class LogsPipe implements PipeTransform {
}
private getMessage(success: boolean): string {
return `<p style="color: ${
return `<div style="color: ${
success ? 'var(--tui-status-positive)' : 'var(--tui-status-negative)'
}; text-align: center;">${this.i18n.transform(
success ? 'Reconnected' : 'Disconnected',
)} at ${toLocalIsoString(new Date())}</p>`
)} at ${toLocalIsoString(new Date())}</div>`
}
private get options() {

View File

@@ -26,7 +26,7 @@ import { HeaderComponent } from './components/header/header.component'
</main>
<app-tabs />
@if (update(); as update) {
<tui-action-bar *tuiActionBar="bar">
<tui-action-bar *tuiActionBar="bar()">
@if (update === true) {
<tui-icon icon="@tui.check" class="g-positive" />
Download complete, restart to apply changes
@@ -77,8 +77,7 @@ import { HeaderComponent } from './components/header/header.component'
@include taiga.transition(filter);
header:has([data-status='success']) + &,
header:has([data-status='neutral']) + & {
header:has([data-status='success']) + & {
filter: none;
}
}
@@ -104,7 +103,7 @@ export class PortalComponent {
readonly name = toSignal(this.patch.watch$('ui', 'name'))
readonly update = toSignal(inject(OSService).updating$)
bar = true
readonly bar = signal(true)
getProgress(size: number, downloaded: number): number {
return Math.round((100 * downloaded) / (size || 1))
@@ -114,7 +113,7 @@ export class PortalComponent {
const loader = this.loader.open('Beginning restart').subscribe()
try {
this.bar = false
this.bar.set(false)
await this.api.restartServer({})
} catch (e: any) {
this.errorService.handleError(e)

View File

@@ -3,11 +3,9 @@ import {
ChangeDetectionStrategy,
Component,
inject,
Input,
input,
} from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { Router } from '@angular/router'
import { MarketplacePkgBase } from '@start9labs/marketplace'
import {
ErrorService,
Exver,
@@ -30,21 +28,20 @@ import {
import { dryUpdate } from 'src/app/utils/dry-update'
import { getAllPackages, getManifest } from 'src/app/utils/get-package-data'
import { hasCurrentDeps } from 'src/app/utils/has-deps'
import { MarketplacePreviewComponent } from '../modals/preview.component'
import { MarketplaceAlertsService } from '../services/alerts.service'
@Component({
selector: 'marketplace-controls',
template: `
@if (localPkg) {
@if (localPkg | toManifest; as localManifest) {
@switch (localManifest.version | compareExver: version() || '') {
@if (localPkg(); as local) {
@if (local.stateInfo.state === 'installed') {
@switch ((local | toManifest).version | compareExver: version()) {
@case (1) {
<button
tuiButton
type="button"
appearance="secondary-destructive"
appearance="warning"
(click)="tryInstall()"
>
{{ 'Downgrade' | i18n }}
@@ -81,17 +78,17 @@ import { MarketplaceAlertsService } from '../services/alerts.service'
{{
('View' | i18n) +
' ' +
($any(localPkg.stateInfo.state | titlecase) | i18n)
($any(local.stateInfo.state | titlecase) | i18n)
}}
</button>
} @else {
<button
tuiButton
type="button"
[appearance]="localFlavor ? 'warning' : 'primary'"
appearance="primary"
(click)="tryInstall()"
>
{{ localFlavor ? ('Switch' | i18n) : ('Install' | i18n) }}
{{ localFlavor() ? ('Switch' | i18n) : ('Install' | i18n) }}
</button>
}
`,
@@ -116,29 +113,23 @@ export class MarketplaceControlsComponent {
private readonly api = inject(ApiService)
private readonly preview = inject(MarketplacePreviewComponent)
protected readonly version = toSignal(this.preview.version$)
@Input({ required: true })
pkg!: MarketplacePkgBase
@Input()
localPkg!: PackageDataEntry | null
@Input()
localFlavor!: boolean
version = input.required<string>()
installAlert = input.required<string | null>()
localPkg = input.required<PackageDataEntry | null>()
localFlavor = input.required<boolean>()
// only present if side loading
@Input()
file?: File
file = input<File>()
async tryInstall() {
const currentUrl = this.file
const localPkg = this.localPkg()
const currentUrl = this.file()
? null
: await firstValueFrom(this.marketplaceService.currentRegistryUrl$)
const originalUrl = this.localPkg?.registry || null
const originalUrl = localPkg?.registry || null
if (!this.localPkg) {
if (await this.alerts.alertInstall(this.pkg)) {
if (!localPkg) {
if (await this.alerts.alertInstall(this.installAlert() || '')) {
this.installOrUpload(currentUrl)
}
return
@@ -152,12 +143,11 @@ export class MarketplaceControlsComponent {
return
}
const localManifest = getManifest(this.localPkg)
const version = this.version() || ''
const localManifest = getManifest(localPkg)
if (
hasCurrentDeps(localManifest.id, await getAllPackages(this.patch)) &&
this.exver.compareExver(localManifest.version, version) !== 0
this.exver.compareExver(localManifest.version, this.version()) !== 0
) {
this.dryInstall(currentUrl)
} else {
@@ -171,9 +161,8 @@ export class MarketplaceControlsComponent {
private async dryInstall(url: string | null) {
const id = this.preview.pkgId
const version = this.version() || ''
const breakages = dryUpdate(
{ id, version },
{ id, version: this.version() },
await getAllPackages(this.patch),
this.exver,
)
@@ -187,7 +176,7 @@ export class MarketplaceControlsComponent {
}
private async installOrUpload(url: string | null) {
if (this.file) {
if (this.file()) {
await this.upload()
this.router.navigate(['/portal', 'services'])
} else if (url) {
@@ -197,11 +186,10 @@ export class MarketplaceControlsComponent {
private async install(url: string) {
const loader = this.loader.open('Beginning install').subscribe()
const version = this.version() || ''
const id = this.preview.pkgId
try {
await this.marketplaceService.installPackage(id, version, url)
await this.marketplaceService.installPackage(id, this.version(), url)
} catch (e: any) {
this.errorService.handleError(e)
} finally {
@@ -210,11 +198,14 @@ export class MarketplaceControlsComponent {
}
private async upload() {
const file = this.file()
if (!file) throw new Error('no file detected')
const loader = this.loader.open('Starting upload').subscribe()
try {
const { upload } = await this.api.sideloadPackage()
this.api.uploadPackage(upload, this.file!).catch(console.error)
this.api.uploadPackage(upload, file).catch(console.error)
} catch (e: any) {
this.errorService.handleError(e)
} finally {

View File

@@ -56,7 +56,8 @@ import { MarketplaceControlsComponent } from './controls.component'
<marketplace-controls
slot="controls"
class="controls-wrapper"
[pkg]="pkg()"
[version]="pkg().version"
[installAlert]="pkg().alerts.install"
[localPkg]="local$ | async"
[localFlavor]="!!(flavor$ | async)"
/>

View File

@@ -62,9 +62,7 @@ export class MarketplaceAlertsService {
})
}
async alertInstall({ alerts }: MarketplacePkgBase): Promise<boolean> {
const content = alerts.install
async alertInstall(content: string): Promise<boolean> {
return (
!content ||
(!!content &&

View File

@@ -90,17 +90,20 @@ export type PackageActionData = {
`,
styles: `
tui-notification {
font-size: 1rem;
margin-bottom: 1.4rem;
margin-bottom: 1.5rem;
}
.service-title {
display: inline-flex;
align-items: center;
margin-bottom: 1.4rem;
margin-bottom: 1.5rem;
img {
height: 20px;
margin-right: 4px;
height: 1.25rem;
margin-right: 0.25rem;
border-radius: 100%;
}
h4 {
margin: 0;
}
@@ -192,7 +195,7 @@ export class ActionInputModal {
task.when?.condition === 'input-not-matches' &&
task.input &&
json
.compare(input, task.input)
.compare(input, task.input.value)
.some(op => op.op === 'add' || op.op === 'replace'),
),
)
@@ -201,9 +204,8 @@ export class ActionInputModal {
if (!breakages.length) return true
const message = `${this.i18n.transform('As a result of this change, the following services will no longer work properly and may crash')}:<ul>`
const content = `${message}${breakages.map(
id => `<li><b>${getManifest(packages[id]!).title}</b></li>`,
)}</ul>` as i18nKey
const content =
`${message}${breakages.map(id => `<li><b>${getManifest(packages[id]!).title}</b></li>`)}</ul>` as i18nKey
return firstValueFrom(
this.dialog

View File

@@ -29,7 +29,8 @@ import { MarketplacePkgSideload } from './sideload.utils'
<marketplace-controls
slot="controls"
class="controls-wrapper"
[pkg]="pkg()"
[version]="pkg().version"
[installAlert]="pkg().alerts.install"
[localPkg]="local$ | async"
[localFlavor]="!!(flavor$ | async)"
[file]="file()"

View File

@@ -1,11 +1,7 @@
import { inject, Pipe, PipeTransform } from '@angular/core'
import { Exver } from '@start9labs/shared'
import { MarketplacePkg } from '@start9labs/marketplace'
import {
InstalledState,
PackageDataEntry,
UpdatingState,
} from 'src/app/services/patch-db/data-model'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@Pipe({
name: 'filterUpdates',
@@ -15,15 +11,14 @@ export class FilterUpdatesPipe implements PipeTransform {
transform(
pkgs: MarketplacePkg[],
local: Record<
string,
PackageDataEntry<InstalledState | UpdatingState>
> = {},
local: Record<string, PackageDataEntry> = {},
): MarketplacePkg[] {
return pkgs.filter(({ id, version, flavor }) => {
return pkgs.filter(({ id, flavor, version }) => {
const localPkg = local[id]
return (
localPkg &&
!!localPkg &&
(localPkg.stateInfo.state === 'installed' ||
localPkg.stateInfo.state === 'updating') &&
this.exver.getFlavor(localPkg.stateInfo.manifest.version) === flavor &&
this.exver.compareExver(
version,

View File

@@ -44,7 +44,7 @@ import UpdatesComponent from './updates.component'
template: `
<tr (click)="expanded.set(!expanded())">
<td>
<div [style.gap.rem]="0.75">
<div [style.gap.rem]="0.75" [style.padding-inline-end.rem]="1">
<tui-avatar size="s"><img alt="" [src]="item().icon" /></tui-avatar>
<span tuiTitle [style.margin]="'-0.125rem 0 0'">
<b tuiFade>{{ item().title }}</b>
@@ -81,7 +81,6 @@ import UpdatesComponent from './updates.component'
</button>
@if (local().stateInfo.state === 'updating') {
<tui-progress-circle
class="g-positive"
size="xs"
[max]="100"
[value]="
@@ -175,7 +174,7 @@ import UpdatesComponent from './updates.component'
white-space: nowrap;
div {
justify-content: flex-end;
justify-content: flex-start;
}
}

View File

@@ -1,5 +1,4 @@
import { inject, Injectable } from '@angular/core'
import { Exver } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client'
import {
combineLatest,
@@ -19,13 +18,13 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
import { NotificationService } from 'src/app/services/notification.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/utils/get-package-data'
import { FilterUpdatesPipe } from '../routes/portal/routes/updates/filter-updates.pipe'
@Injectable({
providedIn: 'root',
})
export class BadgeService {
private readonly notifications = inject(NotificationService)
private readonly exver = inject(Exver)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly system$ = inject(OSService).updateAvailable$.pipe(
map(Number),
@@ -34,6 +33,7 @@ export class BadgeService {
.watch$('serverInfo', 'ntpSynced')
.pipe(map(synced => Number(!synced)))
private readonly marketplaceService = inject(MarketplaceService)
private readonly filterUpdatesPipe = inject(FilterUpdatesPipe)
private readonly local$ = inject(ConnectionService).pipe(
filter(Boolean),
@@ -66,17 +66,9 @@ export class BadgeService {
([marketplace, local]) =>
Object.entries(marketplace).reduce(
(list, [_, store]) =>
store?.packages.reduce(
(result, { id, version }) =>
local[id] &&
this.exver.compareExver(
version,
getManifest(local[id]!).version,
) === 1
? result.add(id)
: result,
list,
) || list,
this.filterUpdatesPipe
.transform(store?.packages || [], local)
.reduce((result, { id }) => result.add(id), list),
new Set<string>(),
).size,
),

View File

@@ -57,6 +57,10 @@ export class StandardActionsService {
content = `${content}${content ? ' ' : ''}${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
}
if (!content) {
return this.doUninstall({ id, force, soft })
}
this.dialog
.openConfirm({
label: 'Warning',

View File

@@ -1,6 +1,6 @@
import { inject, Injectable } from '@angular/core'
import { CanActivateFn, IsActiveMatchOptions, Router } from '@angular/router'
import { DialogService, i18nPipe } from '@start9labs/shared'
import { i18nPipe } from '@start9labs/shared'
import { TUI_TRUE_HANDLER } from '@taiga-ui/cdk'
import { TuiAlertService } from '@taiga-ui/core'
import {
@@ -47,9 +47,7 @@ export class StateService extends Observable<RR.ServerState | null> {
private readonly api = inject(ApiService)
private readonly router = inject(Router)
private readonly network$ = inject(NetworkService)
private readonly single$ = new Subject<RR.ServerState>()
private readonly trigger$ = new BehaviorSubject<void>(undefined)
private readonly poll$ = this.trigger$.pipe(
switchMap(() =>
@@ -101,7 +99,7 @@ export class StateService extends Observable<RR.ServerState | null> {
})
.pipe(
takeUntil(
combineLatest([this.stream$, this.network$]).pipe(
combineLatest([this.stream$.pipe(skip(1)), this.network$]).pipe(
filter(state => state.every(Boolean)),
),
),