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:
@@ -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"
|
||||
|
||||
@@ -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,53 +660,66 @@ export function ScenarioDetailPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Table<FileMetadata>
|
||||
loading={filesLoading}
|
||||
data={files}
|
||||
rowKey={(f) => f.id}
|
||||
columns={
|
||||
[
|
||||
{
|
||||
key: 'name',
|
||||
header: t('files.name'),
|
||||
render: (f) => f.name,
|
||||
<Table<FileMetadata>
|
||||
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 ? <Timestamp value={f.expiresAt} /> : '—'),
|
||||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
header: t('files.createdAt'),
|
||||
render: (f) => <Timestamp value={f.createdAt} />,
|
||||
},
|
||||
{
|
||||
key: 'download',
|
||||
header: '',
|
||||
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>
|
||||
);
|
||||
},
|
||||
{
|
||||
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 ? <Timestamp value={f.expiresAt} /> : '—'),
|
||||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
header: t('files.createdAt'),
|
||||
render: (f) => <Timestamp value={f.createdAt} />,
|
||||
},
|
||||
{
|
||||
key: 'download',
|
||||
header: '',
|
||||
render: (f) => (
|
||||
<a href={scenarioFiles.contentUrl(id!, f.id)} download={f.name}>
|
||||
{t('files.download')}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
] satisfies TableColumn<FileMetadata>[]
|
||||
}
|
||||
emptyMessage={t('files.empty')}
|
||||
/>
|
||||
<p>{t('files.totalCount', { count: filesTotal })}</p>
|
||||
</Card>
|
||||
},
|
||||
] satisfies TableColumn<FileMetadata>[]
|
||||
}
|
||||
emptyMessage={t('files.empty')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Upload modal ─────────────────────────────────────────────── */}
|
||||
|
||||
Reference in New Issue
Block a user