diff --git a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html index 685d9f3cf..2f2155412 100644 --- a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html +++ b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html @@ -51,14 +51,39 @@
+ @if (bundle.status === statuses.Failed && bundle.last_error) { + + + @if (bundle.last_error.timestamp) { +
+ {{ bundle.last_error.timestamp | date: 'short' }} +
+ } +
{{ bundle.last_error.exception_type || ($localize`Unknown error`) }}
+ @if (bundle.last_error.message) { +
{{ bundle.last_error.message }}
+ } +
+ } @if (bundle.status === statuses.Processing || bundle.status === statuses.Pending) { } - {{ statusLabel(bundle.status) }} + @if (bundle.status !== statuses.Failed) { + {{ statusLabel(bundle.status) }} + }
- @if (bundle.last_error && bundle.status === statuses.Failed) { -
{{ bundle.last_error }}
- } @if (bundle.size_bytes !== undefined && bundle.size_bytes !== null) { diff --git a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.scss b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.scss new file mode 100644 index 000000000..c8ffc4d5d --- /dev/null +++ b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.scss @@ -0,0 +1,4 @@ +:host ::ng-deep .popover { + min-width: 300px; + max-width: 400px; + } diff --git a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.spec.ts b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.spec.ts index 1784f6ff6..113cd65a3 100644 --- a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.spec.ts @@ -81,6 +81,7 @@ describe('ShareLinkBundleManageDialogComponent', () => { documents: [1], status: ShareLinkBundleStatus.Pending, file_version: FileVersion.Archive, + last_error: undefined, ...overrides, }) as ShareLinkBundleSummary diff --git a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts index fdc9f0126..6eef144f9 100644 --- a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts +++ b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts @@ -1,7 +1,7 @@ import { Clipboard } from '@angular/cdk/clipboard' import { CommonModule } from '@angular/common' import { Component, OnDestroy, OnInit, inject } from '@angular/core' -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' +import { NgbActiveModal, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { Subject, catchError, of, switchMap, takeUntil, timer } from 'rxjs' import { FileVersion } from 'src/app/data/share-link' @@ -21,9 +21,11 @@ import { ConfirmButtonComponent } from '../confirm-button/confirm-button.compone @Component({ selector: 'pngx-share-link-bundle-manage-dialog', templateUrl: './share-link-bundle-manage-dialog.component.html', + styleUrls: ['./share-link-bundle-manage-dialog.component.scss'], imports: [ ConfirmButtonComponent, CommonModule, + NgbPopoverModule, NgxBootstrapIconsModule, FileSizePipe, ], diff --git a/src-ui/src/app/data/share-link-bundle.ts b/src-ui/src/app/data/share-link-bundle.ts index cdabd1d15..fe6134997 100644 --- a/src-ui/src/app/data/share-link-bundle.ts +++ b/src-ui/src/app/data/share-link-bundle.ts @@ -7,6 +7,13 @@ export enum ShareLinkBundleStatus { Failed = 'failed', } +export type ShareLinkBundleError = { + bundle_id: number + message?: string + exception_type?: string + timestamp?: string +} + export interface ShareLinkBundleSummary { id: number slug: string @@ -18,7 +25,7 @@ export interface ShareLinkBundleSummary { status: ShareLinkBundleStatus built_at?: string size_bytes?: number - last_error?: string + last_error?: ShareLinkBundleError } export interface ShareLinkBundleCreatePayload { diff --git a/src/documents/migrations/0007_sharelinkbundle.py b/src/documents/migrations/0007_sharelinkbundle.py index 7216a09dd..7efc197a4 100644 --- a/src/documents/migrations/0007_sharelinkbundle.py +++ b/src/documents/migrations/0007_sharelinkbundle.py @@ -120,8 +120,10 @@ class Migration(migrations.Migration): ), ( "last_error", - models.TextField( + models.JSONField( blank=True, + null=True, + default=None, verbose_name="last error", ), ), diff --git a/src/documents/models.py b/src/documents/models.py index bb4af0b32..a9f4f7a72 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -817,9 +817,11 @@ class ShareLinkBundle(SoftDeleteModel): null=True, ) - last_error = models.TextField( + last_error = models.JSONField( _("last error"), blank=True, + null=True, + default=None, ) file_path = models.CharField( diff --git a/src/documents/tasks.py b/src/documents/tasks.py index e2ddf4c35..c5c8146de 100644 --- a/src/documents/tasks.py +++ b/src/documents/tasks.py @@ -647,7 +647,7 @@ def build_share_link_bundle(bundle_id: int): bundle.remove_file() bundle.status = ShareLinkBundle.Status.PROCESSING - bundle.last_error = "" + bundle.last_error = None bundle.size_bytes = None bundle.built_at = None bundle.file_path = "" @@ -695,7 +695,7 @@ def build_share_link_bundle(bundle_id: int): bundle.size_bytes = final_path.stat().st_size bundle.status = ShareLinkBundle.Status.READY bundle.built_at = timezone.now() - bundle.last_error = "" + bundle.last_error = None bundle.save( update_fields=[ "file_path", @@ -713,7 +713,12 @@ def build_share_link_bundle(bundle_id: int): exc, ) bundle.status = ShareLinkBundle.Status.FAILED - bundle.last_error = str(exc) + bundle.last_error = { + "bundle_id": bundle_id, + "exception_type": exc.__class__.__name__, + "message": str(exc), + "timestamp": timezone.now().isoformat(), + } bundle.save(update_fields=["status", "last_error"]) try: temp_zip_path.unlink() diff --git a/src/documents/tests/test_share_link_bundles.py b/src/documents/tests/test_share_link_bundles.py index 507097276..62b815ec5 100644 --- a/src/documents/tests/test_share_link_bundles.py +++ b/src/documents/tests/test_share_link_bundles.py @@ -81,7 +81,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase): status=ShareLinkBundle.Status.FAILED, ) bundle.documents.set([self.document]) - bundle.last_error = "Something went wrong" + bundle.last_error = {"message": "Something went wrong"} bundle.size_bytes = 100 bundle.file_path = "path/to/file.zip" bundle.save() @@ -91,7 +91,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) bundle.refresh_from_db() self.assertEqual(bundle.status, ShareLinkBundle.Status.PENDING) - self.assertEqual(bundle.last_error, "") + self.assertIsNone(bundle.last_error) self.assertIsNone(bundle.size_bytes) self.assertEqual(bundle.file_path, "") delay_mock.assert_called_once_with(bundle.pk) @@ -172,7 +172,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase): file_version=ShareLink.FileVersion.ARCHIVE, status=ShareLinkBundle.Status.FAILED, file_path=str(bundle_path.relative_to(settings.MEDIA_ROOT)), - last_error="Boom", + last_error={"message": "Boom"}, size_bytes=10, ) bundle.documents.set([self.document]) @@ -183,7 +183,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase): self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE) bundle.refresh_from_db() self.assertEqual(bundle.status, ShareLinkBundle.Status.PENDING) - self.assertEqual(bundle.last_error, "") + self.assertIsNone(bundle.last_error) self.assertIsNone(bundle.size_bytes) self.assertEqual(bundle.file_path, "") delay_mock.assert_called_once_with(bundle.pk) @@ -335,7 +335,7 @@ class ShareLinkBundleBuildTaskTests(DirectoriesMixin, APITestCase): bundle.refresh_from_db() self.assertEqual(bundle.status, ShareLinkBundle.Status.READY) - self.assertEqual(bundle.last_error, "") + self.assertIsNone(bundle.last_error) self.assertIsNotNone(bundle.built_at) self.assertGreater(bundle.size_bytes or 0, 0) final_path = bundle.absolute_file_path @@ -404,7 +404,9 @@ class ShareLinkBundleBuildTaskTests(DirectoriesMixin, APITestCase): bundle.refresh_from_db() self.assertEqual(bundle.status, ShareLinkBundle.Status.FAILED) - self.assertEqual(bundle.last_error, "zip failure") + self.assertIsInstance(bundle.last_error, dict) + self.assertEqual(bundle.last_error.get("message"), "zip failure") + self.assertEqual(bundle.last_error.get("exception_type"), "RuntimeError") scratch_zips = list(Path(settings.SCRATCH_DIR).glob("*.zip")) self.assertTrue(scratch_zips) for path in scratch_zips: diff --git a/src/documents/views.py b/src/documents/views.py index 735028077..b5ba2bf31 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -2865,7 +2865,7 @@ class ShareLinkBundleViewSet(ModelViewSet, PassUserMixin): ) bundle.remove_file() bundle.status = ShareLinkBundle.Status.PENDING - bundle.last_error = "" + bundle.last_error = None bundle.size_bytes = None bundle.built_at = None bundle.file_path = "" @@ -2898,7 +2898,7 @@ class ShareLinkBundleViewSet(ModelViewSet, PassUserMixin): ) bundle.remove_file() bundle.status = ShareLinkBundle.Status.PENDING - bundle.last_error = "" + bundle.last_error = None bundle.size_bytes = None bundle.built_at = None bundle.file_path = "" @@ -2958,7 +2958,7 @@ class SharedLinkView(View): if bundle.status == ShareLinkBundle.Status.FAILED: bundle.remove_file() bundle.status = ShareLinkBundle.Status.PENDING - bundle.last_error = "" + bundle.last_error = None bundle.size_bytes = None bundle.built_at = None bundle.file_path = "" @@ -2982,7 +2982,7 @@ class SharedLinkView(View): file_path = bundle.absolute_file_path if file_path is None or not file_path.exists(): bundle.status = ShareLinkBundle.Status.PENDING - bundle.last_error = "" + bundle.last_error = None bundle.size_bytes = None bundle.built_at = None bundle.file_path = ""