main
vue 243 lines 7.27 KB
Raw
1 <template>
2 <CardEntity hoverable embedded>
3 <template #headerMain>
4 <div class="flex items-center gap-2">
5 <Icon name="carbon:integration" :size="16" />
6 <span class="font-medium">{{ integration.display_name }}</span>
7 </div>
8 </template>
9
10 <template #headerExtra>
11 <n-button size="small" secondary :type="integration.enabled ? 'warning' : 'success'" @click="toggleEnabled">
12 <template #icon>
13 <Icon :name="integration.enabled ? PauseIcon : PlayIcon" :size="14" />
14 </template>
15 {{ integration.enabled ? "Disable" : "Enable" }}
16 </n-button>
17 </template>
18
19 <template #default>
20 <div class="flex flex-col gap-2">
21 <div class="flex flex-col gap-1 text-sm">
22 <div class="flex flex-wrap items-center gap-1">
23 <span class="font-medium">Org-Id:</span>
24 <code class="break-all">{{ integration.shuffle_org_id }}</code>
25 </div>
26 </div>
27 <div class="flex flex-wrap gap-1">
28 <Badge v-if="verifyResult === 'ok'" type="splitted" color="success" size="small">
29 <template #label>Probe</template>
30 <template #value>{{ verifyAppCount }} app(s)</template>
31 </Badge>
32 <Badge v-else-if="verifyResult === 'fail'" type="splitted" color="danger" size="small">
33 <template #label>Probe</template>
34 <template #value>failed</template>
35 </Badge>
36
37 <Badge type="splitted" size="small">
38 <template #label>Last used</template>
39 <template v-if="integration.last_used_at" #value>
40 {{ formatDate(integration.last_used_at, dFormats.datetime) }}
41 </template>
42 <template v-else #value>never used</template>
43 </Badge>
44
45 <Badge type="splitted" size="small">
46 <template #label>Owner</template>
47 <template #value>{{ integration.created_by }}</template>
48 </Badge>
49 </div>
50 </div>
51 </template>
52
53 <template #footer>
54 <div class="flex items-center justify-end gap-2">
55 <n-tooltip>
56 <template #trigger>
57 <n-button
58 size="tiny"
59 quaternary
60 circle
61 :loading="verifying"
62 :type="verifyButtonType"
63 @click="verify"
64 >
65 <template #icon>
66 <Icon :name="verifyButtonIcon" :size="14" />
67 </template>
68 </n-button>
69 </template>
70 {{ verifyTooltip }}
71 </n-tooltip>
72
73 <n-tooltip>
74 <template #trigger>
75 <n-button size="tiny" quaternary circle @click="$emit('manageApps')">
76 <template #icon>
77 <Icon :name="ManageAppsIcon" :size="14" />
78 </template>
79 </n-button>
80 </template>
81 Manage apps
82 </n-tooltip>
83
84 <n-tooltip>
85 <template #trigger>
86 <n-button size="tiny" quaternary circle @click="$emit('edit')">
87 <template #icon>
88 <Icon :name="EditIcon" :size="14" />
89 </template>
90 </n-button>
91 </template>
92 Edit
93 </n-tooltip>
94
95 <n-tooltip>
96 <template #trigger>
97 <n-popconfirm to="body" @positive-click="confirmDelete">
98 <template #trigger>
99 <n-button size="tiny" quaternary circle>
100 <template #icon>
101 <Icon :name="DeleteIcon" :size="14" />
102 </template>
103 </n-button>
104 </template>
105 Delete this integration?
106 <template #icon>
107 <Icon :name="WarningIcon" :size="14" />
108 </template>
109 </n-popconfirm>
110 </template>
111 Delete
112 </n-tooltip>
113 </div>
114 </template>
115 </CardEntity>
116 </template>
117
118 <script setup lang="ts">
119 import type { ApiError } from "@/types/common"
120 import type { ShuffleIntegration } from "@/types/notifications.d"
121 import { NButton, NPopconfirm, NTooltip, useMessage } from "naive-ui"
122 import { computed, ref } from "vue"
123 import Api from "@/api"
124 import Badge from "@/components/common/Badge.vue"
125 import CardEntity from "@/components/common/cards/CardEntity.vue"
126 import Icon from "@/components/common/Icon.vue"
127 import { useSettingsStore } from "@/stores/settings"
128 import { getApiErrorMessage } from "@/utils"
129 import { formatDate } from "@/utils/format"
130
131 const props = defineProps<{
132 integration: ShuffleIntegration
133 }>()
134
135 const emit = defineEmits<{
136 (e: "edit"): void
137 (e: "deleted"): void
138 (e: "toggled"): void
139 (e: "manageApps"): void
140 }>()
141
142 const EditIcon = "carbon:edit"
143 const DeleteIcon = "carbon:trash-can"
144 const PauseIcon = "carbon:pause"
145 const PlayIcon = "carbon:play"
146 const ManageAppsIcon = "carbon:catalog"
147 const VerifyIcon = "carbon:checkmark-outline"
148 const VerifyOkIcon = "carbon:checkmark-filled"
149 const VerifyFailIcon = "carbon:misuse"
150 const WarningIcon = "carbon:warning"
151
152 const message = useMessage()
153 const dFormats = useSettingsStore().dateFormat
154
155 // Verify state — null until the user clicks "Test connection." Drives
156 // both the inline status badge and the verify button's color/icon so
157 // admins get an at-a-glance signal that the probe succeeded without
158 // having to read the badge text.
159 const verifying = ref(false)
160 const verifyResult = ref<"ok" | "fail" | null>(null)
161 const verifyAppCount = ref<number | null>(null)
162
163 // naive-ui n-button accepts type='success'|'error'|'default'|... — we
164 // map verify result to a colored button so the icon turns green on a
165 // good probe and red on a bad one, and stays neutral before/while
166 // running. `quaternary` keeps the button visually subtle when there's
167 // no result yet.
168 const verifyButtonType = computed<"default" | "success" | "error">(() => {
169 if (verifyResult.value === "ok") return "success"
170 if (verifyResult.value === "fail") return "error"
171 return "default"
172 })
173
174 const verifyButtonIcon = computed(() => {
175 if (verifyResult.value === "ok") return VerifyOkIcon
176 if (verifyResult.value === "fail") return VerifyFailIcon
177 return VerifyIcon
178 })
179
180 const verifyTooltip = computed(() => {
181 if (verifyResult.value === "ok") return `Probe OK · ${verifyAppCount.value ?? "?"} app(s) · click to re-test`
182 if (verifyResult.value === "fail") return "Probe failed · click to re-test"
183 return "Test connection"
184 })
185
186 async function verify() {
187 verifying.value = true
188
189 try {
190 const res = await Api.notifications.verifyShuffleIntegration(
191 props.integration.customer_code,
192 props.integration.id
193 )
194 if (res.data.success) {
195 verifyResult.value = "ok"
196 verifyAppCount.value = res.data.app_count
197 } else {
198 verifyResult.value = "fail"
199 message.warning(res.data.error || res.data.message || "Verification failed")
200 }
201 } catch (err) {
202 verifyResult.value = "fail"
203 message.error(getApiErrorMessage(err as ApiError) || "Verification failed")
204 } finally {
205 verifying.value = false
206 }
207 }
208
209 async function toggleEnabled() {
210 try {
211 const res = await Api.notifications.updateShuffleIntegration(
212 props.integration.customer_code,
213 props.integration.id,
214 { enabled: !props.integration.enabled }
215 )
216 if (res.data.success) {
217 message.success(`Integration ${res.data.integration.enabled ? "enabled" : "disabled"}`)
218 emit("toggled")
219 } else {
220 message.warning(res.data.message || "Failed to toggle integration")
221 }
222 } catch (err: unknown) {
223 message.error(getApiErrorMessage(err as never) || "Failed to toggle integration")
224 }
225 }
226
227 async function confirmDelete() {
228 try {
229 const res = await Api.notifications.deleteShuffleIntegration(
230 props.integration.customer_code,
231 props.integration.id
232 )
233 if (res.data.success) {
234 message.success("Integration deleted")
235 emit("deleted")
236 } else {
237 message.warning(res.data.message || "Failed to delete integration")
238 }
239 } catch (err: unknown) {
240 message.error(getApiErrorMessage(err as never) || "Failed to delete integration")
241 }
242 }
243 </script>