main
vue 250 lines 6.2 KB
Raw
1 <template>
2 <div class="agent-data-store-tab-compact">
3 <div class="flex flex-col gap-3">
4 <div class="filters-bar flex flex-wrap items-center gap-2">
5 <n-input
6 v-model:value="textFilter"
7 placeholder="Search artifacts..."
8 clearable
9 size="small"
10 style="max-width: 250px"
11 >
12 <template #prefix>
13 <Icon :name="SearchIcon" :size="14" />
14 </template>
15 </n-input>
16
17 <n-select
18 v-model:value="statusFilter"
19 :options="statusOptions"
20 placeholder="Status"
21 size="small"
22 clearable
23 style="max-width: 150px"
24 />
25
26 <n-button type="primary" secondary size="small" :loading @click="getArtifacts()">
27 <template #icon>
28 <Icon :name="RefreshIcon" />
29 </template>
30 </n-button>
31
32 <div class="flex-1"></div>
33
34 <div class="text-secondary-color flex items-center gap-3 text-xs">
35 <span>
36 Total:
37 <strong class="font-mono">{{ artifacts.length }}</strong>
38 </span>
39 <span>
40 Filtered:
41 <strong class="font-mono">{{ artifactsFiltered.length }}</strong>
42 </span>
43 </div>
44 </div>
45
46 <n-spin :show="loading">
47 <div class="artifacts-list">
48 <n-scrollbar style="max-height: 400px">
49 <div class="flex flex-col gap-2 pr-2">
50 <template v-if="artifactsFiltered.length">
51 <ArtifactCardCompact
52 v-for="artifact in itemsPaginated"
53 :key="artifact.id"
54 :artifact
55 show-actions
56 @download="downloadArtifact(artifact)"
57 @delete="deleteArtifact(artifact)"
58 @details="showArtifactDetails(artifact)"
59 />
60 </template>
61 <template v-else>
62 <n-empty
63 v-if="!loading"
64 description="No artifacts found"
65 class="h-32 justify-center"
66 size="small"
67 />
68 </template>
69 </div>
70 </n-scrollbar>
71
72 <div v-if="artifactsFiltered.length > pageSize" class="mt-3 flex justify-end">
73 <n-pagination
74 v-model:page="page"
75 :page-size
76 :page-slot="5"
77 :item-count="artifactsFiltered.length"
78 size="small"
79 />
80 </div>
81 </div>
82 </n-spin>
83 </div>
84
85 <!-- Artifact Details Modal -->
86 <n-modal
87 v-model:show="showDetailsModal"
88 preset="card"
89 title="Artifact Details"
90 :style="{ width: '700px' }"
91 :segmented="{ content: true }"
92 >
93 <ArtifactDetails v-if="selectedArtifact" :artifact="selectedArtifact" />
94 </n-modal>
95 </div>
96 </template>
97
98 <script setup lang="ts">
99 import type { SelectOption } from "naive-ui"
100 import type { AgentArtifactData } from "@/types/agents.d"
101 import { refDebounced } from "@vueuse/core"
102 import { saveAs } from "file-saver"
103 import {
104 NButton,
105 NEmpty,
106 NInput,
107 NModal,
108 NPagination,
109 NScrollbar,
110 NSelect,
111 NSpin,
112 useDialog,
113 useMessage
114 } from "naive-ui"
115 import { computed, onBeforeMount, ref } from "vue"
116 import Api from "@/api"
117 import Icon from "@/components/common/Icon.vue"
118 import ArtifactCardCompact from "./ArtifactCardCompact.vue"
119 import ArtifactDetails from "./ArtifactDetails.vue"
120
121 const props = defineProps<{
122 agentId: string
123 }>()
124
125 const message = useMessage()
126 const dialog = useDialog()
127
128 const SearchIcon = "carbon:search"
129 const RefreshIcon = "carbon:renew"
130
131 const loading = ref(false)
132 const artifacts = ref<AgentArtifactData[]>([])
133 const textFilter = ref<string | null>(null)
134 const textFilterDebounced = refDebounced<string | null>(textFilter, 300)
135 const statusFilter = ref<string | null>(null)
136 const page = ref(1)
137 const pageSize = ref(10)
138 const showDetailsModal = ref(false)
139 const selectedArtifact = ref<AgentArtifactData | null>(null)
140
141 const statusOptions: SelectOption[] = [
142 { label: "All", value: undefined },
143 { label: "Completed", value: "completed" },
144 { label: "Failed", value: "failed" },
145 { label: "Processing", value: "processing" }
146 ]
147
148 const artifactsFiltered = computed(() => {
149 return artifacts.value.filter(artifact => {
150 const matchesText = (artifact.artifact_name + artifact.flow_id + artifact.file_name)
151 .toString()
152 .toLowerCase()
153 .includes((textFilterDebounced.value || "").toString().toLowerCase())
154
155 const matchesStatus = !statusFilter.value || artifact.status === statusFilter.value
156
157 return matchesText && matchesStatus
158 })
159 })
160
161 const itemsPaginated = computed(() => {
162 const from = (page.value - 1) * pageSize.value
163 const to = page.value * pageSize.value
164
165 return artifactsFiltered.value.slice(from, to)
166 })
167
168 function getArtifacts() {
169 loading.value = true
170
171 Api.agents
172 .listAgentArtifacts(props.agentId)
173 .then(res => {
174 if (res.data.success) {
175 artifacts.value = res.data.data || []
176 } else {
177 message.error(res.data?.message || "An error occurred. Please try again later.")
178 }
179 })
180 .catch(err => {
181 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
182 })
183 .finally(() => {
184 loading.value = false
185 })
186 }
187
188 function downloadArtifact(artifact: AgentArtifactData) {
189 message.loading(`Downloading ${artifact.file_name}...`)
190
191 Api.agents
192 .downloadAgentArtifact(props.agentId, artifact.id)
193 .then(res => {
194 saveAs(res.data, artifact.file_name)
195
196 message.success(`Downloaded ${artifact.file_name}`)
197 })
198 .catch(err => {
199 message.error(err.response?.data?.message || "Failed to download artifact")
200 })
201 }
202
203 function deleteArtifact(artifact: AgentArtifactData) {
204 dialog.warning({
205 title: "Delete Artifact",
206 content: `Are you sure you want to delete "${artifact.file_name}"? This action cannot be undone.`,
207 positiveText: "Delete",
208 negativeText: "Cancel",
209 onPositiveClick: () => {
210 Api.agents
211 .deleteAgentArtifact(props.agentId, artifact.id)
212 .then(res => {
213 if (res.data.success) {
214 message.success("Artifact deleted successfully")
215 getArtifacts()
216 } else {
217 message.error(res.data?.message || "Failed to delete artifact")
218 }
219 })
220 .catch(err => {
221 message.error(err.response?.data?.message || "Failed to delete artifact")
222 })
223 }
224 })
225 }
226
227 function showArtifactDetails(artifact: AgentArtifactData) {
228 selectedArtifact.value = artifact
229 showDetailsModal.value = true
230 }
231
232 onBeforeMount(() => {
233 getArtifacts()
234 })
235 </script>
236
237 <style lang="scss" scoped>
238 // TODO-FE: remove style
239
240 .agent-data-store-tab-compact {
241 .filters-bar {
242 padding-bottom: 8px;
243 border-bottom: 1px solid var(--border-color);
244 }
245
246 .artifacts-list {
247 min-height: 200px;
248 }
249 }
250 </style>