mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
migration
This commit is contained in:
@@ -1,35 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent} from "@/components/ui/card";
|
||||
import {
|
||||
FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm
|
||||
} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Form} from "@/components/ui/form"
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
|
||||
import {
|
||||
EmailFormSchema,
|
||||
EmailFormType
|
||||
} from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.schema";
|
||||
import Link from "next/link";
|
||||
import {PasswordInput} from "@/components/wrappers/auth/PaswordInput/password-input";
|
||||
import {
|
||||
updateEmailSettingsAction
|
||||
} from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import { EmailFormSchema, EmailFormType } from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.schema";
|
||||
import { PasswordInput } from "@/components/wrappers/auth/PaswordInput/password-input";
|
||||
import { updateEmailSettingsAction } from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.action";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export type EmailFormProps = {
|
||||
defaultValues?: EmailFormType;
|
||||
}
|
||||
};
|
||||
|
||||
export const EmailForm = (props: EmailFormProps) => {
|
||||
|
||||
const isCreate = !Boolean(props.defaultValues)
|
||||
|
||||
const form = useZodForm({
|
||||
schema: EmailFormSchema,
|
||||
defaultValues: props.defaultValues,
|
||||
@@ -38,45 +27,40 @@ export const EmailForm = (props: EmailFormProps) => {
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: EmailFormType) => {
|
||||
const updateEmailSettings = await updateEmailSettingsAction({name: "system", data: values})
|
||||
const data = updateEmailSettings?.data?.data
|
||||
const updateEmailSettings = await updateEmailSettingsAction({ name: "system", data: values });
|
||||
const data = updateEmailSettings?.data?.data;
|
||||
if (updateEmailSettings?.serverError || !data) {
|
||||
console.log(updateEmailSettings?.serverError);
|
||||
toast.error(updateEmailSettings?.serverError);
|
||||
return;
|
||||
}
|
||||
toast.success(`Success updating email informations`);
|
||||
router.refresh()
|
||||
}
|
||||
})
|
||||
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Card>
|
||||
<CardContent>
|
||||
|
||||
<Form form={form}
|
||||
|
||||
className="flex flex-col gap-4 mt-3"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4 mt-3"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpFrom"
|
||||
defaultValue=""
|
||||
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>From Email *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"exemple@portabase.com"} {...field} />
|
||||
<Input placeholder={"exemple@portabase.com"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"The email from where the email will be send"}</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -84,16 +68,14 @@ export const EmailForm = (props: EmailFormProps) => {
|
||||
control={form.control}
|
||||
name="smtpHost"
|
||||
defaultValue=""
|
||||
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server Host *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"ssl0.ovh.net"} {...field} />
|
||||
<Input placeholder={"ssl0.ovh.net"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"Your email server host"}</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -101,16 +83,14 @@ export const EmailForm = (props: EmailFormProps) => {
|
||||
control={form.control}
|
||||
name="smtpPort"
|
||||
defaultValue=""
|
||||
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server Port *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"465"} {...field} />
|
||||
<Input placeholder={"465"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"Your email server port (send)"}</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -119,15 +99,15 @@ export const EmailForm = (props: EmailFormProps) => {
|
||||
control={form.control}
|
||||
name="smtpPassword"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput placeholder="Password" {...field}/>
|
||||
<PasswordInput placeholder="Password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"Your email server password"}</FormDescription>
|
||||
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -135,29 +115,23 @@ export const EmailForm = (props: EmailFormProps) => {
|
||||
control={form.control}
|
||||
name="smtpUser"
|
||||
defaultValue=""
|
||||
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>User Email *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"exemple@portabase.com"} {...field} />
|
||||
<Input placeholder={"exemple@portabase.com"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"The email server user"}</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end gap-4">
|
||||
|
||||
<Button>
|
||||
Save
|
||||
</Button>
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
+22
-23
@@ -1,31 +1,30 @@
|
||||
"use server"
|
||||
import {userAction} from "@/safe-actions";
|
||||
import {z} from "zod";
|
||||
import {prisma} from "@/prisma";
|
||||
import {EmailFormSchema} from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.schema";
|
||||
|
||||
"use server";
|
||||
import { userAction } from "@/safe-actions";
|
||||
import { z } from "zod";
|
||||
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.schema";
|
||||
import { setting as drizzleSetting } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
|
||||
export const updateEmailSettingsAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
data: EmailFormSchema,
|
||||
}
|
||||
)
|
||||
)
|
||||
.action(async ({parsedInput, ctx}) => {
|
||||
|
||||
console.log("parsedInput", parsedInput.data)
|
||||
|
||||
const updatedSettings = await prisma.settings.update({
|
||||
where: {
|
||||
name: parsedInput.name,
|
||||
},
|
||||
data: parsedInput.data,
|
||||
name: z.string(),
|
||||
data: EmailFormSchema,
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const { name, data } = parsedInput;
|
||||
|
||||
const [updatedSettings] = await db
|
||||
.update(drizzleSetting)
|
||||
.set({
|
||||
...data,
|
||||
})
|
||||
.where(eq(drizzleSetting.name, name))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
data: updatedSettings,
|
||||
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import {z} from "zod";
|
||||
import { z } from "zod";
|
||||
|
||||
export const EmailFormSchema = z.object({
|
||||
smtpPassword: z.string(),
|
||||
|
||||
@@ -1,39 +1,40 @@
|
||||
import {EmailForm} from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/EmailForm";
|
||||
import {Settings} from "@prisma/client";
|
||||
import {Send} from "lucide-react";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {sendEmail} from "@/utils/email-helper";
|
||||
import { EmailForm } from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/EmailForm";
|
||||
import { Send } from "lucide-react";
|
||||
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { sendEmail } from "@/utils/email-helper";
|
||||
import TestEmailSettings from "../../../../../../emails/TestEmailSettings";
|
||||
import {render} from "@react-email/render";
|
||||
import {toast} from "sonner";
|
||||
|
||||
|
||||
import { render } from "@react-email/render";
|
||||
import { toast } from "sonner";
|
||||
import { Setting } from "@/db/schema";
|
||||
|
||||
export type SettingsEmailTabProps = {
|
||||
settings: Settings
|
||||
}
|
||||
settings: Setting;
|
||||
};
|
||||
|
||||
export const SettingsEmailTab = (props: SettingsEmailTabProps) => {
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!props.settings.smtpUser || !props.settings.smtpFrom) {
|
||||
toast.error("SMTP is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
const email = await sendEmail({
|
||||
to: props.settings.smtpUser,
|
||||
subject: "Portabase",
|
||||
html: await render(TestEmailSettings(),{})
|
||||
html: await render(TestEmailSettings(), {}),
|
||||
from: props.settings.smtpFrom,
|
||||
});
|
||||
if(email.response){
|
||||
if (email.response) {
|
||||
toast.success("Test Email Successfully sent !");
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
const handleSendMailTest = async () => {
|
||||
await mutation.mutateAsync()
|
||||
}
|
||||
await mutation.mutateAsync();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full py-4">
|
||||
@@ -43,18 +44,17 @@ export const SettingsEmailTab = (props: SettingsEmailTabProps) => {
|
||||
<ButtonWithLoading
|
||||
isPending={mutation.isPending}
|
||||
onClick={async () => {
|
||||
await handleSendMailTest()
|
||||
await handleSendMailTest();
|
||||
}}
|
||||
icon={<Send/>}
|
||||
icon={<Send />}
|
||||
text="Send email test"
|
||||
size="default"
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings : null}/>
|
||||
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings : null} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,78 +1,75 @@
|
||||
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
||||
import {Info, Send, ShieldCheck} from "lucide-react";
|
||||
import {Switch} from "@/components/ui/switch";
|
||||
import {Label} from "@/components/ui/label";
|
||||
import {StorageS3Form} from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/StorageS3Form";
|
||||
import {useState} from "react";
|
||||
import {Settings} from "@prisma/client";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {checkConnexionToS3} from "@/features/upload/public/upload.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {
|
||||
updateStorageSettingsAction
|
||||
} from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.action";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Info, ShieldCheck } from "lucide-react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { StorageS3Form } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/StorageS3Form";
|
||||
import { useState } from "react";
|
||||
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { checkConnexionToS3 } from "@/features/upload/public/upload.action";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { updateStorageSettingsAction } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.action";
|
||||
import { Setting } from "@/db/schema";
|
||||
|
||||
export type SettingsStorageTabProps = {
|
||||
settings: Settings
|
||||
}
|
||||
settings: Setting;
|
||||
};
|
||||
|
||||
export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const result = await checkConnexionToS3()
|
||||
if(result.error){
|
||||
toast.error("An error occured during the connexion !")
|
||||
}else{
|
||||
toast.success("Connexion succeed!")
|
||||
const result = await checkConnexionToS3();
|
||||
if (result.error) {
|
||||
toast.error("An error occured during the connexion !");
|
||||
} else {
|
||||
toast.success("Connexion succeed!");
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
const [isSwitched, setIsSwitched] = useState<boolean>(props.settings.storage !== "local");
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: () => updateStorageSettingsAction({name: "system", data: {storage: isSwitched ? "s3": "local"}}),
|
||||
mutationFn: () => updateStorageSettingsAction({ name: "system", data: { storage: isSwitched ? "s3" : "local" } }),
|
||||
onSuccess: () => {
|
||||
toast.success(`Settings updated successfully.`);
|
||||
router.refresh()
|
||||
router.refresh();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(`An error occurred while updating settings information.`);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const HandleSwitchStorage = async () => {
|
||||
setIsSwitched(!isSwitched);
|
||||
await updateMutation.mutateAsync()
|
||||
}
|
||||
await updateMutation.mutateAsync();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full py-4">
|
||||
<h1>Settings for Portabase storage</h1>
|
||||
<Alert className="mt-3">
|
||||
<Info className="h-4 w-4"/>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertTitle>Informations</AlertTitle>
|
||||
<AlertDescription>
|
||||
Actually you can only store you data in one place : s3 compatible or in local.
|
||||
For exemple you cannot choose to store images in one place and .dump files in another.
|
||||
Actually you can only store you data in one place : s3 compatible or in local. For exemple you cannot choose to store images in one place
|
||||
and .dump files in another.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="flex flex-col h-full py-4 ">
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="storage-mode">Storage Mode (Local/s3 compatible)</Label>
|
||||
<Switch
|
||||
checked={isSwitched}
|
||||
onCheckedChange={async () => {
|
||||
await HandleSwitchStorage()
|
||||
}}
|
||||
id="storage-mode"/>
|
||||
<Label htmlFor="storage-mode">Storage Mode (Local/s3 compatible)</Label>
|
||||
<Switch
|
||||
checked={isSwitched}
|
||||
onCheckedChange={async () => {
|
||||
await HandleSwitchStorage();
|
||||
}}
|
||||
id="storage-mode"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ButtonWithLoading
|
||||
@@ -80,7 +77,7 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
||||
disabled={!isSwitched}
|
||||
isPending={mutation.isPending}
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync()
|
||||
await mutation.mutateAsync();
|
||||
}}
|
||||
icon={<ShieldCheck />}
|
||||
text="Test connexion"
|
||||
@@ -89,10 +86,10 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
||||
</div>
|
||||
{isSwitched && (
|
||||
<div className="mt-5">
|
||||
<StorageS3Form defaultValues={props.settings.s3EndPointUrl ? props.settings : null}/>
|
||||
<StorageS3Form defaultValues={props.settings.s3EndPointUrl ? props.settings : null} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
+39
-52
@@ -1,28 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent} from "@/components/ui/card";
|
||||
import {
|
||||
FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm
|
||||
} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Form} from "@/components/ui/form"
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
|
||||
import {
|
||||
S3FormSchema,
|
||||
S3FormType
|
||||
} from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.schema";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {toast} from "sonner";
|
||||
import {
|
||||
updateS3SettingsAction
|
||||
} from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.action";
|
||||
import { S3FormSchema, S3FormType } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.schema";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.action";
|
||||
|
||||
export type S3FormProps = {
|
||||
defaultValues?: S3FormType;
|
||||
}
|
||||
};
|
||||
|
||||
export const StorageS3Form = (props: S3FormProps) => {
|
||||
const form = useZodForm({
|
||||
@@ -34,98 +27,92 @@ export const StorageS3Form = (props: S3FormProps) => {
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: S3FormType) => {
|
||||
console.log(values)
|
||||
const updateS3Settings = await updateS3SettingsAction({name: "system", data: values})
|
||||
const data = updateS3Settings?.data?.data
|
||||
console.log(values);
|
||||
const updateS3Settings = await updateS3SettingsAction({ name: "system", data: values });
|
||||
const data = updateS3Settings?.data?.data;
|
||||
if (updateS3Settings?.serverError || !data) {
|
||||
console.log(updateS3Settings?.serverError);
|
||||
toast.error(updateS3Settings?.serverError);
|
||||
return;
|
||||
}
|
||||
toast.success(`Success updating storage informations`);
|
||||
router.refresh()
|
||||
}
|
||||
})
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Form form={form}
|
||||
className="flex flex-col gap-4 mt-3"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4 mt-3"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="s3EndPointUrl"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Endpoint Url *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"s3.eu-west-3.amazonaws.com"} {...field} />
|
||||
<Input placeholder={"s3.eu-west-3.amazonaws.com"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"Your s3 compatible url"}</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="s3AccessKeyId"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Access Key *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"The access key token"} {...field} />
|
||||
<Input placeholder={"The access key token"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"Add your access key"}</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="s3SecretAccessKey"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Secret Key *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"The secret key token"} {...field} />
|
||||
<Input placeholder={"The secret key token"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"Add your secret key"}</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="S3BucketName"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bucket name *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"my-bucket"} {...field} />
|
||||
<Input placeholder={"my-bucket"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"The bucket name where you want to store your data"}</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end gap-4">
|
||||
|
||||
<Button>
|
||||
Save
|
||||
</Button>
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
+35
-36
@@ -1,51 +1,50 @@
|
||||
"use server"
|
||||
import {userAction} from "@/safe-actions";
|
||||
import {z} from "zod";
|
||||
import {prisma} from "@/prisma";
|
||||
import {
|
||||
S3FormSchema,
|
||||
StorageSwitchSchema
|
||||
} from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.schema";
|
||||
"use server";
|
||||
|
||||
import { userAction } from "@/safe-actions";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/db";
|
||||
import { setting as drizzleSetting } from "@/db/schema";
|
||||
import { S3FormSchema, StorageSwitchSchema } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const updateS3SettingsAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
data: S3FormSchema,
|
||||
}
|
||||
)
|
||||
)
|
||||
.action(async ({parsedInput, ctx}) => {
|
||||
const updatedSettings = await prisma.settings.update({
|
||||
where: {
|
||||
name: parsedInput.name,
|
||||
},
|
||||
data: parsedInput.data,
|
||||
name: z.string(),
|
||||
data: S3FormSchema,
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const { name, data } = parsedInput;
|
||||
|
||||
const [updatedSettings] = await db
|
||||
.update(drizzleSetting)
|
||||
.set({ ...data })
|
||||
.where(eq(drizzleSetting.name, name))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
data: updatedSettings,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
export const updateStorageSettingsAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
data: StorageSwitchSchema,
|
||||
}
|
||||
)
|
||||
)
|
||||
.action(async ({parsedInput, ctx}) => {
|
||||
const updatedSettings = await prisma.settings.update({
|
||||
where: {
|
||||
name: parsedInput.name,
|
||||
},
|
||||
data: parsedInput.data,
|
||||
name: z.string(),
|
||||
data: StorageSwitchSchema,
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const { name, data } = parsedInput;
|
||||
|
||||
const [updatedSettings] = await db
|
||||
.update(drizzleSetting)
|
||||
.set({ ...data })
|
||||
.where(eq(drizzleSetting.name, name))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
data: updatedSettings,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
};
|
||||
});
|
||||
|
||||
+4
-5
@@ -1,4 +1,4 @@
|
||||
import {z} from "zod";
|
||||
import { z } from "zod";
|
||||
|
||||
export const S3FormSchema = z.object({
|
||||
s3EndPointUrl: z.string(),
|
||||
@@ -9,9 +9,8 @@ export const S3FormSchema = z.object({
|
||||
|
||||
export type S3FormType = z.infer<typeof S3FormSchema>;
|
||||
|
||||
|
||||
export const StorageSwitchSchema = z.object({
|
||||
storage: z.string(),
|
||||
})
|
||||
storage: z.enum(["local", "s3"]),
|
||||
});
|
||||
|
||||
export type StorageType= z.infer<typeof StorageSwitchSchema>;
|
||||
export type StorageType = z.infer<typeof StorageSwitchSchema>;
|
||||
|
||||
@@ -1,42 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import {User, Settings} from "@prisma/client";
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {SettingsEmailTab} from "@/components/wrappers/dashboard/admin/AdminEmailTab/SettingsEmailTab";
|
||||
import {SettingsStorageTab} from "@/components/wrappers/dashboard/admin/AdminStorageTab/SettingsStorageTab";
|
||||
import {AdminUsersTable} from "@/components/wrappers/dashboard/admin/admin-user-table";
|
||||
"use client";
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { SettingsEmailTab } from "@/components/wrappers/dashboard/admin/AdminEmailTab/SettingsEmailTab";
|
||||
import { SettingsStorageTab } from "@/components/wrappers/dashboard/admin/AdminStorageTab/SettingsStorageTab";
|
||||
import { AdminUsersTable } from "@/components/wrappers/dashboard/admin/admin-user-table";
|
||||
import { Setting } from "@/db/schema";
|
||||
import { User } from "@/db/schema/01_user";
|
||||
|
||||
export type AdminTabsProps = {
|
||||
currentUser: User;
|
||||
users: User[];
|
||||
settings: Settings;
|
||||
}
|
||||
settings: Setting;
|
||||
};
|
||||
|
||||
export const AdminTabs = (props: AdminTabsProps) => {
|
||||
|
||||
const {currentUser, users, settings} = props;
|
||||
const { users, settings } = props;
|
||||
|
||||
return (
|
||||
|
||||
<Tabs defaultValue="users">
|
||||
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger className="w-full " value="users">Users</TabsTrigger>
|
||||
<TabsTrigger className="w-full " value="email">Email</TabsTrigger>
|
||||
<TabsTrigger className="w-full " value="storage">Storage</TabsTrigger>
|
||||
<TabsTrigger className="w-full " value="users">
|
||||
Users
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="w-full " value="email">
|
||||
Email
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="w-full " value="storage">
|
||||
Storage
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="users">
|
||||
<AdminUsersTable currentUser={currentUser} users={users}/>
|
||||
<AdminUsersTable users={users} />
|
||||
</TabsContent>
|
||||
<TabsContent value="email">
|
||||
<SettingsEmailTab settings={settings}/>
|
||||
<SettingsEmailTab settings={settings} />
|
||||
</TabsContent>
|
||||
<TabsContent value="storage">
|
||||
<SettingsStorageTab settings={settings}/>
|
||||
<SettingsStorageTab settings={settings} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,95 +1,21 @@
|
||||
import {flexRender, Row, RowData} from "@tanstack/react-table";
|
||||
|
||||
import {User, UserOrganization} from "@prisma/client";
|
||||
import {DataTableWithPagination} from "@/components/wrappers/common/table/data-table-with-pagination";
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/columns-users";
|
||||
import { usersColumnsAdmin } from "@/components/wrappers/dashboard/admin/columns-users";
|
||||
import { User } from "@/db/schema/01_user";
|
||||
import { DataTable } from "../../common/table/data-table";
|
||||
|
||||
export type AdminUsersTableProps = {
|
||||
currentUser: User;
|
||||
users: User[]
|
||||
}
|
||||
users: User[];
|
||||
};
|
||||
|
||||
export const AdminUsersTable = (props: AdminUsersTableProps) => {
|
||||
|
||||
const {currentUser, users} = props;
|
||||
const { users } = props;
|
||||
return (
|
||||
<div className="flex flex-col h-full py-4">
|
||||
<div className="flex gap-4 h-fit justify-between">
|
||||
<h1>List of Portabase's users</h1>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<DataTableWithPagination
|
||||
columns={usersColumnsAdmin}
|
||||
data={users}
|
||||
DataTable={UsersDataTableAdmin}
|
||||
dataTableProps={{currentUser}}
|
||||
/>
|
||||
<DataTable columns={usersColumnsAdmin} data={users} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export type usersDataTableProps = {
|
||||
currentUser: User;
|
||||
table: any,
|
||||
}
|
||||
|
||||
export const UsersDataTableAdmin = ({currentUser, table}: usersDataTableProps) => {
|
||||
|
||||
return (
|
||||
<div className="rounded-md border w-full ">
|
||||
<Table className="w-full">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row: Row<User>) => {
|
||||
return(
|
||||
<TableRow
|
||||
className={cn((row.original.id) === currentUser.id ? "opacity-40 pointer-events-none" : "")}
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
|
||||
)) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={table.getAllColumns().length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,31 +1,28 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {ColumnDef} from "@tanstack/react-table"
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {User} from "@prisma/client";
|
||||
import {updateUserAction} from "@/components/wrappers/dashboard/profile/UserForm/user-form.action";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useState} from "react";
|
||||
import {Trash2} from "lucide-react";
|
||||
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/delete-account.action";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { User } from "@prisma/client";
|
||||
import { updateUserAction } from "@/components/wrappers/dashboard/profile/UserForm/user-form.action";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { deleteUserAction } from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/delete-account.action";
|
||||
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
||||
|
||||
export const usersColumnsAdmin: ColumnDef<User>[] = [
|
||||
{
|
||||
accessorKey: "role",
|
||||
header: "Role",
|
||||
cell: ({row}) => {
|
||||
const router = useRouter();
|
||||
const [role, setRole] = useState<string>(row.getValue("role"))
|
||||
cell: ({ row }) => {
|
||||
const [role, setRole] = useState<string>(row.getValue("role"));
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: () => updateUserAction({id: row.original.id, data: {role: role}}),
|
||||
mutationFn: () => updateUserAction({ id: row.original.id, data: { role: role } }),
|
||||
onSuccess: () => {
|
||||
toast.success(`User updated successfully.`);
|
||||
// router.refresh()
|
||||
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(`An error occurred while updating user information.`);
|
||||
@@ -33,55 +30,52 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
|
||||
});
|
||||
|
||||
const handleUpdateRole = async () => {
|
||||
const nextRole = role === "admin" ? "pending"
|
||||
: role === "pending" ? "user"
|
||||
: "admin";
|
||||
const nextRole = role === "admin" ? "pending" : role === "pending" ? "user" : "admin";
|
||||
setRole(nextRole);
|
||||
await updateMutation.mutateAsync()
|
||||
await updateMutation.mutateAsync();
|
||||
};
|
||||
|
||||
return <Badge
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleUpdateRole()}
|
||||
variant="outline">{role}</Badge>
|
||||
return (
|
||||
<Badge className="cursor-pointer" onClick={() => handleUpdateRole()} variant="outline">
|
||||
{role}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name"
|
||||
header: "Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: "Email"
|
||||
header: "Email",
|
||||
},
|
||||
{
|
||||
accessorKey: "updatedAt",
|
||||
header: "Updated At",
|
||||
cell: ({row}) => {
|
||||
cell: ({ row }) => {
|
||||
return new Date(row.getValue("updatedAt")).toLocaleString("fr-FR");
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "authMethod",
|
||||
header: "Method",
|
||||
cell: ({row}) => {
|
||||
return <Badge variant="outline">{row.getValue("authMethod")}</Badge>
|
||||
cell: ({ row }) => {
|
||||
return <Badge variant="outline">{row.getValue("authMethod")}</Badge>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Action",
|
||||
id: "actions",
|
||||
cell: ({ row, table }) => {
|
||||
|
||||
cell: ({ row }) => {
|
||||
const router = useRouter();
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => deleteUserAction(row.original.id),
|
||||
onSuccess: async () => {
|
||||
toast.success('User deleted successfully.');
|
||||
router.refresh()
|
||||
toast.success("User deleted successfully.");
|
||||
router.refresh();
|
||||
},
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -94,9 +88,8 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
|
||||
}}
|
||||
size="icon"
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import Link from "next/link";
|
||||
import {ValueIcon} from "@radix-ui/react-icons";
|
||||
import {Circle} from "lucide-react";
|
||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
|
||||
import { formatDateLastContact } from "@/utils/date-formatting";
|
||||
import { ConnectionCircle } from "@/components/wrappers/common/connection-circle";
|
||||
import { Agent } from "@/db/schema";
|
||||
|
||||
export type agentCardProps = {
|
||||
data: any
|
||||
}
|
||||
data: Agent;
|
||||
};
|
||||
|
||||
export const AgentCard = (props: agentCardProps) => {
|
||||
|
||||
const {data: agent} = props;
|
||||
const { data: agent } = props;
|
||||
|
||||
return (
|
||||
<Link href={`/dashboard/agents/${agent.id}`}>
|
||||
<Card className="flex flex-row justify-between">
|
||||
<div className="">
|
||||
<CardHeader>{agent.name}</CardHeader>
|
||||
<CardContent>
|
||||
Last contact : {formatDateLastContact(agent.lastContact)}
|
||||
</CardContent>
|
||||
<CardContent>Last contact : {formatDateLastContact(agent.lastContact)}</CardContent>
|
||||
</div>
|
||||
<div className="mt-3 mr-3">
|
||||
<ConnectionCircle date={agent.lastContact}/>
|
||||
<ConnectionCircle date={agent.lastContact} />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
"use client"
|
||||
import {generateEdgeKey} from "@/utils/edge_key";
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
import {Agent} from "@prisma/client";
|
||||
import {PasswordInput} from "@/components/wrappers/auth/PaswordInput/password-input";
|
||||
import {useState} from "react";
|
||||
import {CopyButton} from "@/components/wrappers/common/button/copy-button";
|
||||
"use client";
|
||||
import { generateEdgeKey } from "@/utils/edge_key";
|
||||
import { getServerUrl } from "@/utils/get-server-url";
|
||||
import { PasswordInput } from "@/components/wrappers/auth/PaswordInput/password-input";
|
||||
import { useState } from "react";
|
||||
import { CopyButton } from "@/components/wrappers/common/button/copy-button";
|
||||
import { Agent } from "@/db/schema";
|
||||
|
||||
export type AgentCardKeyProps = {
|
||||
agent: Agent
|
||||
|
||||
}
|
||||
agent: Agent;
|
||||
};
|
||||
|
||||
export const AgentCardKey = (props: AgentCardKeyProps) => {
|
||||
const edge_key = generateEdgeKey(getServerUrl(), props.agent.id);
|
||||
const [code, setCode] = useState<string>(`${edge_key}`);
|
||||
|
||||
|
||||
return(
|
||||
return (
|
||||
<>
|
||||
<PasswordInput value={code} onChange={(value: string) => {setCode(edge_key)} }/>
|
||||
<CopyButton className="mt-5" value={code}/>
|
||||
<PasswordInput
|
||||
value={code}
|
||||
onChange={() => {
|
||||
setCode(edge_key);
|
||||
}}
|
||||
/>
|
||||
<CopyButton className="mt-5" value={code} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent} from "@/components/ui/card";
|
||||
import {
|
||||
FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm
|
||||
} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Form} from "@/components/ui/form"
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import {AgentSchema, AgentType} from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.schema";
|
||||
import {toast} from "sonner";
|
||||
import {createAgentAction, updateAgentAction} from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.action";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { AgentSchema, AgentType } from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.schema";
|
||||
import { toast } from "sonner";
|
||||
import { createAgentAction, updateAgentAction } from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.action";
|
||||
|
||||
export type agentFormProps = {
|
||||
defaultValues?: AgentType;
|
||||
agentId?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export const AgentForm = (props: agentFormProps) => {
|
||||
|
||||
const isCreate = !Boolean(props.defaultValues)
|
||||
// const defaultValues = isCreate ? {slug: ""} : props.defaultValues
|
||||
const isCreate = !Boolean(props.defaultValues);
|
||||
|
||||
const form = useZodForm({
|
||||
schema: AgentSchema,
|
||||
@@ -33,14 +29,16 @@ export const AgentForm = (props: agentFormProps) => {
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: AgentType) => {
|
||||
console.log("values", values)
|
||||
console.log("values", values);
|
||||
|
||||
const createAgent = isCreate ? await createAgentAction(values) : await updateAgentAction({
|
||||
id: props.agentId ?? "-",
|
||||
data: values
|
||||
});
|
||||
const createAgent = isCreate
|
||||
? await createAgentAction(values)
|
||||
: await updateAgentAction({
|
||||
id: props.agentId ?? "-",
|
||||
data: values,
|
||||
});
|
||||
|
||||
const data = createAgent?.data?.data
|
||||
const data = createAgent?.data?.data;
|
||||
if (createAgent?.serverError || !data) {
|
||||
console.log(createAgent?.serverError);
|
||||
toast.error(createAgent?.serverError);
|
||||
@@ -48,83 +46,78 @@ export const AgentForm = (props: agentFormProps) => {
|
||||
}
|
||||
toast.success(`Success`);
|
||||
router.push(`/dashboard/agents/${data.id}`);
|
||||
router.refresh()
|
||||
}
|
||||
})
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Form form={form}
|
||||
className="flex flex-col gap-4 mt-3"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4 mt-3"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Agent 1" {...field} />
|
||||
<Input placeholder="Agent 1" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Your agent project name</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
defaultValue=""
|
||||
|
||||
name="slug"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Slug</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
value={field.value ?? ""}
|
||||
placeholder="agent-1" {...field}
|
||||
//value={field.value ?? ""}
|
||||
placeholder="agent-1"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replaceAll(" ", "-").toLowerCase()
|
||||
field.onChange(value)
|
||||
const value = e.target.value.replaceAll(" ", "-").toLowerCase();
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>The slug is used in the url of the agent</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
defaultValue=""
|
||||
|
||||
name="description"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder='This agent is for the client exemple.com' {...field}
|
||||
value={field.value ?? ""}/>
|
||||
<Input placeholder="This agent is for the client exemple.com" {...field} value={field.value ?? ""} />
|
||||
</FormControl>
|
||||
<FormDescription>Enter your project agent description</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button>
|
||||
{isCreate ? `Create agent` : `Save agent`}
|
||||
</Button>
|
||||
<Button>{isCreate ? `Create agent` : `Save agent`}</Button>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,68 +1,44 @@
|
||||
"use server"
|
||||
import {ActionError, userAction} from "@/safe-actions";
|
||||
import {prisma} from "@/prisma";
|
||||
import {AgentSchema} from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.schema";
|
||||
import {z} from "zod";
|
||||
|
||||
"use server";
|
||||
import { ActionError, userAction } from "@/safe-actions";
|
||||
import { AgentSchema } from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.schema";
|
||||
import { z } from "zod";
|
||||
import { eq, and, ne, count } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import { agent } from "@/db/schema";
|
||||
|
||||
const verifySlugUniqueness = async (slug: string, agentId?: string) => {
|
||||
const slugExists = await prisma.agent.count({
|
||||
where: {
|
||||
slug: slug,
|
||||
id: agentId ? {
|
||||
not: agentId
|
||||
} : undefined,
|
||||
},
|
||||
})
|
||||
const conditions = agentId ? and(eq(agent.slug, slug), ne(agent.id, agentId)) : eq(agent.slug, slug);
|
||||
|
||||
console.log(slugExists)
|
||||
if (slugExists) {
|
||||
const [countResult] = await db.select({ count: count() }).from(agent).where(conditions);
|
||||
|
||||
if (countResult.count > 0) {
|
||||
throw new ActionError("Slug already exists");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const createAgentAction = userAction.schema(AgentSchema).action(async ({ parsedInput }) => {
|
||||
await verifySlugUniqueness(parsedInput.slug);
|
||||
|
||||
export const createAgentAction = userAction
|
||||
.schema(AgentSchema)
|
||||
.action(async ({parsedInput, ctx}) => {
|
||||
// Verify if slug already exist
|
||||
await verifySlugUniqueness(parsedInput.slug);
|
||||
const agent = await prisma.agent.create({
|
||||
data: {
|
||||
...parsedInput
|
||||
}
|
||||
})
|
||||
|
||||
// await sendEmailIfUserCreatedFirstForm(ctx.user)
|
||||
|
||||
return {
|
||||
data: agent,
|
||||
}
|
||||
});
|
||||
const [createdAgent] = await db.insert(agent).values(parsedInput).returning();
|
||||
|
||||
return {
|
||||
data: createdAgent,
|
||||
};
|
||||
});
|
||||
|
||||
export const updateAgentAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
data: AgentSchema,
|
||||
}
|
||||
)
|
||||
id: z.string(),
|
||||
data: AgentSchema,
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput, ctx}) => {
|
||||
.action(async ({ parsedInput }) => {
|
||||
await verifySlugUniqueness(parsedInput.data.slug, parsedInput.id);
|
||||
|
||||
console.log("parsedInput", parsedInput.data)
|
||||
|
||||
const updatedAgent = await prisma.agent.update({
|
||||
where: {
|
||||
id: parsedInput.id,
|
||||
},
|
||||
data: parsedInput.data,
|
||||
})
|
||||
const [updatedAgent] = await db.update(agent).set(parsedInput.data).where(eq(agent.id, parsedInput.id)).returning();
|
||||
|
||||
return {
|
||||
data: updatedAgent,
|
||||
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import {z} from "zod";
|
||||
import { z } from "zod";
|
||||
|
||||
export const AgentSchema = z.object({
|
||||
name: z.string(),
|
||||
slug: z.string().regex(/^[a-zA-Z0-9_-]*$/).min(5).max(25),
|
||||
description: z.string().optional().nullable(),
|
||||
slug: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9_-]*$/)
|
||||
.min(5)
|
||||
.max(25),
|
||||
description: z.string(),
|
||||
});
|
||||
|
||||
export type AgentType = z.infer<typeof AgentSchema>;
|
||||
|
||||
@@ -1,39 +1,26 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
|
||||
import {generateEdgeKey} from "@/utils/edge_key";
|
||||
import {Copy} from "lucide-react";
|
||||
import {PropsWithChildren, useState} from "react";
|
||||
import {CopyButton} from "@/components/wrappers/common/button/copy-button";
|
||||
import {Agent} from "@prisma/client";
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
import {CodeSnippet} from "@/components/wrappers/codeSnippet/CodeSnippet";
|
||||
import { generateEdgeKey } from "@/utils/edge_key";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { CopyButton } from "@/components/wrappers/common/button/copy-button";
|
||||
import { getServerUrl } from "@/utils/get-server-url";
|
||||
import { CodeSnippet } from "@/components/wrappers/code-snippet/CodeSnippet";
|
||||
import { Agent } from "@/db/schema";
|
||||
|
||||
export type agentRegistrationDialogProps = PropsWithChildren<{
|
||||
agent: Agent
|
||||
}>
|
||||
|
||||
agent: Agent;
|
||||
}>;
|
||||
|
||||
export function AgentModalKey(props: agentRegistrationDialogProps) {
|
||||
|
||||
const edge_key = generateEdgeKey(getServerUrl(), props.agent.id);
|
||||
const code = `EDGE_KEY = ${edge_key}`;
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
{props.children}
|
||||
</DialogTrigger>
|
||||
<DialogTrigger asChild>{props.children}</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px] w-full">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Agent Edge Key</DialogTitle>
|
||||
@@ -46,11 +33,11 @@ export function AgentModalKey(props: agentRegistrationDialogProps) {
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<CopyButton value={code}/>
|
||||
<CopyButton value={code} />
|
||||
<Button type="submit">Save changes</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,41 @@
|
||||
"use server"
|
||||
"use server";
|
||||
|
||||
import {z} from "zod";
|
||||
import { z } from "zod";
|
||||
import { userAction } from "@/safe-actions";
|
||||
import { db } from "@/db";
|
||||
import { backup } from "@/db/schema";
|
||||
import { ServerActionResult } from "@/types/action-type";
|
||||
import { Backup } from "@/db/schema";
|
||||
|
||||
import {userAction} from "@/safe-actions";
|
||||
import {prisma} from "@/prisma";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {Backup} from "@prisma/client";
|
||||
export const backupButtonAction = userAction.schema(z.string()).action(async ({ parsedInput }): Promise<ServerActionResult<Backup>> => {
|
||||
try {
|
||||
const [createdBackup] = await db
|
||||
.insert(backup)
|
||||
.values({
|
||||
databaseId: parsedInput,
|
||||
status: "waiting",
|
||||
})
|
||||
.returning();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: createdBackup,
|
||||
actionSuccess: {
|
||||
message: "Backup has been successfully created.",
|
||||
messageParams: { databaseId: parsedInput },
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating backup:", error);
|
||||
|
||||
export const backupButtonAction = userAction
|
||||
.schema(z.string())
|
||||
.action(async ({ parsedInput, ctx }): Promise<ServerActionResult<Backup>> => {
|
||||
try {
|
||||
const backup = await prisma.backup.create({
|
||||
data: {
|
||||
databaseId: parsedInput,
|
||||
status: "waiting",
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: backup,
|
||||
actionSuccess: {
|
||||
message: "Backup has been successfully created.",
|
||||
messageParams: { databaseId: parsedInput },
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating backup:", error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to create backup.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: { databaseId: parsedInput },
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to create backup.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: { databaseId: parsedInput },
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {useRouter} from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {toast} from "sonner";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {backupButtonAction} from "@/components/wrappers/dashboard/backup/backup-button/backup-button.action";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import { backupButtonAction } from "@/components/wrappers/dashboard/backup/backup-button/backup-button.action";
|
||||
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
||||
|
||||
export type BackupButtonProps = {
|
||||
databaseId: string
|
||||
disable: boolean
|
||||
}
|
||||
databaseId: string;
|
||||
disable: boolean;
|
||||
};
|
||||
|
||||
export const BackupButton = (props: BackupButtonProps) => {
|
||||
const router = useRouter();
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (databaseId: string) => {
|
||||
const backup = await backupButtonAction(databaseId)
|
||||
console.log(backup)
|
||||
if (backup.data.success) {
|
||||
const backup = await backupButtonAction(databaseId);
|
||||
if (backup?.data?.success) {
|
||||
toast.success(backup.data.actionSuccess?.message || "Backup created successfully!");
|
||||
router.refresh()
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.error(backup.serverError || "Failed to create backup.");
|
||||
toast.error(backup?.serverError || "Failed to create backup.");
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
const HandleAction = async () => {
|
||||
await mutation.mutateAsync(props.databaseId)
|
||||
}
|
||||
await mutation.mutateAsync(props.databaseId);
|
||||
};
|
||||
|
||||
return (
|
||||
<ButtonWithLoading
|
||||
@@ -38,8 +37,8 @@ export const BackupButton = (props: BackupButtonProps) => {
|
||||
isPending={mutation.isPending}
|
||||
size={"default"}
|
||||
onClick={async () => {
|
||||
await HandleAction()
|
||||
await HandleAction();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,17 +2,17 @@ import { useState } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {isValidCronPart} from "@/utils/cron";
|
||||
import { isValidCronPart } from "@/utils/cron";
|
||||
|
||||
export const AdvancedCronSelect = ({
|
||||
id,
|
||||
label,
|
||||
options,
|
||||
type,
|
||||
value,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
}: {
|
||||
id,
|
||||
label,
|
||||
options,
|
||||
type,
|
||||
value,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
}: {
|
||||
id: string;
|
||||
label: string;
|
||||
options: string[];
|
||||
@@ -38,7 +38,9 @@ export const AdvancedCronSelect = ({
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 items-center gap-2">
|
||||
<Label htmlFor={id} className="text-left">{label}</Label>
|
||||
<Label htmlFor={id} className="text-left">
|
||||
{label}
|
||||
</Label>
|
||||
{!isAdvanced ? (
|
||||
<Select
|
||||
id={id}
|
||||
|
||||
@@ -1,95 +1,71 @@
|
||||
"use client"
|
||||
import {Clock9} from "lucide-react";
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription, DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from "@/components/ui/dialog";
|
||||
import {CronInput} from "@/components/wrappers/dashboard/database/CronButton/CronInput";
|
||||
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 {
|
||||
updateStorageSettingsAction
|
||||
} from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {Database} from "@prisma/client";
|
||||
import {
|
||||
updateBackupPolicyAction,
|
||||
updateDatabaseBackupPolicyAction
|
||||
} from "@/components/wrappers/dashboard/database/CronButton/cron.action";
|
||||
|
||||
"use client";
|
||||
import { Clock9 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { CronInput } from "@/components/wrappers/dashboard/database/CronButton/CronInput";
|
||||
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 { toast } from "sonner";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/CronButton/cron.action";
|
||||
import { Database } from "@/db/schema";
|
||||
|
||||
export type CronButtonProps = {
|
||||
database: Database
|
||||
}
|
||||
database: Database;
|
||||
};
|
||||
|
||||
export const CronButton = (props: CronButtonProps) => {
|
||||
const router = useRouter();
|
||||
const [isSwitched, setIsSwitched] = useState(props.database.backupPolicy !== null);
|
||||
|
||||
const updateDatabaseBackupPolicy = useMutation({
|
||||
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({databaseId: props.database.id, backupPolicy:value}),
|
||||
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({ databaseId: props.database.id, backupPolicy: value }),
|
||||
onSuccess: () => {
|
||||
toast.success(`Method updated successfully.`);
|
||||
router.refresh()
|
||||
router.refresh();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(`An error occurred while updating backup method.`);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const handleTypeChange = async (state: boolean) => {
|
||||
setIsSwitched(state);
|
||||
if(state == false) {
|
||||
await updateDatabaseBackupPolicy.mutateAsync("")
|
||||
if (state == false) {
|
||||
await updateDatabaseBackupPolicy.mutateAsync("");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
{...props}
|
||||
|
||||
>
|
||||
<Clock9/>
|
||||
<Button variant="outline" {...props}>
|
||||
<Clock9 />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Backup method</DialogTitle>
|
||||
<DialogDescription>
|
||||
Your settings for the backup method
|
||||
</DialogDescription>
|
||||
<DialogDescription>Your settings for the backup method</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Separator/>
|
||||
<Separator />
|
||||
|
||||
<h1>
|
||||
Select your backup method
|
||||
</h1>
|
||||
<h1>Select your backup method</h1>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label>Manual / Automatic </Label>
|
||||
<Switch
|
||||
checked={isSwitched}
|
||||
onCheckedChange={async () => {
|
||||
await handleTypeChange(!isSwitched)
|
||||
await handleTypeChange(!isSwitched);
|
||||
}}
|
||||
id="type-mode"/>
|
||||
id="type-mode"
|
||||
/>
|
||||
</div>
|
||||
{isSwitched ?
|
||||
<CronInput database={props.database}/>
|
||||
:null}
|
||||
{isSwitched ? <CronInput database={props.database} /> : null}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import { AdvancedCronSelect } from "./AdvancedCronSelect";
|
||||
import {
|
||||
updateBackupPolicyAction,
|
||||
updateDatabaseBackupPolicyAction
|
||||
} from "@/components/wrappers/dashboard/database/CronButton/cron.action";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useState} from "react";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {toast} from "sonner";
|
||||
import {Database} from "@prisma/client";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/CronButton/cron.action";
|
||||
import { useMutation } 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";
|
||||
import { Database } from "@/db/schema";
|
||||
|
||||
export type CronInputProps = {
|
||||
database : Database
|
||||
}
|
||||
|
||||
database: Database;
|
||||
};
|
||||
|
||||
export const CronInput = ({ database }: CronInputProps) => {
|
||||
const [cron, setCron] = useState<string>(database.backupPolicy ?? "* * * * *");
|
||||
@@ -31,7 +27,7 @@ export const CronInput = ({ database }: CronInputProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
const handleChangeCron = (type: string, value: string) => {
|
||||
const handleChangeCron = (type: "minute" | "hour" | "day-of-month" | "month" | "day-of-week", value: string) => {
|
||||
const cronParts = cron.split(" ");
|
||||
const indexMap = { minute: 0, hour: 1, "day-of-month": 2, month: 3, "day-of-week": 4 };
|
||||
cronParts[indexMap[type]] = value;
|
||||
@@ -96,9 +92,7 @@ export const CronInput = ({ database }: CronInputProps) => {
|
||||
<div className="font-semibold">Cron Expression</div>
|
||||
<div className="font-mono text-muted-foreground">{cron}</div>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
This cron expression determines when the job will run.
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">This cron expression determines when the job will run.</div>
|
||||
</div>
|
||||
<div className="flex justify-between gap-2">
|
||||
<Button
|
||||
@@ -120,4 +114,4 @@ export const CronInput = ({ database }: CronInputProps) => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
"use server"
|
||||
import {userAction} from "@/safe-actions";
|
||||
import {z} from "zod";
|
||||
import {prisma} from "@/prisma";
|
||||
"use server";
|
||||
|
||||
import { userAction } from "@/safe-actions";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/db";
|
||||
import { database } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const updateDatabaseBackupPolicyAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
databaseId: z.string(),
|
||||
backupPolicy: z.string(),
|
||||
}
|
||||
)
|
||||
)
|
||||
.action(async ({parsedInput, ctx}) => {
|
||||
const cronPolicy = parsedInput.backupPolicy == "" ? null : parsedInput.backupPolicy
|
||||
|
||||
const updatedDatabase = await prisma.database.update({
|
||||
where: {
|
||||
id: parsedInput.databaseId,
|
||||
},
|
||||
data: {
|
||||
backupPolicy: cronPolicy,
|
||||
}
|
||||
databaseId: z.string(),
|
||||
backupPolicy: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const cronPolicy = parsedInput.backupPolicy === "" ? null : parsedInput.backupPolicy;
|
||||
|
||||
const [updated] = await db
|
||||
.update(database)
|
||||
.set({
|
||||
backupPolicy: cronPolicy,
|
||||
})
|
||||
.where(eq(database.id, parsedInput.databaseId))
|
||||
.returning()
|
||||
.execute();
|
||||
|
||||
return {
|
||||
data: updatedDatabase,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
data: updated,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,119 +1,113 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent} from "@/components/ui/card";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
useZodForm
|
||||
} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Form} from "@/components/ui/form"
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import {DatabaseSchema, DatabaseType} from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.schema";
|
||||
import {updateDatabaseAction} from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.action";
|
||||
import {toast} from "sonner";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { DatabaseSchema, DatabaseType } from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.schema";
|
||||
import { updateDatabaseAction } from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.action";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export type DatabaseFormProps = {
|
||||
defaultValues?: DatabaseType;
|
||||
databaseId?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export const DatabaseForm = (props: DatabaseFormProps) => {
|
||||
const { defaultValues, databaseId } = props;
|
||||
|
||||
const {defaultValues, databaseId} = props;
|
||||
|
||||
const isCreate = !Boolean(defaultValues)
|
||||
const isCreate = !Boolean(defaultValues);
|
||||
|
||||
const form = useZodForm({
|
||||
schema: DatabaseSchema,
|
||||
defaultValues: {...defaultValues},
|
||||
defaultValues: { ...defaultValues },
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: DatabaseType) => {
|
||||
if (!databaseId) {
|
||||
throw new Error("Database ID is required");
|
||||
}
|
||||
const database = await updateDatabaseAction({ id: databaseId, data: values });
|
||||
|
||||
const database = await updateDatabaseAction({id: databaseId, data: values});
|
||||
|
||||
if (!database) {
|
||||
toast.error("Failed to update database");
|
||||
return;
|
||||
}
|
||||
if (database.serverError) {
|
||||
console.error(database?.serverError);
|
||||
toast.error(database?.serverError);
|
||||
toast.error(database.serverError);
|
||||
return;
|
||||
}
|
||||
console.log(database)
|
||||
toast.success(`Database settings successfully updated!`);
|
||||
|
||||
router.back()
|
||||
}
|
||||
})
|
||||
router.back();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Form form={form}
|
||||
className="flex flex-col gap-4 mt-3"
|
||||
onSubmit={async (values) => {
|
||||
console.log("sssssss")
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4 mt-3"
|
||||
onSubmit={async (values) => {
|
||||
console.log("sssssss");
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input disabled placeholder="Database 1" {...field}/>
|
||||
<Input disabled placeholder="Database 1" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Your database project name setup in agent</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dbms"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Database type</FormLabel>
|
||||
<FormControl>
|
||||
<Input disabled placeholder="PostgreSQL" {...field}/>
|
||||
<Input disabled placeholder="PostgreSQL" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Your database project name setup in agent</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Prod database for project 1" {...field}/>
|
||||
<Input placeholder="Prod database for project 1" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Add a short description about this database</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button>
|
||||
{isCreate ? `Create database` : `Save database`}
|
||||
</Button>
|
||||
<Button>{isCreate ? `Create database` : `Save database`}</Button>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
"use server"
|
||||
|
||||
import {z} from "zod";
|
||||
|
||||
import {userAction} from "@/safe-actions";
|
||||
import {prisma} from "@/prisma";
|
||||
import {DatabaseSchema} from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.schema";
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { userAction } from "@/safe-actions";
|
||||
import { db } from "@/db";
|
||||
import { database } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { DatabaseSchema } from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.schema";
|
||||
|
||||
export const updateDatabaseAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
data: DatabaseSchema,
|
||||
}
|
||||
)
|
||||
id: z.string(),
|
||||
data: DatabaseSchema,
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput, ctx}) => {
|
||||
return prisma.database.update({
|
||||
where: {
|
||||
id: parsedInput.id,
|
||||
},
|
||||
data: parsedInput.data,
|
||||
});
|
||||
})
|
||||
.action(async ({ parsedInput }) => {
|
||||
const [updated] = await db.update(database).set(parsedInput.data).where(eq(database.id, parsedInput.id)).returning().execute();
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import {z} from "zod";
|
||||
|
||||
const cronRegex = /^(\d{1,2}|\*|(\d{1,2}-\d{1,2})|(\d{1,2}\/\d{1,2}))\s+(\d{1,2}|\*|(\d{1,2}-\d{1,2})|(\d{1,2}\/\d{1,2}))\s+(\d{1,2}|\*|(\d{1,2}-\d{1,2})|(\d{1,2}\/\d{1,2}))\s+(\d{1,7}|\*|(\d{1,7}-\d{1,7})|(\d{1,7}\/\d{1,7}))$/;
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export const DatabaseSchema = z.object({
|
||||
name: z.string().readonly(),
|
||||
description: z.string().optional(),
|
||||
dbms: z.string().readonly(),
|
||||
dbms: z.enum(["active", "inactive"]).readonly(),
|
||||
});
|
||||
|
||||
export type DatabaseType = z.infer<typeof DatabaseSchema>;
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
"use client"
|
||||
import {buttonVariants} from "@/components/ui/button";
|
||||
import {GearIcon} from "@radix-ui/react-icons";
|
||||
"use client";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { GearIcon } from "@radix-ui/react-icons";
|
||||
import Link from "next/link";
|
||||
import {usePathname} from "next/navigation";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export type EditButtonProps = {}
|
||||
export type EditButtonProps = {};
|
||||
|
||||
export const EditButton = (props: EditButtonProps) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return(
|
||||
<Link
|
||||
className={buttonVariants({ variant: "outline" })}
|
||||
href={`${pathname}/edit`}
|
||||
>
|
||||
return (
|
||||
<Link className={buttonVariants({ variant: "outline" })} href={`${pathname}/edit`}>
|
||||
<GearIcon className="w-7 h-7" />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,38 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import {useState} from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import {DateTimePicker} from "@/components/wrappers/common/daytime-picker";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
useZodForm
|
||||
} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {RestoreSchema} from "@/components/wrappers/dashboard/database/restore-form.schema";
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {ComboBox, ComboBoxFormItem} from "@/components/wrappers/common/combobox";
|
||||
import {Label} from "@/components/ui/label";
|
||||
import { DateTimePicker } from "@/components/wrappers/common/day-time-picker";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RestoreSchema } from "@/components/wrappers/dashboard/database/restore-form.schema";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ComboBox, ComboBoxFormItem } from "@/components/wrappers/common/combobox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
export type restoreFormProps = {
|
||||
databaseToRestore: any
|
||||
databases: any[]
|
||||
backups: any[]
|
||||
}
|
||||
|
||||
databaseToRestore: any;
|
||||
databases: any[];
|
||||
backups: any[];
|
||||
};
|
||||
|
||||
export const RestoreForm = (props: restoreFormProps) => {
|
||||
|
||||
const {databaseToRestore, databases, backups} = props
|
||||
|
||||
console.log("bacups", backups)
|
||||
console.log("bacups", databaseToRestore)
|
||||
const { databaseToRestore, databases, backups } = props;
|
||||
|
||||
const backupLocations = [
|
||||
{
|
||||
@@ -43,7 +29,7 @@ export const RestoreForm = (props: restoreFormProps) => {
|
||||
value: "desktop-file",
|
||||
label: "Desktop File",
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const executionModes = [
|
||||
{
|
||||
@@ -54,29 +40,23 @@ export const RestoreForm = (props: restoreFormProps) => {
|
||||
value: "scheduled",
|
||||
label: "Scheduled",
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const [selectedDatabaseId, setSelectedDatabaseId] = useState(databaseToRestore.id)
|
||||
const [selectedDatabaseId, setSelectedDatabaseId] = useState(databaseToRestore.id);
|
||||
|
||||
|
||||
const filteredBackups = backups.filter(backup => backup.databaseId == selectedDatabaseId)
|
||||
const filteredBackups = backups.filter((backup) => backup.databaseId == selectedDatabaseId);
|
||||
|
||||
const defaultValues = {
|
||||
executionMode: "immediate",
|
||||
backupLocation: "remote-file",
|
||||
}
|
||||
executionMode: "immediate" as const,
|
||||
backupLocation: "remote-file" as const,
|
||||
};
|
||||
|
||||
const form = useZodForm({
|
||||
schema: RestoreSchema,
|
||||
defaultValues: defaultValues
|
||||
defaultValues: defaultValues,
|
||||
});
|
||||
|
||||
const values = form.getValues()
|
||||
console.log("values", values)
|
||||
|
||||
|
||||
const mutation = useMutation({})
|
||||
|
||||
const values = form.getValues();
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -89,36 +69,34 @@ export const RestoreForm = (props: restoreFormProps) => {
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="backupLocation"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Backup location</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue/>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{backupLocations.map((backupLocation, key) =>
|
||||
{backupLocations.map((backupLocation, key) => (
|
||||
<SelectItem key={key} value={backupLocation.value}>
|
||||
{backupLocation.label}
|
||||
</SelectItem>
|
||||
)}
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{values.backupLocation == "remote-file" ?
|
||||
{values.backupLocation == "remote-file" ? (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<Label>Database</Label>
|
||||
<ComboBox
|
||||
values={databases.map(database =>
|
||||
({"value": database.id, "label": database.name})
|
||||
)}
|
||||
values={databases.map((database) => ({ value: database.id, label: database.name }))}
|
||||
onValueChange={setSelectedDatabaseId}
|
||||
defaultValue={selectedDatabaseId}
|
||||
searchField
|
||||
@@ -127,81 +105,82 @@ export const RestoreForm = (props: restoreFormProps) => {
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remoteBackup"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Remote backup</FormLabel>
|
||||
<ComboBoxFormItem
|
||||
values={filteredBackups.map(backup => ({
|
||||
"value": backup.id,
|
||||
"label": backup.createdAt.toString()
|
||||
})
|
||||
)}
|
||||
values={filteredBackups.map((backup) => ({
|
||||
value: backup.id,
|
||||
label: backup.createdAt.toString(),
|
||||
}))}
|
||||
{...field}
|
||||
/>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
: null}
|
||||
) : null}
|
||||
|
||||
{values.backupLocation == "desktop-file" ?
|
||||
{values.backupLocation == "desktop-file" ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="uploadedBackupFile"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>File</FormLabel>
|
||||
<FormControl>
|
||||
<Input id="uploadedBackupFile" type="file" {...field}/>
|
||||
<Input id="uploadedBackupFile" type="file" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/> : null}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="executionMode"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Execution mode</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue/>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{executionModes.map((executionMode, key) =>
|
||||
{executionModes.map((executionMode, key) => (
|
||||
<SelectItem key={key} value={executionMode.value}>
|
||||
{executionMode.label}
|
||||
</SelectItem>
|
||||
)}
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{values.executionMode == "scheduled" ?
|
||||
{values.executionMode == "scheduled" ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="scheduledDatetime"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Scheduled date</FormLabel>
|
||||
<FormControl>
|
||||
<DateTimePicker value={field.value} onChange={field.onChange}/>
|
||||
<DateTimePicker value={field.value} onSelect={field.onChange} />
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/> : null}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Button type="submit">Launch restore</Button>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,24 +1,39 @@
|
||||
import {z} from "zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const ImmediateExecutionSchema = z.object({
|
||||
executionMode: z.literal('immediate'),
|
||||
executionMode: z.literal("immediate"),
|
||||
});
|
||||
|
||||
const ScheduledExecutionSchema = z.object({
|
||||
executionMode: z.literal('scheduled'),
|
||||
executionMode: z.literal("scheduled"),
|
||||
scheduledDatetime: z.date(),
|
||||
});
|
||||
|
||||
const CommonBackupSchema = z.union([ImmediateExecutionSchema, ScheduledExecutionSchema]);
|
||||
const RemoteImmediateSchema = z
|
||||
.object({
|
||||
backupLocation: z.literal("remote-file"),
|
||||
})
|
||||
.merge(ImmediateExecutionSchema);
|
||||
|
||||
const RemoteBackupSchema = z.object({
|
||||
backupLocation: z.literal('remote-file'),
|
||||
}).merge(CommonBackupSchema);
|
||||
const RemoteScheduledSchema = z
|
||||
.object({
|
||||
backupLocation: z.literal("remote-file"),
|
||||
})
|
||||
.merge(ScheduledExecutionSchema);
|
||||
|
||||
const DesktopBackupSchema = z.object({
|
||||
backupLocation: z.literal('desktop-file'),
|
||||
uploadedBackupFile: z.instanceof(File),
|
||||
}).merge(CommonBackupSchema);
|
||||
const DesktopImmediateSchema = z
|
||||
.object({
|
||||
backupLocation: z.literal("desktop-file"),
|
||||
uploadedBackupFile: z.instanceof(File),
|
||||
})
|
||||
.merge(ImmediateExecutionSchema);
|
||||
|
||||
export const RestoreSchema = z.union([RemoteBackupSchema, DesktopBackupSchema]);
|
||||
const DesktopScheduledSchema = z
|
||||
.object({
|
||||
backupLocation: z.literal("desktop-file"),
|
||||
uploadedBackupFile: z.instanceof(File),
|
||||
})
|
||||
.merge(ScheduledExecutionSchema);
|
||||
|
||||
export const RestoreSchema = z.union([RemoteImmediateSchema, RemoteScheduledSchema, DesktopImmediateSchema, DesktopScheduledSchema]);
|
||||
export type RestoreType = z.infer<typeof RestoreSchema>;
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar"
|
||||
import {currentUser} from "@/auth/current-user";
|
||||
import {SidebarMenuButton} from "@/components/ui/sidebar";
|
||||
import {ChevronUp} from "lucide-react";
|
||||
import {LoggedInDropdown} from "@/components/wrappers/dashboard/loggedInDropdown/LoggedInDropdown";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { SidebarMenuButton } from "@/components/ui/sidebar";
|
||||
import { ChevronUp } from "lucide-react";
|
||||
import { LoggedInDropdown } from "@/components/wrappers/dashboard/loggedInDropdown/LoggedInDropdown";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
|
||||
export const LoggedInButton = async () => {
|
||||
const user = await currentUser();
|
||||
|
||||
const user = await currentUser()
|
||||
// if (!user) {
|
||||
// return <SignInButton/>
|
||||
// }
|
||||
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<LoggedInDropdown user={user}>
|
||||
<LoggedInDropdown
|
||||
user={{
|
||||
...user,
|
||||
image: user.image ?? null,
|
||||
role: user.role ?? null,
|
||||
banned: user.banned ?? null,
|
||||
banReason: user.banReason ?? null,
|
||||
banExpires: user.banExpires ?? null,
|
||||
deletedAt: user.deletedAt ? new Date(user.deletedAt) : null,
|
||||
}}
|
||||
>
|
||||
<SidebarMenuButton>
|
||||
<Avatar className="size-6">
|
||||
<AvatarFallback>{user.name?.[0]}</AvatarFallback>
|
||||
{user.image ? (
|
||||
<AvatarImage src={user.image} alt={`${user.name ?? "-"}'s profile picture`}/>
|
||||
) : null}
|
||||
<AvatarFallback>{user.name[0].toUpperCase()}</AvatarFallback>
|
||||
{user.image ? <AvatarImage src={user.image} alt={`${user.name ?? "-"}'s profile picture`} /> : null}
|
||||
</Avatar>
|
||||
<span>{user.name}</span>
|
||||
<ChevronUp className="ml-auto"/>
|
||||
<span className="first-letter:capitalize">{user.name}</span>
|
||||
<ChevronUp className="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
</LoggedInDropdown>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,55 +1,63 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {PropsWithChildren} from "react";
|
||||
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
|
||||
import {signOutAction} from "@/features/auth/auth.action";
|
||||
import {redirect} from "next/navigation";
|
||||
import {CircleUser, LogOut, ShieldHalf} from "lucide-react";
|
||||
import {User} from "@prisma/client";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { redirect } from "next/navigation";
|
||||
import { CircleUser, LogOut, ShieldHalf } from "lucide-react";
|
||||
import { signOut } from "@/lib/auth/auth-client";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { User } from "@/db/schema/01_user";
|
||||
|
||||
export type LoggedInDropdownProps = PropsWithChildren<{
|
||||
user: User
|
||||
}>
|
||||
user: User;
|
||||
}>;
|
||||
|
||||
export const LoggedInDropdown = (props: LoggedInDropdownProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{props.children}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
className="w-[--radix-popper-anchor-width]"
|
||||
>
|
||||
<DropdownMenuItem onClick={() => {
|
||||
redirect("/dashboard/profile")
|
||||
}}>
|
||||
<DropdownMenuTrigger asChild>{props.children}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" className="w-[--radix-popper-anchor-width]">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
redirect("/dashboard/profile");
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-start items-center gap-2">
|
||||
<CircleUser size={16}/>
|
||||
<CircleUser size={16} />
|
||||
<span>Account</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
{props.user.role == "admin" ?
|
||||
<DropdownMenuItem onClick={() => {
|
||||
redirect("/dashboard/admin")
|
||||
}}>
|
||||
{(props.user.role === "superadmin" || props.user.role === "admin") && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
redirect("/dashboard/admin");
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-start items-center gap-2">
|
||||
<ShieldHalf size={16}/>
|
||||
<ShieldHalf size={16} />
|
||||
<span>Administration Panel</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
: null}
|
||||
<DropdownMenuItem onClick={() => {
|
||||
signOutAction()
|
||||
}}>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
router.push("/login");
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-start items-center gap-2">
|
||||
<LogOut size={16}/>
|
||||
<LogOut size={16} />
|
||||
<span>Log out</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
+19
-23
@@ -1,42 +1,38 @@
|
||||
"use client"
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import {
|
||||
deleteOrganizationAction,
|
||||
} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {setCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {toast} from "sonner";
|
||||
"use client";
|
||||
import { ButtonWithConfirm } from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import { deleteOrganizationAction } from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { setCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export type DeleteOrganizationButtonProps = {
|
||||
organizationSlug: string;
|
||||
}
|
||||
};
|
||||
|
||||
export const DeleteOrganizationButton = (props: DeleteOrganizationButtonProps) => {
|
||||
const router = useRouter();
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => deleteOrganizationAction(props.organizationSlug),
|
||||
onSuccess: async (result) => {
|
||||
console.log(result);
|
||||
if(result.data.success) {
|
||||
await setCurrentOrganizationSlug("default")
|
||||
router.push("/")
|
||||
if (result.data.success) {
|
||||
await setCurrentOrganizationSlug("default");
|
||||
router.push("/");
|
||||
toast.success(result.data.actionSuccess.message);
|
||||
}else{
|
||||
} else {
|
||||
toast.error(result.data.actionError.message);
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
|
||||
return(
|
||||
return (
|
||||
<ButtonWithConfirm
|
||||
onClick={() => {
|
||||
mutation.mutate()
|
||||
mutation.mutate();
|
||||
}}
|
||||
isPending={mutation.isPending}
|
||||
text="Delete Organization"
|
||||
variant="destructive"/>
|
||||
)
|
||||
}
|
||||
variant="destructive"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+51
-78
@@ -1,111 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
useZodForm
|
||||
} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Form} from "@/components/ui/form"
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {Organization, User} from "@prisma/client"
|
||||
import {MultiSelect} from "@/components/wrappers/common/multiSelect/MultiSelect";
|
||||
import {
|
||||
OrganizationFormSchema,
|
||||
OrganizationFormType
|
||||
} from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema";
|
||||
import {
|
||||
createOrganizationAction,
|
||||
updateOrganizationAction
|
||||
} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import {toast} from "sonner";
|
||||
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Organization, User } from "@prisma/client";
|
||||
import { MultiSelect } from "@/components/wrappers/common/multiselect/multi-select";
|
||||
import { OrganizationFormSchema, OrganizationFormType } from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema";
|
||||
import { createOrganizationAction, updateOrganizationAction } from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export type organizationFormProps = {
|
||||
defaultValues?: Organization;
|
||||
users: User[]
|
||||
|
||||
}
|
||||
users: User[];
|
||||
};
|
||||
|
||||
export const OrganizationForm = (props: organizationFormProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
const isCreate = !Boolean(props.defaultValues)
|
||||
|
||||
const isCreate = !Boolean(props.defaultValues);
|
||||
|
||||
const formatUsersList = (users: User[]) => {
|
||||
return users.map(user => ({
|
||||
return users.map((user) => ({
|
||||
value: user.id,
|
||||
label: `${user.name} | ${user.email}`,
|
||||
}));
|
||||
};
|
||||
|
||||
const formatDefaultUsers = (users: OrganizationFormType['users']): string[] => {
|
||||
console.log(users)
|
||||
return users.map(user => user.userId );
|
||||
const formatDefaultUsers = (users: OrganizationFormType["users"]): string[] => {
|
||||
console.log(users);
|
||||
return users.map((user) => user.userId);
|
||||
};
|
||||
|
||||
const formattedDefaultValues = {
|
||||
...props.defaultValues,
|
||||
users: !isCreate ? formatDefaultUsers(props.defaultValues?.users) : []
|
||||
}
|
||||
|
||||
users: !isCreate ? formatDefaultUsers(props.defaultValues?.users) : [],
|
||||
};
|
||||
|
||||
const form = useZodForm({
|
||||
schema: OrganizationFormSchema,
|
||||
defaultValues: formattedDefaultValues,
|
||||
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: OrganizationFormType) => {
|
||||
console.log(values)
|
||||
const organization = await updateOrganizationAction({data: values, organizationId: props.defaultValues.id})
|
||||
console.log(organization)
|
||||
console.log(values);
|
||||
const organization = await updateOrganizationAction({ data: values, organizationId: props.defaultValues.id });
|
||||
console.log(organization);
|
||||
if (organization.data.success) {
|
||||
toast.success(organization.data.actionSuccess.message);
|
||||
router.push(`/dashboard/${organization.data.value.slug}/settings`);
|
||||
router.refresh()
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.success(organization.data.actionError.message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
</CardHeader>
|
||||
<CardHeader></CardHeader>
|
||||
<CardContent>
|
||||
<Form form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Organization 1" {...field} />
|
||||
<Input placeholder="Organization 1" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -113,29 +88,30 @@ export const OrganizationForm = (props: organizationFormProps) => {
|
||||
control={form.control}
|
||||
name="slug"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Slug</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="project-1" {...field}
|
||||
placeholder="project-1"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replaceAll(" ", "-").toLowerCase()
|
||||
field.onChange(value)
|
||||
}}/>
|
||||
const value = e.target.value.replaceAll(" ", "-").toLowerCase();
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="users"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Databases</FormLabel>
|
||||
<FormControl>
|
||||
|
||||
<MultiSelect
|
||||
options={formatUsersList(props.users)}
|
||||
onValueChange={field.onChange}
|
||||
@@ -147,16 +123,13 @@ export const OrganizationForm = (props: organizationFormProps) => {
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Select users you want to add to this organization</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<Button>
|
||||
{isCreate ? `Create Organization` : `Update Organization`}
|
||||
</Button>
|
||||
<Button>{isCreate ? `Create Organization` : `Update Organization`}</Button>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger} from "@/components/ui/dialog";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema";
|
||||
import {createOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useState} from "react";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { OrganizationSchema } from "@/components/wrappers/dashboard/organization/organization.schema";
|
||||
import { createOrganizationAction } from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { authClient } from "@/lib/auth/auth-client";
|
||||
|
||||
export type createOrganizationModalProps = {
|
||||
children: any
|
||||
}
|
||||
|
||||
children: any;
|
||||
};
|
||||
|
||||
export function CreateOrganizationModal(props: createOrganizationModalProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const {children} = props;
|
||||
const { children } = props;
|
||||
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
|
||||
const form = useZodForm({
|
||||
schema: OrganizationSchema,
|
||||
@@ -29,46 +29,49 @@ export function CreateOrganizationModal(props: createOrganizationModalProps) {
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: OrganizationSchema) => {
|
||||
console.log(values)
|
||||
const result = await createOrganizationAction(values)
|
||||
setOpen(false);
|
||||
router.refresh()
|
||||
}
|
||||
})
|
||||
console.log(values);
|
||||
|
||||
const result = await createOrganizationAction(values);
|
||||
|
||||
if (result && result.data) {
|
||||
if (result.data.success && result.data.value) {
|
||||
setOpen(false);
|
||||
await authClient.organization.setActive({ organizationSlug: result.data.value.slug });
|
||||
router.replace(`/dashboard/${result.data.value.slug}/home`);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
|
||||
<DialogTrigger asChild>
|
||||
{children}
|
||||
</DialogTrigger>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
|
||||
<DialogContent className="sm:max-w-[425px] w-full">
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create a new organization</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="sm:max-w-[375px] w-full">
|
||||
<Form form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
console.log(values)
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
console.log(values);
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -76,18 +79,19 @@ export function CreateOrganizationModal(props: createOrganizationModalProps) {
|
||||
control={form.control}
|
||||
name="slug"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Slug</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replaceAll(" ", "-").toLowerCase()
|
||||
field.onChange(value)
|
||||
}}
|
||||
<Input
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replaceAll(" ", "-").toLowerCase();
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -98,10 +102,7 @@ export function CreateOrganizationModal(props: createOrganizationModalProps) {
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
|
||||
</DialogContent>
|
||||
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,76 +1,36 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
import { ComboBox } from "@/components/wrappers/common/combobox";
|
||||
import { useSidebar } from "@/components/ui/sidebar";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { authClient } from "@/lib/auth/auth-client";
|
||||
|
||||
import {ComboBox} from "@/components/wrappers/common/combobox";
|
||||
import {Organization} from "@prisma/client";
|
||||
import {
|
||||
getCurrentOrganizationSlug,
|
||||
setCurrentOrganizationSlug
|
||||
} from "@/features/dashboard/organization-cookie";
|
||||
import {useSidebar} from "@/components/ui/sidebar";
|
||||
import {useRouter} from "next/navigation";
|
||||
export function OrganizationCombobox() {
|
||||
const router = useRouter();
|
||||
|
||||
export type organizationComboBoxProps = {
|
||||
organizations: Organization[]
|
||||
defaultOrganization: Organization
|
||||
}
|
||||
const { data: organizations } = authClient.useListOrganizations();
|
||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||
|
||||
if (!organizations) return null;
|
||||
|
||||
export function OrganizationCombobox(props: organizationComboBoxProps) {
|
||||
const router = useRouter()
|
||||
// const {organizationId, moveToAnotherOrganization} = useStore((state) => state);
|
||||
console.log("organizations", organizations);
|
||||
console.log("activeOrganization", activeOrganization);
|
||||
|
||||
const [organizationSlug, setOrganizationSlug] = useState<string>()
|
||||
|
||||
const {organizations, defaultOrganization} = props
|
||||
|
||||
useEffect(() => {
|
||||
getCurrentOrganizationSlug().then(slug => {
|
||||
|
||||
if (slug == "") {
|
||||
setOrganizationSlug(defaultOrganization.id)
|
||||
setCurrentOrganizationSlug(defaultOrganization.slug)
|
||||
} else {
|
||||
setOrganizationSlug(slug)
|
||||
const organization = organizations.find(organization => organization.slug === slug)
|
||||
if (!organization) {
|
||||
setOrganizationSlug(defaultOrganization.id)
|
||||
setCurrentOrganizationSlug(defaultOrganization.slug)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}, [organizationSlug])
|
||||
|
||||
const values = organizations.map(organization => {
|
||||
return ({
|
||||
const values = organizations.map((organization) => {
|
||||
return {
|
||||
value: organization.slug,
|
||||
label: organization.name,
|
||||
})
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
const onValueChange = (slug: string) => {
|
||||
if (organizationSlug !== slug) {
|
||||
setOrganizationSlug(slug)
|
||||
setCurrentOrganizationSlug(slug)
|
||||
router.replace("/dashboard")
|
||||
}
|
||||
}
|
||||
const {state, isMobile} = useSidebar();
|
||||
const onValueChange = async (slug: string) => {
|
||||
await authClient.organization.setActive({
|
||||
organizationSlug: slug,
|
||||
});
|
||||
router.replace(`/dashboard/${slug}/home`);
|
||||
router.refresh();
|
||||
};
|
||||
const { state } = useSidebar();
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{state === 'expanded' && (
|
||||
<ComboBox
|
||||
sideBar
|
||||
values={values}
|
||||
defaultValue={organizationSlug}
|
||||
onValueChange={onValueChange}/>
|
||||
)}
|
||||
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
return <>{state === "expanded" && <ComboBox sideBar values={values} defaultValue={activeOrganization?.slug} onValueChange={onValueChange} />}</>;
|
||||
}
|
||||
|
||||
@@ -1,218 +1,160 @@
|
||||
"use server"
|
||||
"use server";
|
||||
|
||||
import {ActionError, userAction} from "@/safe-actions";
|
||||
import {prisma} from "@/prisma";
|
||||
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {Organization} from "@prisma/client";
|
||||
import {z} from "zod";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import {getDb} from "@/db";
|
||||
import {
|
||||
OrganizationFormSchema
|
||||
} from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema";
|
||||
import {ProjectSchema} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.schema";
|
||||
import { userAction } from "@/safe-actions";
|
||||
import { OrganizationSchema } from "@/components/wrappers/dashboard/organization/organization.schema";
|
||||
import { ServerActionResult } from "@/types/action-type";
|
||||
import { z } from "zod";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { OrganizationFormSchema } from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema";
|
||||
import { db } from "@/db";
|
||||
import { Organization, organization as drizzleOrganization, organizationMember as drizzleOrganizationMember } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { checkSlugOrganization, createOrganization } from "@/lib/auth/auth";
|
||||
|
||||
|
||||
const verifySlugUniqueness = async (slug: string) => {
|
||||
const slugExists = await prisma.organization.count({
|
||||
where: {
|
||||
slug: slug
|
||||
},
|
||||
})
|
||||
|
||||
if (slugExists) {
|
||||
throw new ActionError("Slug already exists.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const createOrganizationAction = userAction
|
||||
.schema(OrganizationSchema)
|
||||
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
|
||||
|
||||
try {
|
||||
|
||||
await verifySlugUniqueness(parsedInput.slug)
|
||||
|
||||
const organization = await prisma.organization.create({
|
||||
data: {
|
||||
...parsedInput
|
||||
}
|
||||
})
|
||||
|
||||
await prisma.userOrganization.create({
|
||||
data: {
|
||||
userId: ctx.user.id,
|
||||
organizationId: organization.id,
|
||||
role: "admin"
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: organization,
|
||||
actionSuccess: {
|
||||
message: "Organization has been successfully created.",
|
||||
messageParams: {organizationId: organization.id},
|
||||
},
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error creating organization:", error);
|
||||
export const createOrganizationAction = userAction.schema(OrganizationSchema).action(async ({ parsedInput }): Promise<ServerActionResult<Organization>> => {
|
||||
try {
|
||||
if (!checkSlugOrganization(parsedInput.slug)) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to create organization.",
|
||||
message: "Slug is already taken",
|
||||
status: 500,
|
||||
cause: error.message ?? "Unknown error",
|
||||
messageParams: {message: "Error creating the organization"},
|
||||
messageParams: { message: "Error creating the organization" },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
const organization = await createOrganization(parsedInput.name, parsedInput.slug);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: organization!,
|
||||
actionSuccess: {
|
||||
message: "Organization has been successfully created.",
|
||||
messageParams: { organizationId: organization!.id },
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating organization:", error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to create organization.",
|
||||
status: 500,
|
||||
messageParams: { message: "Error creating the organization" },
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const updateOrganizationAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
data: OrganizationFormSchema,
|
||||
organizationId: z.string()
|
||||
}))
|
||||
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
|
||||
organizationId: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }): Promise<ServerActionResult<Organization>> => {
|
||||
try {
|
||||
const newUserList = parsedInput.data.users;
|
||||
|
||||
const newUserList = parsedInput.data.users
|
||||
const organization = await db.select().from(organization).where(eq(organization.id, parsedInput.organizationId)).execute();
|
||||
|
||||
const organization = await prisma.organization.findFirst({
|
||||
where:{
|
||||
id: parsedInput.organizationId,
|
||||
},
|
||||
include: {
|
||||
users : {}
|
||||
}
|
||||
})
|
||||
|
||||
const existingItemIds = organization.users.map((user) => user.userId);
|
||||
const usersToAdd = newUserList.filter(
|
||||
(id) => !existingItemIds.includes(id)
|
||||
);
|
||||
const usersToRemove = existingItemIds.filter(
|
||||
(id) => !newUserList.includes(id)
|
||||
);
|
||||
|
||||
console.log(usersToAdd);
|
||||
console.log(usersToRemove);
|
||||
if (organization.length === 0) {
|
||||
throw new Error("Organization not found.");
|
||||
}
|
||||
|
||||
const existingItemIds = organization[0].users.map((user) => user.userId);
|
||||
const usersToAdd = newUserList.filter((id) => !existingItemIds.includes(id));
|
||||
const usersToRemove = existingItemIds.filter((id) => !newUserList.includes(id));
|
||||
|
||||
if (usersToAdd.length > 0) {
|
||||
|
||||
await prisma.userOrganization.createMany({
|
||||
data: usersToAdd.map((userId) => ({
|
||||
userId: userId,
|
||||
organizationId: organization.id,
|
||||
role: "member"
|
||||
})),
|
||||
skipDuplicates: true, // Optional: to avoid duplicate insertion errors
|
||||
});
|
||||
await db
|
||||
.insert(userOrganization)
|
||||
.values(
|
||||
usersToAdd.map((userId) => ({
|
||||
userId,
|
||||
organizationId: organization[0].id,
|
||||
role: "member",
|
||||
}))
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
if (usersToRemove.length > 0) {
|
||||
await prisma.userOrganization.deleteMany({
|
||||
where: {
|
||||
userId: { in: usersToRemove },
|
||||
},
|
||||
|
||||
});
|
||||
await db.delete().from(userOrganization).where(inArray(userOrganization.userId, usersToRemove)).execute();
|
||||
}
|
||||
|
||||
const updatedOrganization = await prisma.organization.update({
|
||||
where:{
|
||||
id: organization.id
|
||||
},
|
||||
data:{
|
||||
const updatedOrganization = await db
|
||||
.update(organization)
|
||||
.set({
|
||||
name: parsedInput.data.name,
|
||||
slug: parsedInput.data.slug,
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
.where(eq(organization.id, parsedInput.organizationId))
|
||||
.returning()
|
||||
.execute();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: updatedOrganization,
|
||||
value: updatedOrganization[0],
|
||||
actionSuccess: {
|
||||
message: "Organization has been successfully updated.",
|
||||
messageParams: {organizationId: updatedOrganization.id},
|
||||
messageParams: { organizationId: updatedOrganization[0].id },
|
||||
},
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error updating organization:", error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to update organization.",
|
||||
status: 500,
|
||||
cause: error.message ?? "Unknown error",
|
||||
messageParams: {message: "Error updating the organization"},
|
||||
messageParams: { message: "Error updating the organization" },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
export const deleteOrganizationAction = userAction.schema(z.string()).action(async ({ parsedInput, ctx }): Promise<ServerActionResult<Organization>> => {
|
||||
try {
|
||||
const uuid = uuidv4();
|
||||
const organization = await db.select().from(organization).where(eq(organization.slug, parsedInput)).execute();
|
||||
|
||||
|
||||
|
||||
|
||||
export const deleteOrganizationAction = userAction
|
||||
.schema(z.string())
|
||||
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
|
||||
console.log(parsedInput);
|
||||
try {
|
||||
|
||||
const db = await getDb()
|
||||
const uuid = uuidv4()
|
||||
const organization = await db.organization.findFirst({
|
||||
where: {
|
||||
slug: parsedInput,
|
||||
}
|
||||
})
|
||||
console.log(organization);
|
||||
const organizationUpdated = await db.organization.update({
|
||||
where: {
|
||||
slug: parsedInput,
|
||||
},
|
||||
data: {
|
||||
name: `${organization.name}-${uuid}`,
|
||||
slug: `${organization.slug}-${uuid}`,
|
||||
deleted : true
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: organizationUpdated,
|
||||
actionSuccess: {
|
||||
message: "Organization has been successfully deleted.",
|
||||
messageParams: {organizationId: organizationUpdated.id},
|
||||
},
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error deleting organization:", error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to delete organization.",
|
||||
status: 500,
|
||||
cause: error.message ?? "Unknown error",
|
||||
messageParams: {message: "Error deleting the organization"},
|
||||
},
|
||||
};
|
||||
if (organization.length === 0) {
|
||||
throw new Error("Organization not found.");
|
||||
}
|
||||
|
||||
});
|
||||
const updatedOrganization = await db
|
||||
.update(organization)
|
||||
.set({
|
||||
name: `${organization[0].name}-${uuid}`,
|
||||
slug: `${organization[0].slug}-${uuid}`,
|
||||
deleted: true,
|
||||
})
|
||||
.where(eq(organization.id, organization[0].id))
|
||||
.returning()
|
||||
.execute();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: updatedOrganization[0],
|
||||
actionSuccess: {
|
||||
message: "Organization has been successfully deleted.",
|
||||
messageParams: { organizationId: updatedOrganization[0].id },
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error deleting organization:", error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to delete organization.",
|
||||
status: 500,
|
||||
cause: error.message ?? "Unknown error",
|
||||
messageParams: { message: "Error deleting the organization" },
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {z} from "zod";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export const OrganizationSchema = z.object({
|
||||
name: z.string().min(5, 'Name must be at least 5 characters long').max(40, 'Name must be at most 40 characters long'),
|
||||
slug: z.string()
|
||||
.regex(/^[a-zA-Z0-9_-]*$/, 'Slug can only contain letters, numbers, underscores, and hyphens')
|
||||
.min(5, 'Slug must be at least 5 characters long')
|
||||
.max(20, 'Slug must be at most 20 characters long'),
|
||||
name: z.string().min(5, "Name must be at least 5 characters long").max(40, "Name must be at most 40 characters long"),
|
||||
slug: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9_-]*$/, "Slug can only contain letters, numbers, underscores, and hyphens")
|
||||
.min(5, "Slug must be at least 5 characters long")
|
||||
.max(20, "Slug must be at most 20 characters long"),
|
||||
});
|
||||
|
||||
export type OrganizationSchema = z.infer<typeof OrganizationSchema>;
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
"use client"
|
||||
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
|
||||
import {User} from "@prisma/client";
|
||||
import {UploadIcon} from "lucide-react";
|
||||
import {toast} from "sonner";
|
||||
import {uploadImageAction} from "@/features/upload/public/upload.action";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {prisma} from "@/prisma";
|
||||
import {updateImageUserAction} from "@/components/wrappers/dashboard/profile/Avatar/avatar.action";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useSession} from "next-auth/react";
|
||||
"use client";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { UploadIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { uploadImageAction } from "@/features/upload/public/upload.action";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { updateImageUserAction } from "@/components/wrappers/dashboard/profile/Avatar/avatar.action";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { User } from "@/db/schema/01_user";
|
||||
|
||||
export type AvatarWithUploadProps = {
|
||||
user: User
|
||||
}
|
||||
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
||||
const user = props.user
|
||||
const user = props.user;
|
||||
const router = useRouter();
|
||||
const { data: session, update } = useSession();
|
||||
|
||||
|
||||
const submitImage = useMutation({
|
||||
mutationFn: async (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.set("file", file);
|
||||
const uploadImage = await uploadImageAction(formData)
|
||||
const data = uploadImage?.data?.data
|
||||
const uploadImage = await uploadImageAction(formData);
|
||||
const data = uploadImage?.data?.data;
|
||||
|
||||
if (uploadImage?.serverError || !data) {
|
||||
console.log(uploadImage?.serverError);
|
||||
@@ -34,8 +29,8 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateUser = await updateImageUserAction(data.url)
|
||||
const dataUser = updateUser?.data?.data
|
||||
const updateUser = await updateImageUserAction(data.url);
|
||||
const dataUser = updateUser?.data?.data;
|
||||
|
||||
if (updateUser?.serverError || !dataUser) {
|
||||
console.log(updateUser?.serverError);
|
||||
@@ -43,42 +38,27 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const newSession = {
|
||||
...session,
|
||||
user: {
|
||||
...session?.user,
|
||||
image: data.url
|
||||
},
|
||||
};
|
||||
|
||||
await update(newSession);
|
||||
toast.success("Successfully uploaded user image!");
|
||||
router.refresh()
|
||||
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
const handleImageUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
if (!file.type.includes("image")) {
|
||||
toast.error("File not an image")
|
||||
toast.error("File not an image");
|
||||
return;
|
||||
}
|
||||
submitImage.mutate(file)
|
||||
submitImage.mutate(file);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="relative " >
|
||||
<Avatar className="size-14 mr-3 ">
|
||||
<AvatarFallback>{user.name?.[0]}</AvatarFallback>
|
||||
{user.image ? (
|
||||
<AvatarImage src={user.image} alt={`${user.name ?? "-"}'s profile picture`}/>
|
||||
) : null}
|
||||
</Avatar>
|
||||
<div className="relative ">
|
||||
<Avatar className="size-14 mr-3 ">
|
||||
<AvatarFallback>{user.name[0]}</AvatarFallback>
|
||||
{user.image ? <AvatarImage src={user.image} alt={`${user.name ?? "-"}'s profile picture`} /> : null}
|
||||
</Avatar>
|
||||
<div
|
||||
onClick={() => {
|
||||
const fileInput = document.createElement("input");
|
||||
@@ -90,8 +70,8 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
||||
}}
|
||||
className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-100 hover:bg-gray-500 hover:bg-opacity-50 rounded-full size-14"
|
||||
>
|
||||
<UploadIcon className="w-8 h-8 text-primary"/>
|
||||
<UploadIcon className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
"use server"
|
||||
import {userAction} from "@/safe-actions";
|
||||
import {z} from "zod";
|
||||
import {prisma} from "@/prisma";
|
||||
"use server";
|
||||
import { db } from "@/db";
|
||||
import { user as drizzleUser } from "@/db/schema";
|
||||
import { userAction } from "@/safe-actions";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
export const updateImageUserAction = userAction.schema(z.string()).action(async ({ parsedInput, ctx }) => {
|
||||
const [updatedUser] = await db.update(drizzleUser).set({ image: parsedInput }).where(eq(drizzleUser.id, ctx.user.id)).returning();
|
||||
|
||||
export const updateImageUserAction = userAction
|
||||
.schema(z.string())
|
||||
.action(async ({parsedInput, ctx}) => {
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: {
|
||||
id: ctx.user.id,
|
||||
},
|
||||
data: {
|
||||
image: parsedInput,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return {
|
||||
data: user,
|
||||
}
|
||||
});
|
||||
return {
|
||||
data: updatedUser,
|
||||
};
|
||||
});
|
||||
|
||||
+22
-14
@@ -1,34 +1,42 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {signOutAction} from "@/features/auth/auth.action";
|
||||
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/delete-account.action";
|
||||
import {Trash2} from "lucide-react";
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { ButtonWithConfirm } from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import { signOut } from "@/lib/auth/auth-client";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { deleteUserAction } from "./delete-account.action";
|
||||
|
||||
export type ButtonDeleteAccountProps = {
|
||||
text? : string
|
||||
}
|
||||
text?: string;
|
||||
};
|
||||
|
||||
export const ButtonDeleteAccount = (props: ButtonDeleteAccountProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => deleteUserAction(""),
|
||||
onSuccess: async () => {
|
||||
await signOutAction();
|
||||
await signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
router.push("/login");
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<ButtonWithConfirm
|
||||
text={props.text ? props.text : ""}
|
||||
onClick={() => {
|
||||
mutation.mutate()
|
||||
mutation.mutate();
|
||||
}}
|
||||
variant={"destructive"}
|
||||
isPending={mutation.isPending}
|
||||
className="gap-2"
|
||||
icon={<Trash2/>}
|
||||
icon={<Trash2 />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
+23
-40
@@ -1,44 +1,27 @@
|
||||
"use server"
|
||||
import {userAction} from "@/safe-actions";
|
||||
import {prisma} from "@/prisma";
|
||||
import {z} from "zod";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import {EmailFormSchema} from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.schema";
|
||||
"use server";
|
||||
import { userAction } from "@/safe-actions";
|
||||
import { z } from "zod";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { db } from "@/db";
|
||||
import { user as drizzleUser } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const deleteUserAction = userAction.schema(z.string()).action(async ({ parsedInput, ctx }) => {
|
||||
const userId = parsedInput.length > 0 ? parsedInput : ctx.user.id;
|
||||
const uuid = uuidv4();
|
||||
|
||||
export const deleteUserAction = userAction
|
||||
.schema(z.string())
|
||||
.action(async ({parsedInput, ctx}) => {
|
||||
const userId = parsedInput.length > 0 ? parsedInput : ctx.user.id;
|
||||
|
||||
const uuid = uuidv4()
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
email: `${uuid}@portabase.com`,
|
||||
name: `${uuid}`,
|
||||
deleted: true
|
||||
}
|
||||
const [updatedUser] = await db
|
||||
.update(drizzleUser)
|
||||
.set({
|
||||
email: `${uuid}@portabase.com`,
|
||||
name: `${uuid}`,
|
||||
//deleted: true,
|
||||
//todo: add deleted
|
||||
})
|
||||
.where(eq(drizzleUser.id, userId))
|
||||
.returning();
|
||||
|
||||
const account = await prisma.account.findFirst({
|
||||
where: {
|
||||
userId: userId,
|
||||
}
|
||||
})
|
||||
if (account) {
|
||||
await prisma.account.delete({
|
||||
where: {
|
||||
id: account.id
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
return {
|
||||
data: user,
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
data: updatedUser,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import {currentUser, requiredCurrentUser} from "@/auth/current-user";
|
||||
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export type UserAvatarProps = {}
|
||||
export type UserAvatarProps = {};
|
||||
|
||||
export const UserAvatar = async () => {
|
||||
const user = await currentUser();
|
||||
|
||||
const user = await currentUser()
|
||||
|
||||
if (!user) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Avatar className="size-6">
|
||||
<AvatarFallback>{user.name?.[0]}</AvatarFallback>
|
||||
{user.image ? (
|
||||
<AvatarImage src={user.image} alt={`${user.name ?? "-"}'s profile picture`}/>
|
||||
) : null}
|
||||
<AvatarFallback>{user.name[0]}</AvatarFallback>
|
||||
{user.image ? <AvatarImage src={user.image} alt={`${user.name ?? "-"}'s profile picture`} /> : null}
|
||||
</Avatar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {
|
||||
FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm
|
||||
} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Form} from "@/components/ui/form"
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import {UserSchema, UserType} from "@/components/wrappers/dashboard/profile/UserForm/user-form.schema";
|
||||
import {toast} from "sonner";
|
||||
import {updateUserAction} from "@/components/wrappers/dashboard/profile/UserForm/user-form.action";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { UserSchema, UserType } from "@/components/wrappers/dashboard/profile/UserForm/user-form.schema";
|
||||
import { toast } from "sonner";
|
||||
import { updateUserAction } from "@/components/wrappers/dashboard/profile/UserForm/user-form.action";
|
||||
|
||||
export type userFormProps = {
|
||||
defaultValues?: UserType;
|
||||
userId?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export const UserForm = (props: userFormProps) => {
|
||||
|
||||
const isCreate = !Boolean(props.defaultValues)
|
||||
const isCreate = !Boolean(props.defaultValues);
|
||||
|
||||
const form = useZodForm({
|
||||
schema: UserSchema,
|
||||
@@ -30,96 +26,73 @@ export const UserForm = (props: userFormProps) => {
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { data: session, update } = useSession();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: UserType) => {
|
||||
console.log("values", values)
|
||||
console.log(props.userId)
|
||||
console.log("values", values);
|
||||
console.log(props.userId);
|
||||
const updateUser = await updateUserAction({
|
||||
id: props.userId ?? "-",
|
||||
data: values
|
||||
})
|
||||
data: values,
|
||||
});
|
||||
|
||||
const data = updateUser?.data?.data
|
||||
const data = updateUser?.data?.data;
|
||||
if (updateUser?.serverError || !data) {
|
||||
console.log(updateUser?.serverError);
|
||||
toast.error(updateUser?.serverError);
|
||||
return;
|
||||
}
|
||||
console.log("email:", values.email)
|
||||
|
||||
const newSession = {
|
||||
...session,
|
||||
user: {
|
||||
...session?.user,
|
||||
name: values.name,
|
||||
email: values.email
|
||||
},
|
||||
};
|
||||
|
||||
const updateSession = await update(newSession);
|
||||
console.log(updateSession);
|
||||
toast.success(`Success updating user informations`);
|
||||
toast.success(`Profile updated successfully.`);
|
||||
router.push(`/dashboard/profile`);
|
||||
router.refresh()
|
||||
}
|
||||
})
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Account
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Your informations
|
||||
</CardDescription>
|
||||
|
||||
<CardTitle>Account</CardTitle>
|
||||
<CardDescription>Your informations</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"Your Name"} {...field} />
|
||||
<Input placeholder={"Your Name"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={'exemple@portabase.com'} disabled {...field}
|
||||
value={field.value ?? ""}/>
|
||||
<Input placeholder={"exemple@portabase.com"} disabled {...field} value={field.value ?? ""} />
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button>
|
||||
{isCreate ? `` : `Save`}
|
||||
</Button>
|
||||
<Button>{isCreate ? `` : `Save`}</Button>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
"use server"
|
||||
import {userAction} from "@/safe-actions";
|
||||
import {z} from "zod";
|
||||
import {prisma} from "@/prisma";
|
||||
import {UserSchema} from "@/components/wrappers/dashboard/profile/UserForm/user-form.schema";
|
||||
"use server";
|
||||
import { userAction } from "@/safe-actions";
|
||||
import { z } from "zod";
|
||||
import { UserSchema } from "@/components/wrappers/dashboard/profile/UserForm/user-form.schema";
|
||||
import { db } from "@/db";
|
||||
import { user as drizzleUser } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const updateUserAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
data: UserSchema,
|
||||
}
|
||||
)
|
||||
)
|
||||
.action(async ({parsedInput, ctx}) => {
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: {
|
||||
id: parsedInput.id,
|
||||
},
|
||||
data: parsedInput.data,
|
||||
id: z.string(),
|
||||
data: UserSchema,
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const [updatedUser] = await db.update(drizzleUser).set(parsedInput.data).where(eq(drizzleUser.id, parsedInput.id)).returning();
|
||||
|
||||
return {
|
||||
data: updatedUser,
|
||||
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
+19
-21
@@ -1,43 +1,41 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {Trash2} from "lucide-react";
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {deleteProjectAction} from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/delete-project.action";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {toast} from "sonner";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { ButtonWithConfirm } from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { deleteProjectAction } from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/delete-project.action";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export type ButtonDeleteProjectProps = {
|
||||
text? : string
|
||||
projectId: string
|
||||
}
|
||||
text?: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const ButtonDeleteProject = (props: ButtonDeleteProjectProps) => {
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => deleteProjectAction(props.projectId),
|
||||
onSuccess: async (result: any) => {
|
||||
console.log(result)
|
||||
if(result.data?.success) {
|
||||
if (result.data?.success) {
|
||||
toast.success(result.data.actionSuccess.message);
|
||||
router.push("/")
|
||||
|
||||
}else{
|
||||
router.push("/");
|
||||
} else {
|
||||
toast.error(result.data.actionError.message || "Unknown error occurred.");
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<ButtonWithConfirm
|
||||
text={props.text ? props.text : ""}
|
||||
onClick={() => {
|
||||
mutation.mutate()
|
||||
mutation.mutate();
|
||||
}}
|
||||
variant={"destructive"}
|
||||
isPending={mutation.isPending}
|
||||
className="gap-2"
|
||||
icon={<Trash2/>}
|
||||
icon={<Trash2 />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
+42
-40
@@ -1,47 +1,49 @@
|
||||
"use server"
|
||||
import {userAction} from "@/safe-actions";
|
||||
import {z} from "zod";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import {prisma} from "@/prisma";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {Projects} from "@prisma/client";
|
||||
"use server";
|
||||
|
||||
import { userAction } from "@/safe-actions";
|
||||
import { z } from "zod";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { ServerActionResult } from "@/types/action-type";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import { project } from "@/db/schema";
|
||||
|
||||
export const deleteProjectAction = userAction
|
||||
.schema(z.string())
|
||||
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Projects>> => {
|
||||
export const deleteProjectAction = userAction.schema(z.string()).action(async ({ parsedInput }): Promise<ServerActionResult<typeof project.$inferSelect>> => {
|
||||
try {
|
||||
const uuid = uuidv4();
|
||||
|
||||
try {
|
||||
const uuid = uuidv4()
|
||||
|
||||
const project = await prisma.project.update({
|
||||
where: {
|
||||
id: parsedInput
|
||||
},
|
||||
data:{
|
||||
isArchived: true,
|
||||
slug: uuid
|
||||
}
|
||||
const updatedProjects = await db
|
||||
.update(project)
|
||||
.set({
|
||||
isArchived: true,
|
||||
slug: uuid,
|
||||
})
|
||||
.where(eq(project.id, parsedInput))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: project,
|
||||
actionSuccess: {
|
||||
message: "Projects has been successfully archived.",
|
||||
messageParams: {projectId: parsedInput},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to archived Projects.",
|
||||
status: 500, // Optional: Use a meaningful status code
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {projectId: parsedInput},
|
||||
},
|
||||
};
|
||||
const updatedProject = updatedProjects[0];
|
||||
|
||||
if (!updatedProject) {
|
||||
throw new Error("Project not found or update failed");
|
||||
}
|
||||
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
value: updatedProject,
|
||||
actionSuccess: {
|
||||
message: "Projects has been successfully archived.",
|
||||
messageParams: { projectId: parsedInput },
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to archive Projects.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: { projectId: parsedInput },
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,41 +1,30 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||
import {Database} from "@prisma/client";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Database } from "@/db/schema";
|
||||
import { formatDateLastContact } from "@/utils/date-formatting";
|
||||
|
||||
export type DatabaseKpiPro = {
|
||||
successRate: any,
|
||||
database: Database,
|
||||
totalBackups: number
|
||||
}
|
||||
|
||||
successRate: any;
|
||||
database: Database;
|
||||
totalBackups: number;
|
||||
};
|
||||
|
||||
export const DatabaseKpi = (props: DatabaseKpiPro) => {
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between gap-8 mb-6">
|
||||
<Card className="w-full sm:w-auto flex-1">
|
||||
<CardHeader className="font-bold text-xl">
|
||||
Backups
|
||||
</CardHeader>
|
||||
<CardHeader className="font-bold text-xl">Backups</CardHeader>
|
||||
<CardContent>{props.totalBackups}</CardContent>
|
||||
</Card>
|
||||
<Card className="w-full sm:w-auto flex-1">
|
||||
<CardHeader className="font-bold text-xl">
|
||||
Success rate
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{props.successRate ? `${props.successRate} %` : "Unavailable for now."}
|
||||
</CardContent>
|
||||
<CardHeader className="font-bold text-xl">Success rate</CardHeader>
|
||||
<CardContent>{props.successRate ? `${props.successRate} %` : "Unavailable for now."}</CardContent>
|
||||
</Card>
|
||||
<Card className="w-full sm:w-auto flex-1">
|
||||
<CardHeader className="font-bold text-xl">
|
||||
Last contact
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formatDateLastContact(props.database.lastContact)}
|
||||
</CardContent>
|
||||
<CardHeader className="font-bold text-xl">Last contact</CardHeader>
|
||||
<CardContent>{formatDateLastContact(props.database.lastContact)}</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {DataTableWithPagination} from "@/components/wrappers/common/table/data-table-with-pagination";
|
||||
import {Backup, Database, Restoration} from "@prisma/client";
|
||||
import {useEffect} from "react";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {eventUpdate} from "@/types/events";
|
||||
import {backupColumns} from "@/features/dashboard/backup/columns";
|
||||
import {restoreColumns} from "@/features/dashboard/restore/columns";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { eventUpdate } from "@/types/events";
|
||||
import { backupColumns } from "@/features/dashboard/backup/columns";
|
||||
import { restoreColumns } from "@/features/dashboard/restore/columns";
|
||||
import { DataTable } from "@/components/wrappers/common/table/data-table";
|
||||
import { Backup, Database, Restoration } from "@/db/schema";
|
||||
|
||||
export type DatabaseTabsProps = {
|
||||
backups: Backup[]
|
||||
restorations: Restoration[]
|
||||
isAlreadyRestore: boolean
|
||||
database: Database
|
||||
}
|
||||
backups: Backup[];
|
||||
restorations: Restoration[];
|
||||
isAlreadyRestore: boolean;
|
||||
database: Database;
|
||||
};
|
||||
|
||||
export const DatabaseTabs = (props: DatabaseTabsProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
const eventSource = new EventSource('/api/events');
|
||||
const eventSource = new EventSource("/api/events");
|
||||
|
||||
eventSource.addEventListener('modification', (event) => {
|
||||
const data: eventUpdate = JSON.parse(event.data)
|
||||
eventSource.addEventListener("modification", (event) => {
|
||||
const data: eventUpdate = JSON.parse(event.data);
|
||||
if (data.update) {
|
||||
console.log("update", data.update)
|
||||
router.refresh()
|
||||
router.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -37,20 +35,21 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
|
||||
|
||||
return (
|
||||
<Tabs className="flex flex-col flex-1" defaultValue="backup">
|
||||
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="backup">Backup</TabsTrigger>
|
||||
<TabsTrigger value="restore">Restoration</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent className="h-full justify-between" value="backup">
|
||||
<DataTableWithPagination columns={backupColumns} data={props.backups}
|
||||
extendedProps={props.isAlreadyRestore}/>
|
||||
{/*
|
||||
<DataTable columns={backupColumns} data={props.backups} extendedProps={props.isAlreadyRestore} />
|
||||
*/}
|
||||
<DataTable columns={backupColumns} data={props.backups} enablePagination />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent className="h-full justify-between" value="restore">
|
||||
<DataTableWithPagination columns={restoreColumns} data={props.restorations}/>
|
||||
<DataTable columns={restoreColumns} data={props.restorations} enablePagination />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { ProjectWith } from "@/db/schema";
|
||||
import Link from "next/link";
|
||||
|
||||
export type projectCardProps = {
|
||||
data: any,
|
||||
organizationSlug?: string
|
||||
}
|
||||
data: ProjectWith;
|
||||
organizationSlug?: string;
|
||||
};
|
||||
|
||||
export const ProjectCard = (props: projectCardProps) => {
|
||||
|
||||
const {data: project, organizationSlug} = props;
|
||||
|
||||
const { data: project, organizationSlug } = props;
|
||||
|
||||
return (
|
||||
<Link href={`/dashboard/${organizationSlug}/projects/${project.id}`}>
|
||||
<Card className="flex flex-row justify-between">
|
||||
<div className="">
|
||||
<CardHeader>{project.name}</CardHeader>
|
||||
<CardContent>
|
||||
{project.databases.length} databases
|
||||
</CardContent>
|
||||
<CardContent>{project.databases.length} databases</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,57 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image"
|
||||
import {Database} from "@prisma/client";
|
||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
|
||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||
import Image from "next/image";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { ConnectionCircle } from "@/components/wrappers/common/connection-circle";
|
||||
import { formatDateLastContact } from "@/utils/date-formatting";
|
||||
import { Database } from "@/db/schema";
|
||||
|
||||
export type projectDatabaseCardProps = {
|
||||
data: Database,
|
||||
extendedProps: any
|
||||
organizationSlug: string
|
||||
}
|
||||
data: Database;
|
||||
extendedProps: any;
|
||||
organizationSlug: string;
|
||||
};
|
||||
|
||||
export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
|
||||
|
||||
const {organizationSlug,data: database, extendedProps: extendedProps} = props;
|
||||
const { organizationSlug, data: database, extendedProps: extendedProps } = props;
|
||||
|
||||
return (
|
||||
<Link href={`/dashboard/${organizationSlug}/projects/${extendedProps.id}/database/${database.id}`}>
|
||||
<DatabaseCard data={database}/>
|
||||
<DatabaseCard data={database} />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export type databaseCardProps = {
|
||||
data: Database,
|
||||
}
|
||||
data: Database;
|
||||
};
|
||||
|
||||
export const DatabaseCard = (props: databaseCardProps) => {
|
||||
|
||||
const {data: database} = props;
|
||||
const { data: database } = props;
|
||||
|
||||
return (
|
||||
<Card className="flex flex-row justify-between">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Image
|
||||
src="/PostgreSQL.png"
|
||||
alt="Database type Icon"
|
||||
width={60}
|
||||
height={60}
|
||||
className="object-cover ml-4"
|
||||
/>
|
||||
<Image src="/PostgreSQL.png" alt="Database type Icon" width={60} height={60} className="object-cover ml-4" />
|
||||
<div>
|
||||
<CardHeader>Name : {database.name}</CardHeader>
|
||||
<CardContent>
|
||||
Last contact: {formatDateLastContact(database.lastContact)}
|
||||
</CardContent>
|
||||
<CardContent>Last contact: {formatDateLastContact(database.lastContact)}</CardContent>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 mr-3">
|
||||
<ConnectionCircle date={database.lastContact}/>
|
||||
<ConnectionCircle date={database.lastContact} />
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {z} from "zod";
|
||||
import Database from "@prisma/client"
|
||||
import { z } from "zod";
|
||||
|
||||
export const ProjectSchema = z.object({
|
||||
name: z.string(),
|
||||
|
||||
@@ -1,115 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
useZodForm
|
||||
} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Form} from "@/components/ui/form"
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {ProjectSchema, ProjectType} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.schema";
|
||||
import {
|
||||
createProjectAction,
|
||||
updateProjectAction
|
||||
} from "@/components/wrappers/dashboard/projects/ProjectsForm/project-form.action";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {Database, Organization, Projects} from "@prisma/client"
|
||||
import {MultiSelect} from "@/components/wrappers/common/multiSelect/MultiSelect";
|
||||
import {toast} from "sonner";
|
||||
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { ProjectSchema, ProjectType } from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.schema";
|
||||
import { createProjectAction, updateProjectAction } from "@/components/wrappers/dashboard/projects/ProjectsForm/project-form.action";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { MultiSelect } from "@/components/wrappers/common/multiselect/multi-select";
|
||||
import { toast } from "sonner";
|
||||
import { DatabaseWith, Organization } from "@/db/schema";
|
||||
|
||||
export type projectFormProps = {
|
||||
defaultValues?: ProjectType;
|
||||
databases: Database[],
|
||||
organization: Organization,
|
||||
databases: DatabaseWith[];
|
||||
organization: Organization;
|
||||
projectId?: string;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
export const ProjectForm = (props: projectFormProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
const isCreate = !Boolean(props.defaultValues)
|
||||
const isCreate = !Boolean(props.defaultValues);
|
||||
|
||||
|
||||
const formatDatabasesList = (databases: Database[]) => {
|
||||
return databases.map(database => ({
|
||||
const formatDatabasesList = (databases: DatabaseWith[]) => {
|
||||
return databases.map((database) => ({
|
||||
value: database.id,
|
||||
label: `${database.name} (${database.generatedId}) | ${database.agent.name}`,
|
||||
label: `${database.name} (${database.id}) | ${database.agent.name}`,
|
||||
}));
|
||||
};
|
||||
|
||||
const formatDefaultDatabases = (databases: ProjectType['databases']): string[] => {
|
||||
return databases.map(database => database.id);
|
||||
const formatDefaultDatabases = (databases: string[]): string[] => {
|
||||
return databases;
|
||||
};
|
||||
|
||||
const formattedDefaultValues = {
|
||||
...props.defaultValues,
|
||||
databases: !isCreate ? formatDefaultDatabases(props.defaultValues?.databases) : []
|
||||
}
|
||||
|
||||
databases: !isCreate ? formatDefaultDatabases(props.defaultValues?.databases ?? []) : [],
|
||||
};
|
||||
|
||||
const form = useZodForm({
|
||||
schema: ProjectSchema,
|
||||
defaultValues: formattedDefaultValues,
|
||||
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: ProjectType) => {
|
||||
console.log(values)
|
||||
const project: Projects = isCreate ? await createProjectAction({
|
||||
data: values,
|
||||
organizationId: props.organization.id
|
||||
}) : await updateProjectAction({
|
||||
data: values,
|
||||
organizationId: props.organization.id,
|
||||
projectId: props.projectId
|
||||
});
|
||||
|
||||
if (project.data?.success) {
|
||||
toast.success(project.data.actionSuccess.message);
|
||||
router.push(`/dashboard/${props.organization.slug}/projects/${project.data.value.id}`);
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.error(project.data.actionError.message || "Unknown error occurred.");
|
||||
console.log(values);
|
||||
if (!isCreate && !props.projectId) {
|
||||
throw new Error("Project ID is required for updates");
|
||||
}
|
||||
const project = isCreate
|
||||
? await createProjectAction({
|
||||
data: values,
|
||||
organizationId: props.organization.id,
|
||||
})
|
||||
: await updateProjectAction({
|
||||
data: values,
|
||||
organizationId: props.organization.id,
|
||||
projectId: props.projectId!,
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
if (project && project.data) {
|
||||
if (project.data.success) {
|
||||
project.data.actionSuccess && toast.success(project.data.actionSuccess.message);
|
||||
router.push(`/dashboard/${props.organization.slug}/projects/${project.data.value!.id}`);
|
||||
router.refresh();
|
||||
} else {
|
||||
project.data.actionError && toast.error(project.data.actionError.message || "Unknown error occurred.");
|
||||
router.refresh();
|
||||
}
|
||||
} else {
|
||||
toast.error("Failed to process request. No response received.");
|
||||
router.refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
</CardHeader>
|
||||
<CardHeader></CardHeader>
|
||||
<CardContent>
|
||||
<Form form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Project 1" {...field} />
|
||||
<Input placeholder="Project 1" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -117,29 +107,30 @@ export const ProjectForm = (props: projectFormProps) => {
|
||||
control={form.control}
|
||||
name="slug"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Slug</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="project-1" {...field}
|
||||
placeholder="project-1"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replaceAll(" ", "-").toLowerCase()
|
||||
field.onChange(value)
|
||||
}}/>
|
||||
const value = e.target.value.replaceAll(" ", "-").toLowerCase();
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="databases"
|
||||
render={({field}) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Databases</FormLabel>
|
||||
<FormControl>
|
||||
|
||||
<MultiSelect
|
||||
options={formatDatabasesList(props.databases)}
|
||||
onValueChange={field.onChange}
|
||||
@@ -151,16 +142,13 @@ export const ProjectForm = (props: projectFormProps) => {
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Select databases you want to add to this project</FormDescription>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<Button>
|
||||
{isCreate ? `Create Project` : `Update Project`}
|
||||
</Button>
|
||||
<Button>{isCreate ? `Create Project` : `Update Project`}</Button>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use server"
|
||||
|
||||
import {userAction} from "@/safe-actions";
|
||||
import {prisma} from "@/prisma";
|
||||
import {ProjectSchema} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.schema";
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {Projects} from "@prisma/client";
|
||||
"use server";
|
||||
|
||||
import { userAction } from "@/safe-actions";
|
||||
import { ProjectSchema } from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.schema";
|
||||
import { z } from "zod";
|
||||
import { ServerActionResult } from "@/types/action-type";
|
||||
import { Database, database as drizzleDatabase, project as drizzleProject, Project } from "@/db/schema";
|
||||
import { db } from "@/db";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
|
||||
export const createProjectAction = userAction
|
||||
.schema(
|
||||
@@ -15,53 +15,42 @@ export const createProjectAction = userAction
|
||||
organizationId: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Projects>> => {
|
||||
.action(async ({ parsedInput }): Promise<ServerActionResult<Project>> => {
|
||||
try {
|
||||
|
||||
const project = await prisma.project.create({
|
||||
data: {
|
||||
const [createdProject] = await db
|
||||
.insert(drizzleProject)
|
||||
.values({
|
||||
name: parsedInput.data.name,
|
||||
slug: parsedInput.data.slug,
|
||||
organizationId: parsedInput.organizationId,
|
||||
}
|
||||
})
|
||||
|
||||
for (const db of parsedInput.data.databases) {
|
||||
|
||||
await prisma.database.update({
|
||||
where: {
|
||||
id: db,
|
||||
},
|
||||
data:{
|
||||
projectId: project.id,
|
||||
}
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (parsedInput.data.databases.length > 0) {
|
||||
await db.update(drizzleDatabase).set({ projectId: createdProject.id }).where(inArray(drizzleDatabase.id, parsedInput.data.databases));
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: project,
|
||||
value: createdProject,
|
||||
actionSuccess: {
|
||||
message: "Projects has been successfully created.",
|
||||
messageParams: {projectName: parsedInput.data.name},
|
||||
message: "Project has been successfully created.",
|
||||
messageParams: { projectName: parsedInput.data.name },
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to create Projects.",
|
||||
status: 500, // Optional: Use a meaningful status code
|
||||
message: "Failed to create project.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {projectName: parsedInput.data.name},
|
||||
messageParams: { projectName: parsedInput.data.name },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
export const updateProjectAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
@@ -70,68 +59,48 @@ export const updateProjectAction = userAction
|
||||
projectId: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Projects>> => {
|
||||
.action(async ({ parsedInput }): Promise<ServerActionResult<Project>> => {
|
||||
try {
|
||||
const newDatabaseList = parsedInput.data.databases
|
||||
const project = await prisma.project.findFirst({
|
||||
where:{
|
||||
id: parsedInput.projectId,
|
||||
const existing = await db.query.project.findFirst({
|
||||
where: eq(drizzleProject.id, parsedInput.projectId),
|
||||
with: {
|
||||
databases: true,
|
||||
},
|
||||
include: {
|
||||
databases : {}
|
||||
}
|
||||
})
|
||||
const existingItemIds = project.databases.map((db) => db.id);
|
||||
});
|
||||
|
||||
const databasesToAdd = newDatabaseList.filter(
|
||||
(id) => !existingItemIds.includes(id)
|
||||
);
|
||||
const databasesToRemove = existingItemIds.filter(
|
||||
(id) => !newDatabaseList.includes(id)
|
||||
);
|
||||
if (!existing) {
|
||||
throw new Error("Project not found.");
|
||||
}
|
||||
|
||||
console.log(databasesToAdd);
|
||||
console.log(databasesToRemove);
|
||||
const existingDbIds = existing.databases.map((db: Database) => db.id);
|
||||
const newDbIds = parsedInput.data.databases;
|
||||
|
||||
const databasesToAdd = newDbIds.filter((id) => !existingDbIds.includes(id));
|
||||
const databasesToRemove = existingDbIds.filter((id: string) => !newDbIds.includes(id));
|
||||
|
||||
if (databasesToAdd.length > 0) {
|
||||
await prisma.database.updateMany({
|
||||
where: {
|
||||
id: { in: databasesToAdd },
|
||||
},
|
||||
data: {
|
||||
projectId: parsedInput.projectId,
|
||||
},
|
||||
});
|
||||
await db.update(drizzleDatabase).set({ projectId: parsedInput.projectId }).where(inArray(drizzleDatabase.id, databasesToAdd));
|
||||
}
|
||||
|
||||
if (databasesToRemove.length > 0) {
|
||||
await prisma.database.updateMany({
|
||||
where: {
|
||||
id: { in: databasesToRemove },
|
||||
},
|
||||
data: {
|
||||
projectId: null,
|
||||
},
|
||||
});
|
||||
await db.update(drizzleDatabase).set({ projectId: null }).where(inArray(drizzleDatabase.id, databasesToRemove));
|
||||
}
|
||||
|
||||
const updatedProject = await prisma.project.update({
|
||||
where:{
|
||||
id: parsedInput.projectId
|
||||
},
|
||||
data:{
|
||||
const [updatedProject] = await db
|
||||
.update(drizzleProject)
|
||||
.set({
|
||||
name: parsedInput.data.name,
|
||||
slug: parsedInput.data.slug,
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
.where(eq(drizzleProject.id, parsedInput.projectId))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: updatedProject,
|
||||
actionSuccess: {
|
||||
message: "Project has been successfully updated.",
|
||||
messageParams: {projectName: parsedInput.data.name},
|
||||
messageParams: { projectName: parsedInput.data.name },
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -139,12 +108,10 @@ export const updateProjectAction = userAction
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to update project.",
|
||||
status: 500, // Optional: Use a meaningful status code
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {projectName: parsedInput.data.name},
|
||||
messageParams: { projectName: parsedInput.data.name },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
+26
-33
@@ -1,34 +1,28 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {ColumnDef} from "@tanstack/react-table"
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {User, UserOrganization} from "@prisma/client";
|
||||
import {updateUserAction} from "@/components/wrappers/dashboard/profile/UserForm/user-form.action";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useState} from "react";
|
||||
import {Trash2} from "lucide-react";
|
||||
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/delete-account.action";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {
|
||||
updateUserOrganizationAction
|
||||
} from "@/components/wrappers/dashboard/settings/SettingsUsersTab/settings-user-tab.action";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
export const usersColumns: ColumnDef<UserOrganization>[] = [
|
||||
import { updateUserOrganizationAction } from "@/components/wrappers/dashboard/settings/SettingsUsersTab/settings-user-tab.action";
|
||||
import { OrganizationMember } from "@/db/schema/02_organization";
|
||||
|
||||
export const usersColumns: ColumnDef<OrganizationMember>[] = [
|
||||
{
|
||||
accessorKey: "role",
|
||||
header: "Role",
|
||||
cell: ({row}) => {
|
||||
cell: ({ row }) => {
|
||||
const router = useRouter();
|
||||
const [role, setRole] = useState<string>(row.getValue("role"))
|
||||
const [role, setRole] = useState<string>(row.getValue("role"));
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: () => updateUserOrganizationAction({id: row.original.id, role: role}),
|
||||
mutationFn: () => updateUserOrganizationAction({ id: row.original.id, role: role }),
|
||||
onSuccess: () => {
|
||||
toast.success(`User updated successfully.`);
|
||||
// router.refresh()
|
||||
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(`An error occurred while updating user information.`);
|
||||
@@ -36,32 +30,31 @@ export const usersColumns: ColumnDef<UserOrganization>[] = [
|
||||
});
|
||||
|
||||
const handleUpdateRole = async () => {
|
||||
const nextRole = role === "admin" ? "member"
|
||||
: role === "member" ? "admin"
|
||||
: "admin";
|
||||
const nextRole = role === "admin" ? "member" : role === "member" ? "admin" : "admin";
|
||||
setRole(nextRole);
|
||||
await updateMutation.mutateAsync()
|
||||
await updateMutation.mutateAsync();
|
||||
};
|
||||
|
||||
return <Badge
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleUpdateRole()}
|
||||
variant="outline">{role}</Badge>
|
||||
return (
|
||||
<Badge className="cursor-pointer" onClick={() => handleUpdateRole()} variant="outline">
|
||||
{role}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "user.name",
|
||||
header: "Name"
|
||||
header: "Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "user.email",
|
||||
header: "Email"
|
||||
header: "Email",
|
||||
},
|
||||
{
|
||||
accessorKey: "updatedAt",
|
||||
header: "Updated At",
|
||||
cell: ({row}) => {
|
||||
cell: ({ row }) => {
|
||||
return new Date(row.getValue("updatedAt")).toLocaleString("fr-FR");
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
+9
-8
@@ -1,18 +1,19 @@
|
||||
"use client"
|
||||
import {useSidebar} from "@/components/ui/sidebar";
|
||||
"use client";
|
||||
import { useSidebar } from "@/components/ui/sidebar";
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
export type SideBarFooterCreditProps = {}
|
||||
export type SideBarFooterCreditProps = {};
|
||||
|
||||
export const SideBarFooterCredit = (props: SideBarFooterCreditProps) => {
|
||||
const {state, isMobile} = useSidebar();
|
||||
const { state } = useSidebar();
|
||||
|
||||
return (
|
||||
<>
|
||||
{state === 'expanded' && (
|
||||
{state === "expanded" && (
|
||||
<div className="text-center">
|
||||
<h1 className="text-[10px]">Portabase Community Edition</h1>
|
||||
<h1 className="text-[10px]">Portabase Community Edition v{env.NEXT_PUBLIC_PROJECT_VERSION}</h1>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,53 +1,73 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
import { JSX, useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import {SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem} from "@/components/ui/sidebar";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {buttonVariants} from "@/components/ui/button";
|
||||
import {ChartArea, Layers, Settings, ShieldHalf} from "lucide-react";
|
||||
import {usePathname} from "next/navigation";
|
||||
import {UserOrganization} from "@prisma/client";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu as SM,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { ChevronDown, MoreHorizontal } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
|
||||
export type SidebarMenuCustomProps = {
|
||||
currentOrganizationSlug: string,
|
||||
currentOrganizationUser: UserOrganization,
|
||||
}
|
||||
export type SidebarMenuProps = {
|
||||
baseUrl: string;
|
||||
items: SidebarItem[];
|
||||
};
|
||||
|
||||
export const SidebarMenuCustom = (props: SidebarMenuCustomProps) => {
|
||||
type SidebarItemContent = {
|
||||
title: string;
|
||||
url: string;
|
||||
icon: JSX.Element;
|
||||
dropdown?: {
|
||||
title: string;
|
||||
url: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
const BASE_URL = `/dashboard/${props.currentOrganizationSlug}`;
|
||||
type SidebarListItem = {
|
||||
type: "list";
|
||||
content: SidebarItemContent;
|
||||
};
|
||||
|
||||
type SidebarCollapseItem = {
|
||||
type: "collapse";
|
||||
title: string;
|
||||
content: SidebarItemContent[];
|
||||
};
|
||||
|
||||
export type SidebarItem = SidebarListItem | SidebarCollapseItem;
|
||||
|
||||
export const SidebarMenu = (props: SidebarMenuProps) => {
|
||||
const pathname = usePathname();
|
||||
// Menu items.
|
||||
const items = [
|
||||
{
|
||||
title: "Projects",
|
||||
url: "projects",
|
||||
icon: Layers,
|
||||
},
|
||||
{
|
||||
title: "Statistics",
|
||||
url: "statistics",
|
||||
icon: ChartArea,
|
||||
}
|
||||
]
|
||||
|
||||
if (props.currentOrganizationUser.role === "admin") {
|
||||
items.push({
|
||||
title: "Settings",
|
||||
url: "settings",
|
||||
icon: Settings,
|
||||
},)
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const currentUrl = pathname;
|
||||
const currentItem = items.find((item) => `${BASE_URL}/${item.url}` === currentUrl);
|
||||
|
||||
const currentItem = props.items.find((item) => {
|
||||
if (item.type === "list") {
|
||||
return `${props.baseUrl}/${item.content.url}` === currentUrl;
|
||||
} else if (item.type === "collapse") {
|
||||
return item.content.some((content) => `${props.baseUrl}/${content.url}` === currentUrl);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (currentItem) {
|
||||
setActiveItem(currentItem.title);
|
||||
}
|
||||
else{
|
||||
if (currentItem.type === "list") {
|
||||
setActiveItem(currentItem.content.title);
|
||||
} else if (currentItem.type === "collapse") {
|
||||
setActiveItem(currentItem.title!);
|
||||
}
|
||||
} else {
|
||||
setActiveItem("");
|
||||
}
|
||||
}, [pathname]);
|
||||
@@ -55,66 +75,88 @@ export const SidebarMenuCustom = (props: SidebarMenuCustomProps) => {
|
||||
const [activeItem, setActiveItem] = useState("");
|
||||
const handleItemClick = (title: string) => {
|
||||
setActiveItem(title);
|
||||
console.log(title)
|
||||
}
|
||||
return (
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link
|
||||
className={cn(buttonVariants({
|
||||
size: "lg",
|
||||
variant: activeItem === item.title ? "secondary" : 'ghost'
|
||||
}), "justify-start p-0")}
|
||||
href={`${BASE_URL}/${item.url}`}
|
||||
onClick={() => handleItemClick(item.title)}
|
||||
>
|
||||
<item.icon/>
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuAction
|
||||
className={`peer-data-[active=true]/menu-button:opacity-100 ${activeItem === item.title ? 'active' : ''}`}/>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// export type SidebarMenuItemCustomProps = {
|
||||
// title: string
|
||||
// url: string
|
||||
// icon: Element
|
||||
// }
|
||||
//
|
||||
//
|
||||
// export const SidebarMenuItemCustom = (props: SidebarMenuItemCustomProps) => {
|
||||
//
|
||||
// const {title, url, icon} = props;
|
||||
//
|
||||
// const BASE_URL = "/dashboard";
|
||||
// const pathname = usePathname();
|
||||
//
|
||||
// const [activeItem, setActiveItem] = useState(title);
|
||||
//
|
||||
// return (
|
||||
// <SidebarMenuItem key={title}>
|
||||
// <SidebarMenuButton asChild>
|
||||
// <Link
|
||||
// className={cn(buttonVariants({
|
||||
// size: "lg",
|
||||
// variant: activeItem === title ? "secondary" : 'ghost'
|
||||
// }), "justify-start p-0")}
|
||||
// href={`${BASE_URL}/${url}`}
|
||||
// onClick={() => handleItemClick(item.title)}
|
||||
// >
|
||||
// <icon/>
|
||||
// <span>{title}</span>
|
||||
// </Link>
|
||||
// </SidebarMenuButton>
|
||||
// <SidebarMenuAction
|
||||
// className={`peer-data-[active=true]/menu-button:opacity-100 ${activeItem === title ? 'active' : ''}`}/>
|
||||
// </SidebarMenuItem>
|
||||
// )
|
||||
// }
|
||||
return (
|
||||
<SM>
|
||||
{props.items.map((item, index) =>
|
||||
item.type === "collapse" ? (
|
||||
<Collapsible defaultOpen key={index} className={`group/collapsible`}>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel asChild>
|
||||
<CollapsibleTrigger>
|
||||
{item.title ?? "Please define a title"}
|
||||
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
</SidebarGroupLabel>
|
||||
<CollapsibleContent>
|
||||
<SidebarGroupContent>
|
||||
{item.content.map((content) => (
|
||||
<SidebarMenuItem key={content.title}>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: "lg",
|
||||
variant: activeItem === content.title ? "secondary" : "ghost",
|
||||
}),
|
||||
"justify-start p-0"
|
||||
)}
|
||||
href={`${props.baseUrl}/${content.url}`}
|
||||
onClick={() => handleItemClick(content.title)}
|
||||
>
|
||||
{content.icon}
|
||||
<span>{content.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
{content.dropdown && (
|
||||
<SidebarMenuAction>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction>
|
||||
<MoreHorizontal />
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="right" align="start">
|
||||
{content.dropdown.map((dropdown) => (
|
||||
<Link href={`${props.baseUrl}/${dropdown.url}`} className="justify-start p-0">
|
||||
<DropdownMenuItem>
|
||||
<span>{dropdown.title ?? "Please define a title"}</span>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuAction>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarGroupContent>
|
||||
</CollapsibleContent>
|
||||
</SidebarGroup>
|
||||
</Collapsible>
|
||||
) : (
|
||||
<SidebarMenuItem key={index}>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: "lg",
|
||||
variant: activeItem === item.content.title ? "secondary" : "ghost",
|
||||
}),
|
||||
"justify-start p-0"
|
||||
)}
|
||||
href={`${props.baseUrl}/${item.content.url}`}
|
||||
onClick={() => handleItemClick(item.content.title)}
|
||||
>
|
||||
{item.content.icon}
|
||||
<span>{item.content.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
)}
|
||||
</SM>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
import Link from "next/link";
|
||||
import {SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem} from "@/components/ui/sidebar";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {buttonVariants} from "@/components/ui/button";
|
||||
import {ChartArea, Layers, Settings, ShieldHalf} from "lucide-react";
|
||||
import {usePathname} from "next/navigation";
|
||||
|
||||
export type SidebarMenuAdminProps = {}
|
||||
|
||||
export const SidebarMenuAdmin = (props: SidebarMenuAdminProps) => {
|
||||
|
||||
const BASE_URL = "/dashboard";
|
||||
const pathname = usePathname();
|
||||
// Menu items.
|
||||
const items = [
|
||||
{
|
||||
title: "Agents",
|
||||
url: "agents",
|
||||
icon: ShieldHalf,
|
||||
},
|
||||
{
|
||||
title: "Administration panel",
|
||||
url: "admin",
|
||||
icon: Settings,
|
||||
},
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
const currentUrl = pathname;
|
||||
const currentItem = items.find((item) => `${BASE_URL}/${item.url}` === currentUrl);
|
||||
if (currentItem) {
|
||||
setActiveItem(currentItem.title);
|
||||
}else{
|
||||
setActiveItem("");
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
const [activeItem, setActiveItem] = useState("");
|
||||
const handleItemClick = (title: string) => {
|
||||
setActiveItem(title);
|
||||
console.log(title)
|
||||
}
|
||||
return (
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link
|
||||
className={cn(buttonVariants({
|
||||
size: "lg",
|
||||
variant: activeItem === item.title ? "secondary" : 'ghost'
|
||||
}), "justify-start p-0")}
|
||||
href={`${BASE_URL}/${item.url}`}
|
||||
onClick={() => handleItemClick(item.title)}
|
||||
>
|
||||
<item.icon/>
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuAction
|
||||
className={`peer-data-[active=true]/menu-button:opacity-100 ${activeItem === item.title ? 'active' : ''}`}/>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
@@ -1,91 +1,58 @@
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenu as SM,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
import {SidebarMenuCustom} from "@/components/wrappers/dashboard/sideBar/SideBarMenu/SideBarMenu";
|
||||
import {OrganizationCombobox} from "@/components/wrappers/dashboard/organization/organization-combobox";
|
||||
import {prisma} from "@/prisma";
|
||||
import {currentUser, requiredCurrentUser} from "@/auth/current-user";
|
||||
import {SideBarLogo} from "@/components/wrappers/dashboard/sideBar/SideBarLogo/SideBarLogo";
|
||||
import {SideBarFooterCredit} from "@/components/wrappers/dashboard/sideBar/SideBarFooterCredit/SideBarFooterCredit";
|
||||
import {LoggedInButton} from "@/components/wrappers/dashboard/loggedInButton/LoggedInButton";
|
||||
import {SidebarMenuAdmin} from "@/components/wrappers/dashboard/sideBar/SideBarMenu/SideBarMenuAdmin";
|
||||
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
||||
} from "@/components/ui/sidebar";
|
||||
import { SidebarItem, SidebarMenu } from "@/components/wrappers/dashboard/sideBar/SideBarMenu/SideBarMenu";
|
||||
import { OrganizationCombobox } from "@/components/wrappers/dashboard/organization/organization-combobox";
|
||||
import { SideBarLogo } from "@/components/wrappers/dashboard/sideBar/SideBarLogo/SideBarLogo";
|
||||
import { SideBarFooterCredit } from "@/components/wrappers/dashboard/sideBar/SideBarFooterCredit/SideBarFooterCredit";
|
||||
import { LoggedInButton } from "@/components/wrappers/dashboard/loggedInButton/LoggedInButton";
|
||||
import { Layers, ChartArea, Settings, ShieldHalf } from "lucide-react";
|
||||
import { authClient } from "@/lib/auth/auth-client";
|
||||
import { SidebarContentA } from "./sidebar-content";
|
||||
|
||||
export async function AppSidebar() {
|
||||
/*const member = await getActiveMember();
|
||||
|
||||
const user = await requiredCurrentUser()
|
||||
const currentOrganizationSlug = await getCurrentOrganizationSlug()
|
||||
const organizations = await prisma.organization.findMany({
|
||||
where: {
|
||||
users: {
|
||||
some: {
|
||||
userId: user.id
|
||||
},
|
||||
},
|
||||
deleted: {not: true},
|
||||
},
|
||||
})
|
||||
const defaultOrganization = await prisma.organization.findUnique({
|
||||
where: {
|
||||
slug: "default"
|
||||
}
|
||||
})
|
||||
console.log("member", member);
|
||||
|
||||
if (!member) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const currentOrganizationUser = await prisma.userOrganization.findFirst({
|
||||
where:{
|
||||
userId: user.id,
|
||||
organization:{
|
||||
slug: currentOrganizationSlug != "" ? currentOrganizationSlug : "default",
|
||||
}
|
||||
}
|
||||
})
|
||||
const organization = await getOrganization(member.organizationId);
|
||||
|
||||
//todo: à revoir
|
||||
|
||||
console.log("memebrer", member);
|
||||
console.log("aoaoaoaoaoa", organization);*/
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader>
|
||||
<SideBarLogo/>
|
||||
<SidebarMenu>
|
||||
<SideBarLogo />
|
||||
<SM>
|
||||
<SidebarMenuItem>
|
||||
<OrganizationCombobox
|
||||
organizations={organizations}
|
||||
defaultOrganization={defaultOrganization}
|
||||
/>
|
||||
<OrganizationCombobox />
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
|
||||
|
||||
</SM>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Application</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenuCustom currentOrganizationUser={currentOrganizationUser} currentOrganizationSlug={currentOrganizationSlug}/>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
{user.role == "admin" ?
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Administration</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenuAdmin/>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
:null}
|
||||
</SidebarContent>
|
||||
<SidebarContentA />
|
||||
<SidebarFooter>
|
||||
<SidebarMenu>
|
||||
<SM>
|
||||
<SidebarMenuItem>
|
||||
<LoggedInButton/>
|
||||
<LoggedInButton />
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
<SideBarFooterCredit/>
|
||||
</SM>
|
||||
<SideBarFooterCredit />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
import { SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarContent } from "@/components/ui/sidebar";
|
||||
import { authClient, useSession } from "@/lib/auth/auth-client";
|
||||
import { Layers, ChartArea, Settings, ShieldHalf } from "lucide-react";
|
||||
import { SidebarItem, SidebarMenu } from "./SideBarMenu/SideBarMenu";
|
||||
|
||||
export const SidebarContentA = () => {
|
||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||
const { data: organizations } = authClient.useListOrganizations();
|
||||
|
||||
const { data: session } = useSession();
|
||||
|
||||
console.log("sesssssion", session);
|
||||
|
||||
const appItems: SidebarItem[] = [
|
||||
{
|
||||
type: "list",
|
||||
content: {
|
||||
title: "Projects",
|
||||
url: "projects",
|
||||
icon: <Layers />,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
content: {
|
||||
title: "Statistics",
|
||||
url: "statistics",
|
||||
icon: <ChartArea />,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (activeOrganization) {
|
||||
const member = authClient.useActiveMember();
|
||||
|
||||
if (member && member.data && (member.data.role === "admin" || member.data.role === "owner")) {
|
||||
appItems.push({
|
||||
type: "list",
|
||||
content: {
|
||||
title: "Settings",
|
||||
url: "settings",
|
||||
icon: <Settings />,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const adminItems: SidebarItem[] = [
|
||||
{
|
||||
type: "list",
|
||||
content: {
|
||||
title: "Agents",
|
||||
url: "agents",
|
||||
icon: <ShieldHalf />,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
content: {
|
||||
title: "Administration panel",
|
||||
url: "admin",
|
||||
icon: <Settings />,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
activeOrganization && (
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Application</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu items={appItems} baseUrl={`/dashboard/${activeOrganization.slug}`} />
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
{/*(user.role === "superadmin" || user.role === "admin") && (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Administration</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu items={adminItems} baseUrl={`/dashboard/${organization.slug}`} />
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)*/}
|
||||
</SidebarContent>
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -1,54 +1,37 @@
|
||||
"use client"
|
||||
|
||||
import {ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent} from "@/components/ui/chart";
|
||||
import {CartesianGrid, Line, LineChart, XAxis, YAxis} from "recharts";
|
||||
import {humanReadableDate} from "@/utils/date-formatting";
|
||||
|
||||
const data = [
|
||||
{date: "2024-12-01", count: 1},
|
||||
{date: "2024-12-02", count: 2},
|
||||
{date: "2024-12-03", count: 4},
|
||||
{date: "2024-12-04", count: 8},
|
||||
{date: "2024-12-05", count: 9},
|
||||
{date: "2024-12-06", count: 9},
|
||||
{date: "2024-12-07", count: 10},
|
||||
{date: "2024-12-08", count: 13},
|
||||
{date: "2024-12-09", count: 15},
|
||||
{date: "2024-12-10", count: 18},
|
||||
|
||||
]
|
||||
"use client";
|
||||
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
||||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
||||
import { humanReadableDate } from "@/utils/date-formatting";
|
||||
|
||||
type Data = {
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export type evolutionLineChartProps = {
|
||||
data: Data[]
|
||||
}
|
||||
data: Data[];
|
||||
};
|
||||
|
||||
export function EvolutionLineChart(props: evolutionLineChartProps) {
|
||||
|
||||
const {data} = props
|
||||
console.log("aaa data", data)
|
||||
|
||||
const { data } = props;
|
||||
|
||||
// Process data to calculate cumulative count
|
||||
const cumulativeData = data.reduce((acc, backup) => {
|
||||
const date = backup.createdAt.toISOString().split("T")[0]; // Format as YYYY-MM-DD
|
||||
const cumulativeData = data.reduce(
|
||||
(acc, backup) => {
|
||||
const date = backup.createdAt.toISOString().split("T")[0]; // Format as YYYY-MM-DD
|
||||
|
||||
// Increment count for the current date or initialize it
|
||||
if (acc.length && acc[acc.length - 1].date === date) {
|
||||
acc[acc.length - 1].count += 1;
|
||||
} else {
|
||||
const lastCount = acc.length ? acc[acc.length - 1].count : 0;
|
||||
acc.push({date, count: lastCount + 1});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as { date: string; count: number }[]);
|
||||
// Increment count for the current date or initialize it
|
||||
if (acc.length && acc[acc.length - 1].date === date) {
|
||||
acc[acc.length - 1].count += 1;
|
||||
} else {
|
||||
const lastCount = acc.length ? acc[acc.length - 1].count : 0;
|
||||
acc.push({ date, count: lastCount + 1 });
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[] as { date: string; count: number }[]
|
||||
);
|
||||
|
||||
const chartConfig = {
|
||||
date: {
|
||||
@@ -59,8 +42,7 @@ export function EvolutionLineChart(props: evolutionLineChartProps) {
|
||||
label: "Number of backups",
|
||||
color: "#60a5fa",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
} satisfies ChartConfig;
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig}>
|
||||
@@ -72,27 +54,18 @@ export function EvolutionLineChart(props: evolutionLineChartProps) {
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false}/>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => humanReadableDate(Date(value)).split(' ')[0]}
|
||||
/>
|
||||
<YAxis/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel/>}
|
||||
/>
|
||||
<Line
|
||||
dataKey="count"
|
||||
type="linear"
|
||||
stroke="var(--color-desktop)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
tickFormatter={(value) => humanReadableDate(new Date(value)).split(" ")[0]}
|
||||
/>
|
||||
<YAxis />
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Line dataKey="count" type="linear" stroke="var(--color-desktop)" strokeWidth={2} dot={false} />
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent} from "@/components/ui/chart";
|
||||
import {CartesianGrid, Line, LineChart, XAxis, YAxis} from "recharts";
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
||||
import { EStatusSchema } from "@/db/schema/types";
|
||||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
||||
|
||||
type Data = {
|
||||
createdAt: Date;
|
||||
status: "success" | "failed";
|
||||
_count: { id: number };
|
||||
}
|
||||
status: EStatusSchema;
|
||||
_count: number;
|
||||
};
|
||||
|
||||
export type percentageLineChartProps = {
|
||||
data: Data[]
|
||||
}
|
||||
|
||||
data: Data[];
|
||||
};
|
||||
|
||||
export function PercentageLineChart(props: percentageLineChartProps) {
|
||||
|
||||
const {data} = props
|
||||
const { data } = props;
|
||||
|
||||
const chartConfig = {
|
||||
date: {
|
||||
@@ -27,21 +26,24 @@ export function PercentageLineChart(props: percentageLineChartProps) {
|
||||
label: "Success Rate",
|
||||
color: "#60a5fa",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const dailyStats = data.reduce((acc, backup) => {
|
||||
const date = backup.createdAt.toISOString().split("T")[0]; // Format YYYY-MM-DD
|
||||
const status = backup.status;
|
||||
const dailyStats = data.reduce(
|
||||
(acc, backup) => {
|
||||
const date = backup.createdAt.toISOString().split("T")[0]; // Format YYYY-MM-DD
|
||||
const status = backup.status;
|
||||
|
||||
if (!acc[date]) {
|
||||
acc[date] = {success: 0, failed: 0, total: 0};
|
||||
}
|
||||
if (!acc[date]) {
|
||||
acc[date] = { success: 0, failed: 0, total: 0 };
|
||||
}
|
||||
|
||||
acc[date][status === "success" ? "success" : "failed"] += backup._count.id;
|
||||
acc[date].total += backup._count.id;
|
||||
acc[date][status === "success" ? "success" : "failed"] += backup._count.id;
|
||||
acc[date].total += backup._count.id;
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, { success: number; failed: number; total: number }>);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, { success: number; failed: number; total: number }>
|
||||
);
|
||||
|
||||
// Format data for the chart
|
||||
const formattedData = Object.entries(dailyStats).map(([date, stats]) => ({
|
||||
@@ -51,35 +53,26 @@ export function PercentageLineChart(props: percentageLineChartProps) {
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={formattedData}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3"/>
|
||||
<XAxis dataKey="date"/>
|
||||
<YAxis domain={[0, 100]} tickFormatter={(tick) => `${tick}%`}/>
|
||||
<LineChart accessibilityLayer data={formattedData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis domain={[0, 100]} tickFormatter={(tick) => `${tick}%`} />
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent/>}
|
||||
content={<ChartTooltipContent />}
|
||||
cursor={false}
|
||||
defaultIndex={1}
|
||||
formatter={(value, name) => (
|
||||
<div className="flex min-w-[130px] items-center text-xs text-muted-foreground">
|
||||
{chartConfig[name as keyof typeof chartConfig]?.label ||
|
||||
name}
|
||||
<div
|
||||
className="ml-auto flex items-baseline gap-0.5 font-mono font-medium tabular-nums text-foreground">
|
||||
{chartConfig[name as keyof typeof chartConfig]?.label || name}
|
||||
<div className="ml-auto flex items-baseline gap-0.5 font-mono font-medium tabular-nums text-foreground">
|
||||
{value}
|
||||
<span className="font-normal text-muted-foreground">
|
||||
%
|
||||
</span>
|
||||
<span className="font-normal text-muted-foreground">%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Line type="step" dataKey="successRate" stroke="#8884d8" strokeWidth={2}/>
|
||||
<Line type="step" dataKey="successRate" stroke="#8884d8" strokeWidth={2} />
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user