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 {eq, and, inArray} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {getOrganizationProjectDatabases} from "@/lib/services";
|
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 {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
|
||||||
import {capitalizeFirstLetter} from "@/utils/text";
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
|
|
||||||
@@ -22,8 +22,9 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
const {projectId, databaseId} = await props.params;
|
const {projectId, databaseId} = await props.params;
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
|
const activeMember = await getActiveMember()
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization || !activeMember) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +84,9 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
|
|
||||||
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||||
|
|
||||||
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="justify-between gap-2 sm:flex">
|
<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">
|
<div className=" w-full md:w-fit">
|
||||||
{capitalizeFirstLetter(dbItem.name)}
|
{capitalizeFirstLetter(dbItem.name)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 md:justify-between w-full">
|
{!isMember && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 md:justify-between w-full">
|
||||||
{/* Do not delete*/}
|
<div className="flex items-center gap-2">
|
||||||
{/*<EditButton/>*/}
|
{/* Do not delete*/}
|
||||||
<RetentionPolicySheet database={dbItem}/>
|
{/*<EditButton/>*/}
|
||||||
<CronButton database={dbItem}/>
|
<RetentionPolicySheet database={dbItem}/>
|
||||||
|
<CronButton database={dbItem}/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
)}
|
||||||
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -108,9 +114,9 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
|
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
|
||||||
)}
|
)}
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
<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}/>
|
totalBackups={totalBackups}/>
|
||||||
<DatabaseTabs settings={settings} database={dbItem} isAlreadyRestore={isAlreadyRestore}
|
<DatabaseTabs activeMember={activeMember} settings={settings} database={dbItem} isAlreadyRestore={isAlreadyRestore}
|
||||||
backups={backups}
|
backups={backups}
|
||||||
restorations={restorations}/>
|
restorations={restorations}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {notFound, redirect} from "next/navigation";
|
|||||||
|
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {capitalizeFirstLetter} from "@/utils/text";
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
|
|
||||||
@@ -24,6 +24,8 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
} = await props.params;
|
} = await props.params;
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
|
const activeMember = await getActiveMember()
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
@@ -48,23 +50,27 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
redirect("/dashboard/projects");
|
redirect("/dashboard/projects");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="justify-between gap-2 sm:flex">
|
<div className="justify-between gap-2 sm:flex">
|
||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex items-center">
|
||||||
{capitalizeFirstLetter(proj.name)}
|
{capitalizeFirstLetter(proj.name)}
|
||||||
<Link className={buttonVariants({variant: "outline"})} href={`/dashboard/projects/${proj.id}/edit`}>
|
{!isMember && (
|
||||||
<GearIcon className="w-7 h-7"/>
|
<Link className={buttonVariants({variant: "outline"})}
|
||||||
</Link>
|
href={`/dashboard/projects/${proj.id}/edit`}>
|
||||||
|
<GearIcon className="w-7 h-7"/>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<PageActions className="justify-between">
|
{!isMember && (
|
||||||
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
<PageActions className="justify-between">
|
||||||
</PageActions>
|
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
||||||
|
</PageActions>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PageDescription>The list of associated databases</PageDescription>
|
<PageDescription>The list of associated databases</PageDescription>
|
||||||
|
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
<PageContent className="flex flex-col w-full h-full">
|
||||||
{proj.databases.length > 0 ? (
|
{proj.databases.length > 0 ? (
|
||||||
<CardsWithPagination
|
<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 {ProjectCard} from "@/components/wrappers/dashboard/projects/project-card/project-card";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {notFound} from "next/navigation";
|
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";
|
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 organization = await getOrganization({});
|
||||||
|
const activeMember = await getActiveMember()
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -28,12 +29,14 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
databases: true,
|
databases: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>Projects</PageTitle>
|
<PageTitle>Projects</PageTitle>
|
||||||
{projects.length > 0 && (
|
{(projects.length > 0 && !isMember) && (
|
||||||
<PageActions>
|
<PageActions>
|
||||||
<Link href={`/dashboard/projects/new`}>
|
<Link href={`/dashboard/projects/new`}>
|
||||||
<Button>+ Create Project</Button>
|
<Button>+ Create Project</Button>
|
||||||
@@ -44,13 +47,17 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
|
|
||||||
<PageContent>
|
<PageContent>
|
||||||
{projects.length > 0 ? (
|
{projects.length > 0 ? (
|
||||||
<CardsWithPagination organizationSlug={organization.slug} data={projects} cardItem={ProjectCard}
|
<CardsWithPagination
|
||||||
cardsPerPage={4} numberOfColumns={1}/>
|
organizationSlug={organization.slug}
|
||||||
) : (
|
data={projects}
|
||||||
<EmptyStatePlaceholder
|
cardItem={ProjectCard}
|
||||||
url={"/dashboard/projects/new"}
|
cardsPerPage={4}
|
||||||
text={"Create new Project"}
|
numberOfColumns={1}
|
||||||
/>
|
/>
|
||||||
|
) : isMember ? (
|
||||||
|
<EmptyStatePlaceholder text="No project available"/>
|
||||||
|
) : (
|
||||||
|
<EmptyStatePlaceholder url="/dashboard/projects/new" text="Create new Project"/>
|
||||||
)}
|
)}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isMember = activeMember?.role === "member";
|
const isMember = activeMember?.role === "member";
|
||||||
|
const isOwner = activeMember?.role === "owner";
|
||||||
if (isMember) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
@@ -37,14 +34,11 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
|||||||
)}
|
)}
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<PageActions>
|
<PageActions>
|
||||||
{!isMember && organization.slug !== "default" && (
|
{isOwner && organization.slug !== "default" && (
|
||||||
<DeleteOrganizationButton organizationSlug={organization.slug}/>
|
<DeleteOrganizationButton organizationSlug={organization.slug}/>
|
||||||
)}
|
)}
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
{/*<PageDescription>*/}
|
|
||||||
{/* Manage your organization settings.*/}
|
|
||||||
{/*</PageDescription>*/}
|
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<SettingsOrganizationMembersTable organization={organization}/>
|
<SettingsOrganizationMembersTable organization={organization}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {db} from "@/db";
|
|||||||
import {and, asc, count, eq, inArray} from "drizzle-orm";
|
import {and, asc, count, eq, inArray} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
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<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
const organization = await getOrganization({});
|
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
|
const backupsRate = await db
|
||||||
.select({
|
.select({
|
||||||
createdAt: drizzleDb.schemas.backup.createdAt,
|
createdAt: drizzleDb.schemas.backup.createdAt,
|
||||||
@@ -139,60 +66,84 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
.where(inArray(drizzleDb.schemas.restoration.databaseId, databaseIds));
|
.where(inArray(drizzleDb.schemas.restoration.databaseId, databaseIds));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const restorationsCount = restorationsCountResult[0]?.count ?? 0;
|
const restorationsCount = restorationsCountResult[0]?.count ?? 0;
|
||||||
const projectsCount = projects.length;
|
const projectsCount = projects.length;
|
||||||
const backupsEvolutionCount = backupsEvolution.length;
|
const backupsEvolutionCount = backupsEvolution.length;
|
||||||
|
|
||||||
|
|
||||||
const sortedBackupsEvolution = backupsEvolution.sort(
|
const sortedBackupsEvolution = backupsEvolution.sort(
|
||||||
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
(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 (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>Statistics</PageTitle>
|
<PageTitle>Statistics Overview</PageTitle>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<PageContent className="flex flex-col gap-y-4">
|
<PageContent className="flex flex-col gap-y-4">
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<Card className="w-full flex-1">
|
<Card className="w-full">
|
||||||
<CardHeader className="flex items-center gap-2">
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
<Folder className="w-5 h-5 text-muted-foreground" />
|
<CardTitle className="text-sm font-medium">Projects</CardTitle>
|
||||||
<CardTitle>Projects</CardTitle>
|
<Building2 className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</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>
|
||||||
<Card className="w-full flex-1">
|
|
||||||
<CardHeader className="flex items-center gap-2">
|
<Card className="w-full">
|
||||||
<DatabaseBackup className="w-5 h-5 text-muted-foreground" />
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
<CardTitle>Backups</CardTitle>
|
<CardTitle className="text-sm font-medium">Backups</CardTitle>
|
||||||
|
<DatabaseBackup className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</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>
|
||||||
<Card className="w-full flex-1">
|
|
||||||
<CardHeader className="flex items-center gap-2">
|
<Card className="w-full">
|
||||||
<RefreshCcw className="w-5 h-5 text-muted-foreground" />
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
<CardTitle>Restorations</CardTitle>
|
<CardTitle className="text-sm font-medium">Restorations</CardTitle>
|
||||||
|
<RefreshCcw className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</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>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Evolution of the number of backups</CardTitle>
|
<CardTitle>Evolution of the number of backups</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<EvolutionLineChart data={sortedBackupsEvolution}/>
|
{sortedBackupsEvolution.length > 0 ? (
|
||||||
|
<EvolutionLineChart data={sortedBackupsEvolution} />
|
||||||
|
) : (
|
||||||
|
<Placeholder text="No backup data available" />
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Success rate of backups</CardTitle>
|
<CardTitle>Success rate of backups</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<PercentageLineChart data={backupsRate}/>
|
{backupsRate.length > 0 ? (
|
||||||
|
<PercentageLineChart data={backupsRate} />
|
||||||
|
) : (
|
||||||
|
<Placeholder text="No backup rate data available" />
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-4
@@ -1,15 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import type {Metadata} from "next";
|
import type {Metadata} from "next";
|
||||||
import {Inter} from "next/font/google";
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import {Providers} from "./providers";
|
import {Providers} from "./providers";
|
||||||
import {cn} from "@/lib/utils";
|
import {cn} from "@/lib/utils";
|
||||||
import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer";
|
import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer";
|
||||||
|
import {inter} from "@/fonts/fonts";
|
||||||
const inter = Inter({subsets: ["latin"]});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
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,
|
description: process.env.NEXT_PUBLIC_PROJECT_DESCRIPTION ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||||
import { FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
import {FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import {Input} from "@/components/ui/input";
|
||||||
import { Form } from "@/components/ui/form";
|
import {Form} from "@/components/ui/form";
|
||||||
import { Button } from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import { toast } from "sonner";
|
import {toast} from "sonner";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { PasswordInput } from "@/components/wrappers/auth/password-input/password-input";
|
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
|
||||||
import { LoginSchema, LoginType } from "@/components/wrappers/auth/login/login-form/login-form.schema";
|
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 {SocialAuthButton, SocialProviderType} from "@/components/wrappers/auth/login/button-auth/social-auth-button";
|
||||||
import { signIn } from "@/lib/auth/auth-client";
|
import {signIn} from "@/lib/auth/auth-client";
|
||||||
import { useRouter } from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import { Icon } from "@iconify/react";
|
import {Icon} from "@iconify/react";
|
||||||
|
import {env} from "@/env.mjs";
|
||||||
|
|
||||||
export type loginFormProps = {
|
export type loginFormProps = {
|
||||||
defaultValues?: LoginType;
|
defaultValues?: LoginType;
|
||||||
@@ -29,7 +30,7 @@ export const LoginForm = (props: loginFormProps) => {
|
|||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async (values: LoginType) => {
|
mutationFn: async (values: LoginType) => {
|
||||||
const { error } = await signIn.email(values, {
|
const {error} = await signIn.email(values, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Login success");
|
toast.success("Login success");
|
||||||
router.push("/dashboard/profile");
|
router.push("/dashboard/profile");
|
||||||
@@ -41,13 +42,18 @@ export const LoginForm = (props: loginFormProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const availableProviders: SocialProviderType[] = [
|
const availableProviders: SocialProviderType[] = [];
|
||||||
{
|
|
||||||
id: "google",
|
if (env.NEXT_PUBLIC_GOOGLE_AUTH) {
|
||||||
name: "Google",
|
availableProviders.push(
|
||||||
icon: <Icon icon={"logos:google-icon"} width="25" height="25" />,
|
{
|
||||||
},
|
id: "google",
|
||||||
];
|
name: "Google",
|
||||||
|
icon: <Icon icon={"logos:google-icon"} width="25" height="25"/>,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -70,13 +76,14 @@ export const LoginForm = (props: loginFormProps) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="email"
|
name="email"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field }) => (
|
render={({field}) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Email</FormLabel>
|
<FormLabel>Email</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input autoComplete="email webauthn" placeholder="exemple@portabase.io" {...field} />
|
<Input autoComplete="email webauthn"
|
||||||
|
placeholder="exemple@portabase.io" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -84,7 +91,7 @@ export const LoginForm = (props: loginFormProps) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="password"
|
name="password"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field }) => (
|
render={({field}) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>Password</FormLabel>
|
||||||
@@ -93,9 +100,10 @@ export const LoginForm = (props: loginFormProps) => {
|
|||||||
</Link>*/}
|
</Link>*/}
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<PasswordInput autoComplete="current-password webauthn" placeholder="Your password" {...field} />
|
<PasswordInput autoComplete="current-password webauthn"
|
||||||
|
placeholder="Your password" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -107,7 +115,7 @@ export const LoginForm = (props: loginFormProps) => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
<SocialAuthButton providers={availableProviders} />
|
<SocialAuthButton providers={availableProviders}/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -3,22 +3,29 @@ import {cn} from "@/lib/utils";
|
|||||||
import {Plus} from "lucide-react";
|
import {Plus} from "lucide-react";
|
||||||
|
|
||||||
type EmptyStatePlaceholderProps = {
|
type EmptyStatePlaceholderProps = {
|
||||||
url: string;
|
url?: string;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const EmptyStatePlaceholder = ({url, text}: EmptyStatePlaceholderProps) => {
|
export const EmptyStatePlaceholder = ({url, text}: EmptyStatePlaceholderProps) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<>{url ?
|
||||||
href={url}
|
<Link
|
||||||
className={cn(
|
href={url}
|
||||||
"flex flex-col items-center justify-center w-full rounded-2xl border border-dashed border-muted p-6 lg:p-10",
|
className={cn(
|
||||||
"hover:bg-muted/50 transition-colors text-muted-foreground hover:text-primary text-center space-y-2"
|
"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>
|
<Plus className="w-5 h-5 lg:w-6 lg:h-6"/>
|
||||||
</Link>
|
<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 {useEffect, useState} from "react";
|
||||||
import {useRouter, useSearchParams} from "next/navigation";
|
import {useRouter, useSearchParams} from "next/navigation";
|
||||||
import {AdminUsersTable} from "@/components/wrappers/dashboard/admin/admin-user-tab/admin-user-table";
|
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 = {
|
export type AdminTabsProps = {
|
||||||
users: UserWithAccounts[];
|
users: UserWithAccounts[];
|
||||||
@@ -42,11 +41,7 @@ export const AdminTabs = ({users, settings}: AdminTabsProps) => {
|
|||||||
<TabsTrigger className="w-full" value="storage">
|
<TabsTrigger className="w-full" value="storage">
|
||||||
Storage
|
Storage
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger className="w-full" value="settings">
|
|
||||||
Settings
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="users">
|
<TabsContent value="users">
|
||||||
<AdminUsersTable users={users}/>
|
<AdminUsersTable users={users}/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@@ -56,9 +51,6 @@ export const AdminTabs = ({users, settings}: AdminTabsProps) => {
|
|||||||
<TabsContent value="storage">
|
<TabsContent value="storage">
|
||||||
<SettingsStorageTab settings={settings}/>
|
<SettingsStorageTab settings={settings}/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="settings">
|
|
||||||
<AdminSettingsTab/>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
</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 {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||||
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/admin-user-tab/columns-users";
|
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>
|
<CardDescription>Manage your users</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<DataTable columns={usersColumnsAdmin} data={users}/>
|
<DataTable
|
||||||
|
enableSelect={false}
|
||||||
|
columns={usersColumnsAdmin}
|
||||||
|
data={users}/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
|||||||
accessorKey: "accounts",
|
accessorKey: "accounts",
|
||||||
header: "Provider ID",
|
header: "Provider ID",
|
||||||
cell: ({row}) => {
|
cell: ({row}) => {
|
||||||
return(
|
return (
|
||||||
<div>
|
<div>
|
||||||
{row.original.accounts.map((item) => (
|
{row.original.accounts.map((item) => (
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
@@ -107,18 +107,20 @@ export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<>
|
||||||
<ButtonWithLoading
|
<div className="flex items-center gap-2">
|
||||||
disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
|
<ButtonWithLoading
|
||||||
variant="outline"
|
disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
|
||||||
text=""
|
variant="outline"
|
||||||
icon={<Trash2 color="red" size={15}/>}
|
text=""
|
||||||
onClick={async () => {
|
icon={<Trash2 color="red" size={15}/>}
|
||||||
await mutation.mutateAsync();
|
onClick={async () => {
|
||||||
}}
|
await mutation.mutateAsync();
|
||||||
size="sm"
|
}}
|
||||||
/>
|
size="sm"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ export const AgentCard = (props: agentCardProps) => {
|
|||||||
const { data: agent } = props;
|
const { data: agent } = props;
|
||||||
|
|
||||||
return (
|
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">
|
<Card className="flex flex-row justify-between">
|
||||||
<div className="flex-1 text-left">
|
<div className="flex-1 text-left">
|
||||||
<CardHeader className="text-2xl font-bold">{agent.name}</CardHeader>
|
<CardHeader className="text-2xl font-bold">{agent.name}</CardHeader>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const LoggedInDropdown = (props: LoggedInDropdownProps) => {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>{props.children}</DropdownMenuTrigger>
|
<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
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
redirect("/dashboard/profile");
|
redirect("/dashboard/profile");
|
||||||
|
|||||||
@@ -29,11 +29,12 @@ export const SidebarMenuCustomMain = () => {
|
|||||||
const groupContent: SidebarGroupItem["group_content"] = [
|
const groupContent: SidebarGroupItem["group_content"] = [
|
||||||
{ title: "Projects", url: "/projects", icon: Layers, details:true },
|
{ title: "Projects", url: "/projects", icon: Layers, details:true },
|
||||||
{ title: "Statistics", url: "/statistics", icon: ChartArea },
|
{ title: "Statistics", url: "/statistics", icon: ChartArea },
|
||||||
|
{ title: "Settings", url: "/settings", icon: Settings, details:true }
|
||||||
];
|
];
|
||||||
|
|
||||||
if (activeOrganization && (member?.data?.role === "admin" || member?.data?.role === "owner")) {
|
// if (activeOrganization && (member?.data?.role === "admin" || member?.data?.role === "owner")) {
|
||||||
groupContent.push({ title: "Settings", url: "/settings", icon: Settings, details:true });
|
// groupContent.push({ title: "Settings", url: "/settings", icon: Settings, details:true });
|
||||||
}
|
// }
|
||||||
|
|
||||||
const items: SidebarGroupItem[] = [
|
const items: SidebarGroupItem[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export function CreateOrganizationModal({open, onOpenChange, onSuccess}: createO
|
|||||||
await authClient.organization.setActive({organizationSlug: result.data.value.slug});
|
await authClient.organization.setActive({organizationSlug: result.data.value.slug});
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
toast.success(result.data.actionSuccess?.message || "Organization Created.");
|
toast.success(result.data.actionSuccess?.message || "Organization Created.");
|
||||||
// router.push("/");
|
|
||||||
router.replace(`/dashboard/home`);
|
router.replace(`/dashboard/home`);
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -82,26 +81,6 @@ export function CreateOrganizationModal({open, onOpenChange, onSuccess}: createO
|
|||||||
</FormItem>
|
</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>
|
<DialogFooter>
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
<Button type="submit">Create</Button>
|
<Button type="submit">Create</Button>
|
||||||
|
|||||||
@@ -39,5 +39,6 @@ export function OrganizationCombobox() {
|
|||||||
|
|
||||||
return <>{state === "expanded" &&
|
return <>{state === "expanded" &&
|
||||||
<ComboBox sideBar values={values} defaultValue={activeOrganization?.slug} onValueChange={onValueChange}
|
<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({
|
.set({
|
||||||
isArchived: true,
|
isArchived: true,
|
||||||
slug: uuid,
|
slug: uuid,
|
||||||
|
name: uuid,
|
||||||
})
|
})
|
||||||
.where(eq(drizzleDb.schemas.project.id, parsedInput))
|
.where(eq(drizzleDb.schemas.project.id, parsedInput))
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {useMutation} from "@tanstack/react-query";
|
|||||||
import {deleteBackupAction} from "@/features/dashboard/restore/restore.action";
|
import {deleteBackupAction} from "@/features/dashboard/restore/restore.action";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
|
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||||
|
|
||||||
|
|
||||||
type DatabaseBackupListProps = {
|
type DatabaseBackupListProps = {
|
||||||
@@ -19,6 +20,7 @@ type DatabaseBackupListProps = {
|
|||||||
settings: Setting;
|
settings: Setting;
|
||||||
database: DatabaseWith;
|
database: DatabaseWith;
|
||||||
backups: Backup[];
|
backups: Backup[];
|
||||||
|
activeMember: MemberWithUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -69,7 +71,7 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
|||||||
backupId: backup.id,
|
backupId: backup.id,
|
||||||
databaseId: backup.databaseId,
|
databaseId: backup.databaseId,
|
||||||
status: backup.status,
|
status: backup.status,
|
||||||
file: backup.file!,
|
file: backup.file ?? "",
|
||||||
projectSlug: props.database?.project?.slug!
|
projectSlug: props.database?.project?.slug!
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -97,50 +99,58 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isMember = props.activeMember.role === "member";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={backupColumns(props.isAlreadyRestore, props.settings, props.database)}
|
enableSelect={!isMember}
|
||||||
|
columns={backupColumns(props.isAlreadyRestore, props.settings, props.database, props.activeMember)}
|
||||||
data={filteredBackups}
|
data={filteredBackups}
|
||||||
enablePagination
|
enablePagination
|
||||||
selectedActions={(rows) => (
|
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={() => {
|
|
||||||
|
|
||||||
}}
|
<div className="flex justify-start md:justify-between gap-3 md:gap-0 items-center w-full ml-0">
|
||||||
disabled={rows.length === 0 || mutationDeleteBackups.isPending}
|
<div className="flex gap-2">
|
||||||
icon={<MoreHorizontal/>}
|
{!isMember && (
|
||||||
isPending={mutationDeleteBackups.isPending}
|
<DropdownMenu>
|
||||||
size="sm"
|
<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>
|
</div>
|
||||||
<DropdownMenuContent align="start">
|
</div>
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import {useMutation} from "@tanstack/react-query";
|
|||||||
import {deleteRestoreAction} from "@/features/dashboard/restore/restore.action";
|
import {deleteRestoreAction} from "@/features/dashboard/restore/restore.action";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
|
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||||
|
|
||||||
|
|
||||||
type DatabaseRestoreListProps = {
|
type DatabaseRestoreListProps = {
|
||||||
isAlreadyRestore: boolean;
|
isAlreadyRestore: boolean;
|
||||||
restorations: Restoration[];
|
restorations: Restoration[];
|
||||||
|
activeMember: MemberWithUser
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
|
export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
|
||||||
@@ -47,40 +49,46 @@ export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
|
|||||||
router.refresh();
|
router.refresh();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const isMember = props.activeMember.role === "member";
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={restoreColumns(props.isAlreadyRestore)}
|
enableSelect={!isMember}
|
||||||
|
columns={restoreColumns(props.isAlreadyRestore, props.activeMember)}
|
||||||
data={props.restorations}
|
data={props.restorations}
|
||||||
enablePagination
|
enablePagination
|
||||||
selectedActions={(rows) => (
|
selectedActions={(rows) => (
|
||||||
<DropdownMenu>
|
<>
|
||||||
<DropdownMenuTrigger asChild>
|
{!isMember && (
|
||||||
<ButtonWithLoading
|
<DropdownMenu>
|
||||||
variant="outline"
|
<DropdownMenuTrigger asChild>
|
||||||
text="Actions"
|
<ButtonWithLoading
|
||||||
onClick={() => {
|
variant="outline"
|
||||||
}}
|
text="Actions"
|
||||||
disabled={rows.length === 0 || mutationDeleteRestorations.isPending}
|
onClick={() => {
|
||||||
icon={<MoreHorizontal/>}
|
}}
|
||||||
isPending={mutationDeleteRestorations.isPending}
|
disabled={rows.length === 0 || mutationDeleteRestorations.isPending}
|
||||||
size="sm"
|
icon={<MoreHorizontal/>}
|
||||||
/>
|
isPending={mutationDeleteRestorations.isPending}
|
||||||
</DropdownMenuTrigger>
|
size="sm"
|
||||||
<DropdownMenuContent align="start">
|
/>
|
||||||
<DropdownMenuItem
|
</DropdownMenuTrigger>
|
||||||
onClick={async () => {
|
<DropdownMenuContent align="start">
|
||||||
await mutationDeleteRestorations.mutateAsync(rows)
|
<DropdownMenuItem
|
||||||
}}
|
onClick={async () => {
|
||||||
disabled={props.isAlreadyRestore}
|
await mutationDeleteRestorations.mutateAsync(rows)
|
||||||
className="text-red-600 focus:text-red-700"
|
}}
|
||||||
>
|
disabled={props.isAlreadyRestore}
|
||||||
<Trash2 className="w-4 h-4 mr-2"/>
|
className="text-red-600 focus:text-red-700"
|
||||||
Delete Selected
|
>
|
||||||
</DropdownMenuItem>
|
<Trash2 className="w-4 h-4 mr-2"/>
|
||||||
</DropdownMenuContent>
|
Delete Selected
|
||||||
</DropdownMenu>
|
</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 {Setting} from "@/db/schema/01_setting";
|
||||||
import {DatabaseBackupList} from "@/components/wrappers/dashboard/projects/database/database-backup-list";
|
import {DatabaseBackupList} from "@/components/wrappers/dashboard/projects/database/database-backup-list";
|
||||||
import {DatabaseRestoreList} from "@/components/wrappers/dashboard/projects/database/database-restore-list";
|
import {DatabaseRestoreList} from "@/components/wrappers/dashboard/projects/database/database-restore-list";
|
||||||
|
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||||
|
|
||||||
export type DatabaseTabsProps = {
|
export type DatabaseTabsProps = {
|
||||||
settings: Setting
|
settings: Setting,
|
||||||
backups: Backup[];
|
backups: Backup[],
|
||||||
restorations: Restoration[];
|
restorations: Restoration[],
|
||||||
isAlreadyRestore: boolean;
|
isAlreadyRestore: boolean,
|
||||||
database: DatabaseWith;
|
database: DatabaseWith,
|
||||||
|
activeMember: MemberWithUser
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DatabaseTabs = (props: DatabaseTabsProps) => {
|
export const DatabaseTabs = (props: DatabaseTabsProps) => {
|
||||||
@@ -58,12 +60,14 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
|
|||||||
settings={props.settings}
|
settings={props.settings}
|
||||||
database={props.database}
|
database={props.database}
|
||||||
backups={props.backups}
|
backups={props.backups}
|
||||||
|
activeMember={props.activeMember}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent className="h-full justify-between" value="restore">
|
<TabsContent className="h-full justify-between" value="restore">
|
||||||
<DatabaseRestoreList
|
<DatabaseRestoreList
|
||||||
isAlreadyRestore={props.isAlreadyRestore}
|
isAlreadyRestore={props.isAlreadyRestore}
|
||||||
restorations={props.restorations}
|
restorations={props.restorations}
|
||||||
|
activeMember={props.activeMember}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const ProjectCard = (props: projectCardProps) => {
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`/dashboard/projects/${project.id}`}
|
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">
|
<Card className="flex flex-row justify-between">
|
||||||
<div className="flex-1 text-left">
|
<div className="flex-1 text-left">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
|
|||||||
const { organizationSlug, data: database, extendedProps: extendedProps } = props;
|
const { organizationSlug, data: database, extendedProps: extendedProps } = props;
|
||||||
|
|
||||||
return (
|
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} />
|
<DatabaseCard data={database} />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ProjectSchema } from "@/components/wrappers/dashboard/projects/project-
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ServerActionResult } from "@/types/action-type";
|
import { ServerActionResult } from "@/types/action-type";
|
||||||
import { db } from "@/db";
|
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 {Project} from "@/db/schema/06_project";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {Database} from "@/db/schema/07_database";
|
import {Database} from "@/db/schema/07_database";
|
||||||
@@ -21,6 +21,22 @@ export const createProjectAction = userAction
|
|||||||
.action(async ({ parsedInput }): Promise<ServerActionResult<Project>> => {
|
.action(async ({ parsedInput }): Promise<ServerActionResult<Project>> => {
|
||||||
try {
|
try {
|
||||||
const slug = slugify(parsedInput.data.name);
|
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
|
const [createdProject] = await db
|
||||||
.insert(drizzleDb.schemas.project)
|
.insert(drizzleDb.schemas.project)
|
||||||
.values({
|
.values({
|
||||||
@@ -31,7 +47,10 @@ export const createProjectAction = userAction
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (parsedInput.data.databases.length > 0) {
|
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 {
|
return {
|
||||||
@@ -43,6 +62,7 @@ export const createProjectAction = userAction
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
actionError: {
|
actionError: {
|
||||||
@@ -55,6 +75,9 @@ export const createProjectAction = userAction
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const updateProjectAction = userAction
|
export const updateProjectAction = userAction
|
||||||
.schema(
|
.schema(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {ColumnDef} from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
import { MemberWithUser } from "@/db/schema/03_organization";
|
||||||
import {useState} from "react";
|
import { useState } from "react";
|
||||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
import { authClient, useSession } from "@/lib/auth/auth-client";
|
||||||
import {useMutation} from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import {toast} from "sonner";
|
import { toast } from "sonner";
|
||||||
import {Badge} from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {updateMemberRoleAction} from "@/components/wrappers/dashboard/settings/update-member.action";
|
import {
|
||||||
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
|
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>[] = [
|
export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
|
||||||
{
|
{
|
||||||
@@ -18,7 +23,7 @@ export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const [role, setRole] = useState<string>(row.getValue("role"));
|
const [role, setRole] = useState<string>(row.getValue("role"));
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
const activeOrgaMember = authClient.useActiveMember();
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
@@ -28,36 +33,59 @@ export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
|
|||||||
role: RoleSchemaMember.parse(role),
|
role: RoleSchemaMember.parse(role),
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(`User updated successfully.`);
|
toast.success("User updated successfully.");
|
||||||
},
|
},
|
||||||
onError: () => {
|
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 handleUpdateRole = async () => {
|
||||||
const nextRole =
|
const nextRole = role === "admin" ? "member" : "admin";
|
||||||
role === "owner" ? "admin" : role === "admin" ? "member" : "owner";
|
|
||||||
setRole(nextRole);
|
setRole(nextRole);
|
||||||
await updateMutation.mutateAsync();
|
await updateMutation.mutateAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const isCurrentUser = session?.user.email === row.original.user.email;
|
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
|
<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}
|
onClick={isDisabled ? undefined : handleUpdateRole}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{role}
|
{role}
|
||||||
</Badge>
|
</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",
|
accessorKey: "user.email",
|
||||||
header: "Email",
|
header: "Email",
|
||||||
}
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
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 {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";
|
import {organizationMemberColumns} from "@/components/wrappers/dashboard/settings/columns-organization-members";
|
||||||
|
|
||||||
|
|
||||||
@@ -11,12 +9,12 @@ interface SettingsOrganizationMembersTableProps {
|
|||||||
|
|
||||||
export const SettingsOrganizationMembersTable = ({organization}: SettingsOrganizationMembersTableProps) => {
|
export const SettingsOrganizationMembersTable = ({organization}: SettingsOrganizationMembersTableProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full py-4">
|
<div className="flex flex-col h-full ">
|
||||||
<div className="flex gap-4 h-fit justify-between">
|
<div className=" h-full">
|
||||||
<h1>List of Organization members</h1>
|
<DataTable
|
||||||
</div>
|
columns={organizationMemberColumns}
|
||||||
<div className="mt-5 h-full">
|
enableSelect={false}
|
||||||
<DataTable columns={organizationMemberColumns} data={organization.members as MemberWithUser[]}/>
|
data={organization.members as MemberWithUser[]}/>
|
||||||
</div>
|
</div>
|
||||||
</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,
|
"when": 1756473637441,
|
||||||
"tag": "0004_dazzling_hawkeye",
|
"tag": "0004_dazzling_hawkeye",
|
||||||
"breakpoints": true
|
"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 {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 {organization} from "@/db/schema/03_organization";
|
||||||
import {relations} from "drizzle-orm";
|
import {relations} from "drizzle-orm";
|
||||||
import {createSelectSchema} from "drizzle-zod";
|
import {createSelectSchema} from "drizzle-zod";
|
||||||
@@ -33,3 +33,4 @@ export const memberRelations = relations(member, ({ one }) => ({
|
|||||||
|
|
||||||
export const organizationMemberSchema = createSelectSchema(member);
|
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 {pgTable, text, boolean, uuid} from "drizzle-orm/pg-core";
|
||||||
import { relations } from "drizzle-orm";
|
import {relations} from "drizzle-orm";
|
||||||
import { Organization, organization } from "./03_organization";
|
import {Organization, organization} from "./03_organization";
|
||||||
import { createSelectSchema } from "drizzle-zod";
|
import {createSelectSchema} from "drizzle-zod";
|
||||||
import { z } from "zod";
|
import {z} from "zod";
|
||||||
import { Database, database } from "./07_database";
|
import {Database, database} from "./07_database";
|
||||||
import {timestamps} from "@/db/schema/00_common";
|
import {timestamps} from "@/db/schema/00_common";
|
||||||
|
|
||||||
export const project = pgTable("projects", {
|
export const project = pgTable("projects", {
|
||||||
@@ -13,12 +13,11 @@ export const project = pgTable("projects", {
|
|||||||
isArchived: boolean("is_archived").default(false),
|
isArchived: boolean("is_archived").default(false),
|
||||||
organizationId: uuid("organization_id")
|
organizationId: uuid("organization_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => organization.id),
|
.references(() => organization.id, {onDelete: "cascade"}),
|
||||||
...timestamps
|
...timestamps
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const projectRelations = relations(project, ({ one, many }) => ({
|
export const projectRelations = relations(project, ({one, many}) => ({
|
||||||
organization: one(organization, {
|
organization: one(organization, {
|
||||||
fields: [project.organizationId],
|
fields: [project.organizationId],
|
||||||
references: [organization.id],
|
references: [organization.id],
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const env = createEnv({
|
|||||||
|
|
||||||
AUTH_GOOGLE_ID: z.string().optional(),
|
AUTH_GOOGLE_ID: z.string().optional(),
|
||||||
AUTH_GOOGLE_SECRET: z.string().optional(),
|
AUTH_GOOGLE_SECRET: z.string().optional(),
|
||||||
|
NEXT_PUBLIC_GOOGLE_AUTH: z.boolean().default(false).optional(),
|
||||||
|
|
||||||
S3_ENDPOINT: z.string().optional(),
|
S3_ENDPOINT: z.string().optional(),
|
||||||
S3_ACCESS_KEY: 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_DESCRIPTION: z.string().optional(),
|
||||||
NEXT_PUBLIC_PROJECT_URL: z.string().optional(),
|
NEXT_PUBLIC_PROJECT_URL: z.string().optional(),
|
||||||
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
|
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
|
||||||
|
|
||||||
|
NEXT_PUBLIC_GOOGLE_AUTH: z.boolean().default(false).optional(),
|
||||||
},
|
},
|
||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
NEXT_PUBLIC_PROJECT_NAME: process.env.NEXT_PUBLIC_PROJECT_NAME,
|
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_ID: process.env.AUTH_GOOGLE_ID,
|
||||||
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
|
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_ENDPOINT: process.env.S3_ENDPOINT,
|
||||||
S3_ACCESS_KEY: process.env.S3_ACCESS_KEY,
|
S3_ACCESS_KEY: process.env.S3_ACCESS_KEY,
|
||||||
|
|||||||
@@ -30,9 +30,15 @@ import {ZodString} from "zod";
|
|||||||
import {ServerActionResult} from "@/types/action-type";
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
import {cn} from "@/lib/utils";
|
import {cn} from "@/lib/utils";
|
||||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
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 [
|
return [
|
||||||
{
|
{
|
||||||
id: "availability",
|
id: "availability",
|
||||||
@@ -136,7 +142,7 @@ export function backupColumns(isAlreadyRestore: boolean, settings: Setting, data
|
|||||||
}, readonly [], ServerActionResult<string>, object> | undefined
|
}, readonly [], ServerActionResult<string>, object> | undefined
|
||||||
|
|
||||||
if (settings.storage == "local") {
|
if (settings.storage == "local") {
|
||||||
data = await getFileUrlPresignedLocal({fileName:fileName!})
|
data = await getFileUrlPresignedLocal({fileName: fileName!})
|
||||||
} else if (settings.storage == "s3") {
|
} else if (settings.storage == "s3") {
|
||||||
data = await getFileUrlPreSignedS3Action(`backups/${database.project?.slug}/${fileName}`);
|
data = await getFileUrlPreSignedS3Action(`backups/${database.project?.slug}/${fileName}`);
|
||||||
}
|
}
|
||||||
@@ -154,8 +160,7 @@ export function backupColumns(isAlreadyRestore: boolean, settings: Setting, data
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{rowData.deletedAt == null && (
|
{(rowData.deletedAt == null && activeMember.role != "member") && (
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
|||||||
@@ -24,9 +24,13 @@ import {
|
|||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom";
|
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[
|
return[
|
||||||
{
|
{
|
||||||
accessorKey: "id",
|
accessorKey: "id",
|
||||||
@@ -102,39 +106,43 @@ export function restoreColumns(isAlreadyRestore: boolean): ColumnDef<Restoration
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<>
|
||||||
<DropdownMenuTrigger asChild>
|
{activeMember.role != "member" && (
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
<DropdownMenu>
|
||||||
<span className="sr-only">Open menu</span>
|
<DropdownMenuTrigger asChild>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
</Button>
|
<span className="sr-only">Open menu</span>
|
||||||
</DropdownMenuTrigger>
|
<MoreHorizontal className="h-4 w-4"/>
|
||||||
<DropdownMenuContent align="end">
|
</Button>
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
</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
|
||||||
<DropdownMenuItem
|
disabled={status == "waiting"}
|
||||||
disabled={mutationRerunRestore.isPending || isAlreadyRestore}
|
className="text-red-600"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await handleRerunRestore();
|
await handleDelete();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ReloadIcon/> Rerun
|
<Trash2/> Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</TooltipCustom>
|
</DropdownMenuContent>
|
||||||
<DropdownMenuSeparator/>
|
</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 {ac, admin, orgAdmin, orgMember, orgOwner, pending, superadmin, user} from "@/lib/auth/permissions";
|
||||||
import {headers} from "next/headers";
|
import {headers} from "next/headers";
|
||||||
import {count, eq} from "drizzle-orm";
|
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({
|
export const auth = betterAuth({
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
@@ -455,7 +455,7 @@ export const getActiveMember = async () => {
|
|||||||
});
|
});
|
||||||
console.log(member);
|
console.log(member);
|
||||||
|
|
||||||
return member;
|
return member as MemberWithUser;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("err", e);
|
console.log("err", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const statement = {
|
|||||||
|
|
||||||
const ac = createAccessControl(statement);
|
const ac = createAccessControl(statement);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const superadmin = ac.newRole({
|
const superadmin = ac.newRole({
|
||||||
project: ["create", "list", "update", "delete"],
|
project: ["create", "list", "update", "delete"],
|
||||||
database: ["create", "list", "update", "delete"],
|
database: ["create", "list", "update", "delete"],
|
||||||
|
|||||||
Reference in New Issue
Block a user