main
vue 149 lines 5.18 KB
Raw
1 <template>
2 <div class="flex flex-col gap-4">
3 <!-- No customer code on alert -->
4 <n-alert v-if="!alert.customer_code" type="warning" title="No customer associated with this alert">
5 Try MCP needs the alert's customer to look up their Shuffle integration.
6 </n-alert>
7
8 <!-- Loading customer integration -->
9 <div v-else-if="loadingIntegration" class="text-tertiary py-6 text-center text-sm">
10 Loading Shuffle integration for customer {{ alert.customer_code }}
11 </div>
12
13 <!-- No integration configured -->
14 <n-alert v-else-if="!activeIntegration" type="info" title="No Shuffle integration configured">
15 This customer has no enabled Shuffle integration. Configure one in
16 <strong>Customer → AI Notifications → Shuffle integrations</strong>
17 before using Try MCP.
18 </n-alert>
19
20 <!-- Loading the org auth token -->
21 <div v-else-if="loadingToken" class="text-tertiary py-6 text-center text-sm">Fetching org auth token…</div>
22
23 <!-- Token fetch error -->
24 <n-alert v-else-if="tokenError" type="error" title="Couldn't fetch Shuffle auth token">
25 {{ tokenError }}
26 </n-alert>
27
28 <!-- Active flow: pick an app, then mount TryMcpSection -->
29 <template v-else-if="orgAuthToken">
30 <div class="flex flex-col gap-2">
31 <div class="flex items-center justify-between">
32 <span class="text-sm">
33 Acting as
34 <strong>{{ activeIntegration.display_name }}</strong>
35 <span class="text-secondary">&nbsp;• alert #{{ alert.id }}</span>
36 </span>
37 <n-button v-if="selectedAppName" size="tiny" @click="selectedAppName = null">
38 <template #icon>
39 <Icon :name="ResetIcon" :size="14" />
40 </template>
41 Pick a different app
42 </n-button>
43 </div>
44
45 <!-- App picker: only mounted until a selection is made -->
46 <CollapseKeepAlive :show="!selectedAppName">
47 <ShuffleMCPEmbed
48 :auth-token="orgAuthToken"
49 placeholder="Pick an app to act on this alert (e.g. Crowdstrike, Cloudflare, VirusTotal)…"
50 inline
51 class="[&_.singul-container]:max-h-100!"
52 disable-app-drawer
53 prevent-default
54 @app-selected="onAppSelected"
55 />
56 </CollapseKeepAlive>
57
58 <!-- Try MCP for the chosen app -->
59 <n-collapse-transition :show="!!selectedAppName">
60 <TryMcpEmbed v-if="selectedAppName" :app-name="selectedAppName" :auth-token="orgAuthToken" />
61 </n-collapse-transition>
62 </div>
63 </template>
64 </div>
65 </template>
66
67 <script setup lang="ts">
68 import type { AppSelectedEvent } from "@shuffleio/shuffle-mcps"
69 import type { Alert } from "@/types/incidentManagement/alerts.d"
70 import type { ShuffleIntegration } from "@/types/notifications.d"
71 import { NAlert, NButton, NCollapseTransition, useMessage } from "naive-ui"
72 import { computed, onBeforeMount, ref } from "vue"
73 import Api from "@/api"
74 import CollapseKeepAlive from "@/components/common/CollapseKeepAlive.vue"
75 import Icon from "@/components/common/Icon.vue"
76 import ShuffleMCPEmbed from "@/components/shuffle/ShuffleMCPEmbed.vue"
77 import TryMcpEmbed from "@/components/shuffle/TryMcpEmbed.vue"
78
79 const props = defineProps<{ alert: Alert }>()
80
81 const ResetIcon = "carbon:reset"
82
83 const message = useMessage()
84
85 const integrations = ref<ShuffleIntegration[]>([])
86 const loadingIntegration = ref(false)
87 const orgAuthToken = ref<string | null>(null)
88 const loadingToken = ref(false)
89 const tokenError = ref<string | null>(null)
90 const selectedAppName = ref<string | null>(null)
91
92 // First enabled integration wins. Multi-integration customers will need
93 // a picker in a follow-up; vast majority have one.
94 const activeIntegration = computed<ShuffleIntegration | null>(
95 () => integrations.value.find(i => i.enabled) ?? integrations.value[0] ?? null
96 )
97
98 async function loadIntegrations() {
99 if (!props.alert.customer_code) return
100 loadingIntegration.value = true
101 try {
102 const res = await Api.notifications.listShuffleIntegrations(props.alert.customer_code)
103 if (res.data.success) {
104 integrations.value = res.data?.integrations || []
105 if (activeIntegration.value?.shuffle_org_id) {
106 await loadOrgToken(activeIntegration.value.shuffle_org_id)
107 }
108 } else {
109 message.warning(res.data?.message || "Failed to load Shuffle integrations.")
110 }
111 } catch (err: unknown) {
112 const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message
113 message.error(msg || "Failed to load Shuffle integrations.")
114 } finally {
115 loadingIntegration.value = false
116 }
117 }
118
119 async function loadOrgToken(orgId: string) {
120 loadingToken.value = true
121 tokenError.value = null
122 try {
123 const res = await Api.shuffle.getOrganization(orgId)
124 if (res.data.success) {
125 orgAuthToken.value = res.data.data?.org_auth?.token ?? null
126 if (!orgAuthToken.value) {
127 tokenError.value = "Shuffle returned an org without an org_auth.token."
128 }
129 } else {
130 tokenError.value = res.data.message || "Failed to fetch org auth token."
131 }
132 } catch (err: unknown) {
133 const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message
134 tokenError.value = msg || "Failed to fetch org auth token."
135 } finally {
136 loadingToken.value = false
137 }
138 }
139
140 function onAppSelected(payload: AppSelectedEvent) {
141 const name = payload.app?.name
142 if (!name) return
143 selectedAppName.value = name
144 }
145
146 onBeforeMount(() => {
147 loadIntegrations()
148 })
149 </script>