## 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
217 lines
7.6 KiB
Vue
217 lines
7.6 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useAppStore } from '@/stores/app'
|
|
import {
|
|
dataManagementAPI,
|
|
type BackupJob,
|
|
type BackupType
|
|
} from '@/api/admin/dataManagement'
|
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
|
|
|
const { t } = useI18n()
|
|
const appStore = useAppStore()
|
|
|
|
const loading = ref(false)
|
|
const jobs = ref<BackupJob[]>([])
|
|
const showModal = ref(false)
|
|
const saving = ref(false)
|
|
|
|
const form = ref<{
|
|
backup_type: BackupType
|
|
postgres_profile_id: string
|
|
redis_profile_id: string
|
|
s3_profile_id: string
|
|
}>({
|
|
backup_type: 'full',
|
|
postgres_profile_id: '',
|
|
redis_profile_id: '',
|
|
s3_profile_id: ''
|
|
})
|
|
|
|
function formatTime(time: string): string {
|
|
if (!time) return '-'
|
|
return new Date(time).toLocaleString()
|
|
}
|
|
|
|
function getJobStatusClass(status: string): string {
|
|
switch (status) {
|
|
case 'completed':
|
|
return 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400'
|
|
case 'running':
|
|
return 'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400'
|
|
case 'failed':
|
|
return 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400'
|
|
case 'pending':
|
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400'
|
|
default:
|
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400'
|
|
}
|
|
}
|
|
|
|
async function fetchJobs() {
|
|
loading.value = true
|
|
try {
|
|
const resp = await dataManagementAPI.listBackupJobs()
|
|
jobs.value = resp.items
|
|
} catch (err: any) {
|
|
console.error('[BackupJobsCard] Failed to fetch jobs', err)
|
|
appStore.showError(err?.response?.data?.detail || t('common.loadFailed'))
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function openModal() {
|
|
form.value = {
|
|
backup_type: 'full',
|
|
postgres_profile_id: '',
|
|
redis_profile_id: '',
|
|
s3_profile_id: ''
|
|
}
|
|
showModal.value = true
|
|
}
|
|
|
|
function closeModal() {
|
|
showModal.value = false
|
|
}
|
|
|
|
async function createJob() {
|
|
saving.value = true
|
|
try {
|
|
await dataManagementAPI.createBackupJob(form.value)
|
|
await fetchJobs()
|
|
closeModal()
|
|
appStore.showSuccess(t('admin.dataManagement.backupJobs.created'))
|
|
} catch (err: any) {
|
|
console.error('[BackupJobsCard] Failed to create job', err)
|
|
appStore.showError(err?.response?.data?.detail || t('common.saveFailed'))
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(fetchJobs)
|
|
|
|
defineExpose({ refresh: fetchJobs })
|
|
</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.backupJobs.title') }}
|
|
</h3>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
{{ t('admin.dataManagement.backupJobs.description') }}
|
|
</p>
|
|
</div>
|
|
<button type="button" class="btn btn-primary btn-sm" @click="openModal">
|
|
{{ t('admin.dataManagement.backupJobs.create') }}
|
|
</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="jobs.length === 0" class="py-8 text-center text-gray-500 dark:text-gray-400">
|
|
{{ t('admin.dataManagement.backupJobs.noJobs') }}
|
|
</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.backupJobs.jobId') }}
|
|
</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.backupJobs.type') }}
|
|
</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.backupJobs.status') }}
|
|
</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.backupJobs.startedAt') }}
|
|
</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.backupJobs.finishedAt') }}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
<tr v-for="job in jobs" :key="job.job_id">
|
|
<td class="whitespace-nowrap px-4 py-3 text-sm font-mono text-gray-900 dark:text-white">
|
|
{{ job.job_id.slice(0, 8) }}
|
|
</td>
|
|
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
|
{{ job.backup_type }}
|
|
</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="getJobStatusClass(job.status)"
|
|
>
|
|
{{ job.status }}
|
|
</span>
|
|
</td>
|
|
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
|
{{ job.started_at ? formatTime(job.started_at) : '-' }}
|
|
</td>
|
|
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
|
{{ job.finished_at ? formatTime(job.finished_at) : '-' }}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Create Backup Job Modal -->
|
|
<BaseDialog
|
|
:show="showModal"
|
|
:title="t('admin.dataManagement.backupJobs.create')"
|
|
@close="closeModal"
|
|
>
|
|
<form @submit.prevent="createJob">
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
|
{{ t('admin.dataManagement.backupJobs.type') }}
|
|
</label>
|
|
<select v-model="form.backup_type" class="input w-full">
|
|
<option value="full">Full</option>
|
|
<option value="incremental">Incremental</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
|
{{ t('admin.dataManagement.backupJobs.postgresProfile') }}
|
|
</label>
|
|
<input v-model="form.postgres_profile_id" class="input w-full" placeholder="Profile ID" />
|
|
</div>
|
|
<div>
|
|
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
|
{{ t('admin.dataManagement.backupJobs.redisProfile') }}
|
|
</label>
|
|
<input v-model="form.redis_profile_id" class="input w-full" placeholder="Profile ID" />
|
|
</div>
|
|
<div>
|
|
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
|
{{ t('admin.dataManagement.backupJobs.s3Profile') }}
|
|
</label>
|
|
<input v-model="form.s3_profile_id" class="input w-full" placeholder="Profile ID" />
|
|
</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.create') }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</BaseDialog>
|
|
</div>
|
|
</template>
|