@cat-factory/app 0.26.7 → 0.28.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/app/components/layout/AccountDeploymentSettings.vue +236 -0
- package/app/components/layout/AccountTeamSettings.vue +6 -0
- package/app/components/layout/CommandBar.vue +8 -0
- package/app/components/layout/SideBar.vue +13 -0
- package/app/components/sandbox/SandboxPanel.vue +542 -0
- package/app/components/settings/ObservabilityConnectionPanel.vue +92 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +116 -1
- package/app/composables/api/accounts.ts +12 -0
- package/app/composables/api/releaseHealth.ts +17 -0
- package/app/composables/api/sandbox.ts +57 -0
- package/app/composables/useApi.ts +2 -0
- package/app/pages/index.vue +2 -0
- package/app/stores/accountSettings.ts +49 -0
- package/app/stores/releaseHealth.ts +37 -0
- package/app/stores/sandbox.ts +174 -0
- package/app/stores/ui.ts +11 -0
- package/app/stores/workspaceSettings.ts +3 -0
- package/app/types/accountSettings.ts +39 -0
- package/app/types/domain.ts +9 -0
- package/app/types/incidentEnrichment.ts +28 -0
- package/app/types/sandbox.ts +183 -0
- package/package.json +1 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, reactive, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
// Deployment integration secrets for an account (admin only): the Slack app OAuth
|
|
5
|
+
// credentials and the container web-search upstream keys — both moved out of env into
|
|
6
|
+
// the per-account settings store, sealed at rest. Secrets are write-only: the panel only
|
|
7
|
+
// ever shows whether each integration is configured (the `summary`), never the values;
|
|
8
|
+
// blank inputs leave a configured secret unchanged. Hidden when the settings store isn't
|
|
9
|
+
// wired (no ENCRYPTION_KEY).
|
|
10
|
+
const props = defineProps<{ accountId: string }>()
|
|
11
|
+
|
|
12
|
+
const store = useAccountSettingsStore()
|
|
13
|
+
const toast = useToast()
|
|
14
|
+
|
|
15
|
+
const slack = reactive({ clientId: '', clientSecret: '', redirectUrl: '' })
|
|
16
|
+
const web = reactive({ braveApiKey: '', searxngUrl: '', searxngApiKey: '' })
|
|
17
|
+
const savingSlack = ref(false)
|
|
18
|
+
const savingWeb = ref(false)
|
|
19
|
+
|
|
20
|
+
const summary = computed(() => store.view?.summary ?? null)
|
|
21
|
+
|
|
22
|
+
onMounted(async () => {
|
|
23
|
+
try {
|
|
24
|
+
await store.load(props.accountId)
|
|
25
|
+
} catch (e) {
|
|
26
|
+
toast.add({
|
|
27
|
+
title: 'Could not load deployment settings',
|
|
28
|
+
description: e instanceof Error ? e.message : String(e),
|
|
29
|
+
icon: 'i-lucide-triangle-alert',
|
|
30
|
+
color: 'error',
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
async function saveSlack() {
|
|
36
|
+
if (!slack.clientId.trim() || !slack.clientSecret.trim() || !slack.redirectUrl.trim()) {
|
|
37
|
+
toast.add({ title: 'Enter the client id, secret and redirect URL', color: 'error' })
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
savingSlack.value = true
|
|
41
|
+
try {
|
|
42
|
+
await store.save(props.accountId, {
|
|
43
|
+
secrets: {
|
|
44
|
+
slackOAuth: {
|
|
45
|
+
clientId: slack.clientId.trim(),
|
|
46
|
+
clientSecret: slack.clientSecret.trim(),
|
|
47
|
+
redirectUrl: slack.redirectUrl.trim(),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
slack.clientId = ''
|
|
52
|
+
slack.clientSecret = ''
|
|
53
|
+
slack.redirectUrl = ''
|
|
54
|
+
toast.add({ title: 'Slack OAuth saved', icon: 'i-lucide-check', color: 'success' })
|
|
55
|
+
} catch (e) {
|
|
56
|
+
toast.add({
|
|
57
|
+
title: 'Could not save Slack OAuth',
|
|
58
|
+
description: e instanceof Error ? e.message : String(e),
|
|
59
|
+
color: 'error',
|
|
60
|
+
})
|
|
61
|
+
} finally {
|
|
62
|
+
savingSlack.value = false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function clearSlack() {
|
|
67
|
+
savingSlack.value = true
|
|
68
|
+
try {
|
|
69
|
+
await store.save(props.accountId, { secrets: { slackOAuth: null } })
|
|
70
|
+
toast.add({ title: 'Slack OAuth cleared', icon: 'i-lucide-check', color: 'success' })
|
|
71
|
+
} catch (e) {
|
|
72
|
+
toast.add({
|
|
73
|
+
title: 'Could not clear Slack OAuth',
|
|
74
|
+
description: e instanceof Error ? e.message : String(e),
|
|
75
|
+
color: 'error',
|
|
76
|
+
})
|
|
77
|
+
} finally {
|
|
78
|
+
savingSlack.value = false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function saveWeb() {
|
|
83
|
+
const brave = web.braveApiKey.trim()
|
|
84
|
+
const searxng = web.searxngUrl.trim()
|
|
85
|
+
if (!brave && !searxng) {
|
|
86
|
+
toast.add({ title: 'Enter a Brave key or a SearXNG URL', color: 'error' })
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
savingWeb.value = true
|
|
90
|
+
try {
|
|
91
|
+
await store.save(props.accountId, {
|
|
92
|
+
secrets: {
|
|
93
|
+
webSearch: {
|
|
94
|
+
...(brave ? { braveApiKey: brave } : {}),
|
|
95
|
+
...(searxng ? { searxngUrl: searxng } : {}),
|
|
96
|
+
...(web.searxngApiKey.trim() ? { searxngApiKey: web.searxngApiKey.trim() } : {}),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
web.braveApiKey = ''
|
|
101
|
+
web.searxngUrl = ''
|
|
102
|
+
web.searxngApiKey = ''
|
|
103
|
+
toast.add({ title: 'Web search keys saved', icon: 'i-lucide-check', color: 'success' })
|
|
104
|
+
} catch (e) {
|
|
105
|
+
toast.add({
|
|
106
|
+
title: 'Could not save web search keys',
|
|
107
|
+
description: e instanceof Error ? e.message : String(e),
|
|
108
|
+
color: 'error',
|
|
109
|
+
})
|
|
110
|
+
} finally {
|
|
111
|
+
savingWeb.value = false
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function clearWeb() {
|
|
116
|
+
savingWeb.value = true
|
|
117
|
+
try {
|
|
118
|
+
await store.save(props.accountId, { secrets: { webSearch: null } })
|
|
119
|
+
toast.add({ title: 'Web search keys cleared', icon: 'i-lucide-check', color: 'success' })
|
|
120
|
+
} catch (e) {
|
|
121
|
+
toast.add({
|
|
122
|
+
title: 'Could not clear web search keys',
|
|
123
|
+
description: e instanceof Error ? e.message : String(e),
|
|
124
|
+
color: 'error',
|
|
125
|
+
})
|
|
126
|
+
} finally {
|
|
127
|
+
savingWeb.value = false
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
</script>
|
|
131
|
+
|
|
132
|
+
<template>
|
|
133
|
+
<div v-if="store.available !== false" class="space-y-6">
|
|
134
|
+
<div>
|
|
135
|
+
<h3 class="mb-1 font-semibold text-white">Deployment integrations</h3>
|
|
136
|
+
<p class="text-[11px] text-slate-400">
|
|
137
|
+
Credentials shared by every workspace in this account, sealed at rest. Values are never
|
|
138
|
+
shown after saving; leave a field blank to keep the stored secret.
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<!-- Slack app OAuth -->
|
|
143
|
+
<section class="space-y-2">
|
|
144
|
+
<div class="flex items-center gap-2">
|
|
145
|
+
<h4 class="text-sm font-semibold text-slate-200">Slack app (OAuth)</h4>
|
|
146
|
+
<UBadge
|
|
147
|
+
:color="summary?.slackOAuthConfigured ? 'success' : 'neutral'"
|
|
148
|
+
variant="subtle"
|
|
149
|
+
size="xs"
|
|
150
|
+
>
|
|
151
|
+
{{ summary?.slackOAuthConfigured ? 'Configured' : 'Not set' }}
|
|
152
|
+
</UBadge>
|
|
153
|
+
</div>
|
|
154
|
+
<p class="text-[11px] text-slate-400">
|
|
155
|
+
Enables the "Add to Slack" OAuth flow. Without it, workspaces can still connect Slack by
|
|
156
|
+
pasting a bot token.
|
|
157
|
+
</p>
|
|
158
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
|
159
|
+
<UInput v-model="slack.clientId" placeholder="Client ID" size="sm" />
|
|
160
|
+
<UInput
|
|
161
|
+
v-model="slack.clientSecret"
|
|
162
|
+
type="password"
|
|
163
|
+
placeholder="Client secret"
|
|
164
|
+
size="sm"
|
|
165
|
+
/>
|
|
166
|
+
<UInput v-model="slack.redirectUrl" placeholder="Redirect URL" size="sm" />
|
|
167
|
+
</div>
|
|
168
|
+
<div class="flex gap-2">
|
|
169
|
+
<UButton
|
|
170
|
+
color="primary"
|
|
171
|
+
size="xs"
|
|
172
|
+
icon="i-lucide-save"
|
|
173
|
+
:loading="savingSlack"
|
|
174
|
+
@click="saveSlack"
|
|
175
|
+
>
|
|
176
|
+
Save
|
|
177
|
+
</UButton>
|
|
178
|
+
<UButton
|
|
179
|
+
v-if="summary?.slackOAuthConfigured"
|
|
180
|
+
color="neutral"
|
|
181
|
+
variant="ghost"
|
|
182
|
+
size="xs"
|
|
183
|
+
:loading="savingSlack"
|
|
184
|
+
@click="clearSlack"
|
|
185
|
+
>
|
|
186
|
+
Clear
|
|
187
|
+
</UButton>
|
|
188
|
+
</div>
|
|
189
|
+
</section>
|
|
190
|
+
|
|
191
|
+
<!-- Web search keys -->
|
|
192
|
+
<section class="space-y-2 border-t border-slate-800 pt-6">
|
|
193
|
+
<div class="flex items-center gap-2">
|
|
194
|
+
<h4 class="text-sm font-semibold text-slate-200">Container web search</h4>
|
|
195
|
+
<UBadge :color="summary?.webSearch ? 'success' : 'neutral'" variant="subtle" size="xs">
|
|
196
|
+
{{ summary?.webSearch ? `Configured (${summary.webSearch})` : 'Not set' }}
|
|
197
|
+
</UBadge>
|
|
198
|
+
</div>
|
|
199
|
+
<p class="text-[11px] text-slate-400">
|
|
200
|
+
The search upstream container agents reach through the backend proxy. Set a Brave key
|
|
201
|
+
(recommended), or a self-hosted SearXNG URL (with an optional bearer key).
|
|
202
|
+
</p>
|
|
203
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
|
204
|
+
<UInput v-model="web.braveApiKey" type="password" placeholder="Brave API key" size="sm" />
|
|
205
|
+
<UInput v-model="web.searxngUrl" placeholder="SearXNG URL" size="sm" />
|
|
206
|
+
<UInput
|
|
207
|
+
v-model="web.searxngApiKey"
|
|
208
|
+
type="password"
|
|
209
|
+
placeholder="SearXNG key (optional)"
|
|
210
|
+
size="sm"
|
|
211
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="flex gap-2">
|
|
214
|
+
<UButton
|
|
215
|
+
color="primary"
|
|
216
|
+
size="xs"
|
|
217
|
+
icon="i-lucide-save"
|
|
218
|
+
:loading="savingWeb"
|
|
219
|
+
@click="saveWeb"
|
|
220
|
+
>
|
|
221
|
+
Save
|
|
222
|
+
</UButton>
|
|
223
|
+
<UButton
|
|
224
|
+
v-if="summary?.webSearch"
|
|
225
|
+
color="neutral"
|
|
226
|
+
variant="ghost"
|
|
227
|
+
size="xs"
|
|
228
|
+
:loading="savingWeb"
|
|
229
|
+
@click="clearWeb"
|
|
230
|
+
>
|
|
231
|
+
Clear
|
|
232
|
+
</UButton>
|
|
233
|
+
</div>
|
|
234
|
+
</section>
|
|
235
|
+
</div>
|
|
236
|
+
</template>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onMounted, ref } from 'vue'
|
|
3
3
|
import type { AccountRole } from '~/types/domain'
|
|
4
|
+
import AccountDeploymentSettings from '~/components/layout/AccountDeploymentSettings.vue'
|
|
4
5
|
|
|
5
6
|
// Team settings for an org account: the member roster (with combinable admin /
|
|
6
7
|
// developer / product roles), pending email invitations, and the per-account
|
|
@@ -236,5 +237,10 @@ async function disconnectEmail() {
|
|
|
236
237
|
<ProvidersApiKeysSection :account-id="accountId" category="proxy" />
|
|
237
238
|
</div>
|
|
238
239
|
</section>
|
|
240
|
+
|
|
241
|
+
<!-- deployment integration secrets (admin-only): Slack OAuth app + web-search keys -->
|
|
242
|
+
<section v-if="isAdmin">
|
|
243
|
+
<AccountDeploymentSettings :account-id="accountId" />
|
|
244
|
+
</section>
|
|
239
245
|
</div>
|
|
240
246
|
</template>
|
|
@@ -181,6 +181,14 @@ const commands = computed<Command[]>(() => {
|
|
|
181
181
|
keywords: 'local model runner ollama lm studio llamacpp vllm endpoint',
|
|
182
182
|
run: () => ui.openLocalModels(),
|
|
183
183
|
})
|
|
184
|
+
list.push({
|
|
185
|
+
id: 'sandbox',
|
|
186
|
+
label: 'Open Sandbox',
|
|
187
|
+
group: 'Workspace',
|
|
188
|
+
icon: 'i-lucide-flask-conical',
|
|
189
|
+
keywords: 'sandbox prompt model test experiment judge fixture benchmark evaluate',
|
|
190
|
+
run: () => ui.openSandbox(),
|
|
191
|
+
})
|
|
184
192
|
|
|
185
193
|
return list
|
|
186
194
|
})
|
|
@@ -125,6 +125,19 @@ watch(
|
|
|
125
125
|
>
|
|
126
126
|
Integrations
|
|
127
127
|
</UButton>
|
|
128
|
+
<!-- The Sandbox: try prompt versions/models against graded fixtures, off to the
|
|
129
|
+
side of the board. Opens the on-demand testing window. -->
|
|
130
|
+
<UButton
|
|
131
|
+
block
|
|
132
|
+
color="primary"
|
|
133
|
+
variant="soft"
|
|
134
|
+
size="sm"
|
|
135
|
+
icon="i-lucide-flask-conical"
|
|
136
|
+
class="justify-start"
|
|
137
|
+
@click="ui.openSandbox()"
|
|
138
|
+
>
|
|
139
|
+
Sandbox
|
|
140
|
+
</UButton>
|
|
128
141
|
</div>
|
|
129
142
|
</section>
|
|
130
143
|
|