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
This commit is contained in:
2026-04-21 16:46:33 +03:00
parent 86f9ac5603
commit 9580a7b2df
5 changed files with 115 additions and 52 deletions
+1
View File
@@ -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"
+34 -6
View File
@@ -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<ScenarioRunStatus, BadgeVariant> = {
@@ -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 (
<div>
<div
@@ -481,11 +493,27 @@ export function RunDetailPage() {
{
key: 'download',
header: '',
render: (f) => (
<a href={runFiles.contentUrl(id!, runId!, f.id)} download={f.name}>
{t('files.download')}
</a>
),
render: (f) => {
const expired = isFileExpired(f);
const tooltip = expired ? t('files.expiredTooltip') : t('files.download');
return (
<span title={tooltip}>
<Button
variant="secondary"
size="sm"
title={tooltip}
aria-label={tooltip}
disabled={expired}
onClick={() =>
handleFileDownload(runFiles.contentUrl(id!, runId!, f.id), f.name)
}
>
<Download size={14} />
</Button>
</span>
);
},
},
] satisfies TableColumn<FileMetadata>[]
}
@@ -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,7 +660,6 @@ export function ScenarioDetailPage() {
</Button>
</div>
<Card>
<Table<FileMetadata>
loading={filesLoading}
data={files}
@@ -684,18 +694,32 @@ export function ScenarioDetailPage() {
{
key: 'download',
header: '',
render: (f) => (
<a href={scenarioFiles.contentUrl(id!, f.id)} download={f.name}>
{t('files.download')}
</a>
),
render: (f) => {
const expired = isFileExpired(f);
const tooltip = expired ? t('files.expiredTooltip') : t('files.download');
return (
<span title={tooltip}>
<Button
variant="secondary"
size="sm"
title={tooltip}
aria-label={tooltip}
disabled={expired}
onClick={() =>
handleFileDownload(scenarioFiles.contentUrl(id!, f.id), f.name)
}
>
<Download size={14} />
</Button>
</span>
);
},
},
] satisfies TableColumn<FileMetadata>[]
}
emptyMessage={t('files.empty')}
/>
<p>{t('files.totalCount', { count: filesTotal })}</p>
</Card>
</div>
{/* ── Upload modal ─────────────────────────────────────────────── */}
+4
View File
@@ -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),
};
}
+6
View File
@@ -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,
};