@cat-factory/app 0.70.0 → 0.71.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/auth/LoginScreen.vue +3 -2
- package/app/components/board/AddTaskModal.vue +7 -1
- package/app/components/board/AgentFailureCard.vue +3 -0
- package/app/components/board/RecurringPipelineModal.vue +6 -1
- package/app/components/board/nodes/BlockNode.vue +10 -2
- package/app/components/board/nodes/TaskCard.vue +12 -1
- package/app/components/bootstrap/BootstrapModal.vue +3 -0
- package/app/components/common/ConfirmDialog.vue +59 -0
- package/app/components/common/EmptyState.vue +35 -0
- package/app/components/common/KeyboardShortcutsHelp.vue +48 -0
- package/app/components/documents/ContextDocumentPicker.vue +8 -4
- package/app/components/documents/DocumentSourceConnectModal.vue +4 -3
- package/app/components/focus/BlockFocusView.vue +12 -7
- package/app/components/fragments/FragmentLibraryManager.vue +10 -0
- package/app/components/github/GitHubPanel.vue +9 -0
- package/app/components/humanTest/HumanTestWindow.vue +15 -1
- package/app/components/layout/AccountDeploymentSettings.vue +4 -0
- package/app/components/layout/AccountTeamSettings.vue +8 -2
- package/app/components/layout/AiProvidersBanner.vue +3 -1
- package/app/components/layout/BoardSwitcher.vue +11 -0
- package/app/components/layout/CommandBar.vue +8 -0
- package/app/components/layout/InfraSetupBanner.vue +192 -0
- package/app/components/layout/NotificationsInbox.vue +11 -0
- package/app/components/layout/ProviderConfigBanner.vue +3 -1
- package/app/components/panels/InspectorPanel.vue +18 -20
- package/app/components/panels/StepContainerStatus.vue +37 -0
- package/app/components/panels/inspector/FrontendConfig.vue +19 -4
- package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +4 -0
- package/app/components/panels/inspector/TaskDependencies.vue +18 -4
- package/app/components/panels/inspector/TaskExecution.vue +24 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +8 -1
- package/app/components/pipeline/PipelineBuilder.vue +13 -1
- package/app/components/providers/ApiKeysSection.vue +4 -0
- package/app/components/providers/PersonalSubscriptionSection.vue +9 -0
- package/app/components/providers/VendorCredentialsModal.vue +12 -3
- package/app/components/settings/CustomManifestTypeEditor.vue +3 -0
- package/app/components/settings/InfraHandlersConfigurator.vue +72 -12
- package/app/components/settings/KubernetesEngineForm.vue +41 -5
- package/app/components/settings/LocalModelEndpointsPanel.vue +11 -0
- package/app/components/settings/MergeThresholdsPanel.vue +9 -0
- package/app/components/settings/ModelConfigurationPanel.vue +9 -0
- package/app/components/settings/ObservabilityConnectionPanel.vue +7 -0
- package/app/components/settings/ProviderConnectionTab.vue +2 -0
- package/app/components/settings/UserSecretsSection.vue +11 -0
- package/app/components/slack/SlackPanel.vue +9 -0
- package/app/components/tasks/ContextIssuePicker.vue +8 -4
- package/app/components/tasks/TaskSourceConnectModal.vue +4 -3
- package/app/composables/useBlockDeletion.ts +63 -0
- package/app/composables/useConfirm.ts +63 -0
- package/app/composables/useConfirmAction.ts +96 -0
- package/app/composables/useKeyboardShortcuts.ts +76 -0
- package/app/composables/usePipelineErrorToast.ts +2 -0
- package/app/composables/useWorkspaceStream.ts +26 -6
- package/app/pages/index.vue +28 -4
- package/app/stores/agentRuns.spec.ts +74 -0
- package/app/stores/agentRuns.ts +33 -5
- package/app/stores/ui.ts +33 -1
- package/app/stores/workspace.ts +10 -2
- package/app/types/domain.ts +3 -0
- package/app/utils/pipeline.ts +22 -0
- package/i18n/locales/en.json +174 -12
- package/i18n/locales/es.json +176 -17
- package/i18n/locales/fr.json +176 -17
- package/i18n/locales/he.json +176 -17
- package/i18n/locales/ja.json +176 -17
- package/i18n/locales/pl.json +176 -17
- package/i18n/locales/tr.json +176 -17
- package/i18n/locales/uk.json +176 -17
- package/package.json +3 -2
|
@@ -14,6 +14,7 @@ const ui = useUiStore()
|
|
|
14
14
|
const workspace = useWorkspaceStore()
|
|
15
15
|
const creds = useVendorCredentialsStore()
|
|
16
16
|
const toast = useToast()
|
|
17
|
+
const { confirm } = useConfirm()
|
|
17
18
|
|
|
18
19
|
const open = computed({
|
|
19
20
|
get: () => ui.vendorCredentialsOpen,
|
|
@@ -139,9 +140,17 @@ async function add() {
|
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
async function remove(id: string) {
|
|
143
|
+
async function remove(cred: { id: string; label: string }) {
|
|
144
|
+
const ok = await confirm({
|
|
145
|
+
title: t('providers.vendorCredentials.confirmRemove.title'),
|
|
146
|
+
description: t('providers.vendorCredentials.confirmRemove.body', { name: cred.label }),
|
|
147
|
+
variant: 'destructive',
|
|
148
|
+
confirmLabel: t('common.remove'),
|
|
149
|
+
icon: 'i-lucide-trash-2',
|
|
150
|
+
})
|
|
151
|
+
if (!ok) return
|
|
143
152
|
try {
|
|
144
|
-
await creds.remove(id)
|
|
153
|
+
await creds.remove(cred.id)
|
|
145
154
|
} catch (e) {
|
|
146
155
|
toast.add({
|
|
147
156
|
title: t('providers.vendorCredentials.toast.removeFailed'),
|
|
@@ -258,7 +267,7 @@ function vendorLabel(v: SubscriptionVendor): string {
|
|
|
258
267
|
color="error"
|
|
259
268
|
variant="ghost"
|
|
260
269
|
size="xs"
|
|
261
|
-
@click="remove(c
|
|
270
|
+
@click="remove(c)"
|
|
262
271
|
/>
|
|
263
272
|
</div>
|
|
264
273
|
</div>
|
|
@@ -10,6 +10,7 @@ import type { CustomManifestType } from '@cat-factory/contracts'
|
|
|
10
10
|
const { t } = useI18n()
|
|
11
11
|
const infra = useInfraConfigStore()
|
|
12
12
|
const toast = useToast()
|
|
13
|
+
const { confirmAction, toastDone } = useConfirmAction()
|
|
13
14
|
|
|
14
15
|
// A draft for the add/edit form. `manifestId` is locked on edit (it's the PK).
|
|
15
16
|
const draft = reactive({
|
|
@@ -79,10 +80,12 @@ async function save() {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
async function remove(type: CustomManifestType) {
|
|
83
|
+
if (!(await confirmAction('remove', type.label))) return
|
|
82
84
|
busy.value = true
|
|
83
85
|
try {
|
|
84
86
|
await infra.removeCustomType(type.manifestId)
|
|
85
87
|
if (editing.value && draft.manifestId === type.manifestId) startAdd()
|
|
88
|
+
toastDone('remove', type.label)
|
|
86
89
|
} catch (e) {
|
|
87
90
|
toast.add({
|
|
88
91
|
title: t('settings.infrastructure.customType.removeFailed'),
|
|
@@ -31,6 +31,7 @@ const infra = useInfraConfigStore()
|
|
|
31
31
|
const auth = useAuthStore()
|
|
32
32
|
const ui = useUiStore()
|
|
33
33
|
const toast = useToast()
|
|
34
|
+
const { confirmAction } = useConfirmAction()
|
|
34
35
|
|
|
35
36
|
const isLocal = computed(() => auth.localMode?.enabled === true)
|
|
36
37
|
|
|
@@ -116,6 +117,26 @@ async function testKube(payload: { config: KubeHandlerConfig; secrets: Record<st
|
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
// Test the ALREADY-SAVED workspace kube handler as-is (from its stored non-secret config), so
|
|
121
|
+
// an operator can verify an established connection straight from the list without re-opening the
|
|
122
|
+
// form or re-typing the token — the backend fills the stored token for the probe (empty secrets
|
|
123
|
+
// here ⇒ "use the saved one"). Distinct state from the form's own test so results don't cross.
|
|
124
|
+
const kubeSavedTesting = ref(false)
|
|
125
|
+
const kubeSavedTestResult = ref<TestResult>(null)
|
|
126
|
+
async function testSavedKube() {
|
|
127
|
+
const config = kubeHandler.value?.config
|
|
128
|
+
if (!config) return
|
|
129
|
+
kubeSavedTesting.value = true
|
|
130
|
+
kubeSavedTestResult.value = null
|
|
131
|
+
try {
|
|
132
|
+
kubeSavedTestResult.value = await infra.testHandler({ config, secrets: {} })
|
|
133
|
+
} catch (e) {
|
|
134
|
+
kubeSavedTestResult.value = { ok: false, message: e instanceof Error ? e.message : String(e) }
|
|
135
|
+
} finally {
|
|
136
|
+
kubeSavedTesting.value = false
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
119
140
|
async function testKubeOverride(payload: {
|
|
120
141
|
config: KubeHandlerConfig
|
|
121
142
|
secrets: Record<string, string>
|
|
@@ -154,6 +175,7 @@ async function saveKube(payload: { config: KubeHandlerConfig; secrets: Record<st
|
|
|
154
175
|
}
|
|
155
176
|
|
|
156
177
|
async function removeKube() {
|
|
178
|
+
if (!(await confirmAction('remove', t('settings.infrastructure.handler.kubeNoun')))) return
|
|
157
179
|
busy.value = true
|
|
158
180
|
try {
|
|
159
181
|
await infra.unregisterHandler('kubernetes')
|
|
@@ -190,6 +212,8 @@ async function saveKubeOverride(payload: {
|
|
|
190
212
|
}
|
|
191
213
|
|
|
192
214
|
async function removeKubeOverride() {
|
|
215
|
+
if (!(await confirmAction('remove', t('settings.infrastructure.handler.kubeOverrideNoun'))))
|
|
216
|
+
return
|
|
193
217
|
busy.value = true
|
|
194
218
|
try {
|
|
195
219
|
await infra.removeUserHandler('kubernetes')
|
|
@@ -311,6 +335,7 @@ async function saveCustom(payload: {
|
|
|
311
335
|
|
|
312
336
|
async function removeCustom() {
|
|
313
337
|
if (!selectedCustomId.value) return
|
|
338
|
+
if (!(await confirmAction('remove', t('settings.infrastructure.handler.customNoun')))) return
|
|
314
339
|
busy.value = true
|
|
315
340
|
try {
|
|
316
341
|
await infra.unregisterHandler('custom', selectedCustomId.value)
|
|
@@ -354,22 +379,57 @@ function notifyError(e: unknown) {
|
|
|
354
379
|
<h3 class="text-sm font-semibold text-slate-200">
|
|
355
380
|
{{ t('inspector.testConfig.provisionTypes.kubernetes') }}
|
|
356
381
|
</h3>
|
|
357
|
-
|
|
382
|
+
<!-- Established-connection card: a prominent checkbox signals a connection is stored, and
|
|
383
|
+
a Test button verifies the SAVED connection (the token is reused server-side) without
|
|
384
|
+
re-opening the form. Absent ⇒ the "not connected yet" hint. -->
|
|
385
|
+
<div
|
|
358
386
|
v-if="kubeHandler"
|
|
359
|
-
class="
|
|
387
|
+
class="space-y-2 rounded-md border border-emerald-500/30 bg-emerald-500/5 p-2.5"
|
|
360
388
|
>
|
|
361
|
-
<
|
|
389
|
+
<div class="flex items-start justify-between gap-2">
|
|
390
|
+
<UCheckbox
|
|
391
|
+
:model-value="true"
|
|
392
|
+
disabled
|
|
393
|
+
size="lg"
|
|
394
|
+
:label="t('settings.infrastructure.handler.connectionEstablished')"
|
|
395
|
+
:ui="{ label: 'text-[13px] font-semibold text-emerald-300' }"
|
|
396
|
+
/>
|
|
397
|
+
<UButton
|
|
398
|
+
icon="i-lucide-trash-2"
|
|
399
|
+
color="error"
|
|
400
|
+
variant="ghost"
|
|
401
|
+
size="xs"
|
|
402
|
+
:disabled="busy"
|
|
403
|
+
:aria-label="t('settings.infrastructure.handler.disconnect')"
|
|
404
|
+
@click="removeKube"
|
|
405
|
+
/>
|
|
406
|
+
</div>
|
|
407
|
+
<p class="pl-7 text-[11px] text-slate-300">
|
|
362
408
|
{{ t('settings.infrastructure.handler.activeEngine') }}
|
|
363
409
|
<span class="text-slate-200">{{ kubeHandlerEngineLabel }}</span>
|
|
364
|
-
</
|
|
365
|
-
<
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
410
|
+
</p>
|
|
411
|
+
<div class="flex items-center gap-2 pl-7">
|
|
412
|
+
<UButton
|
|
413
|
+
color="neutral"
|
|
414
|
+
variant="soft"
|
|
415
|
+
size="xs"
|
|
416
|
+
icon="i-lucide-plug-zap"
|
|
417
|
+
:loading="kubeSavedTesting"
|
|
418
|
+
@click="testSavedKube"
|
|
419
|
+
>
|
|
420
|
+
{{ t('settings.providerConnection.test.button') }}
|
|
421
|
+
</UButton>
|
|
422
|
+
<span v-if="kubeSavedTestResult?.ok" class="text-xs text-emerald-400">
|
|
423
|
+
{{ kubeSavedTestResult.message ?? t('settings.providerConnection.test.ok') }}
|
|
424
|
+
</span>
|
|
425
|
+
<span v-else-if="kubeSavedTestResult" class="text-xs text-rose-400">
|
|
426
|
+
{{ kubeSavedTestResult.message ?? t('settings.providerConnection.test.failed') }}
|
|
427
|
+
</span>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
<p v-else class="flex items-center gap-1.5 text-[12px] text-slate-500">
|
|
431
|
+
<UIcon name="i-lucide-circle-dashed" class="h-3.5 w-3.5" />
|
|
432
|
+
{{ t('settings.infrastructure.handler.notConnected') }}
|
|
373
433
|
</p>
|
|
374
434
|
|
|
375
435
|
<div class="space-y-1">
|
|
@@ -184,9 +184,19 @@ const urlValid = computed(() => {
|
|
|
184
184
|
})
|
|
185
185
|
|
|
186
186
|
const connected = computed(() => !!props.handler)
|
|
187
|
+
// Whether a token is already SAVED for this handler. The value is write-only (never returned),
|
|
188
|
+
// so on edit the token field starts blank and the operator may leave it blank to keep the stored
|
|
189
|
+
// token — the backend falls back to it for both the connection test and the save. A new value
|
|
190
|
+
// entered here replaces it.
|
|
191
|
+
const tokenStored = computed(
|
|
192
|
+
() => !!props.handler && props.handler.secretKeys.includes(KUBERNETES_ENV_TOKEN_SECRET_KEY),
|
|
193
|
+
)
|
|
187
194
|
const canSave = computed(
|
|
188
195
|
() =>
|
|
189
|
-
!!form.label.trim() &&
|
|
196
|
+
!!form.label.trim() &&
|
|
197
|
+
!!form.apiServerUrl.trim() &&
|
|
198
|
+
(tokenStored.value || !!apiToken.value.trim()) &&
|
|
199
|
+
urlValid.value,
|
|
190
200
|
)
|
|
191
201
|
|
|
192
202
|
// Why the Connect button is disabled, surfaced as a red hint next to it so a mandatory-field gap
|
|
@@ -198,7 +208,10 @@ const connectBlockedReason = computed(() => {
|
|
|
198
208
|
if (!form.label.trim()) missing.push(t('settings.infrastructure.kubernetesEngine.label'))
|
|
199
209
|
if (!form.apiServerUrl.trim())
|
|
200
210
|
missing.push(t('settings.infrastructure.kubernetesEngine.apiServerUrl'))
|
|
201
|
-
|
|
211
|
+
// The token is required only for a FIRST connect; on edit a saved token is preserved when
|
|
212
|
+
// the field is left blank, so don't flag it as missing.
|
|
213
|
+
if (!tokenStored.value && !apiToken.value.trim())
|
|
214
|
+
missing.push(t('settings.infrastructure.kubernetesEngine.apiToken'))
|
|
202
215
|
if (form.urlSource === 'ingressTemplate' && !form.hostTemplate.trim())
|
|
203
216
|
missing.push(t('settings.infrastructure.kubernetesEngine.hostTemplate'))
|
|
204
217
|
if (form.urlSource === 'serviceStatus' && !form.serviceName.trim())
|
|
@@ -239,9 +252,12 @@ function buildPayload(): KubeHandlerPayload {
|
|
|
239
252
|
if (form.imageTemplate.trim()) kubernetes.imageTemplate = form.imageTemplate.trim()
|
|
240
253
|
// One honest assertion at the boundary that actually builds the shape (the reactive form is
|
|
241
254
|
// dynamically assembled, then validated server-side); the emitted config flows typed onward.
|
|
255
|
+
// OMIT the token when the field is blank so the backend preserves the saved one (a blank
|
|
256
|
+
// secret means "keep it") — only a typed value is sent, and it replaces the stored token.
|
|
257
|
+
const token = apiToken.value.trim()
|
|
242
258
|
return {
|
|
243
259
|
config: { engine: props.engine, kubernetes } as unknown as KubeHandlerConfig,
|
|
244
|
-
secrets: { [KUBERNETES_ENV_TOKEN_SECRET_KEY]:
|
|
260
|
+
secrets: token ? { [KUBERNETES_ENV_TOKEN_SECRET_KEY]: token } : {},
|
|
245
261
|
}
|
|
246
262
|
}
|
|
247
263
|
|
|
@@ -322,9 +338,29 @@ async function copyAutoSetupCommand() {
|
|
|
322
338
|
|
|
323
339
|
<UFormField
|
|
324
340
|
:label="t('settings.infrastructure.kubernetesEngine.apiToken')"
|
|
325
|
-
:help="
|
|
341
|
+
:help="
|
|
342
|
+
tokenStored
|
|
343
|
+
? t('settings.infrastructure.kubernetesEngine.apiTokenHelpStored')
|
|
344
|
+
: t('settings.infrastructure.kubernetesEngine.apiTokenHelp')
|
|
345
|
+
"
|
|
326
346
|
>
|
|
327
|
-
<
|
|
347
|
+
<template v-if="tokenStored" #hint>
|
|
348
|
+
<span class="inline-flex items-center gap-1 text-[11px] text-emerald-400">
|
|
349
|
+
<UIcon name="i-lucide-check-circle-2" class="h-3.5 w-3.5" />
|
|
350
|
+
{{ t('settings.infrastructure.kubernetesEngine.tokenSaved') }}
|
|
351
|
+
</span>
|
|
352
|
+
</template>
|
|
353
|
+
<UInput
|
|
354
|
+
v-model="apiToken"
|
|
355
|
+
type="password"
|
|
356
|
+
class="font-mono"
|
|
357
|
+
autocomplete="off"
|
|
358
|
+
:placeholder="
|
|
359
|
+
tokenStored
|
|
360
|
+
? t('settings.infrastructure.kubernetesEngine.apiTokenPlaceholderStored')
|
|
361
|
+
: undefined
|
|
362
|
+
"
|
|
363
|
+
/>
|
|
328
364
|
</UFormField>
|
|
329
365
|
|
|
330
366
|
<!-- URL derivation: how the live environment URL is resolved once the service's
|
|
@@ -13,6 +13,7 @@ const { t } = useI18n()
|
|
|
13
13
|
const ui = useUiStore()
|
|
14
14
|
const store = useLocalModelsStore()
|
|
15
15
|
const toast = useToast()
|
|
16
|
+
const { confirm } = useConfirm()
|
|
16
17
|
|
|
17
18
|
const open = computed({
|
|
18
19
|
get: () => ui.localModelsOpen,
|
|
@@ -140,6 +141,16 @@ async function save() {
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
async function remove(p: LocalRunner) {
|
|
144
|
+
const ok = await confirm({
|
|
145
|
+
title: t('settings.localModelEndpoints.confirmRemove.title'),
|
|
146
|
+
description: t('settings.localModelEndpoints.confirmRemove.body', {
|
|
147
|
+
name: LOCAL_RUNNER_LABELS[p],
|
|
148
|
+
}),
|
|
149
|
+
variant: 'destructive',
|
|
150
|
+
confirmLabel: t('common.remove'),
|
|
151
|
+
icon: 'i-lucide-trash-2',
|
|
152
|
+
})
|
|
153
|
+
if (!ok) return
|
|
143
154
|
busy.value = true
|
|
144
155
|
try {
|
|
145
156
|
await store.remove(p)
|
|
@@ -29,6 +29,7 @@ const CONCERN_LEVELS = computed<{ value: RequirementConcernLevel; label: string
|
|
|
29
29
|
|
|
30
30
|
const store = useMergePresetsStore()
|
|
31
31
|
const toast = useToast()
|
|
32
|
+
const { confirm } = useConfirm()
|
|
32
33
|
|
|
33
34
|
// Local editable copy per preset, kept in sync with the store. Percentages are
|
|
34
35
|
// edited 0..100 and stored 0..1.
|
|
@@ -116,6 +117,14 @@ async function makeDefault(p: MergeThresholdPreset) {
|
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
async function remove(p: MergeThresholdPreset) {
|
|
120
|
+
const ok = await confirm({
|
|
121
|
+
title: t('settings.mergeThresholds.confirmDelete.title'),
|
|
122
|
+
description: t('settings.mergeThresholds.confirmDelete.body', { name: p.name }),
|
|
123
|
+
variant: 'destructive',
|
|
124
|
+
confirmLabel: t('common.delete'),
|
|
125
|
+
icon: 'i-lucide-trash-2',
|
|
126
|
+
})
|
|
127
|
+
if (!ok) return
|
|
119
128
|
busy.value = p.id
|
|
120
129
|
try {
|
|
121
130
|
await store.remove(p.id)
|
|
@@ -25,6 +25,7 @@ const agents = useAgentsStore()
|
|
|
25
25
|
const creds = useVendorCredentialsStore()
|
|
26
26
|
const workspace = useWorkspaceStore()
|
|
27
27
|
const toast = useToast()
|
|
28
|
+
const { confirm } = useConfirm()
|
|
28
29
|
|
|
29
30
|
const open = computed({
|
|
30
31
|
get: () => ui.modelConfigOpen,
|
|
@@ -144,6 +145,14 @@ async function setDefault(p: ModelPreset) {
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
async function remove(p: ModelPreset) {
|
|
148
|
+
const ok = await confirm({
|
|
149
|
+
title: t('settings.modelConfiguration.confirmDelete.title'),
|
|
150
|
+
description: t('settings.modelConfiguration.confirmDelete.body', { name: p.name }),
|
|
151
|
+
variant: 'destructive',
|
|
152
|
+
confirmLabel: t('common.delete'),
|
|
153
|
+
icon: 'i-lucide-trash-2',
|
|
154
|
+
})
|
|
155
|
+
if (!ok) return
|
|
147
156
|
busy.value = true
|
|
148
157
|
try {
|
|
149
158
|
await presets.remove(p.id)
|
|
@@ -12,6 +12,7 @@ const { t } = useI18n()
|
|
|
12
12
|
const ui = useUiStore()
|
|
13
13
|
const store = useReleaseHealthStore()
|
|
14
14
|
const toast = useToast()
|
|
15
|
+
const { confirmAction, toastDone } = useConfirmAction()
|
|
15
16
|
|
|
16
17
|
const open = computed({
|
|
17
18
|
get: () => ui.observabilityConnectionOpen,
|
|
@@ -95,9 +96,12 @@ async function saveIncident() {
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
async function disconnectIncident() {
|
|
99
|
+
const noun = t('settings.observabilityConnection.incidentNoun')
|
|
100
|
+
if (!(await confirmAction('disconnect', noun))) return
|
|
98
101
|
incidentBusy.value = true
|
|
99
102
|
try {
|
|
100
103
|
await store.removeIncident()
|
|
104
|
+
toastDone('disconnect', noun)
|
|
101
105
|
} catch (e) {
|
|
102
106
|
notifyError(t('settings.observabilityConnection.toast.incidentDisconnectFailed'), e)
|
|
103
107
|
} finally {
|
|
@@ -127,9 +131,12 @@ async function saveConnection() {
|
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
async function disconnect() {
|
|
134
|
+
const noun = t('settings.observabilityConnection.connectionNoun')
|
|
135
|
+
if (!(await confirmAction('disconnect', noun))) return
|
|
130
136
|
busy.value = true
|
|
131
137
|
try {
|
|
132
138
|
await store.removeConnection()
|
|
139
|
+
toastDone('disconnect', noun)
|
|
133
140
|
} catch (e) {
|
|
134
141
|
notifyError(t('settings.observabilityConnection.toast.disconnectFailed'), e)
|
|
135
142
|
} finally {
|
|
@@ -29,6 +29,7 @@ const emit = defineEmits<{ connected: [] }>()
|
|
|
29
29
|
const { t } = useI18n()
|
|
30
30
|
const store = useProviderConnectionsStore()
|
|
31
31
|
const toast = useToast()
|
|
32
|
+
const { confirmAction } = useConfirmAction()
|
|
32
33
|
|
|
33
34
|
const descriptor = computed(() => store.descriptorFor(props.kind))
|
|
34
35
|
const connection = computed(() => store.connectionFor(props.kind))
|
|
@@ -250,6 +251,7 @@ async function saveConfig(payload: {
|
|
|
250
251
|
}
|
|
251
252
|
|
|
252
253
|
async function remove() {
|
|
254
|
+
if (!(await confirmAction('remove', descriptor.value?.label ?? props.kind))) return
|
|
253
255
|
busy.value = true
|
|
254
256
|
try {
|
|
255
257
|
await store.remove(props.kind)
|
|
@@ -12,6 +12,7 @@ const { t } = useI18n()
|
|
|
12
12
|
const ui = useUiStore()
|
|
13
13
|
const store = useUserSecretsStore()
|
|
14
14
|
const toast = useToast()
|
|
15
|
+
const { confirm } = useConfirm()
|
|
15
16
|
|
|
16
17
|
const open = computed({
|
|
17
18
|
get: () => ui.userSecretsOpen,
|
|
@@ -117,6 +118,16 @@ async function save() {
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
async function remove() {
|
|
121
|
+
const ok = await confirm({
|
|
122
|
+
title: t('settings.userSecrets.confirmRemove.title'),
|
|
123
|
+
description: t('settings.userSecrets.confirmRemove.body', {
|
|
124
|
+
name: descriptor.value?.label ?? t('settings.userSecrets.secretFallback'),
|
|
125
|
+
}),
|
|
126
|
+
variant: 'destructive',
|
|
127
|
+
confirmLabel: t('common.remove'),
|
|
128
|
+
icon: 'i-lucide-trash-2',
|
|
129
|
+
})
|
|
130
|
+
if (!ok) return
|
|
120
131
|
busy.value = true
|
|
121
132
|
try {
|
|
122
133
|
await store.remove(kind.value)
|
|
@@ -13,6 +13,7 @@ const ui = useUiStore()
|
|
|
13
13
|
const slack = useSlackStore()
|
|
14
14
|
const toast = useToast()
|
|
15
15
|
const { t } = useI18n()
|
|
16
|
+
const { confirm } = useConfirm()
|
|
16
17
|
|
|
17
18
|
const open = computed({
|
|
18
19
|
get: () => ui.slackOpen,
|
|
@@ -108,6 +109,14 @@ async function connectWithToken() {
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
async function disconnect() {
|
|
112
|
+
const ok = await confirm({
|
|
113
|
+
title: t('slack.confirmDisconnect.title'),
|
|
114
|
+
description: t('slack.confirmDisconnect.body'),
|
|
115
|
+
variant: 'destructive',
|
|
116
|
+
confirmLabel: t('common.disconnect'),
|
|
117
|
+
icon: 'i-lucide-unplug',
|
|
118
|
+
})
|
|
119
|
+
if (!ok) return
|
|
111
120
|
try {
|
|
112
121
|
await slack.disconnect()
|
|
113
122
|
} catch (e) {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// them once the block exists (see useContextLinking). A search hit / pasted ref
|
|
8
8
|
// carries `needsImport: true` so it's fetched + persisted before linking.
|
|
9
9
|
import type { SourceTask, TaskSearchResult, TaskSourceKind } from '~/types/domain'
|
|
10
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
10
11
|
|
|
11
12
|
const props = defineProps<{
|
|
12
13
|
/** contextKeys already staged by the caller, so they're filtered out / not re-offered. */
|
|
@@ -245,15 +246,18 @@ onMounted(() => {
|
|
|
245
246
|
</span>
|
|
246
247
|
</button>
|
|
247
248
|
|
|
248
|
-
<
|
|
249
|
-
|
|
249
|
+
<EmptyState
|
|
250
|
+
v-if="empty"
|
|
251
|
+
compact
|
|
252
|
+
icon="i-lucide-search-x"
|
|
253
|
+
:title="
|
|
250
254
|
query.trim()
|
|
251
255
|
? t('tasks.picker.noMatches')
|
|
252
256
|
: searchable
|
|
253
257
|
? t('tasks.picker.emptySearchable')
|
|
254
258
|
: t('tasks.picker.emptyRefOnly')
|
|
255
|
-
|
|
256
|
-
|
|
259
|
+
"
|
|
260
|
+
/>
|
|
257
261
|
</div>
|
|
258
262
|
</div>
|
|
259
263
|
</template>
|
|
@@ -17,6 +17,7 @@ const { t } = useI18n()
|
|
|
17
17
|
const ui = useUiStore()
|
|
18
18
|
const tasks = useTasksStore()
|
|
19
19
|
const toast = useToast()
|
|
20
|
+
const { confirmAction } = useConfirmAction()
|
|
20
21
|
|
|
21
22
|
const source = computed(() => ui.taskConnect?.source ?? null)
|
|
22
23
|
const descriptor = computed(() => (source.value ? tasks.descriptorFor(source.value) : undefined))
|
|
@@ -99,12 +100,12 @@ async function startOAuth() {
|
|
|
99
100
|
|
|
100
101
|
async function disconnect() {
|
|
101
102
|
if (!source.value) return
|
|
103
|
+
const label = descriptor.value?.label ?? t('tasks.connect.sourceFallback')
|
|
104
|
+
if (!(await confirmAction('disconnect', label))) return
|
|
102
105
|
await tasks.disconnect(source.value)
|
|
103
106
|
await tasks.probe()
|
|
104
107
|
toast.add({
|
|
105
|
-
title: t('tasks.connect.disconnectedToast', {
|
|
106
|
-
label: descriptor.value?.label ?? t('tasks.connect.sourceFallback'),
|
|
107
|
-
}),
|
|
108
|
+
title: t('tasks.connect.disconnectedToast', { label }),
|
|
108
109
|
icon: 'i-lucide-unplug',
|
|
109
110
|
})
|
|
110
111
|
ui.closeTaskConnect()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Block } from '~/types/domain'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The single, confirm-gated block deletion used by BOTH the inspector's Delete button and
|
|
5
|
+
* the global keyboard shortcut, so the two paths can never drift (one safe, one not). It
|
|
6
|
+
* mirrors the ordering the inspector always used — close the inspector, cancel any run, then
|
|
7
|
+
* optimistically delete (the store restores + toasts on backend failure via `RemovalSnapshot`)
|
|
8
|
+
* — and only inserts the confirmation prompt in front of the mutation.
|
|
9
|
+
*
|
|
10
|
+
* A recurring-pipeline task owns its reused block + run history, so removing the schedule
|
|
11
|
+
* deletes them server-side; that case deletes the schedule rather than orphaning it.
|
|
12
|
+
*/
|
|
13
|
+
export function useBlockDeletion() {
|
|
14
|
+
const board = useBoardStore()
|
|
15
|
+
const execution = useExecutionStore()
|
|
16
|
+
const ui = useUiStore()
|
|
17
|
+
const recurring = useRecurringPipelinesStore()
|
|
18
|
+
const { confirm } = useConfirm()
|
|
19
|
+
const { t } = useI18n()
|
|
20
|
+
|
|
21
|
+
/** Resolve the confirm title/body for a block, matching the inspector's delete-label kinds. */
|
|
22
|
+
function copyFor(block: Block): { title: string; body: string } {
|
|
23
|
+
const schedule = recurring.byBlock(block.id)
|
|
24
|
+
const kind = schedule
|
|
25
|
+
? 'recurring'
|
|
26
|
+
: block.level === 'task'
|
|
27
|
+
? 'task'
|
|
28
|
+
: block.level === 'module'
|
|
29
|
+
? 'module'
|
|
30
|
+
: 'service'
|
|
31
|
+
return {
|
|
32
|
+
title: t(`panels.inspector.confirmDelete.${kind}.title`),
|
|
33
|
+
body: t(`panels.inspector.confirmDelete.${kind}.body`, { name: block.title }),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function deleteBlock(block: Block | undefined | null): Promise<boolean> {
|
|
38
|
+
if (!block) return false
|
|
39
|
+
const { title, body } = copyFor(block)
|
|
40
|
+
const ok = await confirm({
|
|
41
|
+
title,
|
|
42
|
+
description: body,
|
|
43
|
+
variant: 'destructive',
|
|
44
|
+
confirmLabel: t('common.delete'),
|
|
45
|
+
icon: 'i-lucide-trash-2',
|
|
46
|
+
})
|
|
47
|
+
if (!ok) return false
|
|
48
|
+
|
|
49
|
+
// Close the inspector right away; the stores hide the block optimistically and restore
|
|
50
|
+
// it (with a toast) only if the backend delete fails.
|
|
51
|
+
ui.select(null)
|
|
52
|
+
const schedule = recurring.byBlock(block.id)
|
|
53
|
+
if (schedule) {
|
|
54
|
+
void recurring.remove(schedule.id)
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
execution.cancel(block.id)
|
|
58
|
+
void board.removeBlock(block.id)
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { deleteBlock }
|
|
63
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readonly, ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A shared, promise-based confirmation dialog. Call `confirm(...)` from anywhere and
|
|
5
|
+
* `await` the boolean the user chose — resolve on Confirm, reject-to-`false` on Cancel or
|
|
6
|
+
* any dismissal (backdrop click, Escape, route change). One `<ConfirmDialog />` is mounted
|
|
7
|
+
* app-wide (in `pages/index.vue`) and reads this singleton, so callers never render a modal
|
|
8
|
+
* themselves — they just `await useConfirm().confirm({...})` and branch on the result.
|
|
9
|
+
*
|
|
10
|
+
* The state lives at module scope (outside the exported function) so every caller and the
|
|
11
|
+
* single mounted dialog share ONE request queue.
|
|
12
|
+
*/
|
|
13
|
+
export interface ConfirmRequest {
|
|
14
|
+
title: string
|
|
15
|
+
description?: string
|
|
16
|
+
confirmLabel?: string
|
|
17
|
+
cancelLabel?: string
|
|
18
|
+
/** `destructive` renders the confirm button in the error colour. */
|
|
19
|
+
variant?: 'default' | 'destructive'
|
|
20
|
+
icon?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const open = ref(false)
|
|
24
|
+
const current = ref<ConfirmRequest | null>(null)
|
|
25
|
+
let resolver: ((ok: boolean) => void) | null = null
|
|
26
|
+
|
|
27
|
+
/** Settle the pending promise (if any) with `ok`, then clear it. */
|
|
28
|
+
function settle(ok: boolean): void {
|
|
29
|
+
const resolve = resolver
|
|
30
|
+
resolver = null
|
|
31
|
+
resolve?.(ok)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useConfirm() {
|
|
35
|
+
function confirm(request: ConfirmRequest): Promise<boolean> {
|
|
36
|
+
// A new request while one is open supersedes the old one — resolve the previous
|
|
37
|
+
// `false` so its awaiter never hangs, then show the new request.
|
|
38
|
+
if (resolver) settle(false)
|
|
39
|
+
current.value = request
|
|
40
|
+
open.value = true
|
|
41
|
+
return new Promise<boolean>((resolve) => {
|
|
42
|
+
resolver = resolve
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function accept(): void {
|
|
47
|
+
open.value = false
|
|
48
|
+
settle(true)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function cancel(): void {
|
|
52
|
+
open.value = false
|
|
53
|
+
settle(false)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Called by the dialog when `open` flips to false without an explicit accept/cancel
|
|
57
|
+
// (backdrop, Escape, unmount). Any still-pending promise resolves `false`.
|
|
58
|
+
function dismissed(): void {
|
|
59
|
+
if (resolver) settle(false)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { open, current: readonly(current), confirm, accept, cancel, dismissed }
|
|
63
|
+
}
|