feat: add webhook notification service and refactor data management
## Backend Changes - Add WebhookService for sending alert notifications via HTTP webhooks - Implement HMAC-SHA256 signature for webhook payload authentication - Add webhook configuration API endpoints and settings - Integrate webhook calls into OpsAlertEvaluatorService - Fix routes/common.go string conversion (use strconv.Itoa) - Add comprehensive webhook service tests ## Frontend Changes - Add webhook notification configuration UI in OpsSettingsDialog - Add WebhookNotificationConfig types and API functions - Add i18n translations for webhook features (zh/en) - Refactor DataManagementView.vue into modular components: - PostgresProfilesCard.vue (356 lines) - RedisProfilesCard.vue (331 lines) - S3ProfilesCard.vue (363 lines) - BackupJobsCard.vue (216 lines) - DataManagementView.vue (94 lines) - Add OpsSettingsDialog component tests ## Testing - All backend tests pass - All frontend tests pass - Webhook service tests cover signature, HTTP, timeout, error handling
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import {
|
||||
dataManagementAPI,
|
||||
type DataManagementSourceProfile
|
||||
} from '@/api/admin/dataManagement'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const profiles = ref<DataManagementSourceProfile[]>([])
|
||||
const showModal = ref(false)
|
||||
const editing = ref<DataManagementSourceProfile | null>(null)
|
||||
const saving = ref(false)
|
||||
|
||||
const form = ref<{
|
||||
profile_id: string
|
||||
name: string
|
||||
config: {
|
||||
host: string
|
||||
port: number
|
||||
user: string
|
||||
password: string
|
||||
database: string
|
||||
ssl_mode: string
|
||||
container_name: string
|
||||
}
|
||||
set_active: boolean
|
||||
}>({
|
||||
profile_id: '',
|
||||
name: '',
|
||||
config: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
user: 'postgres',
|
||||
password: '',
|
||||
database: '',
|
||||
ssl_mode: 'disable',
|
||||
container_name: ''
|
||||
},
|
||||
set_active: false
|
||||
})
|
||||
|
||||
async function fetchProfiles() {
|
||||
loading.value = true
|
||||
try {
|
||||
const resp = await dataManagementAPI.listSourceProfiles('postgres')
|
||||
profiles.value = resp.items
|
||||
} catch (err: any) {
|
||||
console.error('[PostgresProfilesCard] Failed to fetch profiles', err)
|
||||
appStore.showError(err?.response?.data?.detail || t('common.loadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openModal(profile?: DataManagementSourceProfile) {
|
||||
if (profile) {
|
||||
editing.value = profile
|
||||
form.value = {
|
||||
profile_id: profile.profile_id,
|
||||
name: profile.name,
|
||||
config: {
|
||||
host: profile.config.host,
|
||||
port: profile.config.port,
|
||||
user: profile.config.user,
|
||||
password: '',
|
||||
database: profile.config.database,
|
||||
ssl_mode: profile.config.ssl_mode,
|
||||
container_name: profile.config.container_name
|
||||
},
|
||||
set_active: false
|
||||
}
|
||||
} else {
|
||||
editing.value = null
|
||||
form.value = {
|
||||
profile_id: '',
|
||||
name: '',
|
||||
config: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
user: 'postgres',
|
||||
password: '',
|
||||
database: '',
|
||||
ssl_mode: 'disable',
|
||||
container_name: ''
|
||||
},
|
||||
set_active: false
|
||||
}
|
||||
}
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal.value = false
|
||||
editing.value = null
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
if (editing.value) {
|
||||
await dataManagementAPI.updateSourceProfile('postgres', form.value.profile_id, {
|
||||
name: form.value.name,
|
||||
config: {
|
||||
host: form.value.config.host,
|
||||
port: form.value.config.port,
|
||||
user: form.value.config.user,
|
||||
password: form.value.config.password,
|
||||
database: form.value.config.database,
|
||||
ssl_mode: form.value.config.ssl_mode,
|
||||
container_name: form.value.config.container_name,
|
||||
addr: '',
|
||||
username: '',
|
||||
db: 0
|
||||
}
|
||||
})
|
||||
} else {
|
||||
await dataManagementAPI.createSourceProfile('postgres', {
|
||||
profile_id: form.value.profile_id,
|
||||
name: form.value.name,
|
||||
config: {
|
||||
host: form.value.config.host,
|
||||
port: form.value.config.port,
|
||||
user: form.value.config.user,
|
||||
password: form.value.config.password,
|
||||
database: form.value.config.database,
|
||||
ssl_mode: form.value.config.ssl_mode,
|
||||
container_name: form.value.config.container_name,
|
||||
addr: '',
|
||||
username: '',
|
||||
db: 0
|
||||
},
|
||||
set_active: form.value.set_active
|
||||
})
|
||||
}
|
||||
await fetchProfiles()
|
||||
closeModal()
|
||||
appStore.showSuccess(t('common.saved'))
|
||||
} catch (err: any) {
|
||||
console.error('[PostgresProfilesCard] Failed to save profile', err)
|
||||
appStore.showError(err?.response?.data?.detail || t('common.saveFailed'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function activate(profileId: string) {
|
||||
try {
|
||||
await dataManagementAPI.setActiveSourceProfile('postgres', profileId)
|
||||
await fetchProfiles()
|
||||
appStore.showSuccess(t('common.saved'))
|
||||
} catch (err: any) {
|
||||
console.error('[PostgresProfilesCard] Failed to activate profile', err)
|
||||
appStore.showError(err?.response?.data?.detail || t('common.saveFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(profileId: string) {
|
||||
if (!confirm(t('admin.dataManagement.profiles.confirmDelete'))) return
|
||||
try {
|
||||
await dataManagementAPI.deleteSourceProfile('postgres', profileId)
|
||||
await fetchProfiles()
|
||||
appStore.showSuccess(t('common.deleted'))
|
||||
} catch (err: any) {
|
||||
console.error('[PostgresProfilesCard] Failed to delete profile', err)
|
||||
appStore.showError(err?.response?.data?.detail || t('common.deleteFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchProfiles)
|
||||
|
||||
defineExpose({ refresh: fetchProfiles })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card p-6">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('admin.dataManagement.postgres.title') }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.dataManagement.postgres.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary btn-sm" @click="openModal()">
|
||||
{{ t('admin.dataManagement.postgres.addProfile') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
{{ t('common.loading') }}
|
||||
</div>
|
||||
<div v-else-if="profiles.length === 0" class="py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.dataManagement.postgres.noProfiles') }}
|
||||
</div>
|
||||
<div v-else class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.dataManagement.profiles.name') }}
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.dataManagement.profiles.host') }}
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.dataManagement.profiles.database') }}
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.dataManagement.profiles.status') }}
|
||||
</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
{{ t('common.actions') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="profile in profiles" :key="profile.profile_id">
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-white">
|
||||
{{ profile.name }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ profile.config.host }}:{{ profile.config.port }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ profile.config.database }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm">
|
||||
<span
|
||||
class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
|
||||
:class="profile.is_active ? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400' : 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400'"
|
||||
>
|
||||
{{ profile.is_active ? t('admin.dataManagement.profiles.active') : t('admin.dataManagement.profiles.inactive') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-right text-sm">
|
||||
<button
|
||||
v-if="!profile.is_active"
|
||||
type="button"
|
||||
class="mr-2 text-primary-600 hover:text-primary-700"
|
||||
@click="activate(profile.profile_id)"
|
||||
>
|
||||
{{ t('admin.dataManagement.profiles.activate') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="mr-2 text-primary-600 hover:text-primary-700"
|
||||
@click="openModal(profile)"
|
||||
>
|
||||
{{ t('common.edit') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-red-600 hover:text-red-700"
|
||||
@click="remove(profile.profile_id)"
|
||||
>
|
||||
{{ t('common.delete') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<BaseDialog
|
||||
:show="showModal"
|
||||
:title="editing ? t('admin.dataManagement.postgres.editProfile') : t('admin.dataManagement.postgres.addProfile')"
|
||||
width="wide"
|
||||
@close="closeModal"
|
||||
>
|
||||
<form @submit.prevent="save">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.dataManagement.profiles.profileId') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.profile_id"
|
||||
:disabled="!!editing"
|
||||
class="input w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.dataManagement.profiles.name') }}
|
||||
</label>
|
||||
<input v-model="form.name" class="input w-full" required />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.dataManagement.postgres.host') }}
|
||||
</label>
|
||||
<input v-model="form.config.host" class="input w-full" required />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.dataManagement.postgres.port') }}
|
||||
</label>
|
||||
<input v-model.number="form.config.port" type="number" class="input w-full" required />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.dataManagement.postgres.user') }}
|
||||
</label>
|
||||
<input v-model="form.config.user" class="input w-full" required />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.dataManagement.postgres.password') }}
|
||||
</label>
|
||||
<input v-model="form.config.password" type="password" class="input w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.dataManagement.postgres.database') }}
|
||||
</label>
|
||||
<input v-model="form.config.database" class="input w-full" required />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.dataManagement.postgres.sslMode') }}
|
||||
</label>
|
||||
<select v-model="form.config.ssl_mode" class="input w-full">
|
||||
<option value="disable">disable</option>
|
||||
<option value="require">require</option>
|
||||
<option value="verify-ca">verify-ca</option>
|
||||
<option value="verify-full">verify-full</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.dataManagement.postgres.containerName') }}
|
||||
</label>
|
||||
<input v-model="form.config.container_name" class="input w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button type="button" class="btn btn-secondary" @click="closeModal">
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="saving">
|
||||
{{ saving ? t('common.loading') : t('common.save') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user