| 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> |