mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Refactoring in roles.
This commit is contained in:
+20
-14
@@ -11,7 +11,7 @@ import {db} from "@/db";
|
||||
import {eq, and, inArray} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {getOrganizationProjectDatabases} from "@/lib/services";
|
||||
import {getOrganization} from "@/lib/auth/auth";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
|
||||
@@ -22,8 +22,9 @@ export default async function RoutePage(props: PageParams<{
|
||||
const {projectId, databaseId} = await props.params;
|
||||
|
||||
const organization = await getOrganization({});
|
||||
const activeMember = await getActiveMember()
|
||||
|
||||
if (!organization) {
|
||||
if (!organization || !activeMember) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
@@ -83,6 +84,9 @@ export default async function RoutePage(props: PageParams<{
|
||||
|
||||
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||
|
||||
const isMember = activeMember?.role === "member";
|
||||
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="justify-between gap-2 sm:flex">
|
||||
@@ -90,17 +94,19 @@ export default async function RoutePage(props: PageParams<{
|
||||
<div className=" w-full md:w-fit">
|
||||
{capitalizeFirstLetter(dbItem.name)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 md:justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Do not delete*/}
|
||||
{/*<EditButton/>*/}
|
||||
<RetentionPolicySheet database={dbItem}/>
|
||||
<CronButton database={dbItem}/>
|
||||
{!isMember && (
|
||||
<div className="flex items-center gap-2 md:justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Do not delete*/}
|
||||
{/*<EditButton/>*/}
|
||||
<RetentionPolicySheet database={dbItem}/>
|
||||
<CronButton database={dbItem}/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PageTitle>
|
||||
</div>
|
||||
|
||||
@@ -108,9 +114,9 @@ export default async function RoutePage(props: PageParams<{
|
||||
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
|
||||
)}
|
||||
<PageContent className="flex flex-col w-full h-full">
|
||||
<DatabaseKpi successRate={successRate} database={dbItem} availableBackups={availableBackups}
|
||||
<DatabaseKpi successRate={successRate} database={dbItem} availableBackups={availableBackups}
|
||||
totalBackups={totalBackups}/>
|
||||
<DatabaseTabs settings={settings} database={dbItem} isAlreadyRestore={isAlreadyRestore}
|
||||
<DatabaseTabs activeMember={activeMember} settings={settings} database={dbItem} isAlreadyRestore={isAlreadyRestore}
|
||||
backups={backups}
|
||||
restorations={restorations}/>
|
||||
</PageContent>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {notFound, redirect} from "next/navigation";
|
||||
|
||||
import {db} from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {getOrganization} from "@/lib/auth/auth";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
|
||||
@@ -24,6 +24,8 @@ export default async function RoutePage(props: PageParams<{
|
||||
} = await props.params;
|
||||
|
||||
const organization = await getOrganization({});
|
||||
const activeMember = await getActiveMember()
|
||||
|
||||
if (!organization) {
|
||||
notFound();
|
||||
}
|
||||
@@ -48,23 +50,27 @@ export default async function RoutePage(props: PageParams<{
|
||||
redirect("/dashboard/projects");
|
||||
}
|
||||
|
||||
const isMember = activeMember?.role === "member";
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="justify-between gap-2 sm:flex">
|
||||
<PageTitle className="flex items-center">
|
||||
{capitalizeFirstLetter(proj.name)}
|
||||
<Link className={buttonVariants({variant: "outline"})} href={`/dashboard/projects/${proj.id}/edit`}>
|
||||
<GearIcon className="w-7 h-7"/>
|
||||
</Link>
|
||||
{!isMember && (
|
||||
<Link className={buttonVariants({variant: "outline"})}
|
||||
href={`/dashboard/projects/${proj.id}/edit`}>
|
||||
<GearIcon className="w-7 h-7"/>
|
||||
</Link>
|
||||
)}
|
||||
</PageTitle>
|
||||
<PageActions className="justify-between">
|
||||
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
||||
</PageActions>
|
||||
{!isMember && (
|
||||
<PageActions className="justify-between">
|
||||
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
||||
</PageActions>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PageDescription>The list of associated databases</PageDescription>
|
||||
|
||||
<PageContent className="flex flex-col w-full h-full">
|
||||
{proj.databases.length > 0 ? (
|
||||
<CardsWithPagination
|
||||
|
||||
@@ -7,12 +7,13 @@ import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/
|
||||
import {ProjectCard} from "@/components/wrappers/dashboard/projects/project-card/project-card";
|
||||
import {db} from "@/db";
|
||||
import {notFound} from "next/navigation";
|
||||
import {getOrganization} from "@/lib/auth/auth";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{ }>) {
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
const organization = await getOrganization({});
|
||||
const activeMember = await getActiveMember()
|
||||
|
||||
if (!organization) {
|
||||
notFound();
|
||||
@@ -28,12 +29,14 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
||||
databases: true,
|
||||
},
|
||||
});
|
||||
const isMember = activeMember?.role === "member";
|
||||
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>Projects</PageTitle>
|
||||
{projects.length > 0 && (
|
||||
{(projects.length > 0 && !isMember) && (
|
||||
<PageActions>
|
||||
<Link href={`/dashboard/projects/new`}>
|
||||
<Button>+ Create Project</Button>
|
||||
@@ -44,13 +47,17 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
||||
|
||||
<PageContent>
|
||||
{projects.length > 0 ? (
|
||||
<CardsWithPagination organizationSlug={organization.slug} data={projects} cardItem={ProjectCard}
|
||||
cardsPerPage={4} numberOfColumns={1}/>
|
||||
) : (
|
||||
<EmptyStatePlaceholder
|
||||
url={"/dashboard/projects/new"}
|
||||
text={"Create new Project"}
|
||||
<CardsWithPagination
|
||||
organizationSlug={organization.slug}
|
||||
data={projects}
|
||||
cardItem={ProjectCard}
|
||||
cardsPerPage={4}
|
||||
numberOfColumns={1}
|
||||
/>
|
||||
) : isMember ? (
|
||||
<EmptyStatePlaceholder text="No project available"/>
|
||||
) : (
|
||||
<EmptyStatePlaceholder url="/dashboard/projects/new" text="Create new Project"/>
|
||||
)}
|
||||
</PageContent>
|
||||
</Page>
|
||||
|
||||
@@ -22,10 +22,7 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||
}
|
||||
|
||||
const isMember = activeMember?.role === "member";
|
||||
|
||||
if (isMember) {
|
||||
notFound();
|
||||
}
|
||||
const isOwner = activeMember?.role === "owner";
|
||||
|
||||
return (
|
||||
<Page>
|
||||
@@ -37,14 +34,11 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||
)}
|
||||
</PageTitle>
|
||||
<PageActions>
|
||||
{!isMember && organization.slug !== "default" && (
|
||||
{isOwner && organization.slug !== "default" && (
|
||||
<DeleteOrganizationButton organizationSlug={organization.slug}/>
|
||||
)}
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
{/*<PageDescription>*/}
|
||||
{/* Manage your organization settings.*/}
|
||||
{/*</PageDescription>*/}
|
||||
<PageContent>
|
||||
<SettingsOrganizationMembersTable organization={organization}/>
|
||||
</PageContent>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {db} from "@/db";
|
||||
import {and, asc, count, eq, inArray} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {getOrganization} from "@/lib/auth/auth";
|
||||
import {DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
|
||||
import {Building2, DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
const organization = await getOrganization({});
|
||||
@@ -45,79 +45,6 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
});
|
||||
|
||||
|
||||
|
||||
// const tomorrow = new Date();
|
||||
// tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
//
|
||||
// const before = new Date();
|
||||
// before.setDate(before.getDate() - 1);
|
||||
//
|
||||
// const backupsEvolution = [
|
||||
// {
|
||||
// id: '22e84aa4-228c-45b3-82ec-846a639cd509',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
||||
// createdAt: new Date(before)
|
||||
// },
|
||||
// {
|
||||
// id: '6a6106fe-7f45-48eb-a56f-0a1e734126a1',
|
||||
// createdAt: new Date(before)
|
||||
// },
|
||||
// {
|
||||
// id: 'a529d790-502e-4609-ad37-9b1c00c73477',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: 'a8c105a8-3e29-423e-b7dd-d3218092cde1',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: 'd33aaf4f-8525-4490-addb-12e3c8650d6d',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
|
||||
// createdAt: new Date(tomorrow)
|
||||
// },
|
||||
// {
|
||||
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
|
||||
// createdAt: new Date(tomorrow)
|
||||
// },
|
||||
// {
|
||||
// id: 'e88a588a-2353-4470-9976-8c3eb2ffc88d',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: 'ee11441e-4b41-4c1b-9d91-929565b4204a',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
//
|
||||
// // Entries with tomorrow's date
|
||||
// {
|
||||
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
|
||||
// createdAt: new Date(tomorrow)
|
||||
// },
|
||||
// {
|
||||
// id: 'c0a8323d-9241-4896-9e64-01e905c24e51',
|
||||
// createdAt: new Date(tomorrow)
|
||||
// }
|
||||
// ];
|
||||
|
||||
|
||||
const backupsRate = await db
|
||||
.select({
|
||||
createdAt: drizzleDb.schemas.backup.createdAt,
|
||||
@@ -139,60 +66,84 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
.where(inArray(drizzleDb.schemas.restoration.databaseId, databaseIds));
|
||||
|
||||
|
||||
|
||||
const restorationsCount = restorationsCountResult[0]?.count ?? 0;
|
||||
const projectsCount = projects.length;
|
||||
const backupsEvolutionCount = backupsEvolution.length;
|
||||
|
||||
|
||||
const sortedBackupsEvolution = backupsEvolution.sort(
|
||||
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||
);
|
||||
|
||||
const Placeholder = ({text}: {text: string}) => (
|
||||
<div className="flex h-48 items-center justify-center text-sm text-muted-foreground">{text}</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>Statistics</PageTitle>
|
||||
<PageTitle>Statistics Overview</PageTitle>
|
||||
</PageHeader>
|
||||
|
||||
<PageContent className="flex flex-col gap-y-4">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<Card className="w-full flex-1">
|
||||
<CardHeader className="flex items-center gap-2">
|
||||
<Folder className="w-5 h-5 text-muted-foreground" />
|
||||
<CardTitle>Projects</CardTitle>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Projects</CardTitle>
|
||||
<Building2 className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-bold">{projectsCount}</CardContent>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{projectsCount}</div>
|
||||
<p className="text-xs text-muted-foreground">Active projects in this organization</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="w-full flex-1">
|
||||
<CardHeader className="flex items-center gap-2">
|
||||
<DatabaseBackup className="w-5 h-5 text-muted-foreground" />
|
||||
<CardTitle>Backups</CardTitle>
|
||||
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Backups</CardTitle>
|
||||
<DatabaseBackup className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-bold">{backupsEvolutionCount}</CardContent>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{backupsEvolutionCount}</div>
|
||||
<p className="text-xs text-muted-foreground">Total backups executed across all databases</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="w-full flex-1">
|
||||
<CardHeader className="flex items-center gap-2">
|
||||
<RefreshCcw className="w-5 h-5 text-muted-foreground" />
|
||||
<CardTitle>Restorations</CardTitle>
|
||||
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Restorations</CardTitle>
|
||||
<RefreshCcw className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-bold">{restorationsCount}</CardContent>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{restorationsCount}</div>
|
||||
<p className="text-xs text-muted-foreground">Total restoration operations performed</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Evolution of the number of backups</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<EvolutionLineChart data={sortedBackupsEvolution}/>
|
||||
{sortedBackupsEvolution.length > 0 ? (
|
||||
<EvolutionLineChart data={sortedBackupsEvolution} />
|
||||
) : (
|
||||
<Placeholder text="No backup data available" />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Success rate of backups</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PercentageLineChart data={backupsRate}/>
|
||||
{backupsRate.length > 0 ? (
|
||||
<PercentageLineChart data={backupsRate} />
|
||||
) : (
|
||||
<Placeholder text="No backup rate data available" />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
+2
-4
@@ -1,15 +1,13 @@
|
||||
import React from "react";
|
||||
import type {Metadata} from "next";
|
||||
import {Inter} from "next/font/google";
|
||||
import "./globals.css";
|
||||
import {Providers} from "./providers";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer";
|
||||
|
||||
const inter = Inter({subsets: ["latin"]});
|
||||
import {inter} from "@/fonts/fonts";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: process.env.NEXT_PUBLIC_PROJECT_NAME ?? "App Title",
|
||||
title: process.env.NEXT_PUBLIC_PROJECT_NAME ?? "Portabase",
|
||||
description: process.env.NEXT_PUBLIC_PROJECT_DESCRIPTION ?? undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader } 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 { toast } from "sonner";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import {Card, CardContent, CardHeader} 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 {toast} from "sonner";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import Link from "next/link";
|
||||
import { PasswordInput } from "@/components/wrappers/auth/password-input/password-input";
|
||||
import { LoginSchema, LoginType } from "@/components/wrappers/auth/login/login-form/login-form.schema";
|
||||
import { SocialAuthButton, SocialProviderType } from "@/components/wrappers/auth/login/button-auth/social-auth-button";
|
||||
import { signIn } from "@/lib/auth/auth-client";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Icon } from "@iconify/react";
|
||||
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
|
||||
import {LoginSchema, LoginType} from "@/components/wrappers/auth/login/login-form/login-form.schema";
|
||||
import {SocialAuthButton, SocialProviderType} from "@/components/wrappers/auth/login/button-auth/social-auth-button";
|
||||
import {signIn} from "@/lib/auth/auth-client";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {Icon} from "@iconify/react";
|
||||
import {env} from "@/env.mjs";
|
||||
|
||||
export type loginFormProps = {
|
||||
defaultValues?: LoginType;
|
||||
@@ -29,7 +30,7 @@ export const LoginForm = (props: loginFormProps) => {
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: LoginType) => {
|
||||
const { error } = await signIn.email(values, {
|
||||
const {error} = await signIn.email(values, {
|
||||
onSuccess: () => {
|
||||
toast.success("Login success");
|
||||
router.push("/dashboard/profile");
|
||||
@@ -41,13 +42,18 @@ export const LoginForm = (props: loginFormProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
const availableProviders: SocialProviderType[] = [
|
||||
{
|
||||
id: "google",
|
||||
name: "Google",
|
||||
icon: <Icon icon={"logos:google-icon"} width="25" height="25" />,
|
||||
},
|
||||
];
|
||||
const availableProviders: SocialProviderType[] = [];
|
||||
|
||||
if (env.NEXT_PUBLIC_GOOGLE_AUTH) {
|
||||
availableProviders.push(
|
||||
{
|
||||
id: "google",
|
||||
name: "Google",
|
||||
icon: <Icon icon={"logos:google-icon"} width="25" height="25"/>,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
@@ -70,13 +76,14 @@ export const LoginForm = (props: loginFormProps) => {
|
||||
control={form.control}
|
||||
name="email"
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input autoComplete="email webauthn" placeholder="exemple@portabase.io" {...field} />
|
||||
<Input autoComplete="email webauthn"
|
||||
placeholder="exemple@portabase.io" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -84,7 +91,7 @@ export const LoginForm = (props: loginFormProps) => {
|
||||
control={form.control}
|
||||
name="password"
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center">
|
||||
<FormLabel>Password</FormLabel>
|
||||
@@ -93,9 +100,10 @@ export const LoginForm = (props: loginFormProps) => {
|
||||
</Link>*/}
|
||||
</div>
|
||||
<FormControl>
|
||||
<PasswordInput autoComplete="current-password webauthn" placeholder="Your password" {...field} />
|
||||
<PasswordInput autoComplete="current-password webauthn"
|
||||
placeholder="Your password" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -107,7 +115,7 @@ export const LoginForm = (props: loginFormProps) => {
|
||||
</Link>
|
||||
</div>
|
||||
</Form>
|
||||
<SocialAuthButton providers={availableProviders} />
|
||||
<SocialAuthButton providers={availableProviders}/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -3,22 +3,29 @@ import {cn} from "@/lib/utils";
|
||||
import {Plus} from "lucide-react";
|
||||
|
||||
type EmptyStatePlaceholderProps = {
|
||||
url: string;
|
||||
url?: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
|
||||
export const EmptyStatePlaceholder = ({url, text}: EmptyStatePlaceholderProps) => {
|
||||
return (
|
||||
<Link
|
||||
href={url}
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-center w-full rounded-2xl border border-dashed border-muted p-6 lg:p-10",
|
||||
"hover:bg-muted/50 transition-colors text-muted-foreground hover:text-primary text-center space-y-2"
|
||||
)}
|
||||
>
|
||||
<Plus className="w-5 h-5 lg:w-6 lg:h-6"/>
|
||||
<span className="text-sm lg:text-base font-medium">{text}</span>
|
||||
</Link>
|
||||
<>{url ?
|
||||
<Link
|
||||
href={url}
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-center w-full rounded-2xl border border-dashed border-muted p-6 lg:p-10",
|
||||
"hover:bg-muted/50 transition-colors text-muted-foreground hover:text-primary text-center space-y-2"
|
||||
)}
|
||||
>
|
||||
<Plus className="w-5 h-5 lg:w-6 lg:h-6"/>
|
||||
<span className="text-sm lg:text-base font-medium">{text}</span>
|
||||
</Link>
|
||||
:
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<p className="text-lg text-muted-foreground">{text}</p>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import {Setting} from "@/db/schema/01_setting";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useRouter, useSearchParams} from "next/navigation";
|
||||
import {AdminUsersTable} from "@/components/wrappers/dashboard/admin/admin-user-tab/admin-user-table";
|
||||
import {AdminSettingsTab} from "@/components/wrappers/dashboard/admin/admin-settings-tab/admin-settings-tab";
|
||||
|
||||
export type AdminTabsProps = {
|
||||
users: UserWithAccounts[];
|
||||
@@ -42,11 +41,7 @@ export const AdminTabs = ({users, settings}: AdminTabsProps) => {
|
||||
<TabsTrigger className="w-full" value="storage">
|
||||
Storage
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="w-full" value="settings">
|
||||
Settings
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="users">
|
||||
<AdminUsersTable users={users}/>
|
||||
</TabsContent>
|
||||
@@ -56,9 +51,6 @@ export const AdminTabs = ({users, settings}: AdminTabsProps) => {
|
||||
<TabsContent value="storage">
|
||||
<SettingsStorageTab settings={settings}/>
|
||||
</TabsContent>
|
||||
<TabsContent value="settings">
|
||||
<AdminSettingsTab/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {User, UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/admin-user-tab/columns-users";
|
||||
@@ -18,7 +18,10 @@ export const AdminUsersTable = (props: AdminUsersTableProps) => {
|
||||
<CardDescription>Manage your users</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable columns={usersColumnsAdmin} data={users}/>
|
||||
<DataTable
|
||||
enableSelect={false}
|
||||
columns={usersColumnsAdmin}
|
||||
data={users}/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -72,7 +72,7 @@ export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
||||
accessorKey: "accounts",
|
||||
header: "Provider ID",
|
||||
cell: ({row}) => {
|
||||
return(
|
||||
return (
|
||||
<div>
|
||||
{row.original.accounts.map((item) => (
|
||||
<div key={item.id}>
|
||||
@@ -107,18 +107,20 @@ export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonWithLoading
|
||||
disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
|
||||
variant="outline"
|
||||
text=""
|
||||
icon={<Trash2 color="red" size={15}/>}
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync();
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonWithLoading
|
||||
disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
|
||||
variant="outline"
|
||||
text=""
|
||||
icon={<Trash2 color="red" size={15}/>}
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync();
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,7 +14,9 @@ export const AgentCard = (props: agentCardProps) => {
|
||||
const { data: agent } = props;
|
||||
|
||||
return (
|
||||
<Link href={`/dashboard/agents/${agent.id}`} className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md">
|
||||
<Link href={`/dashboard/agents/${agent.id}`}
|
||||
className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md rounded-xl"
|
||||
>
|
||||
<Card className="flex flex-row justify-between">
|
||||
<div className="flex-1 text-left">
|
||||
<CardHeader className="text-2xl font-bold">{agent.name}</CardHeader>
|
||||
|
||||
@@ -18,7 +18,7 @@ export const LoggedInDropdown = (props: LoggedInDropdownProps) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>{props.children}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" className="w-[--radix-popper-anchor-width]">
|
||||
<DropdownMenuContent side="top" className="min-w-[var(--radix-popper-anchor-width)]">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
redirect("/dashboard/profile");
|
||||
|
||||
@@ -29,11 +29,12 @@ export const SidebarMenuCustomMain = () => {
|
||||
const groupContent: SidebarGroupItem["group_content"] = [
|
||||
{ title: "Projects", url: "/projects", icon: Layers, details:true },
|
||||
{ title: "Statistics", url: "/statistics", icon: ChartArea },
|
||||
{ title: "Settings", url: "/settings", icon: Settings, details:true }
|
||||
];
|
||||
|
||||
if (activeOrganization && (member?.data?.role === "admin" || member?.data?.role === "owner")) {
|
||||
groupContent.push({ title: "Settings", url: "/settings", icon: Settings, details:true });
|
||||
}
|
||||
// if (activeOrganization && (member?.data?.role === "admin" || member?.data?.role === "owner")) {
|
||||
// groupContent.push({ title: "Settings", url: "/settings", icon: Settings, details:true });
|
||||
// }
|
||||
|
||||
const items: SidebarGroupItem[] = [
|
||||
{
|
||||
|
||||
@@ -41,7 +41,6 @@ export function CreateOrganizationModal({open, onOpenChange, onSuccess}: createO
|
||||
await authClient.organization.setActive({organizationSlug: result.data.value.slug});
|
||||
onSuccess?.();
|
||||
toast.success(result.data.actionSuccess?.message || "Organization Created.");
|
||||
// router.push("/");
|
||||
router.replace(`/dashboard/home`);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
@@ -82,26 +81,6 @@ export function CreateOrganizationModal({open, onOpenChange, onSuccess}: createO
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/*<FormField*/}
|
||||
{/* control={form.control}*/}
|
||||
{/* name="slug"*/}
|
||||
{/* defaultValue=""*/}
|
||||
{/* render={({field}) => (*/}
|
||||
{/* <FormItem>*/}
|
||||
{/* <FormLabel>Slug</FormLabel>*/}
|
||||
{/* <FormControl>*/}
|
||||
{/* <Input*/}
|
||||
{/* {...field}*/}
|
||||
{/* onChange={(e) => {*/}
|
||||
{/* const value = e.target.value.replaceAll(" ", "-").toLowerCase();*/}
|
||||
{/* field.onChange(value);*/}
|
||||
{/* }}*/}
|
||||
{/* />*/}
|
||||
{/* </FormControl>*/}
|
||||
{/* <FormMessage/>*/}
|
||||
{/* </FormItem>*/}
|
||||
{/* )}*/}
|
||||
{/*/>*/}
|
||||
<DialogFooter>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<Button type="submit">Create</Button>
|
||||
|
||||
@@ -39,5 +39,6 @@ export function OrganizationCombobox() {
|
||||
|
||||
return <>{state === "expanded" &&
|
||||
<ComboBox sideBar values={values} defaultValue={activeOrganization?.slug} onValueChange={onValueChange}
|
||||
reload={handleReset}/>}</>;
|
||||
reload={handleReset}/>}
|
||||
</>;
|
||||
}
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ export const deleteProjectAction = userAction.schema(z.string()).action(async ({
|
||||
.set({
|
||||
isArchived: true,
|
||||
slug: uuid,
|
||||
name: uuid,
|
||||
})
|
||||
.where(eq(drizzleDb.schemas.project.id, parsedInput))
|
||||
.returning();
|
||||
|
||||
@@ -12,6 +12,7 @@ import {useMutation} from "@tanstack/react-query";
|
||||
import {deleteBackupAction} from "@/features/dashboard/restore/restore.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
|
||||
|
||||
type DatabaseBackupListProps = {
|
||||
@@ -19,6 +20,7 @@ type DatabaseBackupListProps = {
|
||||
settings: Setting;
|
||||
database: DatabaseWith;
|
||||
backups: Backup[];
|
||||
activeMember: MemberWithUser
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +71,7 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
||||
backupId: backup.id,
|
||||
databaseId: backup.databaseId,
|
||||
status: backup.status,
|
||||
file: backup.file!,
|
||||
file: backup.file ?? "",
|
||||
projectSlug: props.database?.project?.slug!
|
||||
});
|
||||
return {
|
||||
@@ -97,50 +99,58 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
const isMember = props.activeMember.role === "member";
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={backupColumns(props.isAlreadyRestore, props.settings, props.database)}
|
||||
enableSelect={!isMember}
|
||||
columns={backupColumns(props.isAlreadyRestore, props.settings, props.database, props.activeMember)}
|
||||
data={filteredBackups}
|
||||
enablePagination
|
||||
selectedActions={(rows) => (
|
||||
<div className="flex justify-start md:justify-between gap-3 md:gap-0 items-center w-full ml-0">
|
||||
<div className="flex gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ButtonWithLoading
|
||||
variant="outline"
|
||||
text="Actions"
|
||||
onClick={() => {
|
||||
<>
|
||||
|
||||
}}
|
||||
disabled={rows.length === 0 || mutationDeleteBackups.isPending}
|
||||
icon={<MoreHorizontal/>}
|
||||
isPending={mutationDeleteBackups.isPending}
|
||||
size="sm"
|
||||
<div className="flex justify-start md:justify-between gap-3 md:gap-0 items-center w-full ml-0">
|
||||
<div className="flex gap-2">
|
||||
{!isMember && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ButtonWithLoading
|
||||
variant="outline"
|
||||
text="Actions"
|
||||
onClick={() => {
|
||||
|
||||
}}
|
||||
disabled={rows.length === 0 || mutationDeleteBackups.isPending}
|
||||
icon={<MoreHorizontal/>}
|
||||
isPending={mutationDeleteBackups.isPending}
|
||||
size="sm"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
console.log("Deleting rows:", rows)
|
||||
await mutationDeleteBackups.mutateAsync(rows)
|
||||
}}
|
||||
className="text-red-600 focus:text-red-700"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2"/>
|
||||
Delete Selected
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<FiltersDropdown
|
||||
items={items}
|
||||
selectedItems={selectedFilters}
|
||||
onSelect={handleSelectFilter}
|
||||
clearFilters={clearFilters}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
console.log("Deleting rows:", rows)
|
||||
await mutationDeleteBackups.mutateAsync(rows)
|
||||
}}
|
||||
className="text-red-600 focus:text-red-700"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2"/>
|
||||
Delete Selected
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<FiltersDropdown
|
||||
items={items}
|
||||
selectedItems={selectedFilters}
|
||||
onSelect={handleSelectFilter}
|
||||
clearFilters={clearFilters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -9,11 +9,13 @@ import {useMutation} from "@tanstack/react-query";
|
||||
import {deleteRestoreAction} from "@/features/dashboard/restore/restore.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
|
||||
|
||||
type DatabaseRestoreListProps = {
|
||||
isAlreadyRestore: boolean;
|
||||
restorations: Restoration[];
|
||||
activeMember: MemberWithUser
|
||||
}
|
||||
|
||||
export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
|
||||
@@ -47,40 +49,46 @@ export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
const isMember = props.activeMember.role === "member";
|
||||
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={restoreColumns(props.isAlreadyRestore)}
|
||||
enableSelect={!isMember}
|
||||
columns={restoreColumns(props.isAlreadyRestore, props.activeMember)}
|
||||
data={props.restorations}
|
||||
enablePagination
|
||||
selectedActions={(rows) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ButtonWithLoading
|
||||
variant="outline"
|
||||
text="Actions"
|
||||
onClick={() => {
|
||||
}}
|
||||
disabled={rows.length === 0 || mutationDeleteRestorations.isPending}
|
||||
icon={<MoreHorizontal/>}
|
||||
isPending={mutationDeleteRestorations.isPending}
|
||||
size="sm"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await mutationDeleteRestorations.mutateAsync(rows)
|
||||
}}
|
||||
disabled={props.isAlreadyRestore}
|
||||
className="text-red-600 focus:text-red-700"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2"/>
|
||||
Delete Selected
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<>
|
||||
{!isMember && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ButtonWithLoading
|
||||
variant="outline"
|
||||
text="Actions"
|
||||
onClick={() => {
|
||||
}}
|
||||
disabled={rows.length === 0 || mutationDeleteRestorations.isPending}
|
||||
icon={<MoreHorizontal/>}
|
||||
isPending={mutationDeleteRestorations.isPending}
|
||||
size="sm"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await mutationDeleteRestorations.mutateAsync(rows)
|
||||
}}
|
||||
disabled={props.isAlreadyRestore}
|
||||
className="text-red-600 focus:text-red-700"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2"/>
|
||||
Delete Selected
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -8,13 +8,15 @@ import {Backup, Database, DatabaseWith, Restoration} from "@/db/schema/07_databa
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {DatabaseBackupList} from "@/components/wrappers/dashboard/projects/database/database-backup-list";
|
||||
import {DatabaseRestoreList} from "@/components/wrappers/dashboard/projects/database/database-restore-list";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
|
||||
export type DatabaseTabsProps = {
|
||||
settings: Setting
|
||||
backups: Backup[];
|
||||
restorations: Restoration[];
|
||||
isAlreadyRestore: boolean;
|
||||
database: DatabaseWith;
|
||||
settings: Setting,
|
||||
backups: Backup[],
|
||||
restorations: Restoration[],
|
||||
isAlreadyRestore: boolean,
|
||||
database: DatabaseWith,
|
||||
activeMember: MemberWithUser
|
||||
};
|
||||
|
||||
export const DatabaseTabs = (props: DatabaseTabsProps) => {
|
||||
@@ -58,12 +60,14 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
|
||||
settings={props.settings}
|
||||
database={props.database}
|
||||
backups={props.backups}
|
||||
activeMember={props.activeMember}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent className="h-full justify-between" value="restore">
|
||||
<DatabaseRestoreList
|
||||
isAlreadyRestore={props.isAlreadyRestore}
|
||||
restorations={props.restorations}
|
||||
activeMember={props.activeMember}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -15,7 +15,7 @@ export const ProjectCard = (props: projectCardProps) => {
|
||||
return (
|
||||
<Link
|
||||
href={`/dashboard/projects/${project.id}`}
|
||||
className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md"
|
||||
className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md rounded-xl"
|
||||
>
|
||||
<Card className="flex flex-row justify-between">
|
||||
<div className="flex-1 text-left">
|
||||
|
||||
@@ -18,7 +18,7 @@ export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
|
||||
const { organizationSlug, data: database, extendedProps: extendedProps } = props;
|
||||
|
||||
return (
|
||||
<Link className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md" href={`/dashboard/projects/${extendedProps.id}/database/${database.id}`}>
|
||||
<Link className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md rounded-xl" href={`/dashboard/projects/${extendedProps.id}/database/${database.id}`}>
|
||||
<DatabaseCard data={database} />
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ProjectSchema } from "@/components/wrappers/dashboard/projects/project-
|
||||
import { z } from "zod";
|
||||
import { ServerActionResult } from "@/types/action-type";
|
||||
import { db } from "@/db";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import {and, eq, inArray} from "drizzle-orm";
|
||||
import {Project} from "@/db/schema/06_project";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {Database} from "@/db/schema/07_database";
|
||||
@@ -21,6 +21,22 @@ export const createProjectAction = userAction
|
||||
.action(async ({ parsedInput }): Promise<ServerActionResult<Project>> => {
|
||||
try {
|
||||
const slug = slugify(parsedInput.data.name);
|
||||
|
||||
const existingProject = await db.query.project.findFirst({
|
||||
where: and(eq(drizzleDb.schemas.project.name, parsedInput.data.name) ),
|
||||
})
|
||||
|
||||
if (existingProject) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "A project with this name already exists.",
|
||||
status: 400,
|
||||
messageParams: { projectName: parsedInput.data.name },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const [createdProject] = await db
|
||||
.insert(drizzleDb.schemas.project)
|
||||
.values({
|
||||
@@ -31,7 +47,10 @@ export const createProjectAction = userAction
|
||||
.returning();
|
||||
|
||||
if (parsedInput.data.databases.length > 0) {
|
||||
await db.update(drizzleDb.schemas.database).set({ projectId: createdProject.id }).where(inArray(drizzleDb.schemas.database.id, parsedInput.data.databases));
|
||||
await db
|
||||
.update(drizzleDb.schemas.database)
|
||||
.set({ projectId: createdProject.id })
|
||||
.where(inArray(drizzleDb.schemas.database.id, parsedInput.data.databases));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -43,6 +62,7 @@ export const createProjectAction = userAction
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
@@ -55,6 +75,9 @@ export const createProjectAction = userAction
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
export const updateProjectAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import {ColumnDef} from "@tanstack/react-table";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
import {useState} from "react";
|
||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {toast} from "sonner";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {updateMemberRoleAction} from "@/components/wrappers/dashboard/settings/update-member.action";
|
||||
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { MemberWithUser } from "@/db/schema/03_organization";
|
||||
import { useState } from "react";
|
||||
import { authClient, useSession } from "@/lib/auth/auth-client";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { updateMemberRoleAction } from "@/components/wrappers/dashboard/settings/update-member.action";
|
||||
import { RoleSchemaMember } from "@/components/wrappers/dashboard/settings/member.schema";
|
||||
|
||||
export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
|
||||
{
|
||||
@@ -18,7 +23,7 @@ export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
|
||||
cell: ({ row }) => {
|
||||
const [role, setRole] = useState<string>(row.getValue("role"));
|
||||
const { data: session } = useSession();
|
||||
|
||||
const activeOrgaMember = authClient.useActiveMember();
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
@@ -28,36 +33,59 @@ export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
|
||||
role: RoleSchemaMember.parse(role),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success(`User updated successfully.`);
|
||||
toast.success("User updated successfully.");
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(`An error occurred while updating user information.`);
|
||||
toast.error("An error occurred while updating user information.");
|
||||
},
|
||||
});
|
||||
|
||||
// Only allow cycling between admin <-> member
|
||||
const handleUpdateRole = async () => {
|
||||
const nextRole =
|
||||
role === "owner" ? "admin" : role === "admin" ? "member" : "owner";
|
||||
const nextRole = role === "admin" ? "member" : "admin";
|
||||
setRole(nextRole);
|
||||
await updateMutation.mutateAsync();
|
||||
};
|
||||
|
||||
|
||||
const isCurrentUser = session?.user.email === row.original.user.email;
|
||||
const isMember = session?.user.role === "member";
|
||||
const isMember = activeOrgaMember.data?.role === "member";
|
||||
const isRowRoleOwner = role === "owner";
|
||||
|
||||
const isDisabled = isMember || isCurrentUser || isRowRoleOwner;
|
||||
|
||||
const isDisabled = isMember || isCurrentUser;
|
||||
// Dynamic tooltip reason
|
||||
const disabledReason = isCurrentUser
|
||||
? "You cannot change your own role"
|
||||
: isRowRoleOwner
|
||||
? "Owner role cannot be modified"
|
||||
: "Members cannot edit roles";
|
||||
|
||||
return (
|
||||
const badge = (
|
||||
<Badge
|
||||
className={isDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"}
|
||||
className={
|
||||
isDisabled
|
||||
? "cursor-not-allowed opacity-50"
|
||||
: "cursor-pointer hover:bg-accent"
|
||||
}
|
||||
onClick={isDisabled ? undefined : handleUpdateRole}
|
||||
variant="outline"
|
||||
>
|
||||
{role}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
return isDisabled ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{badge}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{disabledReason}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
badge
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -67,6 +95,5 @@ export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
|
||||
{
|
||||
accessorKey: "user.email",
|
||||
header: "Email",
|
||||
}
|
||||
|
||||
];
|
||||
},
|
||||
];
|
||||
@@ -1,7 +1,5 @@
|
||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/columns-users";
|
||||
import {MemberWithUser, Organization, OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {OrganizationInvitation} from "@/db/schema/05_invitation";
|
||||
import {organizationMemberColumns} from "@/components/wrappers/dashboard/settings/columns-organization-members";
|
||||
|
||||
|
||||
@@ -11,12 +9,12 @@ interface SettingsOrganizationMembersTableProps {
|
||||
|
||||
export const SettingsOrganizationMembersTable = ({organization}: SettingsOrganizationMembersTableProps) => {
|
||||
return (
|
||||
<div className="flex flex-col h-full py-4">
|
||||
<div className="flex gap-4 h-fit justify-between">
|
||||
<h1>List of Organization members</h1>
|
||||
</div>
|
||||
<div className="mt-5 h-full">
|
||||
<DataTable columns={organizationMemberColumns} data={organization.members as MemberWithUser[]}/>
|
||||
<div className="flex flex-col h-full ">
|
||||
<div className=" h-full">
|
||||
<DataTable
|
||||
columns={organizationMemberColumns}
|
||||
enableSelect={false}
|
||||
data={organization.members as MemberWithUser[]}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "projects" DROP CONSTRAINT "projects_organization_id_organization_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,13 @@
|
||||
"when": 1756473637441,
|
||||
"tag": "0004_dazzling_hawkeye",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "7",
|
||||
"when": 1762004436821,
|
||||
"tag": "0005_old_swarm",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import {pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core";
|
||||
import {user} from "@/db/schema/02_user";
|
||||
import {User, user} from "@/db/schema/02_user";
|
||||
import {organization} from "@/db/schema/03_organization";
|
||||
import {relations} from "drizzle-orm";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
@@ -32,4 +32,5 @@ export const memberRelations = relations(member, ({ one }) => ({
|
||||
|
||||
|
||||
export const organizationMemberSchema = createSelectSchema(member);
|
||||
export type OrganizationMember = z.infer<typeof organizationMemberSchema>;
|
||||
export type OrganizationMember = z.infer<typeof organizationMemberSchema>;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { pgTable, text, boolean, uuid, timestamp } from "drizzle-orm/pg-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { Organization, organization } from "./03_organization";
|
||||
import { createSelectSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { Database, database } from "./07_database";
|
||||
import {pgTable, text, boolean, uuid} from "drizzle-orm/pg-core";
|
||||
import {relations} from "drizzle-orm";
|
||||
import {Organization, organization} from "./03_organization";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
import {Database, database} from "./07_database";
|
||||
import {timestamps} from "@/db/schema/00_common";
|
||||
|
||||
export const project = pgTable("projects", {
|
||||
@@ -13,12 +13,11 @@ export const project = pgTable("projects", {
|
||||
isArchived: boolean("is_archived").default(false),
|
||||
organizationId: uuid("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id),
|
||||
.references(() => organization.id, {onDelete: "cascade"}),
|
||||
...timestamps
|
||||
|
||||
});
|
||||
|
||||
export const projectRelations = relations(project, ({ one, many }) => ({
|
||||
export const projectRelations = relations(project, ({one, many}) => ({
|
||||
organization: one(organization, {
|
||||
fields: [project.organizationId],
|
||||
references: [organization.id],
|
||||
|
||||
@@ -22,6 +22,7 @@ export const env = createEnv({
|
||||
|
||||
AUTH_GOOGLE_ID: z.string().optional(),
|
||||
AUTH_GOOGLE_SECRET: z.string().optional(),
|
||||
NEXT_PUBLIC_GOOGLE_AUTH: z.boolean().default(false).optional(),
|
||||
|
||||
S3_ENDPOINT: z.string().optional(),
|
||||
S3_ACCESS_KEY: z.string().optional(),
|
||||
@@ -41,6 +42,8 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_PROJECT_DESCRIPTION: z.string().optional(),
|
||||
NEXT_PUBLIC_PROJECT_URL: z.string().optional(),
|
||||
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
|
||||
|
||||
NEXT_PUBLIC_GOOGLE_AUTH: z.boolean().default(false).optional(),
|
||||
},
|
||||
runtimeEnv: {
|
||||
NEXT_PUBLIC_PROJECT_NAME: process.env.NEXT_PUBLIC_PROJECT_NAME,
|
||||
@@ -59,6 +62,7 @@ export const env = createEnv({
|
||||
|
||||
AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID,
|
||||
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
|
||||
NEXT_PUBLIC_GOOGLE_AUTH: process.env.NEXT_PUBLIC_GOOGLE_AUTH === "true",
|
||||
|
||||
S3_ENDPOINT: process.env.S3_ENDPOINT,
|
||||
S3_ACCESS_KEY: process.env.S3_ACCESS_KEY,
|
||||
|
||||
@@ -30,9 +30,15 @@ import {ZodString} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
|
||||
|
||||
export function backupColumns(isAlreadyRestore: boolean, settings: Setting, database: DatabaseWith): ColumnDef<Backup>[] {
|
||||
export function backupColumns(
|
||||
isAlreadyRestore: boolean,
|
||||
settings: Setting,
|
||||
database: DatabaseWith,
|
||||
activeMember: MemberWithUser
|
||||
): ColumnDef<Backup>[] {
|
||||
return [
|
||||
{
|
||||
id: "availability",
|
||||
@@ -136,7 +142,7 @@ export function backupColumns(isAlreadyRestore: boolean, settings: Setting, data
|
||||
}, readonly [], ServerActionResult<string>, object> | undefined
|
||||
|
||||
if (settings.storage == "local") {
|
||||
data = await getFileUrlPresignedLocal({fileName:fileName!})
|
||||
data = await getFileUrlPresignedLocal({fileName: fileName!})
|
||||
} else if (settings.storage == "s3") {
|
||||
data = await getFileUrlPreSignedS3Action(`backups/${database.project?.slug}/${fileName}`);
|
||||
}
|
||||
@@ -154,8 +160,7 @@ export function backupColumns(isAlreadyRestore: boolean, settings: Setting, data
|
||||
|
||||
return (
|
||||
<>
|
||||
{rowData.deletedAt == null && (
|
||||
|
||||
{(rowData.deletedAt == null && activeMember.role != "member") && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
|
||||
@@ -24,9 +24,13 @@ import {
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
|
||||
|
||||
export function restoreColumns(isAlreadyRestore: boolean): ColumnDef<Restoration>[] {
|
||||
export function restoreColumns(
|
||||
isAlreadyRestore: boolean,
|
||||
activeMember: MemberWithUser
|
||||
): ColumnDef<Restoration>[] {
|
||||
return[
|
||||
{
|
||||
accessorKey: "id",
|
||||
@@ -102,39 +106,43 @@ export function restoreColumns(isAlreadyRestore: boolean): ColumnDef<Restoration
|
||||
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<>
|
||||
{activeMember.role != "member" && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4"/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<TooltipCustom disabled={isAlreadyRestore} text="Already a restoration waiting">
|
||||
<DropdownMenuItem
|
||||
disabled={mutationRerunRestore.isPending || isAlreadyRestore}
|
||||
onClick={async () => {
|
||||
await handleRerunRestore();
|
||||
}}
|
||||
>
|
||||
<ReloadIcon/> Rerun
|
||||
</DropdownMenuItem>
|
||||
</TooltipCustom>
|
||||
<DropdownMenuSeparator/>
|
||||
|
||||
<TooltipCustom disabled={isAlreadyRestore} text="Already a restoration waiting">
|
||||
<DropdownMenuItem
|
||||
disabled={mutationRerunRestore.isPending || isAlreadyRestore}
|
||||
onClick={async () => {
|
||||
await handleRerunRestore();
|
||||
}}
|
||||
>
|
||||
<ReloadIcon/> Rerun
|
||||
</DropdownMenuItem>
|
||||
</TooltipCustom>
|
||||
<DropdownMenuSeparator/>
|
||||
<DropdownMenuItem
|
||||
disabled={status == "waiting"}
|
||||
className="text-red-600"
|
||||
onClick={async () => {
|
||||
await handleDelete();
|
||||
}}
|
||||
>
|
||||
<Trash2/> Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</>
|
||||
|
||||
<DropdownMenuItem
|
||||
disabled={status == "waiting"}
|
||||
className="text-red-600"
|
||||
onClick={async () => {
|
||||
await handleDelete();
|
||||
}}
|
||||
>
|
||||
<Trash2/> Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import {Inter} from "next/font/google";
|
||||
|
||||
|
||||
|
||||
export const inter = Inter({subsets: ["latin"]});
|
||||
@@ -8,7 +8,7 @@ import {admin as adminPlugin, openAPI, Organization, organization} from "better-
|
||||
import {ac, admin, orgAdmin, orgMember, orgOwner, pending, superadmin, user} from "@/lib/auth/permissions";
|
||||
import {headers} from "next/headers";
|
||||
import {count, eq} from "drizzle-orm";
|
||||
import {OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
@@ -455,7 +455,7 @@ export const getActiveMember = async () => {
|
||||
});
|
||||
console.log(member);
|
||||
|
||||
return member;
|
||||
return member as MemberWithUser;
|
||||
} catch (e) {
|
||||
console.log("err", e);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ const statement = {
|
||||
|
||||
const ac = createAccessControl(statement);
|
||||
|
||||
|
||||
|
||||
const superadmin = ac.newRole({
|
||||
project: ["create", "list", "update", "delete"],
|
||||
database: ["create", "list", "update", "delete"],
|
||||
|
||||
Reference in New Issue
Block a user