Skip to content

Commit ed8df46

Browse files
committed
feat: added artifect delete method in frontend and backend both.
1 parent 127be86 commit ed8df46

File tree

6 files changed

+108
-8
lines changed

6 files changed

+108
-8
lines changed

backend/src/storage/artifacts.controller.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
import { Controller, Get, Query, Param, Res, StreamableFile } from '@nestjs/common';
2-
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
1+
import {
2+
Controller,
3+
Get,
4+
Delete,
5+
Query,
6+
Param,
7+
Res,
8+
StreamableFile,
9+
HttpCode,
10+
HttpStatus,
11+
} from '@nestjs/common';
12+
import { ApiOkResponse, ApiNoContentResponse, ApiTags } from '@nestjs/swagger';
313
import { ZodValidationPipe } from 'nestjs-zod';
414
import type { Response } from 'express';
515

@@ -60,4 +70,16 @@ export class ArtifactsController {
6070

6171
return new StreamableFile(buffer);
6272
}
73+
74+
@Delete(':id')
75+
@HttpCode(HttpStatus.NO_CONTENT)
76+
@ApiNoContentResponse({
77+
description: 'Artifact deleted successfully',
78+
})
79+
async deleteArtifact(
80+
@CurrentAuth() auth: AuthContext | null,
81+
@Param(new ZodValidationPipe(ArtifactIdParamSchema)) params: ArtifactIdParamDto,
82+
) {
83+
await this.artifactsService.deleteArtifact(auth, params.id);
84+
}
6385
}

backend/src/storage/artifacts.repository.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ export class ArtifactsRepository {
8888
return and(base, eq(artifactsTable.organizationId, organizationId));
8989
}
9090

91+
async delete(id: string, options: { organizationId?: string | null } = {}): Promise<boolean> {
92+
const filters = [eq(artifactsTable.id, id)];
93+
if (options.organizationId) {
94+
filters.push(eq(artifactsTable.organizationId, options.organizationId));
95+
}
96+
const where = filters.length > 1 ? and(...filters) : filters[0];
97+
const result = await this.db.delete(artifactsTable).where(where).returning();
98+
return result.length > 0;
99+
}
100+
91101
private buildFilters(options: ArtifactQueryOptions) {
92102
const filters = [];
93103

backend/src/storage/artifacts.service.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,25 @@ export class ArtifactsService {
8585
};
8686
}
8787

