fix: project details table refresh

This commit is contained in:
Théo LAGACHE
2026-02-10 11:44:45 +01:00
parent a49b6d6573
commit 921a355ae0
48 changed files with 808 additions and 566 deletions
@@ -33,7 +33,7 @@ export function DatabaseActionsCell({backup, activeMember, isAlreadyRestore}: Da
<div className={cn("flex items-center space-x-2")}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Button variant="ghost" size="icon" type="button" onClick={(e) => e.stopPropagation()}>
<span className="sr-only">Open menu</span>
<MoreHorizontal className="w-4 h-4"/>
</Button>
@@ -1,7 +1,7 @@
"use client"
import {Backup, BackupWith, Restoration} from "@/db/schema/07_database";
import React from "react";
import {Swiper, SwiperSlide} from "swiper/react";
//@ts-ignore
import "swiper/css";
import "swiper/css/pagination";
import {Pagination, Mousewheel} from "swiper/modules";
@@ -12,7 +12,7 @@ import {
BackupActionsType
} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions.schema";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {useMutation} from "@tanstack/react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import {BackupStorageWith} from "@/db/schema/14_storage-backup";
import {TooltipProvider} from "@/components/ui/tooltip";
import {getChannelIcon} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
@@ -29,8 +29,7 @@ import {SafeActionResult} from "next-safe-action";
import {ServerActionResult} from "@/types/action-type";
import {ZodString} from "zod";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {AlertCircleIcon, Trash2} from "lucide-react";
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
import {AlertCircleIcon} from "lucide-react";
type BackupActionsFormProps = {
backup: BackupWith;
@@ -42,6 +41,7 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
const filteredBackupStorages = backup.storages?.filter((storage) => storage.deletedAt === null) ?? []
const isMobile = useIsMobile();
const {closeModal} = useBackupModal();
const queryClient = useQueryClient();
const form = useZodForm({
schema: BackupActionsSchema,
@@ -74,8 +74,8 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
if (inner?.success) {
toast.success(inner.actionSuccess?.message);
queryClient.invalidateQueries({queryKey: ["database-data", backup.databaseId]});
if (action === "download") {
console.log(inner.value)
const url = inner.value
if (typeof url === "string") {
window.open(url, "_self");
@@ -91,6 +91,7 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
} else {
if (action === "delete") {
toast.success("Backup deleted successfully.")
queryClient.invalidateQueries({queryKey: ["database-data", backup.databaseId]});
closeModal()
} else {
toast.error(inner?.actionError?.message ?? "An error occurred.");
@@ -111,6 +112,7 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
if (inner?.success) {
toast.success(inner.actionSuccess?.message);
queryClient.invalidateQueries({queryKey: ["database-data", backup.databaseId]});
closeModal()
} else {
toast.error(inner?.actionError?.message);
@@ -130,10 +132,7 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
}}
>
{filteredBackupStorages.length > 0 ?
<FormField
control={form.control}
name="backupStorageId"
@@ -230,35 +229,45 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
// >
// Delete entire backup
// </ButtonWithLoading>
<ButtonWithConfirm
title={"Delete entire backup"}
description={"Are you sure you want to delete this entire backup?"}
button={{
main: {
type: "button",
variant: "destructive",
text: "Delete entire backup",
},
confirm: {
className: "w-full",
text: "Delete",
icon: <Trash2/>,
variant: "destructive",
onClick: async () => {
mutationDeleteEntireBackup.mutateAsync()
},
},
cancel: {
className: "w-full",
text: "Cancel",
icon: <Trash2/>,
variant: "outline",
},
}}
<ButtonWithLoading
type="button"
variant="destructive"
onClick={() => mutationDeleteEntireBackup.mutateAsync()}
isPending={mutationDeleteEntireBackup.isPending}
/>
disabled={mutationDeleteEntireBackup.isPending}
>
Delete entire backup
</ButtonWithLoading>
// <ButtonWithConfirm
// title={"Delete entire backup"}
// description={"Are you sure you want to delete this entire backup?"}
// button={{
// main: {
// type: "button",
// variant: "destructive",
// text: "Delete entire backup",
// },
// confirm: {
// className: "w-full",
// text: "Delete",
// icon: <Trash2/>,
// variant: "destructive",
// onClick: async () => {
// mutationDeleteEntireBackup.mutateAsync()
// },
// },
// cancel: {
// className: "w-full",
// text: "Cancel",
// icon: <Trash2/>,
// variant: "outline",
// },
// }}
// isPending={mutationDeleteEntireBackup.isPending}
// />
)}
@@ -57,10 +57,8 @@ export const downloadBackupAction = userAction.schema(
}
};
console.log(input)
const result = await dispatchStorage(input, undefined, backupStorage.storageChannelId);
console.log(result);
return {
success: result.success,
value: result.url,
@@ -0,0 +1,58 @@
"use server"
import {userAction} from "@/lib/safe-actions/actions";
import {z} from "zod";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {BackupWith, Restoration} from "@/db/schema/07_database";
export const getDatabaseDataAction = userAction
.schema(
z.object({
databaseId: z.string(),
})
)
.action(async ({parsedInput}) => {
const {databaseId} = parsedInput;
const database = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.id, databaseId),
with: {
project: true,
}
});
const backups = await db.query.backup.findMany({
where: eq(drizzleDb.schemas.backup.databaseId, databaseId),
with: {
restorations: true,
storages: {
with: {
storageChannel: true
}
}
},
orderBy: (b, {desc}) => [desc(b.createdAt)],
}) as BackupWith[];
const restorations = await db.query.restoration.findMany({
where: eq(drizzleDb.schemas.restoration.databaseId, databaseId),
orderBy: (r, {desc}) => [desc(r.createdAt)],
}) as Restoration[];
const totalBackups = backups.length;
const availableBackups = backups.filter(b => !b.deletedAt).length;
const successfulBackups = backups.filter(b => b.status === "success").length;
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
return {
database,
backups,
restorations,
stats: {
totalBackups,
availableBackups,
successRate
}
};
});
@@ -2,7 +2,6 @@
import {createContext, useContext, useState, ReactNode} from "react";
import {BackupWith} from "@/db/schema/07_database";
import {useRouter} from "next/navigation";
export type DatabaseActionKind = "restore" | "download" | "delete";
@@ -32,7 +31,6 @@ const BackupModalContext = createContext<BackupModalContextType | undefined>(und
export const BackupModalProvider = ({children}: { children: ReactNode }) => {
const [open, setOpen] = useState(false);
const router = useRouter();
const [action, setAction] = useState<DatabaseActionKind | null>(null);
const [backup, setBackup] = useState<BackupWith | null>(null);
@@ -46,7 +44,6 @@ export const BackupModalProvider = ({children}: { children: ReactNode }) => {
setOpen(false);
setAction(null);
setBackup(null);
router.refresh()
};
return (
@@ -8,9 +8,8 @@ import {Button} from "@/components/ui/button";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
import {useMutation} from "@tanstack/react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {Switch} from "@/components/ui/switch";
import {Card} from "@/components/ui/card";
import Link from "next/link";
@@ -48,7 +47,7 @@ export const ChannelPoliciesForm = ({
onSuccess,
kind
}: ChannelPoliciesFormProps) => {
const router = useRouter();
const queryClient = useQueryClient();
const isMobile = useIsMobile();
const channelText = getChannelTextBasedOnKind(kind);
@@ -105,10 +104,6 @@ export const ChannelPoliciesForm = ({
(existing.eventKinds !== policy.eventKinds || existing.enabled !== policy.enabled);
});
console.log(policiesToUpdate);
console.log(policiesToAdd);
const promises = kind === "notification"
? [
policiesToAdd.length > 0 ? await createAlertPoliciesAction({databaseId: database.id, alertPolicies: policiesToAdd}) : null,
@@ -133,7 +128,10 @@ export const ChannelPoliciesForm = ({
if (failedActions.length > 0) throw new Error(failedActions[0].data.actionError?.message || "One or more operations failed");
return {success: true};
},
onSuccess: () => { toast.success("Policies saved successfully"); router.refresh(); },
onSuccess: () => {
toast.success("Policies saved successfully");
queryClient.invalidateQueries({queryKey: ["database-data", database.id]});
},
onError: (error: any) => { toast.error(error.message || "Failed to save policies"); },
});
@@ -37,7 +37,6 @@ export const ChannelPoliciesModal = ({icon, kind, database, channels, organizati
const channelsIds = channelsFiltered
.map(channel => channel.id);
console.log(channelsIds);
const activeAlertPolicies = database.alertPolicies?.filter((policy) => channelsIds.includes(policy.notificationChannelId));
const activeStoragePolicies = database.storagePolicies?.filter((policy) => channelsIds.includes(policy.storageChannelId));
@@ -7,9 +7,8 @@ import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/cron-button/cron.action";
import {Database} from "@/db/schema/07_database";
@@ -18,7 +17,7 @@ export type CronButtonProps = {
};
export const CronButton = (props: CronButtonProps) => {
const router = useRouter();
const queryClient = useQueryClient();
const [isSwitched, setIsSwitched] = useState(props.database.backupPolicy !== null);
const [open, setOpen] = useState(false);
@@ -27,7 +26,7 @@ export const CronButton = (props: CronButtonProps) => {
updateDatabaseBackupPolicyAction({ databaseId: props.database.id, backupPolicy: value }),
onSuccess: () => {
toast.success(`Method updated successfully.`);
router.refresh();
queryClient.invalidateQueries({ queryKey: ["database-data", props.database.id] });
},
onError: () => {
toast.error(`An error occurred while updating backup method.`);
@@ -1,8 +1,7 @@
import {AdvancedCronSelect} from "./advanced-cron-select";
import {updateDatabaseBackupPolicyAction} from "@/components/wrappers/dashboard/database/cron-button/cron.action";
import {useMutation} from "@tanstack/react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import {useState} from "react";
import {useRouter} from "next/navigation";
import {toast} from "sonner";
import {Button} from "@/components/ui/button";
import {Separator} from "@/components/ui/separator";
@@ -15,14 +14,14 @@ export type CronInputProps = {
export const CronInput = ({database, onSuccess}: CronInputProps) => {
const [cron, setCron] = useState<string>(database.backupPolicy ?? "* * * * *");
const router = useRouter();
const queryClient = useQueryClient();
const updateBackupPolicy = useMutation({
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({databaseId: database.id, backupPolicy: value}),
onSuccess: () => {
toast.success(`Cron updated successfully.`);
onSuccess?.()
router.refresh();
queryClient.invalidateQueries({queryKey: ["database-data", database.id]});
},
onError: () => {
toast.error(`An error occurred while updating cron value.`);
@@ -1,10 +1,9 @@
"use client";
import {DropZoneFile} from "@/components/wrappers/common/dropzone/dropzone-file";
import {useMutation} from "@tanstack/react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import {useState} from "react";
import {Loader2} from "lucide-react";
import {useRouter} from "next/navigation";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {toast} from "sonner";
import {uploadBackupAction} from "@/components/wrappers/dashboard/database/import/upload-backup.action";
@@ -16,7 +15,7 @@ type UploadRetentionZoneProps = {
};
export const UploadBackupZone = ({onSuccessAction, databaseId}: UploadRetentionZoneProps) => {
const router = useRouter();
const queryClient = useQueryClient();
const [isProcessing, setIsProcessing] = useState(false);
const [file, setFile] = useState<File | null>(null);
@@ -37,7 +36,7 @@ export const UploadBackupZone = ({onSuccessAction, databaseId}: UploadRetentionZ
if (inner?.success) {
toast.success(inner.actionSuccess?.message);
onSuccessAction?.()
router.refresh();
queryClient.invalidateQueries({queryKey: ["database-data", databaseId]});
} else {
toast.error(inner?.actionError?.message);
}
@@ -45,7 +44,7 @@ export const UploadBackupZone = ({onSuccessAction, databaseId}: UploadRetentionZ
console.error(err);
toast.error("An error occurred while upload in the backup");
} finally {
router.refresh();
queryClient.invalidateQueries({queryKey: ["database-data", databaseId]});
setIsProcessing(false);
}
},
@@ -10,11 +10,10 @@ import {
useZodForm
} from "@/components/ui/form";
import {RetentionSettings, RetentionSettingsSchema} from "./backup-retention-settings.schema";
import {useMutation} from "@tanstack/react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import {updateOrCreateBackupRetentionPolicyAction} from "./backup-retention-settings.action";
import {DatabaseWith, RetentionPolicy} from "@/db/schema/07_database";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {RadioGroup, RadioGroupItem} from "@/components/ui/radio-group";
import {Badge} from "@/components/ui/badge";
import {Separator} from "@/components/ui/separator";
@@ -28,7 +27,7 @@ export type BackupRetentionSettingsFormProps = {
};
export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRetentionSettingsFormProps) => {
const router = useRouter();
const queryClient = useQueryClient();
const defaultValuesFormatted: RetentionSettings = {
type: defaultValues?.type,
@@ -55,7 +54,7 @@ export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRet
}),
onSuccess: () => {
toast.success("Retention policy updated successfully.");
router.refresh();
queryClient.invalidateQueries({queryKey: ["database-data", database.id]});
},
onError: () => {
toast.error("An error occurred while updating retention policy.");
@@ -49,7 +49,7 @@ export function BackupRetentionSettings({database}: BackupRetentionSettingsProps
yearly: 3,
},
})
const router = useRouter()
const queryClient = useQueryClient()
const updateRetentionPolicy = useMutation({
mutationFn: async (payload: RetentionSettings) => await updateOrCreateBackupRetentionPolicyAction({
@@ -58,7 +58,7 @@ export function BackupRetentionSettings({database}: BackupRetentionSettingsProps
}),
onSuccess: () => {
toast.success("Retention policy updated successfully.")
router.refresh()
queryClient.invalidateQueries({queryKey: ["database-data", database.id]})
},
onError: () => {
toast.error("An error occurred while updating retention policy.")