Phase 4: Webhooks, Backup/Restore UI, Onboarding/Tutorial
- Webhooks Backend: model, schema, service (HMAC-SHA256, httpx), routes, event bus dispatcher, migration 0042 - Webhooks Frontend: SettingsWebhooksPage (CRUD, test button, event multi-select), API client - Backup/Restore Backend: model, schema, service (pg_dump/pg_restore), routes (admin-only), migration 0043 - Backup/Restore Frontend: SettingsBackupPage (create, list, restore dialog with RESTORE confirmation, auto-refresh) - Onboarding: OnboardingTour (8 steps, custom CSS overlay), WelcomeDialog, onboardingStore (zustand + localStorage) - Onboarding integrated into AppShell - Routes: /settings/webhooks, /settings/backup registered - Settings nav: Webhooks, Backup & Restore entries added - Migration conflict fixed: 0042_webhooks → 0043_backups chain
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Backup API client — database backup management.
|
||||
*
|
||||
* Features:
|
||||
* - List backups
|
||||
* - Create backup
|
||||
* - Restore backup
|
||||
* - Delete backup
|
||||
*/
|
||||
|
||||
import { apiGet, apiPost, apiDelete } from './client';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface Backup {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
filename: string;
|
||||
size_bytes: number | null;
|
||||
status: 'pending' | 'completed' | 'failed';
|
||||
error_message: string | null;
|
||||
created_by: string | null;
|
||||
created_at: string;
|
||||
completed_at: string | null;
|
||||
}
|
||||
|
||||
export interface BackupListResponse {
|
||||
backups: Backup[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
// ─── API Functions ──────────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchBackups(): Promise<BackupListResponse> {
|
||||
return apiGet<BackupListResponse>('/backups');
|
||||
}
|
||||
|
||||
export async function createBackup(): Promise<Backup> {
|
||||
return apiPost<Backup>('/backups');
|
||||
}
|
||||
|
||||
export async function restoreBackup(backupId: string): Promise<Backup> {
|
||||
return apiPost<Backup>(`/backups/${backupId}/restore`);
|
||||
}
|
||||
|
||||
export async function deleteBackup(backupId: string): Promise<void> {
|
||||
return apiDelete<void>(`/backups/${backupId}`);
|
||||
}
|
||||
|
||||
// ─── React Query Hooks ─────────────────────────────────────────────────────
|
||||
|
||||
export function useBackups() {
|
||||
return useQuery<BackupListResponse>({
|
||||
queryKey: ['backups'],
|
||||
queryFn: fetchBackups,
|
||||
refetchInterval: (query) => {
|
||||
// Auto-refresh while any backup is pending
|
||||
const data = query.state.data;
|
||||
if (data && data.backups.some((b) => b.status === 'pending')) {
|
||||
return 3000; // Poll every 3 seconds
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateBackup() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<Backup, Error>({
|
||||
mutationFn: createBackup,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['backups'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRestoreBackup() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<Backup, Error, string>({
|
||||
mutationFn: restoreBackup,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['backups'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteBackup() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, Error, string>({
|
||||
mutationFn: deleteBackup,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['backups'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Webhooks API client and React Query hooks.
|
||||
*/
|
||||
|
||||
import { apiGet, apiPost, apiPatch, apiDelete } from '@/api/client';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
export interface Webhook {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
url: string;
|
||||
events: string[];
|
||||
secret: string | null;
|
||||
is_active: boolean;
|
||||
retry_count: number;
|
||||
timeout_seconds: number;
|
||||
created_by: string | null;
|
||||
updated_by: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateWebhookPayload {
|
||||
url: string;
|
||||
events: string[];
|
||||
secret?: string | null;
|
||||
is_active?: boolean;
|
||||
retry_count?: number;
|
||||
timeout_seconds?: number;
|
||||
}
|
||||
|
||||
export interface UpdateWebhookPayload {
|
||||
url?: string;
|
||||
events?: string[];
|
||||
secret?: string | null;
|
||||
is_active?: boolean;
|
||||
retry_count?: number;
|
||||
timeout_seconds?: number;
|
||||
}
|
||||
|
||||
export interface WebhookTestResult {
|
||||
success: boolean;
|
||||
status_code: number | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// ─── API Functions ──────────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchWebhooks(event?: string): Promise<Webhook[]> {
|
||||
const params = event ? `?event=${encodeURIComponent(event)}` : '';
|
||||
return apiGet<Webhook[]>(`/webhooks${params}`);
|
||||
}
|
||||
|
||||
export async function fetchWebhook(id: string): Promise<Webhook> {
|
||||
return apiGet<Webhook>(`/webhooks/${id}`);
|
||||
}
|
||||
|
||||
export async function createWebhook(data: CreateWebhookPayload): Promise<Webhook> {
|
||||
return apiPost<Webhook>('/webhooks', data);
|
||||
}
|
||||
|
||||
export async function updateWebhook(id: string, data: UpdateWebhookPayload): Promise<Webhook> {
|
||||
return apiPatch<Webhook>(`/webhooks/${id}`, data);
|
||||
}
|
||||
|
||||
export async function deleteWebhook(id: string): Promise<void> {
|
||||
return apiDelete<void>(`/webhooks/${id}`);
|
||||
}
|
||||
|
||||
export async function testWebhook(id: string): Promise<WebhookTestResult> {
|
||||
return apiPost<WebhookTestResult>(`/webhooks/${id}/test`);
|
||||
}
|
||||
|
||||
// ─── React Query Hooks ───────────────────────────────────────────────────────
|
||||
|
||||
export function useWebhooks(event?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['webhooks', event],
|
||||
queryFn: () => fetchWebhooks(event),
|
||||
});
|
||||
}
|
||||
|
||||
export function useWebhook(id: string) {
|
||||
return useQuery({
|
||||
queryKey: ['webhooks', id],
|
||||
queryFn: () => fetchWebhook(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateWebhook() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateWebhookPayload) => createWebhook(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['webhooks'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateWebhook() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: UpdateWebhookPayload }) =>
|
||||
updateWebhook(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['webhooks'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteWebhook() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => deleteWebhook(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['webhooks'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useTestWebhook() {
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => testWebhook(id),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user