Manage dialog polling

This commit is contained in:
shamoon 2025-11-04 21:25:15 -08:00
parent 01e9988bd2
commit 92be4e37ab
No known key found for this signature in database
3 changed files with 172 additions and 32 deletions

View File

@ -16,6 +16,11 @@
</div>
}
@if (!loading && !error) {
<div class="d-flex justify-content-between align-items-center mb-2">
<p class="mb-0 text-muted small">
<ng-container i18n>Status updates every few seconds while bundles are being prepared.</ng-container>
</p>
</div>
@if (bundles.length === 0) {
<p class="mb-0 text-muted fst-italic" i18n>No bulk share links currently exist.</p>
}
@ -26,17 +31,42 @@
<tr>
<th scope="col" i18n>Created</th>
<th scope="col" i18n>Status</th>
<th scope="col" i18n>Size</th>
<th scope="col" i18n>Expires</th>
<th scope="col" i18n>Documents</th>
<th scope="col" i18n>Actions</th>
<th scope="col" i18n>File version</th>
<th scope="col" class="text-end" i18n>Actions</th>
</tr>
</thead>
<tbody>
@for (bundle of bundles; track bundle.id) {
<tr>
<td>{{ bundle.created | date: 'short' }}</td>
<td>
<span class="badge text-bg-secondary text-uppercase">{{ bundle.status }}</span>
<div>{{ bundle.created | date: 'short' }}</div>
@if (bundle.built_at) {
<div class="small text-muted">
<ng-container i18n>Built:</ng-container> {{ bundle.built_at | date: 'short' }}
</div>
}
</td>
<td>
<div class="d-flex align-items-center gap-2">
@if (bundle.status === statuses.Processing || bundle.status === statuses.Pending) {
<span class="spinner-border spinner-border-sm" role="status"></span>
}
<span>{{ statusLabel(bundle.status) }}</span>
</div>
@if (bundle.last_error && bundle.status === statuses.Failed) {
<div class="small text-danger mt-1">{{ bundle.last_error }}</div>
}
</td>
<td>
@if (bundle.size_bytes !== undefined && bundle.size_bytes !== null) {
{{ bundle.size_bytes | fileSize }}
}
@if (bundle.size_bytes === undefined || bundle.size_bytes === null) {
<span class="text-muted">&mdash;</span>
}
</td>
<td>
@if (bundle.expiration) {
@ -47,11 +77,13 @@
}
</td>
<td>{{ bundle.document_count }}</td>
<td>
<td>{{ fileVersionLabel(bundle.file_version) }}</td>
<td class="text-end">
<div class="btn-group btn-group-sm">
<button
type="button"
class="btn btn-outline-primary"
[disabled]="bundle.status !== statuses.Ready"
(click)="copy(bundle)"
>
@if (copiedSlug === bundle.slug) {
@ -62,9 +94,21 @@
}
<span class="visually-hidden" i18n>Copy link</span>
</button>
@if (bundle.status === statuses.Failed) {
<button
type="button"
class="btn btn-outline-warning"
[disabled]="loading"
(click)="retry(bundle)"
>
<i-bs name="arrow-clockwise"></i-bs>
<span class="visually-hidden" i18n>Retry</span>
</button>
}
<button
type="button"
class="btn btn-outline-danger"
[disabled]="loading"
(click)="delete(bundle)"
>
<i-bs name="trash"></i-bs>

View File

@ -1,10 +1,15 @@
import { Clipboard } from '@angular/cdk/clipboard'
import { CommonModule } from '@angular/common'
import { Component, OnInit, inject } from '@angular/core'
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { first } from 'rxjs'
import { ShareBundleSummary } from 'src/app/data/share-bundle'
import { Subject, catchError, of, switchMap, takeUntil, timer } from 'rxjs'
import {
ShareBundleStatus,
ShareBundleSummary,
} from 'src/app/data/share-bundle'
import { FileVersion } from 'src/app/data/share-link'
import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
import { ShareBundleService } from 'src/app/services/rest/share-bundle.service'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
@ -14,11 +19,11 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
selector: 'pngx-share-bundle-manage-dialog',
templateUrl: './share-bundle-manage-dialog.component.html',
standalone: true,
imports: [CommonModule, NgxBootstrapIconsModule],
imports: [CommonModule, NgxBootstrapIconsModule, FileSizePipe],
})
export class ShareBundleManageDialogComponent
extends LoadingComponentWithPermissions
implements OnInit
implements OnInit, OnDestroy
{
private activeModal = inject(NgbActiveModal)
private shareBundleService = inject(ShareBundleService)
@ -28,33 +33,70 @@ export class ShareBundleManageDialogComponent
title = $localize`Bulk Share Links`
bundles: ShareBundleSummary[] = []
error: string
copiedSlug: string
error: string | null = null
copiedSlug: string | null = null
readonly statuses = ShareBundleStatus
readonly fileVersions = FileVersion
private readonly refresh$ = new Subject<boolean>()
private readonly statusLabels: Record<ShareBundleStatus, string> = {
[ShareBundleStatus.Pending]: $localize`Pending`,
[ShareBundleStatus.Processing]: $localize`Processing`,
[ShareBundleStatus.Ready]: $localize`Ready`,
[ShareBundleStatus.Failed]: $localize`Failed`,
}
private readonly fileVersionLabels: Record<FileVersion, string> = {
[FileVersion.Archive]: $localize`Archive`,
[FileVersion.Original]: $localize`Original`,
}
ngOnInit(): void {
this.fetchBundles()
this.refresh$
.pipe(
switchMap((silent) => {
if (!silent) {
this.loading = true
}
this.error = null
return this.shareBundleService.listAllBundles().pipe(
catchError((error) => {
if (!silent) {
this.loading = false
}
this.error = $localize`Failed to load bulk share links.`
this.toastService.showError(
$localize`Error retrieving bulk share links.`,
error
)
return of(null)
})
)
}),
takeUntil(this.unsubscribeNotifier)
)
.subscribe((results) => {
if (results) {
this.bundles = results
this.copiedSlug = null
}
this.loading = false
})
this.triggerRefresh(false)
timer(5000, 5000)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => this.triggerRefresh(true))
}
ngOnDestroy(): void {
super.ngOnDestroy()
}
fetchBundles(): void {
this.loading = true
this.error = null
this.shareBundleService
.listAllBundles()
.pipe(first())
.subscribe({
next: (results) => {
this.bundles = results
this.loading = false
},
error: (e) => {
this.loading = false
this.error = $localize`Failed to load bulk share links.`
this.toastService.showError(
$localize`Error retrieving bulk share links.`,
e
)
},
})
this.triggerRefresh(false)
}
getShareUrl(bundle: ShareBundleSummary): string {
@ -65,22 +107,29 @@ export class ShareBundleManageDialogComponent
}
copy(bundle: ShareBundleSummary): void {
if (bundle.status !== ShareBundleStatus.Ready) {
return
}
const success = this.clipboard.copy(this.getShareUrl(bundle))
if (success) {
this.copiedSlug = bundle.slug
setTimeout(() => {
this.copiedSlug = null
}, 3000)
this.toastService.showInfo($localize`Share link copied to clipboard.`)
}
}
delete(bundle: ShareBundleSummary): void {
this.error = null
this.loading = true
this.shareBundleService.delete(bundle).subscribe({
next: () => {
this.toastService.showInfo($localize`Bulk share link deleted.`)
this.fetchBundles()
this.triggerRefresh(false)
},
error: (e) => {
this.loading = false
this.toastService.showError(
$localize`Error deleting bulk share link.`,
e
@ -89,7 +138,47 @@ export class ShareBundleManageDialogComponent
})
}
retry(bundle: ShareBundleSummary): void {
this.error = null
this.shareBundleService.rebuildBundle(bundle.id).subscribe({
next: (updated) => {
this.toastService.showInfo(
$localize`Bulk share link rebuild requested.`
)
this.replaceBundle(updated)
},
error: (e) => {
this.toastService.showError($localize`Error requesting rebuild.`, e)
},
})
}
statusLabel(status: ShareBundleStatus): string {
return this.statusLabels[status] ?? status
}
fileVersionLabel(version: FileVersion): string {
return this.fileVersionLabels[version] ?? version
}
close(): void {
this.activeModal.close()
}
private replaceBundle(updated: ShareBundleSummary): void {
const index = this.bundles.findIndex((bundle) => bundle.id === updated.id)
if (index >= 0) {
this.bundles = [
...this.bundles.slice(0, index),
updated,
...this.bundles.slice(index + 1),
]
} else {
this.bundles = [updated, ...this.bundles]
}
}
private triggerRefresh(silent: boolean): void {
this.refresh$.next(silent)
}
}

View File

@ -22,6 +22,13 @@ export class ShareBundleService extends AbstractNameFilterService<ShareBundleSum
this.clearCache()
return this.http.post<ShareBundleSummary>(this.getResourceUrl(), payload)
}
rebuildBundle(bundleId: number): Observable<ShareBundleSummary> {
this.clearCache()
return this.http.post<ShareBundleSummary>(
this.getResourceUrl(bundleId, 'rebuild'),
{}
)
}
listAllBundles(): Observable<ShareBundleSummary[]> {
return this.list(1, 1000, 'created', true).pipe(