@nuxtagentic/integrations 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +9 -0
- package/README.md +175 -2
- package/client.ts +124 -0
- package/module.ts +44 -0
- package/package.json +52 -3
- package/runtime/assets/logos/discord.svg +1 -0
- package/runtime/assets/logos/github.svg +1 -0
- package/runtime/assets/logos/gmail.svg +1 -0
- package/runtime/assets/logos/googleads.svg +1 -0
- package/runtime/assets/logos/googleanalytics.svg +1 -0
- package/runtime/assets/logos/googlecalendar.svg +1 -0
- package/runtime/assets/logos/googledrive.svg +1 -0
- package/runtime/assets/logos/googlesearchconsole.svg +1 -0
- package/runtime/assets/logos/hubspot.svg +1 -0
- package/runtime/assets/logos/intercom.svg +1 -0
- package/runtime/assets/logos/linear.svg +1 -0
- package/runtime/assets/logos/mcp.svg +1 -0
- package/runtime/assets/logos/meta.svg +1 -0
- package/runtime/assets/logos/notion.svg +1 -0
- package/runtime/assets/logos/reddit.svg +1 -0
- package/runtime/assets/logos/sentry.svg +1 -0
- package/runtime/assets/logos/shopify.svg +1 -0
- package/runtime/assets/logos/slack.svg +1 -0
- package/runtime/assets/logos/stripe.svg +1 -0
- package/runtime/assets/logos/tiktok-dark.svg +1 -0
- package/runtime/assets/logos/tiktok.svg +1 -0
- package/runtime/assets/logos/vercel.svg +1 -0
- package/runtime/assets/logos/x.svg +1 -0
- package/runtime/components/IntegrationCard.vue +82 -0
- package/runtime/components/IntegrationCatalog.vue +130 -0
- package/runtime/components/IntegrationConnectionPanel.vue +589 -0
- package/runtime/components/IntegrationLogo.vue +41 -0
- package/runtime/pages/integrations.vue +481 -0
- package/runtime/server/api/integrations/approvals/[id].post.ts +69 -0
- package/runtime/server/api/integrations/catalog/[provider]/action.post.ts +35 -0
- package/runtime/server/api/integrations/catalog/[provider]/connection.post.ts +242 -0
- package/runtime/server/api/integrations/catalog/[provider]/tools.get.ts +40 -0
- package/runtime/server/api/integrations/catalog/index.get.ts +48 -0
- package/runtime/server/api/integrations/catalog/tools.get.ts +27 -0
- package/runtime/server/plugins/connect-maintenance.ts +30 -0
- package/runtime/server/utils/approval-continuation.ts +37 -0
- package/runtime/server/utils/google-mcp.ts +296 -0
- package/runtime/server/utils/integration-adapters.ts +836 -0
- package/runtime/server/utils/integration-context.ts +510 -0
- package/runtime/server/utils/integration-crypto.ts +13 -0
- package/runtime/server/utils/request-body.ts +38 -0
- package/runtime/shared/agent.ts +14 -0
- package/runtime/shared/catalog.ts +566 -0
- package/runtime/shared/google-mcp.ts +8 -0
- package/runtime/workspace.ts +15 -0
- package/schema.ts +179 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, useTemplateRef } from 'vue'
|
|
3
|
+
import type {
|
|
4
|
+
IntegrationConnection,
|
|
5
|
+
IntegrationProvider,
|
|
6
|
+
McpServerTool,
|
|
7
|
+
PendingAccount,
|
|
8
|
+
} from '../../client'
|
|
9
|
+
import type { IntegrationDefinition, IntegrationScope } from '../shared/catalog'
|
|
10
|
+
import IntegrationLogo from './IntegrationLogo.vue'
|
|
11
|
+
|
|
12
|
+
const permissions = defineModel<string[]>('permissions', { required: true })
|
|
13
|
+
const approvals = defineModel<string[]>('approvals', { default: () => [] })
|
|
14
|
+
const credentials = defineModel<Record<string, string>>('credentials', { required: true })
|
|
15
|
+
const settings = defineModel<Record<string, string>>('settings', { default: () => ({}) })
|
|
16
|
+
const scope = defineModel<IntegrationScope>('scope', { required: true })
|
|
17
|
+
const server = defineModel<string>('server', { default: '' })
|
|
18
|
+
const {
|
|
19
|
+
integration,
|
|
20
|
+
connections,
|
|
21
|
+
active,
|
|
22
|
+
adding,
|
|
23
|
+
provider,
|
|
24
|
+
manager,
|
|
25
|
+
organization,
|
|
26
|
+
pending,
|
|
27
|
+
tools,
|
|
28
|
+
endpoint,
|
|
29
|
+
busy,
|
|
30
|
+
message,
|
|
31
|
+
} = defineProps<{
|
|
32
|
+
integration: IntegrationDefinition
|
|
33
|
+
/** This integration's connections the viewer can see. */
|
|
34
|
+
connections: IntegrationConnection[]
|
|
35
|
+
/** The connection being managed; none means the account list or the add form. */
|
|
36
|
+
active?: IntegrationConnection
|
|
37
|
+
adding?: boolean
|
|
38
|
+
provider?: IntegrationProvider
|
|
39
|
+
manager: boolean
|
|
40
|
+
organization: boolean
|
|
41
|
+
pending?: PendingAccount
|
|
42
|
+
tools?: McpServerTool[]
|
|
43
|
+
endpoint?: { url: string; secret: string }
|
|
44
|
+
busy?: boolean
|
|
45
|
+
message?: string
|
|
46
|
+
}>()
|
|
47
|
+
const emit = defineEmits<{
|
|
48
|
+
close: []
|
|
49
|
+
add: []
|
|
50
|
+
back: []
|
|
51
|
+
manage: [connection: IntegrationConnection]
|
|
52
|
+
connect: []
|
|
53
|
+
apiKey: []
|
|
54
|
+
save: []
|
|
55
|
+
disconnect: [connection: IntegrationConnection]
|
|
56
|
+
reconnect: [connection: IntegrationConnection]
|
|
57
|
+
endpoint: [rotate: boolean]
|
|
58
|
+
copy: [value: string]
|
|
59
|
+
choose: [target: string]
|
|
60
|
+
}>()
|
|
61
|
+
const element = useTemplateRef('element')
|
|
62
|
+
defineExpose({ element })
|
|
63
|
+
|
|
64
|
+
const auth = computed(() => integration.auth)
|
|
65
|
+
const oauthReady = computed(() => !!auth.value.oauth && !!provider?.configured)
|
|
66
|
+
/** Without a registered OAuth app, providers that take a token still connect. */
|
|
67
|
+
const tokenForm = computed(() => !!auth.value.apiKey && !oauthReady.value)
|
|
68
|
+
const mode = computed(() => {
|
|
69
|
+
if (pending?.targets?.length) {
|
|
70
|
+
return 'choose'
|
|
71
|
+
}
|
|
72
|
+
if (active) {
|
|
73
|
+
return 'manage'
|
|
74
|
+
}
|
|
75
|
+
if (adding || !connections.length) {
|
|
76
|
+
return 'add'
|
|
77
|
+
}
|
|
78
|
+
return 'list'
|
|
79
|
+
})
|
|
80
|
+
const canShare = computed(() => organization && integration.ownership.includes('workspace'))
|
|
81
|
+
const canPersonal = computed(() => integration.ownership.includes('user'))
|
|
82
|
+
const settingFields = computed(() =>
|
|
83
|
+
integration.fields.filter((field) => !field.secret && !field.apiKeyOnly),
|
|
84
|
+
)
|
|
85
|
+
const tokenFields = computed(() => integration.fields)
|
|
86
|
+
const actionItems = computed(() =>
|
|
87
|
+
auth.value.mcp
|
|
88
|
+
? (tools ?? []).map((tool) => ({
|
|
89
|
+
id: tool.name,
|
|
90
|
+
label: tool.title,
|
|
91
|
+
hint: tool.description,
|
|
92
|
+
write: tool.write,
|
|
93
|
+
}))
|
|
94
|
+
: integration.actions.map((action) => ({ ...action, hint: '', write: false })),
|
|
95
|
+
)
|
|
96
|
+
const isReadOnly = computed(() => !!active && !active.canManage)
|
|
97
|
+
const setupCommand = computed(() =>
|
|
98
|
+
auth.value.oauth ? `pnpm exec cloudconfig connect add ${auth.value.oauth.provider}` : '',
|
|
99
|
+
)
|
|
100
|
+
const accountSummary = computed(() => {
|
|
101
|
+
if (active) {
|
|
102
|
+
const { label, account, scope } = active
|
|
103
|
+
return `${label || account} · ${scopeLabel(scope)}`
|
|
104
|
+
}
|
|
105
|
+
if (connections.length) {
|
|
106
|
+
return `${connections.length} connected ${connections.length === 1 ? 'account' : 'accounts'}`
|
|
107
|
+
}
|
|
108
|
+
return integration.category
|
|
109
|
+
})
|
|
110
|
+
const scopeDescription = computed(() => {
|
|
111
|
+
if (scope.value === 'workspace') {
|
|
112
|
+
return 'Shared with every member’s agents. It keeps working when you leave.'
|
|
113
|
+
}
|
|
114
|
+
return manager || !canShare.value
|
|
115
|
+
? 'Only your own agents and automations can use it.'
|
|
116
|
+
: 'Only your own agents can use it. Admins add workspace connections.'
|
|
117
|
+
})
|
|
118
|
+
const permissionDescription = computed(() => {
|
|
119
|
+
if (isReadOnly.value) {
|
|
120
|
+
return 'Workspace admins manage this connection.'
|
|
121
|
+
}
|
|
122
|
+
if (auth.value.mcp) {
|
|
123
|
+
return 'Tools marked “Ask first” wait for your approval.'
|
|
124
|
+
}
|
|
125
|
+
switch (integration.id) {
|
|
126
|
+
case 'stripe':
|
|
127
|
+
return 'Refunds require your approval.'
|
|
128
|
+
case 'intercom':
|
|
129
|
+
return 'Reply drafts require your approval before sending.'
|
|
130
|
+
default:
|
|
131
|
+
return 'Only the actions you allow can be used by agents.'
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
function getConnectionStatusClass({ status, enabled }: IntegrationConnection) {
|
|
136
|
+
if (status === 'error') {
|
|
137
|
+
return 'bg-error'
|
|
138
|
+
}
|
|
139
|
+
return enabled ? 'bg-success' : 'bg-(--gray-600)'
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function toggle(list: string[], id: string, value: boolean | 'indeterminate') {
|
|
143
|
+
return value === true ? [...new Set([...list, id])] : list.filter((entry) => entry !== id)
|
|
144
|
+
}
|
|
145
|
+
function scopeLabel(value: IntegrationScope) {
|
|
146
|
+
return value === 'workspace' ? 'Workspace' : 'Just me'
|
|
147
|
+
}
|
|
148
|
+
</script>
|
|
149
|
+
|
|
150
|
+
<template>
|
|
151
|
+
<div
|
|
152
|
+
ref="element"
|
|
153
|
+
class="flex max-h-[min(760px,calc(100dvh-48px))] flex-col"
|
|
154
|
+
>
|
|
155
|
+
<div class="flex shrink-0 items-start gap-3.5 px-5 pt-5">
|
|
156
|
+
<UButton
|
|
157
|
+
v-if="mode === 'manage' || (mode === 'add' && connections.length)"
|
|
158
|
+
icon="i-lucide-arrow-left"
|
|
159
|
+
aria-label="Back to accounts"
|
|
160
|
+
color="neutral"
|
|
161
|
+
variant="ghost"
|
|
162
|
+
class="-ms-1.5 mt-2 size-8 rounded-md"
|
|
163
|
+
@click="emit('back')"
|
|
164
|
+
/>
|
|
165
|
+
<span
|
|
166
|
+
class="grid size-12 shrink-0 place-items-center rounded-xl border border-(--gray-400) bg-(--gray-300)"
|
|
167
|
+
>
|
|
168
|
+
<IntegrationLogo
|
|
169
|
+
:name="integration.logo"
|
|
170
|
+
class="size-6"
|
|
171
|
+
/>
|
|
172
|
+
</span>
|
|
173
|
+
<div class="min-w-0 flex-1">
|
|
174
|
+
<h2 class="text-lg leading-6 font-medium">{{ integration.name }}</h2>
|
|
175
|
+
<p class="mt-1 truncate text-[13px] text-dimmed">
|
|
176
|
+
{{ accountSummary }}
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
<UButton
|
|
180
|
+
icon="i-lucide-x"
|
|
181
|
+
aria-label="Close connection"
|
|
182
|
+
color="neutral"
|
|
183
|
+
variant="ghost"
|
|
184
|
+
class="size-8 rounded-md"
|
|
185
|
+
@click="emit('close')"
|
|
186
|
+
/>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div class="flex min-h-0 flex-col gap-5 overflow-y-auto px-5 py-4">
|
|
190
|
+
<p
|
|
191
|
+
v-if="mode !== 'manage'"
|
|
192
|
+
class="text-sm leading-5 text-toned"
|
|
193
|
+
>
|
|
194
|
+
{{ integration.details }}
|
|
195
|
+
</p>
|
|
196
|
+
|
|
197
|
+
<!-- Accounts -->
|
|
198
|
+
<div
|
|
199
|
+
v-if="mode === 'list'"
|
|
200
|
+
class="flex flex-col gap-2"
|
|
201
|
+
>
|
|
202
|
+
<span class="text-xs text-dimmed">Connected accounts</span>
|
|
203
|
+
<div class="divide-y divide-(--gray-400) rounded-[10px] border border-(--gray-400)">
|
|
204
|
+
<button
|
|
205
|
+
v-for="entry in connections"
|
|
206
|
+
:key="entry.id"
|
|
207
|
+
type="button"
|
|
208
|
+
class="flex min-h-12 w-full items-center gap-3 px-3 text-start hover:bg-(--gray-300) focus-visible:outline-2"
|
|
209
|
+
@click="emit('manage', entry)"
|
|
210
|
+
>
|
|
211
|
+
<span
|
|
212
|
+
class="size-1.5 shrink-0 rounded-full"
|
|
213
|
+
:class="getConnectionStatusClass(entry)"
|
|
214
|
+
/>
|
|
215
|
+
<span class="min-w-0 flex-1">
|
|
216
|
+
<span class="block truncate text-[13px] text-highlighted">
|
|
217
|
+
{{ entry.label || entry.account }}
|
|
218
|
+
</span>
|
|
219
|
+
<span class="block truncate text-xs text-dimmed">
|
|
220
|
+
{{
|
|
221
|
+
entry.status === 'error'
|
|
222
|
+
? 'Needs to be reconnected'
|
|
223
|
+
: `${entry.permissions.length} allowed · ${entry.mine ? 'added by you' : 'shared'}`
|
|
224
|
+
}}
|
|
225
|
+
</span>
|
|
226
|
+
</span>
|
|
227
|
+
<UBadge
|
|
228
|
+
:label="scopeLabel(entry.scope)"
|
|
229
|
+
color="neutral"
|
|
230
|
+
variant="subtle"
|
|
231
|
+
size="sm"
|
|
232
|
+
/>
|
|
233
|
+
<UIcon
|
|
234
|
+
name="i-lucide-chevron-right"
|
|
235
|
+
class="size-4 text-dimmed"
|
|
236
|
+
/>
|
|
237
|
+
</button>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<!-- Choose an installation or site -->
|
|
242
|
+
<div
|
|
243
|
+
v-if="mode === 'choose' && pending"
|
|
244
|
+
class="flex flex-col gap-2"
|
|
245
|
+
>
|
|
246
|
+
<span class="text-xs text-dimmed">Choose what this connection covers</span>
|
|
247
|
+
<div class="divide-y divide-(--gray-400) rounded-[10px] border border-(--gray-400)">
|
|
248
|
+
<button
|
|
249
|
+
v-for="target in pending.targets"
|
|
250
|
+
:key="target.id"
|
|
251
|
+
type="button"
|
|
252
|
+
class="flex min-h-11 w-full items-center gap-3 px-3 text-start hover:bg-(--gray-300) focus-visible:outline-2"
|
|
253
|
+
:disabled="busy"
|
|
254
|
+
@click="emit('choose', target.id)"
|
|
255
|
+
>
|
|
256
|
+
<span class="min-w-0 flex-1 truncate text-[13px] text-highlighted">
|
|
257
|
+
{{ target.name }}
|
|
258
|
+
</span>
|
|
259
|
+
<span class="truncate text-xs text-dimmed">{{ target.hint }}</span>
|
|
260
|
+
</button>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<!-- Add an account -->
|
|
265
|
+
<template v-if="mode === 'add'">
|
|
266
|
+
<div
|
|
267
|
+
v-if="integration.triggers.length"
|
|
268
|
+
class="flex flex-col gap-2"
|
|
269
|
+
>
|
|
270
|
+
<span class="text-xs text-dimmed">Can start an agent when</span>
|
|
271
|
+
<div class="divide-y divide-(--gray-400) rounded-[10px] border border-(--gray-400)">
|
|
272
|
+
<div
|
|
273
|
+
v-for="trigger in integration.triggers"
|
|
274
|
+
:key="trigger.id"
|
|
275
|
+
class="flex min-h-10 items-center gap-2.5 px-3 text-[13px]"
|
|
276
|
+
>
|
|
277
|
+
<UIcon
|
|
278
|
+
name="i-lucide-zap"
|
|
279
|
+
class="size-3.5 text-muted"
|
|
280
|
+
/>
|
|
281
|
+
{{ trigger.label }}
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
<div
|
|
286
|
+
v-if="canShare && canPersonal"
|
|
287
|
+
class="flex flex-col gap-2"
|
|
288
|
+
>
|
|
289
|
+
<span class="text-xs text-dimmed">Who agents act as</span>
|
|
290
|
+
<div
|
|
291
|
+
class="grid grid-cols-2 gap-1 rounded-lg border border-(--gray-500) bg-(--gray-200) p-0.5"
|
|
292
|
+
role="radiogroup"
|
|
293
|
+
aria-label="Connection owner"
|
|
294
|
+
>
|
|
295
|
+
<UButton
|
|
296
|
+
v-for="value in ['user', 'workspace'] as const"
|
|
297
|
+
:key="value"
|
|
298
|
+
:label="value === 'user' ? 'Just me' : 'Whole workspace'"
|
|
299
|
+
:icon="value === 'user' ? 'i-lucide-user' : 'i-lucide-users'"
|
|
300
|
+
color="neutral"
|
|
301
|
+
:variant="scope === value ? 'soft' : 'ghost'"
|
|
302
|
+
role="radio"
|
|
303
|
+
:aria-checked="scope === value"
|
|
304
|
+
:disabled="value === 'workspace' && !manager"
|
|
305
|
+
class="h-8 justify-center rounded-md text-[13px]"
|
|
306
|
+
:class="scope === value ? 'bg-(--gray-400) text-highlighted' : ''"
|
|
307
|
+
@click="scope = value"
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
<p class="text-xs leading-4 text-dimmed">
|
|
311
|
+
{{ scopeDescription }}
|
|
312
|
+
</p>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<UFormField
|
|
316
|
+
v-if="auth.mcp && !auth.mcp.server"
|
|
317
|
+
label="Server URL"
|
|
318
|
+
>
|
|
319
|
+
<UInput
|
|
320
|
+
v-model="server"
|
|
321
|
+
type="url"
|
|
322
|
+
placeholder="https://mcp.example.com/mcp"
|
|
323
|
+
autocomplete="off"
|
|
324
|
+
class="w-full"
|
|
325
|
+
/>
|
|
326
|
+
</UFormField>
|
|
327
|
+
|
|
328
|
+
<UAlert
|
|
329
|
+
v-if="auth.oauth && !oauthReady"
|
|
330
|
+
color="neutral"
|
|
331
|
+
variant="subtle"
|
|
332
|
+
icon="i-lucide-key-round"
|
|
333
|
+
:title="`${provider?.label ?? integration.name} sign-in isn’t set up here yet`"
|
|
334
|
+
:description="
|
|
335
|
+
auth.apiKey
|
|
336
|
+
? `Paste an access token below, or register the OAuth app: ${setupCommand}`
|
|
337
|
+
: `Register the OAuth app: ${setupCommand}`
|
|
338
|
+
"
|
|
339
|
+
/>
|
|
340
|
+
<template v-if="tokenForm">
|
|
341
|
+
<UFormField label="Access token">
|
|
342
|
+
<UInput
|
|
343
|
+
v-model="credentials.token"
|
|
344
|
+
type="password"
|
|
345
|
+
autocomplete="off"
|
|
346
|
+
placeholder="Paste your API token"
|
|
347
|
+
class="w-full"
|
|
348
|
+
/>
|
|
349
|
+
</UFormField>
|
|
350
|
+
<UFormField
|
|
351
|
+
v-for="field in tokenFields"
|
|
352
|
+
:key="field.key"
|
|
353
|
+
:label="field.label"
|
|
354
|
+
>
|
|
355
|
+
<UInput
|
|
356
|
+
v-model="credentials[field.key]"
|
|
357
|
+
:type="field.secret ? 'password' : 'text'"
|
|
358
|
+
:placeholder="field.placeholder"
|
|
359
|
+
autocomplete="off"
|
|
360
|
+
class="w-full"
|
|
361
|
+
/>
|
|
362
|
+
</UFormField>
|
|
363
|
+
</template>
|
|
364
|
+
<UFormField
|
|
365
|
+
v-for="field in oauthReady ? (provider?.startFields ?? []) : []"
|
|
366
|
+
:key="field.key"
|
|
367
|
+
:label="field.label"
|
|
368
|
+
>
|
|
369
|
+
<UInput
|
|
370
|
+
v-model="credentials[field.key]"
|
|
371
|
+
:placeholder="field.placeholder"
|
|
372
|
+
autocomplete="off"
|
|
373
|
+
class="w-full"
|
|
374
|
+
/>
|
|
375
|
+
</UFormField>
|
|
376
|
+
</template>
|
|
377
|
+
|
|
378
|
+
<!-- Manage one account -->
|
|
379
|
+
<template v-if="mode === 'manage' && active">
|
|
380
|
+
<UAlert
|
|
381
|
+
v-if="active.status === 'error'"
|
|
382
|
+
color="error"
|
|
383
|
+
variant="subtle"
|
|
384
|
+
title="This account needs to be reconnected"
|
|
385
|
+
:description="active.statusMessage || 'Its access was revoked or expired.'"
|
|
386
|
+
:actions="
|
|
387
|
+
active.canManage && (auth.oauth || auth.mcp)
|
|
388
|
+
? [
|
|
389
|
+
{
|
|
390
|
+
label: 'Reconnect',
|
|
391
|
+
color: 'neutral',
|
|
392
|
+
onClick: () => emit('reconnect', active!),
|
|
393
|
+
},
|
|
394
|
+
]
|
|
395
|
+
: []
|
|
396
|
+
"
|
|
397
|
+
/>
|
|
398
|
+
<div class="flex flex-col gap-2">
|
|
399
|
+
<span class="text-xs text-dimmed">
|
|
400
|
+
{{ auth.mcp ? 'Tools agents may use' : 'Allow agents to' }}
|
|
401
|
+
</span>
|
|
402
|
+
<div
|
|
403
|
+
v-if="auth.mcp && !tools"
|
|
404
|
+
class="flex items-center gap-2 text-[13px] text-muted"
|
|
405
|
+
>
|
|
406
|
+
<UIcon
|
|
407
|
+
name="i-lucide-loader-circle"
|
|
408
|
+
class="size-4 animate-spin"
|
|
409
|
+
/>
|
|
410
|
+
Loading the server’s tools…
|
|
411
|
+
</div>
|
|
412
|
+
<div
|
|
413
|
+
v-else
|
|
414
|
+
class="max-h-72 divide-y divide-(--gray-400) overflow-y-auto rounded-[10px] border border-(--gray-400)"
|
|
415
|
+
>
|
|
416
|
+
<div
|
|
417
|
+
v-for="action in actionItems"
|
|
418
|
+
:key="action.id"
|
|
419
|
+
class="flex min-h-10 items-center gap-3 px-3"
|
|
420
|
+
>
|
|
421
|
+
<UCheckbox
|
|
422
|
+
:model-value="permissions.includes(action.id)"
|
|
423
|
+
:label="action.label"
|
|
424
|
+
:description="action.hint"
|
|
425
|
+
:disabled="isReadOnly"
|
|
426
|
+
color="neutral"
|
|
427
|
+
class="min-w-0 flex-1 py-2"
|
|
428
|
+
:ui="{
|
|
429
|
+
label: 'text-[13px] font-normal',
|
|
430
|
+
description: 'line-clamp-1 text-xs',
|
|
431
|
+
base: 'size-4 rounded',
|
|
432
|
+
}"
|
|
433
|
+
@update:model-value="
|
|
434
|
+
(value) => (permissions = toggle(permissions, action.id, value))
|
|
435
|
+
"
|
|
436
|
+
/>
|
|
437
|
+
<USwitch
|
|
438
|
+
v-if="auth.mcp && permissions.includes(action.id)"
|
|
439
|
+
:model-value="approvals.includes(action.id)"
|
|
440
|
+
:disabled="isReadOnly"
|
|
441
|
+
size="sm"
|
|
442
|
+
label="Ask first"
|
|
443
|
+
:ui="{ label: 'text-xs font-normal text-dimmed' }"
|
|
444
|
+
@update:model-value="(value) => (approvals = toggle(approvals, action.id, value))"
|
|
445
|
+
/>
|
|
446
|
+
</div>
|
|
447
|
+
<p
|
|
448
|
+
v-if="!actionItems.length"
|
|
449
|
+
class="px-3 py-2.5 text-[13px] text-dimmed"
|
|
450
|
+
>
|
|
451
|
+
This server offers no tools.
|
|
452
|
+
</p>
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
<UFormField
|
|
456
|
+
v-for="field in settingFields"
|
|
457
|
+
:key="field.key"
|
|
458
|
+
:label="field.label"
|
|
459
|
+
>
|
|
460
|
+
<UInput
|
|
461
|
+
v-model="settings[field.key]"
|
|
462
|
+
:placeholder="field.placeholder"
|
|
463
|
+
:disabled="isReadOnly"
|
|
464
|
+
autocomplete="off"
|
|
465
|
+
class="w-full"
|
|
466
|
+
/>
|
|
467
|
+
</UFormField>
|
|
468
|
+
<div
|
|
469
|
+
v-if="integration.triggers.length && active.canManage"
|
|
470
|
+
class="flex flex-col gap-2"
|
|
471
|
+
>
|
|
472
|
+
<UButton
|
|
473
|
+
label="Event endpoint"
|
|
474
|
+
icon="i-lucide-webhook"
|
|
475
|
+
color="neutral"
|
|
476
|
+
variant="ghost"
|
|
477
|
+
class="self-start text-xs"
|
|
478
|
+
@click="emit('endpoint', false)"
|
|
479
|
+
/>
|
|
480
|
+
<template v-if="endpoint">
|
|
481
|
+
<p class="text-xs text-muted">
|
|
482
|
+
POST a normalized event to this URL, signed with the secret: send it as a bearer
|
|
483
|
+
token, or as the secret of a GitHub repository webhook. Events start active
|
|
484
|
+
automations that can see this connection.
|
|
485
|
+
</p>
|
|
486
|
+
<UInput
|
|
487
|
+
:model-value="endpoint.url"
|
|
488
|
+
readonly
|
|
489
|
+
aria-label="Event endpoint"
|
|
490
|
+
class="w-full"
|
|
491
|
+
/>
|
|
492
|
+
<UInput
|
|
493
|
+
:model-value="endpoint.secret"
|
|
494
|
+
readonly
|
|
495
|
+
type="password"
|
|
496
|
+
aria-label="Event secret"
|
|
497
|
+
class="w-full"
|
|
498
|
+
/>
|
|
499
|
+
<div class="flex gap-2">
|
|
500
|
+
<UButton
|
|
501
|
+
label="Copy URL"
|
|
502
|
+
color="neutral"
|
|
503
|
+
variant="outline"
|
|
504
|
+
size="xs"
|
|
505
|
+
@click="emit('copy', endpoint.url)"
|
|
506
|
+
/>
|
|
507
|
+
<UButton
|
|
508
|
+
label="Copy secret"
|
|
509
|
+
color="neutral"
|
|
510
|
+
variant="outline"
|
|
511
|
+
size="xs"
|
|
512
|
+
@click="emit('copy', endpoint.secret)"
|
|
513
|
+
/>
|
|
514
|
+
<UButton
|
|
515
|
+
label="Rotate secret"
|
|
516
|
+
color="neutral"
|
|
517
|
+
variant="ghost"
|
|
518
|
+
size="xs"
|
|
519
|
+
@click="emit('endpoint', true)"
|
|
520
|
+
/>
|
|
521
|
+
</div>
|
|
522
|
+
</template>
|
|
523
|
+
</div>
|
|
524
|
+
</template>
|
|
525
|
+
|
|
526
|
+
<UAlert
|
|
527
|
+
v-if="message"
|
|
528
|
+
color="error"
|
|
529
|
+
variant="subtle"
|
|
530
|
+
:title="message"
|
|
531
|
+
/>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
<div class="flex shrink-0 items-center gap-2 border-t border-(--gray-400) px-5 py-3.5">
|
|
535
|
+
<p class="min-w-0 flex-1 text-xs leading-4 text-dimmed">
|
|
536
|
+
{{ permissionDescription }}
|
|
537
|
+
</p>
|
|
538
|
+
<template v-if="mode === 'list'">
|
|
539
|
+
<UButton
|
|
540
|
+
label="Add account"
|
|
541
|
+
icon="i-lucide-plus"
|
|
542
|
+
color="neutral"
|
|
543
|
+
class="h-8 rounded-lg px-3.5 text-[13px]"
|
|
544
|
+
@click="emit('add')"
|
|
545
|
+
/>
|
|
546
|
+
</template>
|
|
547
|
+
<template v-else-if="mode === 'manage' && active">
|
|
548
|
+
<UButton
|
|
549
|
+
v-if="active.canManage"
|
|
550
|
+
label="Disconnect"
|
|
551
|
+
color="neutral"
|
|
552
|
+
variant="outline"
|
|
553
|
+
class="h-8 rounded-lg px-3 text-[13px]"
|
|
554
|
+
:disabled="busy"
|
|
555
|
+
@click="emit('disconnect', active)"
|
|
556
|
+
/>
|
|
557
|
+
<UButton
|
|
558
|
+
v-if="!isReadOnly"
|
|
559
|
+
label="Save"
|
|
560
|
+
color="neutral"
|
|
561
|
+
class="h-8 rounded-lg px-3.5 text-[13px]"
|
|
562
|
+
:loading="busy"
|
|
563
|
+
@click="emit('save')"
|
|
564
|
+
/>
|
|
565
|
+
</template>
|
|
566
|
+
<template v-else-if="mode === 'add'">
|
|
567
|
+
<UButton
|
|
568
|
+
v-if="oauthReady || auth.mcp"
|
|
569
|
+
:label="
|
|
570
|
+
auth.mcp ? 'Sign in to server' : `Connect with ${provider?.label ?? integration.name}`
|
|
571
|
+
"
|
|
572
|
+
color="neutral"
|
|
573
|
+
class="h-8 rounded-lg px-3.5 text-[13px]"
|
|
574
|
+
:loading="busy"
|
|
575
|
+
:disabled="!!auth.mcp && !auth.mcp.server && !server"
|
|
576
|
+
@click="emit('connect')"
|
|
577
|
+
/>
|
|
578
|
+
<UButton
|
|
579
|
+
v-else-if="tokenForm"
|
|
580
|
+
:label="`Connect ${integration.name}`"
|
|
581
|
+
color="neutral"
|
|
582
|
+
class="h-8 rounded-lg px-3.5 text-[13px]"
|
|
583
|
+
:loading="busy"
|
|
584
|
+
@click="emit('apiKey')"
|
|
585
|
+
/>
|
|
586
|
+
</template>
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
589
|
+
</template>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const { name } = defineProps<{ name: string }>()
|
|
3
|
+
|
|
4
|
+
const logos = import.meta.glob<string>('../assets/logos/*.svg', {
|
|
5
|
+
eager: true,
|
|
6
|
+
query: '?url',
|
|
7
|
+
import: 'default',
|
|
8
|
+
})
|
|
9
|
+
const monochrome = ['github', 'linear', 'vercel', 'intercom', 'notion', 'x', 'mcp', 'sentry']
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<span
|
|
14
|
+
aria-hidden="true"
|
|
15
|
+
class="inline-flex shrink-0"
|
|
16
|
+
>
|
|
17
|
+
<UIcon
|
|
18
|
+
v-if="!logos[`../assets/logos/${name}.svg`]"
|
|
19
|
+
name="i-lucide-plug"
|
|
20
|
+
class="size-full text-muted"
|
|
21
|
+
/>
|
|
22
|
+
<img
|
|
23
|
+
v-else
|
|
24
|
+
:src="logos[`../assets/logos/${name}.svg`]"
|
|
25
|
+
alt=""
|
|
26
|
+
:draggable="false"
|
|
27
|
+
class="size-full object-contain"
|
|
28
|
+
:class="[
|
|
29
|
+
monochrome.includes(name) ? 'dark:invert' : '',
|
|
30
|
+
name === 'tiktok' ? 'dark:hidden' : '',
|
|
31
|
+
]"
|
|
32
|
+
/>
|
|
33
|
+
<img
|
|
34
|
+
v-if="name === 'tiktok'"
|
|
35
|
+
:src="logos['../assets/logos/tiktok-dark.svg']"
|
|
36
|
+
alt=""
|
|
37
|
+
:draggable="false"
|
|
38
|
+
class="hidden size-full object-contain dark:block"
|
|
39
|
+
/>
|
|
40
|
+
</span>
|
|
41
|
+
</template>
|