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,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 ─────────────────────────────────────────────── */}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user