88+
async deleteArtifact(auth: AuthContext | null, artifactId: string): Promise<void> {
89+
const artifact = await this.getArtifactRecord(auth, artifactId);
90+
const organizationId = this.requireOrganizationId(auth);
91+
92+
// Delete the associated file first
93+
try {
94+
await this.filesService.deleteFile(auth, artifact.fileId);
95+
} catch (error) {
96+
// Log but don't fail if file is already deleted
97+
console.warn(`Failed to delete file ${artifact.fileId} for artifact ${artifactId}:`, error);
98+
}
99+
100+
// Delete the artifact record
101+
const deleted = await this.repository.delete(artifactId, { organizationId });
102+
if (!deleted) {
103+
throw new NotFoundException(`Artifact ${artifactId} not found`);
104+
}
105+
}
106+
88107
private toMetadata(record: ArtifactRecord): ArtifactMetadataDto {
89108
return {
90109
id: record.id,

frontend/src/pages/ArtifactLibrary.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useState } from 'react';
22
import { Link } from 'react-router-dom';
3-
import { Download, RefreshCw, Search, Copy, ExternalLink } from 'lucide-react';
3+
import { Download, RefreshCw, Search, Copy, ExternalLink, Trash2 } from 'lucide-react';
44
import { useArtifactStore } from '@/store/artifactStore';
55
import { api } from '@/services/api';
66
import { Button } from '@/components/ui/button';
@@ -28,8 +28,16 @@ const formatTimestamp = (value: string) => {
2828

2929
export function ArtifactLibrary() {
3030
const [searchQuery, setSearchQuery] = useState('');
31-
const { library, libraryLoading, libraryError, fetchLibrary, downloadArtifact, downloading } =
32-
useArtifactStore();
31+
const {
32+
library,
33+
libraryLoading,
34+
libraryError,
35+
fetchLibrary,
36+
downloadArtifact,
37+
downloading,
38+
deleteArtifact,
39+
deleting,
40+
} = useArtifactStore();
3341
const [copiedRemoteUri, setCopiedRemoteUri] = useState<string | null>(null);
3442
const [workflows, setWorkflows] = useState<Record<string, string>>({});
3543

@@ -154,6 +162,8 @@ export function ArtifactLibrary() {
154162
artifact={artifact}
155163
workflowName={workflows[artifact.workflowId] || 'Unknown Workflow'}
156164
onDownload={() => downloadArtifact(artifact)}
165+
onDelete={() => deleteArtifact(artifact.id)}
166+
isDeleting={Boolean(deleting[artifact.id])}
157167
onCopyRemoteUri={async (uri: string) => {
158168
try {
159169
await navigator.clipboard.writeText(uri);
@@ -182,16 +192,20 @@ function ArtifactLibraryRow({
182192
artifact,
183193
workflowName,
184194
onDownload,
195+
onDelete,
185196
onCopyRemoteUri,
186197
copiedRemoteUri,
187198
isDownloading,
199+
isDeleting,
188200
}: {
189201
artifact: ArtifactMetadata;
190202
workflowName: string;
191203
onDownload: () => void;
204+
onDelete: () => void;
192205
onCopyRemoteUri: (uri: string) => void;
193206
copiedRemoteUri: string | null;
194207
isDownloading: boolean;
208+
isDeleting: boolean;
195209
}) {
196210
const remoteUploads = getRemoteUploads(artifact);
197211

@@ -261,7 +275,7 @@ function ArtifactLibraryRow({
261275
</td>
262276
<td className="px-3 md:px-4 py-3 md:py-4 align-top text-left">
263277
<div className="flex flex-wrap justify-start gap-1 md:gap-2">
264-
{/* <Button
278+
<Button
265279
type="button"
266280
variant="ghost"
267281
size="sm"
@@ -271,10 +285,11 @@ function ArtifactLibraryRow({
271285
onDelete();
272286
}
273287
}}
288+
disabled={isDeleting}
274289
>
275290
<Trash2 className="h-4 w-4" />
276-
<span className="hidden md:inline">Delete</span>
277-
</Button> */}
291+
<span className="hidden md:inline">{isDeleting ? 'Deleting…' : 'Delete'}</span>
292+
</Button>
278293
<Button
279294
type="button"
280295
variant="ghost"

frontend/src/services/api.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,17 @@ export const api = {
808808
}
809809
return await response.blob();
810810
},
811+
812+
delete: async (id: string): Promise<void> => {
813+
const headers = await getAuthHeaders();
814+
const response = await fetch(`${API_BASE_URL}/api/v1/artifacts/${id}`, {
815+
method: 'DELETE',
816+
headers,
817+
});
818+
if (!response.ok) {
819+
throw new Error('Failed to delete artifact');
820+
}
821+
},
811822
},
812823

813824
humanInputs: {

frontend/src/store/artifactStore.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ interface ArtifactStoreState {
1616
libraryError: string | null;
1717
libraryFilters?: ArtifactListFilters;
1818
downloading: Record<string, boolean>;
19+
deleting: Record<string, boolean>;
1920
fetchRunArtifacts: (runId: string, force?: boolean) => Promise<void>;
2021
fetchLibrary: (filters?: ArtifactListFilters) => Promise<void>;
2122
downloadArtifact: (artifact: ArtifactMetadata, options?: { runId?: string }) => Promise<void>;
23+
deleteArtifact: (artifactId: string) => Promise<void>;
2224
}
2325

2426
export const useArtifactStore = create<ArtifactStoreState>((set, get) => ({
@@ -27,6 +29,7 @@ export const useArtifactStore = create<ArtifactStoreState>((set, get) => ({
2729
libraryLoading: false,
2830
libraryError: null,
2931
downloading: {},
32+
deleting: {},
3033
async fetchRunArtifacts(runId: string, force = false) {
3134
const existing = get().runArtifacts[runId];
3235
if (!force && existing && !existing.error && existing.artifacts.length > 0) {
@@ -113,4 +116,24 @@ export const useArtifactStore = create<ArtifactStoreState>((set, get) => ({
113116
});
114117
}
115118
},
119+
async deleteArtifact(artifactId: string) {
120+
set((state) => ({
121+
deleting: { ...state.deleting, [artifactId]: true },
122+
}));
123+
try {
124+
await api.artifacts.delete(artifactId);
125+
// Remove artifact from library after successful deletion
126+
set((state) => ({
127+
library: state.library.filter((a) => a.id !== artifactId),
128+
}));
129+
} catch (error) {
130+
console.error('Failed to delete artifact', error);
131+
throw error;
132+
} finally {
133+
set((state) => {
134+
const { [artifactId]: _removed, ...next } = state.deleting;
135+
return { deleting: next };
136+
});
137+
}
138+
},
116139
}));

0 commit comments

Comments
 (0)