Refactoring in roles.

This commit is contained in:
charlesgauthereau
2025-11-01 14:50:08 +01:00
parent 027928c29f
commit ae766dab2f
36 changed files with 1800 additions and 383 deletions
@@ -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
View File
@@ -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}/>}
</>;
}
@@ -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>
)
+3
View File
@@ -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
+7
View File
@@ -36,6 +36,13 @@
"when": 1756473637441,
"tag": "0004_dazzling_hawkeye",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1762004436821,
"tag": "0005_old_swarm",
"breakpoints": true
}
]
}
+3 -2
View File
@@ -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>;
+8 -9
View File
@@ -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],
+4
View File
@@ -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,
+9 -4
View File
@@ -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">
+40 -32
View File
@@ -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>
);
},
},
+5
View File
@@ -0,0 +1,5 @@
import {Inter} from "next/font/google";
export const inter = Inter({subsets: ["latin"]});
+2 -2
View File
@@ -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);
}
+2
View File
@@ -12,6 +12,8 @@ const statement = {
const ac = createAccessControl(statement);
const superadmin = ac.newRole({
project: ["create", "list", "update", "delete"],
database: ["create", "list", "update", "delete"],