main
vue 253 lines 6.58 KB
Raw
1 <template>
2 <div class="agent-data-store-tab">
3 <div class="flex gap-4">
4 <div class="filters-section" style="min-width: 280px; max-width: 320px">
5 <n-card size="small" title="Filters" :segmented="{ content: true }">
6 <div class="flex flex-col gap-3">
7 <n-input v-model:value="textFilter" placeholder="Search artifacts..." clearable size="small">
8 <template #prefix>
9 <Icon :name="SearchIcon" :size="16" />
10 </template>
11 </n-input>
12
13 <n-select
14 v-model:value="statusFilter"
15 :options="statusOptions"
16 placeholder="Filter by status"
17 size="small"
18 clearable
19 />
20
21 <n-button type="primary" secondary size="small" :loading @click="getArtifacts()">
22 <template #icon>
23 <Icon :name="RefreshIcon" />
24 </template>
25 Refresh
26 </n-button>
27
28 <n-divider class="my-2!" />
29
30 <div class="flex flex-col gap-2 text-sm">
31 <div class="flex items-center justify-between">
32 <span class="text-secondary-color">Total:</span>
33 <span class="font-mono">{{ artifacts.length }}</span>
34 </div>
35 <div class="flex items-center justify-between">
36 <span class="text-secondary-color">Filtered:</span>
37 <span class="font-mono">{{ artifactsFiltered.length }}</span>
38 </div>
39 </div>
40 </div>
41 </n-card>
42 </div>
43
44 <div class="artifacts-section flex-1">
45 <n-spin :show="loading">
46 <n-scrollbar style="max-height: 600px">
47 <div class="flex flex-col gap-3 pr-2">
48 <template v-if="artifactsFiltered.length">
49 <ArtifactCard
50 v-for="artifact in itemsPaginated"
51 :key="artifact.id"
52 :artifact
53 show-actions
54 hoverable
55 @download="downloadArtifact(artifact)"
56 @delete="deleteArtifact(artifact)"
57 @details="showArtifactDetails(artifact)"
58 />
59 </template>
60 <template v-else>
61 <n-empty v-if="!loading" description="No artifacts found" class="h-48 justify-center" />
62 </template>
63 </div>
64 </n-scrollbar>
65
66 <div v-if="artifactsFiltered.length > pageSize" class="mt-4 flex justify-end">
67 <n-pagination
68 v-model:page="page"
69 :page-size
70 :page-slot="5"
71 :item-count="artifactsFiltered.length"
72 size="small"
73 />
74 </div>
75 </n-spin>
76 </div>
77 </div>
78
79 <!-- Artifact Details Modal -->
80 <n-modal
81 v-model:show="showDetailsModal"
82 preset="card"
83 title="Artifact Details"
84 :style="{ width: '800px' }"
85 :segmented="{ content: true }"
86 >
87 <ArtifactDetails v-if="selectedArtifact" :artifact="selectedArtifact" />
88 </n-modal>
89 </div>
90 </template>
91
92 <script setup lang="ts">
93 import type { SelectOption } from "naive-ui"
94 import type { Agent, AgentArtifactData } from "@/types/agents.d"
95 import { refDebounced } from "@vueuse/core"
96 import { saveAs } from "file-saver"
97 import {
98 NButton,
99 NCard,
100 NDivider,
101 NEmpty,
102 NInput,
103 NModal,
104 NPagination,
105 NScrollbar,
106 NSelect,
107 NSpin,
108 useDialog,
109 useMessage
110 } from "naive-ui"
111 import { computed, onBeforeMount, ref } from "vue"
112 import Api from "@/api"
113 import Icon from "@/components/common/Icon.vue"
114 import ArtifactCard from "./ArtifactCard.vue"
115 import ArtifactDetails from "./ArtifactDetails.vue"
116
117 // TODO-FE: join AgentDataStoreTab + AgentDataStoreTabCompact (Data Store Tab on Agent Details page)
118
119 const props = defineProps<{
120 agent: Agent
121 }>()
122
123 const message = useMessage()
124 const dialog = useDialog()
125
126 const SearchIcon = "carbon:search"
127 const RefreshIcon = "carbon:renew"
128
129 const loading = ref(false)
130 const artifacts = ref<AgentArtifactData[]>([])
131 const textFilter = ref<string | null>(null)
132 const textFilterDebounced = refDebounced<string | null>(textFilter, 300)
133 const statusFilter = ref<string | undefined>(undefined)
134 const page = ref(1)
135 const pageSize = ref(20)
136 const showDetailsModal = ref(false)
137 const selectedArtifact = ref<AgentArtifactData | null>(null)
138
139 const statusOptions: SelectOption[] = [
140 { label: "All", value: undefined },
141 { label: "Completed", value: "completed" },
142 { label: "Failed", value: "failed" },
143 { label: "Processing", value: "processing" }
144 ]
145
146 const artifactsFiltered = computed(() => {
147 return artifacts.value.filter(artifact => {
148 // Text filter
149 const matchesText = (artifact.artifact_name + artifact.flow_id + artifact.file_name)
150 .toString()
151 .toLowerCase()
152 .includes((textFilterDebounced.value || "").toString().toLowerCase())
153
154 // Status filter
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.agent.agent_id)
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.agent.agent_id, 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.agent.agent_id, 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 {
241 .filters-section {
242 :deep() {
243 .n-card__content {
244 padding: 16px;
245 }
246 }
247 }
248
249 .artifacts-section {
250 min-height: 300px;
251 }
252 }
253 </style>