From 9580a7b2dfdf67942461ace6dffeec2da7a0931f Mon Sep 17 00:00:00 2001 From: Andrii Arsenin Date: Tue, 21 Apr 2026 16:46:33 +0300 Subject: [PATCH] fix(files): expose expiry and disable expired downloads - return expiresAt and createdAt for scenario and run file metadata\n- keep download actions visible while preventing expired file access --- client/src/i18n/locales/en.json | 1 + client/src/pages/run/RunDetailPage.tsx | 40 +++++- .../src/pages/scenario/ScenarioDetailPage.tsx | 116 +++++++++++------- server/src/file/file-storage.service.ts | 4 + server/src/scenario/scenario.service.ts | 6 + 5 files changed, 115 insertions(+), 52 deletions(-) diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json index 3e27dea..0547db1 100644 --- a/client/src/i18n/locales/en.json +++ b/client/src/i18n/locales/en.json @@ -308,6 +308,7 @@ "expiresAtLabel": "Expires At (optional)", "createdAt": "Created", "download": "Download", + "expiredTooltip": "File is expired", "empty": "No files uploaded.", "totalCount": "Total: {{count}} file(s)", "artifactsTitle": "Artifacts" diff --git a/client/src/pages/run/RunDetailPage.tsx b/client/src/pages/run/RunDetailPage.tsx index d3ed32c..b7cd4ba 100644 --- a/client/src/pages/run/RunDetailPage.tsx +++ b/client/src/pages/run/RunDetailPage.tsx @@ -17,6 +17,7 @@ import { scenarios } from '../../api'; import { AutoRefreshIndicator, Badge, + Button, Breadcrumbs, Card, CodeBlock, @@ -29,7 +30,7 @@ import { type TableColumn, } from '../../ui'; import type { BadgeVariant } from '../../ui'; -import { ChevronDown, ChevronRight, ClipboardList, Activity } from 'lucide-react'; +import { ChevronDown, ChevronRight, ClipboardList, Activity, Download } from 'lucide-react'; import styles from '../Page.module.css'; const RUN_STATUS_VARIANT: Record = { @@ -95,6 +96,16 @@ export function RunDetailPage() { } }; + const handleFileDownload = (url: string, filename: string) => { + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + }; + + const isFileExpired = (file: FileMetadata) => + file.expiresAt != null && new Date(file.expiresAt).getTime() <= Date.now(); + const manualRefresh = () => { if (!id || !runId) return; runs @@ -240,6 +251,7 @@ export function RunDetailPage() { ), render: (s) => { const isExpanded = expandAll || expandedStepIds.has(s.id); + return (
( - - {t('files.download')} - - ), + render: (f) => { + const expired = isFileExpired(f); + const tooltip = expired ? t('files.expiredTooltip') : t('files.download'); + + return ( + + + + ); + }, }, ] satisfies TableColumn[] } diff --git a/client/src/pages/scenario/ScenarioDetailPage.tsx b/client/src/pages/scenario/ScenarioDetailPage.tsx index b70ff82..eb61602 100644 --- a/client/src/pages/scenario/ScenarioDetailPage.tsx +++ b/client/src/pages/scenario/ScenarioDetailPage.tsx @@ -7,6 +7,7 @@ import { Pencil, Plus, History, + Download, Trash2, Upload, GripVertical, @@ -201,6 +202,16 @@ export function ScenarioDetailPage() { } }; + const handleFileDownload = (url: string, filename: string) => { + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + }; + + const isFileExpired = (file: FileMetadata) => + file.expiresAt != null && new Date(file.expiresAt).getTime() <= Date.now(); + async function handleUpload(e: SubmitEvent) { e.preventDefault(); if (!id || !uploadFile) return; @@ -649,53 +660,66 @@ export function ScenarioDetailPage() {
- - - loading={filesLoading} - data={files} - rowKey={(f) => f.id} - columns={ - [ - { - key: 'name', - header: t('files.name'), - render: (f) => f.name, + + loading={filesLoading} + data={files} + rowKey={(f) => f.id} + columns={ + [ + { + key: 'name', + header: t('files.name'), + render: (f) => f.name, + }, + { + key: 'mimeType', + header: t('files.mimeType'), + render: (f) => f.mimeType, + }, + { + key: 'size', + header: t('files.size'), + render: (f) => `${(f.size / 1024).toFixed(1)} KB`, + }, + { + key: 'expiresAt', + header: t('files.expiresAt'), + render: (f) => (f.expiresAt ? : '—'), + }, + { + key: 'createdAt', + header: t('files.createdAt'), + render: (f) => , + }, + { + key: 'download', + header: '', + render: (f) => { + const expired = isFileExpired(f); + const tooltip = expired ? t('files.expiredTooltip') : t('files.download'); + + return ( + + + + ); }, - { - key: 'mimeType', - header: t('files.mimeType'), - render: (f) => f.mimeType, - }, - { - key: 'size', - header: t('files.size'), - render: (f) => `${(f.size / 1024).toFixed(1)} KB`, - }, - { - key: 'expiresAt', - header: t('files.expiresAt'), - render: (f) => (f.expiresAt ? : '—'), - }, - { - key: 'createdAt', - header: t('files.createdAt'), - render: (f) => , - }, - { - key: 'download', - header: '', - render: (f) => ( - - {t('files.download')} - - ), - }, - ] satisfies TableColumn[] - } - emptyMessage={t('files.empty')} - /> -

{t('files.totalCount', { count: filesTotal })}

-
+ }, + ] satisfies TableColumn[] + } + emptyMessage={t('files.empty')} + />
{/* ── Upload modal ─────────────────────────────────────────────── */} diff --git a/server/src/file/file-storage.service.ts b/server/src/file/file-storage.service.ts index e3d4bab..3453ebb 100644 --- a/server/src/file/file-storage.service.ts +++ b/server/src/file/file-storage.service.ts @@ -19,6 +19,8 @@ export interface FileMetadata { mimeType: string; size: number; sha256: string; + expiresAt: Date | null; + createdAt: Date; path: string; } @@ -271,6 +273,8 @@ export class FileStorageService implements OnModuleInit { mimeType: file.mimeType, size: file.size, sha256: file.sha256, + expiresAt: file.expiresAt, + createdAt: file.createdAt, path: this.getAbsolutePath(file.filePath), }; } diff --git a/server/src/scenario/scenario.service.ts b/server/src/scenario/scenario.service.ts index c951a39..c1e5376 100644 --- a/server/src/scenario/scenario.service.ts +++ b/server/src/scenario/scenario.service.ts @@ -732,6 +732,9 @@ export class ScenarioService { name: item.name, mimeType: item.mimeType, size: item.size, + sha256: item.sha256, + expiresAt: item.expiresAt, + createdAt: item.createdAt, })), total: result.total, }; @@ -790,6 +793,9 @@ export class ScenarioService { name: item.name, mimeType: item.mimeType, size: item.size, + sha256: item.sha256, + expiresAt: item.expiresAt, + createdAt: item.createdAt, })), total: result.total, };