diff --git a/backend/src/storage/artifacts.controller.ts b/backend/src/storage/artifacts.controller.ts index eb51e1b2..ca5a519d 100644 --- a/backend/src/storage/artifacts.controller.ts +++ b/backend/src/storage/artifacts.controller.ts @@ -1,5 +1,15 @@ -import { Controller, Get, Query, Param, Res, StreamableFile } from '@nestjs/common'; -import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { + Controller, + Get, + Delete, + Query, + Param, + Res, + StreamableFile, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { ApiOkResponse, ApiNoContentResponse, ApiTags } from '@nestjs/swagger'; import { ZodValidationPipe } from 'nestjs-zod'; import type { Response } from 'express'; @@ -60,4 +70,16 @@ export class ArtifactsController { return new StreamableFile(buffer); } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiNoContentResponse({ + description: 'Artifact deleted successfully', + }) + async deleteArtifact( + @CurrentAuth() auth: AuthContext | null, + @Param(new ZodValidationPipe(ArtifactIdParamSchema)) params: ArtifactIdParamDto, + ) { + await this.artifactsService.deleteArtifact(auth, params.id); + } } diff --git a/backend/src/storage/artifacts.repository.ts b/backend/src/storage/artifacts.repository.ts index a1eb4388..1fc641c0 100644 --- a/backend/src/storage/artifacts.repository.ts +++ b/backend/src/storage/artifacts.repository.ts @@ -88,6 +88,16 @@ export class ArtifactsRepository { return and(base, eq(artifactsTable.organizationId, organizationId)); } + async delete(id: string, options: { organizationId?: string | null } = {}): Promise { + const filters = [eq(artifactsTable.id, id)]; + if (options.organizationId) { + filters.push(eq(artifactsTable.organizationId, options.organizationId)); + } + const where = filters.length > 1 ? and(...filters) : filters[0]; + const result = await this.db.delete(artifactsTable).where(where).returning(); + return result.length > 0; + } + private buildFilters(options: ArtifactQueryOptions) { const filters = []; diff --git a/backend/src/storage/artifacts.service.ts b/backend/src/storage/artifacts.service.ts index cd3cf13b..9d5813a8 100644 --- a/backend/src/storage/artifacts.service.ts +++ b/backend/src/storage/artifacts.service.ts @@ -85,6 +85,25 @@ export class ArtifactsService { }; } + async deleteArtifact(auth: AuthContext | null, artifactId: string): Promise { + const artifact = await this.getArtifactRecord(auth, artifactId); + const organizationId = this.requireOrganizationId(auth); + + // Delete the associated file first + try { + await this.filesService.deleteFile(auth, artifact.fileId); + } catch (error) { + // Log but don't fail if file is already deleted + console.warn(`Failed to delete file ${artifact.fileId} for artifact ${artifactId}:`, error); + } + + // Delete the artifact record + const deleted = await this.repository.delete(artifactId, { organizationId }); + if (!deleted) { + throw new NotFoundException(`Artifact ${artifactId} not found`); + } + } + private toMetadata(record: ArtifactRecord): ArtifactMetadataDto { return { id: record.id, diff --git a/backend/src/storage/dto/artifacts.dto.ts b/backend/src/storage/dto/artifacts.dto.ts index dfcf2efe..e465a1ba 100644 --- a/backend/src/storage/dto/artifacts.dto.ts +++ b/backend/src/storage/dto/artifacts.dto.ts @@ -17,3 +17,10 @@ export const ArtifactIdParamSchema = z.object({ }); export class ArtifactIdParamDto extends createZodDto(ArtifactIdParamSchema) {} + +// Schema for run artifact downloads where the path param is :artifactId +export const RunArtifactIdParamSchema = z.object({ + artifactId: z.string().uuid(), +}); + +export class RunArtifactIdParamDto extends createZodDto(RunArtifactIdParamSchema) {} diff --git a/backend/src/workflows/workflows.controller.ts b/backend/src/workflows/workflows.controller.ts index d6d1e1e5..f0f9c41d 100644 --- a/backend/src/workflows/workflows.controller.ts +++ b/backend/src/workflows/workflows.controller.ts @@ -62,7 +62,7 @@ import { CurrentAuth } from '../auth/auth-context.decorator'; import type { AuthContext } from '../auth/types'; import { RequireWorkflowRole, WorkflowRoleGuard } from './workflow-role.guard'; import { RunArtifactsResponseDto } from '../storage/dto/artifact.dto'; -import { ArtifactIdParamDto, ArtifactIdParamSchema } from '../storage/dto/artifacts.dto'; +import { RunArtifactIdParamDto, RunArtifactIdParamSchema } from '../storage/dto/artifacts.dto'; import type { WorkflowTerminalRecord } from '../database/schema'; import { NodeIOService } from '../node-io/node-io.service'; @@ -757,14 +757,14 @@ export class WorkflowsController { }) async downloadRunArtifact( @Param('runId') runId: string, - @Param(new ZodValidationPipe(ArtifactIdParamSchema)) params: ArtifactIdParamDto, + @Param(new ZodValidationPipe(RunArtifactIdParamSchema)) params: RunArtifactIdParamDto, @CurrentAuth() auth: AuthContext | null, @Res({ passthrough: true }) res: Response, ) { const { artifact, buffer, file } = await this.artifactsService.downloadArtifactForRun( auth, runId, - params.id, + params.artifactId, ); res.setHeader('Content-Type', file.mimeType); diff --git a/frontend/src/components/artifacts/RunArtifactsPanel.tsx b/frontend/src/components/artifacts/RunArtifactsPanel.tsx index f74aed3e..2953fef3 100644 --- a/frontend/src/components/artifacts/RunArtifactsPanel.tsx +++ b/frontend/src/components/artifacts/RunArtifactsPanel.tsx @@ -146,6 +146,13 @@ export function RunArtifactsPanel({ runId }: RunArtifactsPanelProps) { copiedRemoteUri, ]); + const handleDownloadAll = () => { + if (!entry || !entry.artifacts.length) return; + entry.artifacts.forEach((artifact) => { + downloadArtifact(artifact, { runId: runId || undefined }); + }); + }; + return (
@@ -168,6 +175,18 @@ export function RunArtifactsPanel({ runId }: RunArtifactsPanelProps) { Refresh ) : null} + {entry?.artifacts && entry.artifacts.length > 0 && ( + + )}
{content}
@@ -177,8 +196,6 @@ export function RunArtifactsPanel({ runId }: RunArtifactsPanelProps) { function ArtifactRow({ artifact, onDownload, - onCopy, - copied, onCopyRemoteUri, copiedRemoteUri, isDownloading, @@ -240,12 +257,12 @@ function ArtifactRow({ {formatTimestamp(artifact.createdAt)} - +
- + */}
)} - - {artifact.runId} + + + {workflowName} + - - {artifact.componentRef} + + + {artifact.runId.substring(0, 8)}… + {formatBytes(artifact.size)} @@ -254,17 +273,22 @@ function ArtifactLibraryRow({ {formatTimestamp(artifact.createdAt)} - -
+ +