Merge branch 'main' into feat/github-updater

# Conflicts:
#	CITATION.cff
#	package.json
This commit is contained in:
killian-larcher
2026-01-28 19:14:33 +01:00
33 changed files with 333 additions and 328 deletions
@@ -1,32 +0,0 @@
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { AgentForm } from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
import { notFound } from "next/navigation";
import { db } from "@/db";
export default async function RoutePage(
props: PageParams<{
agentId: string;
}>
) {
const { agentId } = await props.params;
const agent = await db.query.agent.findFirst({
where: (fields, { eq }) => eq(fields.id, agentId),
});
if (!agent) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>Edit {agent.name}</PageTitle>
</PageHeader>
<PageContent>
<AgentForm defaultValues={agent} agentId={agent.id} />
</PageContent>
</Page>
);
}
@@ -12,6 +12,8 @@ import {capitalizeFirstLetter} from "@/utils/text";
import {generateEdgeKey} from "@/utils/edge_key"; import {generateEdgeKey} from "@/utils/edge_key";
import {getServerUrl} from "@/utils/get-server-url"; import {getServerUrl} from "@/utils/get-server-url";
import {AgentContentPage} from "@/components/wrappers/dashboard/agent/agent-content"; import {AgentContentPage} from "@/components/wrappers/dashboard/agent/agent-content";
import {AgentDialog} from "@/features/agents/components/agent.dialog";
import {AgentType} from "@/features/agents/agents.schema";
export default async function RoutePage(props: PageParams<{ agentId: string }>) { export default async function RoutePage(props: PageParams<{ agentId: string }>) {
@@ -39,10 +41,11 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
</div> </div>
<div className="flex items-center gap-2 md:justify-between w-full "> <div className="flex items-center gap-2 md:justify-between w-full ">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Link className={buttonVariants({variant: "outline"})} <AgentDialog agent={agent as AgentType & { id: string }}>
href={`/dashboard/agents/${agent.id}/edit`}> <div className={buttonVariants({variant: "outline", className: "cursor-pointer"})}>
<GearIcon className="w-7 h-7"/> <GearIcon className="w-7 h-7"/>
</Link> </div>
</AgentDialog>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/> <ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
@@ -1,21 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {AgentForm} from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
import {Metadata} from "next";
export const metadata: Metadata = {
title: "Create Agent",
};
export default async function RoutePage(props: PageParams<{}>) {
return (
<Page>
<PageHeader>
<PageTitle>Create new agent</PageTitle>
</PageHeader>
<PageContent>
<AgentForm/>
</PageContent>
</Page>
);
}
@@ -2,7 +2,6 @@ import {PageParams} from "@/types/next";
import {AgentCard} from "@/components/wrappers/dashboard/agent/agent-card/agent-card"; import {AgentCard} from "@/components/wrappers/dashboard/agent/agent-card/agent-card";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {Button} from "@/components/ui/button"; import {Button} from "@/components/ui/button";
import Link from "next/link";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation"; import {notFound} from "next/navigation";
import {db} from "@/db"; import {db} from "@/db";
@@ -10,6 +9,7 @@ import * as drizzleDb from "@/db";
import {desc, eq, not} from "drizzle-orm"; import {desc, eq, not} from "drizzle-orm";
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder"; import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
import {Metadata} from "next"; import {Metadata} from "next";
import {AgentDialog} from "@/features/agents/components/agent.dialog";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Agents", title: "Agents",
@@ -36,9 +36,9 @@ export default async function RoutePage(props: PageParams<{}>) {
<PageTitle>Agents</PageTitle> <PageTitle>Agents</PageTitle>
{agents.length > 0 && ( {agents.length > 0 && (
<PageActions> <PageActions>
<Link href={"/dashboard/agents/new"}> <AgentDialog>
<Button>+ Create Agent</Button> <Button>+ Create Agent</Button>
</Link> </AgentDialog>
</PageActions> </PageActions>
)} )}
</PageHeader> </PageHeader>
@@ -46,12 +46,13 @@ export default async function RoutePage(props: PageParams<{}>) {
{agents.length > 0 ? ( {agents.length > 0 ? (
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1}/> <CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1}/>
) : ( ) : (
<EmptyStatePlaceholder <AgentDialog>
url={"/dashboard/agents/new"} <EmptyStatePlaceholder
text={"Create new Agent"} text={"Create new Agent"}
/> />
</AgentDialog>
)} )}
</PageContent> </PageContent>
</Page> </Page>
); );
} }
@@ -1,65 +0,0 @@
import {notFound} from "next/navigation";
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import {DatabaseWith} from "@/db/schema/07_database";
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
const {projectId} = await props.params;
const proj = await db.query.project.findFirst({
where: eq(drizzleDb.schemas.project.id, projectId),
with: {
databases: true,
},
});
if (!proj) {
notFound();
}
const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, "default"),
});
if (!org) {
notFound();
}
const availableDatabases = (
await db.query.database.findMany({
where: (db, {or, eq, isNull}) => or(isNull(db.projectId), eq(db.projectId, proj.id)),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, {desc}) => [desc(db.createdAt)],
})
)
// .filter((db) => db.project !== null) as DatabaseWith[];
return (
<Page>
<PageHeader>
<PageTitle>Edit {proj.name}</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm
organization={org}
databases={availableDatabases as DatabaseWith[]}
defaultValues={{...proj, databases: proj.databases.map((db) => db.id)}}
projectId={proj.id}
/>
</PageContent>
</Page>
);
}
@@ -1,8 +1,7 @@
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page"; import {Page, PageContent, PageTitle} from "@/features/layout/page";
import {buttonVariants} from "@/components/ui/button"; import {buttonVariants} from "@/components/ui/button";
import {GearIcon} from "@radix-ui/react-icons"; import {GearIcon} from "@radix-ui/react-icons";
import Link from "next/link";
import { import {
ButtonDeleteProject ButtonDeleteProject
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project"; } from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
@@ -15,6 +14,9 @@ import {eq} from "drizzle-orm";
import {getActiveMember, 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";
import {ProjectDialog} from "@/features/projects/components/project.dialog";
import {DatabaseWith} from "@/db/schema/07_database";
import {ProjectWith} from "@/db/schema/06_project";
export default async function RoutePage(props: PageParams<{ export default async function RoutePage(props: PageParams<{
@@ -51,6 +53,19 @@ export default async function RoutePage(props: PageParams<{
redirect("/dashboard/projects"); redirect("/dashboard/projects");
} }
const availableDatabases = (
await db.query.database.findMany({
where: (db, {or, eq, isNull}) => or(isNull(db.projectId), eq(db.projectId, proj.id)),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, {desc}) => [desc(db.createdAt)],
})
) as DatabaseWith[];
const isMember = activeMember?.role === "member"; const isMember = activeMember?.role === "member";
return ( return (
@@ -63,10 +78,15 @@ export default async function RoutePage(props: PageParams<{
{!isMember && ( {!isMember && (
<div className="flex items-center gap-2 md:justify-between w-full "> <div className="flex items-center gap-2 md:justify-between w-full ">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Link className={buttonVariants({variant: "outline"})} <ProjectDialog
href={`/dashboard/projects/${proj.id}/edit`}> databases={availableDatabases}
<GearIcon className="w-7 h-7"/> organization={org}
</Link> project={proj as ProjectWith}
>
<div className={buttonVariants({variant: "outline", className: "cursor-pointer"})}>
<GearIcon className="w-7 h-7"/>
</div>
</ProjectDialog>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/> <ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
@@ -95,4 +115,4 @@ export default async function RoutePage(props: PageParams<{
</PageContent> </PageContent>
</Page> </Page>
); );
} }
@@ -1,50 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
import {notFound} from "next/navigation";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import {getOrganization} from "@/lib/auth/auth";
import * as drizzleDb from "@/db";
import {DatabaseWith} from "@/db/schema/07_database";
export default async function RoutePage(props: PageParams<{}>) {
const organization = await getOrganization({});
if (!organization) {
notFound();
}
const availableDatabases = (
await db.query.database.findMany({
where: (db, {isNull}) => isNull(db.projectId),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, {desc}) => [desc(db.createdAt)],
})
).filter((db) => db.project == null) as DatabaseWith[];
const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
});
if (!org) notFound();
return (
<Page>
<PageHeader>
<PageTitle>Create new project</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm databases={availableDatabases} organization={org}/>
</PageContent>
</Page>
);
}
@@ -1,5 +1,3 @@
import Link from "next/link";
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {Button} from "@/components/ui/button"; import {Button} from "@/components/ui/button";
@@ -10,6 +8,8 @@ import {notFound} from "next/navigation";
import {getActiveMember, 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";
import {Metadata} from "next"; import {Metadata} from "next";
import {ProjectDialog} from "@/features/projects/components/project.dialog";
import {DatabaseWith} from "@/db/schema/07_database";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Projects", title: "Projects",
@@ -36,6 +36,19 @@ export default async function RoutePage(props: PageParams<{}>) {
}); });
const isMember = activeMember?.role === "member"; const isMember = activeMember?.role === "member";
const availableDatabases = (
await db.query.database.findMany({
where: (db, {isNull}) => isNull(db.projectId),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, {desc}) => [desc(db.createdAt)],
})
).filter((db) => db.project == null) as DatabaseWith[];
return ( return (
<Page> <Page>
@@ -43,9 +56,9 @@ export default async function RoutePage(props: PageParams<{}>) {
<PageTitle>Projects</PageTitle> <PageTitle>Projects</PageTitle>
{(projects.length > 0 && !isMember) && ( {(projects.length > 0 && !isMember) && (
<PageActions> <PageActions>
<Link href={`/dashboard/projects/new`}> <ProjectDialog databases={availableDatabases} organization={organization}>
<Button>+ Create Project</Button> <Button>+ Create Project</Button>
</Link> </ProjectDialog>
</PageActions> </PageActions>
)} )}
</PageHeader> </PageHeader>
@@ -62,9 +75,11 @@ export default async function RoutePage(props: PageParams<{}>) {
) : isMember ? ( ) : isMember ? (
<EmptyStatePlaceholder text="No project available"/> <EmptyStatePlaceholder text="No project available"/>
) : ( ) : (
<EmptyStatePlaceholder url="/dashboard/projects/new" text="Create new Project"/> <ProjectDialog databases={availableDatabases} organization={organization}>
<EmptyStatePlaceholder text="Create new Project"/>
</ProjectDialog>
)} )}
</PageContent> </PageContent>
</Page> </Page>
); );
} }
@@ -1,37 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/organization-form/organization-form";
import {getOrganization} from "@/lib/auth/auth";
import {db} from "@/db";
import {isNull} from "drizzle-orm";
import {currentUser} from "@/lib/auth/current-user";
export default async function RoutePage(props: PageParams<{
}>) {
const user = await currentUser();
const organization = await getOrganization({});
const users = await db.query.user.findMany({
where: (fields) => isNull(fields.deletedAt)
});
if (!user || !users || !organization || organization.slug == "default") {
notFound();
}
return(
<Page>
<PageHeader>
<PageTitle>
Edit {organization.name}
</PageTitle>
</PageHeader>
<PageContent>
<OrganizationForm currentUser={user} users={users} defaultValues={organization}/>
</PageContent>
</Page>
)
}
@@ -9,9 +9,13 @@ import {getOrganizationChannels} from "@/db/services/notification-channel";
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl"; import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
import {getOrganizationStorageChannels} from "@/db/services/storage-channel"; import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
import {DeleteOrganizationButton} from "@/components/wrappers/dashboard/organization/delete-organization-button"; import {DeleteOrganizationButton} from "@/components/wrappers/dashboard/organization/delete-organization-button";
import { import {EditOrganizationDialog} from "@/features/organization/components/edit-organization.dialog";
EditButtonSettings import {Button} from "@/components/ui/button";
} from "@/components/wrappers/dashboard/organization/settings/edit-button-settings/edit-button-settings"; import {GearIcon} from "@radix-ui/react-icons";
import {db} from "@/db";
import {isNull} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {eq} from "drizzle-orm";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Settings", title: "Settings",
@@ -22,7 +26,7 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
const user = await currentUser(); const user = await currentUser();
const activeMember = await getActiveMember() const activeMember = await getActiveMember()
if (!organization || !activeMember) { if (!organization || !activeMember || !user) {
notFound(); notFound();
} }
@@ -30,6 +34,30 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
const storageChannels = await getOrganizationStorageChannels(organization.id) const storageChannels = await getOrganizationStorageChannels(organization.id)
const permissions = computeOrganizationPermissions(activeMember); const permissions = computeOrganizationPermissions(activeMember);
// Fetch users for the form
const users = await db.query.user.findMany({
where: (fields) => isNull(fields.deletedAt)
});
// Re-fetch organization with members to match type expectations if needed,
// although getOrganization returns OrganizationWithMembers usually, let's verify or cast.
// getOrganization returns Organization type from database schema, which includes relations if defined in auth lib.
// However, OrganizationForm expects OrganizationWithMembers.
// Let's ensure we have members.
const organizationWithMembers = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.id, organization.id),
with: {
members: {
with: {
user: true
}
}
}
});
if (!organizationWithMembers) notFound();
return ( return (
<Page> <Page>
@@ -41,7 +69,15 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
<div className="flex items-center gap-2 md:justify-between w-full "> <div className="flex items-center gap-2 md:justify-between w-full ">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{permissions.canManageSettings && organization.slug !== "default" && ( {permissions.canManageSettings && organization.slug !== "default" && (
<EditButtonSettings/> <EditOrganizationDialog
organization={organizationWithMembers}
users={users}
currentUser={user}
>
<Button variant="outline">
<GearIcon className="w-7 h-7"/>
</Button>
</EditOrganizationDialog>
)} )}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -62,4 +98,4 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
</PageContent> </PageContent>
</Page> </Page>
) )
} }
+1
View File
@@ -1,6 +1,7 @@
{ {
"name": "portabase", "name": "portabase",
"version": "1.2.4-rc.8", "version": "1.2.4-rc.8",
"version": "1.2.4-rc.5",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack -p 8887", "dev": "next dev --turbopack -p 8887",
+1 -1
View File
@@ -58,7 +58,7 @@ function DialogContent({
data-slot="dialog-content" data-slot="dialog-content"
className={cn( className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg", "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
"max-h-[90vh] overflow-y-auto", "max-h-[90vh] overflow-y-auto overflow-x-hidden",
className className
)} )}
{...props} {...props}
@@ -1,20 +1,22 @@
import Link from "next/link"; import Link from "next/link";
import {cn} from "@/lib/utils"; import {cn} from "@/lib/utils";
import {Plus} from "lucide-react"; import {Plus} from "lucide-react";
import {forwardRef} from "react";
type EmptyStatePlaceholderProps = { type EmptyStatePlaceholderProps = {
url?: string; url?: string;
onClick?: () => void; onClick?: () => void;
text: string; text: string;
className?: string; className?: string;
}; } & React.HTMLAttributes<HTMLDivElement>;
export const EmptyStatePlaceholder = ({ export const EmptyStatePlaceholder = forwardRef<HTMLDivElement, EmptyStatePlaceholderProps>(({
url, url,
onClick, onClick,
text, text,
className, className,
}: EmptyStatePlaceholderProps) => { ...props
}, ref) => {
const Container = ( const Container = (
<div <div
className={cn( className={cn(
@@ -37,5 +39,7 @@ export const EmptyStatePlaceholder = ({
); );
} }
return <div className={cn(className)}>{Container}</div>; return <div className={cn(className)} ref={ref} {...props}>{Container}</div>;
}; });
EmptyStatePlaceholder.displayName = "EmptyStatePlaceholder";
@@ -15,7 +15,7 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
* Variants for the multi-select component to handle different styles. * Variants for the multi-select component to handle different styles.
* Uses class-variance-authority (cva) to define different styles based on "variant" prop. * Uses class-variance-authority (cva) to define different styles based on "variant" prop.
*/ */
const multiSelectVariants = cva("m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300", { const multiSelectVariants = cva("m-1 transition ease-in-out duration-300", {
variants: { variants: {
variant: { variant: {
default: "border-foreground/10 text-foreground bg-card hover:bg-card/80", default: "border-foreground/10 text-foreground bg-card hover:bg-card/80",
@@ -177,13 +177,13 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
return ( return (
<Badge <Badge
key={value} key={value}
className={cn(isAnimating ? "animate-bounce" : "", multiSelectVariants({ variant }))} className={cn(isAnimating ? "animate-bounce" : "", "max-w-full", multiSelectVariants({ variant }))}
style={{ animationDuration: `${animation}s` }} style={{ animationDuration: `${animation}s` }}
> >
{IconComponent && <IconComponent className="h-4 w-4 mr-2" />} {IconComponent && <IconComponent className="h-4 w-4 mr-2" />}
{option?.label} <span className="truncate">{option?.label}</span>
<XCircle <XCircle
className="ml-2 h-4 w-4 cursor-pointer" className="ml-2 h-4 w-4 cursor-pointer shrink-0"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
toggleOption(value); toggleOption(value);
@@ -196,6 +196,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
<Badge <Badge
className={cn( className={cn(
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent", "bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
"max-w-full",
isAnimating ? "animate-bounce" : "", isAnimating ? "animate-bounce" : "",
multiSelectVariants({ variant }) multiSelectVariants({ variant })
)} )}
@@ -203,7 +204,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
> >
{`+ ${selectedValues.length - maxCount} more`} {`+ ${selectedValues.length - maxCount} more`}
<XCircle <XCircle
className="ml-2 h-4 w-4 cursor-pointer" className="ml-2 h-4 w-4 cursor-pointer shrink-0"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
clearExtraOptions(); clearExtraOptions();
@@ -6,7 +6,7 @@ import {Trash2} from "lucide-react";
import {toast} from "sonner"; import {toast} from "sonner";
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm"; import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
import {authClient} from "@/lib/auth/auth-client"; import {authClient} from "@/lib/auth/auth-client";
import {deleteOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action"; import {deleteOrganizationAction} from "@/features/organization/organization.action";
export type ButtonDeleteFleetProps = { export type ButtonDeleteFleetProps = {
text?: string; text?: string;
@@ -13,7 +13,7 @@ import {Input} from "@/components/ui/input";
import {authClient} from "@/lib/auth/auth-client"; import {authClient} from "@/lib/auth/auth-client";
import {toast} from "sonner"; import {toast} from "sonner";
import {OrganizationWithMembersAndUsers} from "@/db/schema/03_organization"; import {OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
import {updateOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action"; import {updateOrganizationAction} from "@/features/organization/organization.action";
type UpdateOrganizationFormProps = { type UpdateOrganizationFormProps = {
onSuccessAction?: () => void; onSuccessAction?: () => void;
@@ -6,8 +6,8 @@ import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle} from "@/
import {Button} from "@/components/ui/button"; import {Button} from "@/components/ui/button";
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form"; import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
import {Input} from "@/components/ui/input"; import {Input} from "@/components/ui/input";
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema"; import {CreateOrganizationSchema, CreateOrganizationType} from "@/features/organization/organization.schema";
import {createOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action"; import {createOrganizationAction} from "@/features/organization/organization.action";
import {useRouter} from "next/navigation"; import {useRouter} from "next/navigation";
import {useState} from "react"; import {useState} from "react";
import {authClient} from "@/lib/auth/auth-client"; import {authClient} from "@/lib/auth/auth-client";
@@ -32,12 +32,12 @@ export function CreateOrganizationModal({
const router = useRouter(); const router = useRouter();
const form = useZodForm({ const form = useZodForm({
schema: OrganizationSchema, schema: CreateOrganizationSchema,
}); });
const mutation = useMutation({ const mutation = useMutation({
mutationFn: async (values: OrganizationSchema) => { mutationFn: async (values: CreateOrganizationType) => {
const result = await createOrganizationAction(values); const result = await createOrganizationAction(values);
@@ -1,6 +1,6 @@
"use client"; "use client";
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm"; import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
import {deleteOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action"; import {deleteOrganizationAction} from "@/features/organization/organization.action";
import {useMutation} from "@tanstack/react-query"; import {useMutation} from "@tanstack/react-query";
import {useRouter} from "next/navigation"; import {useRouter} from "next/navigation";
import {toast} from "sonner"; import {toast} from "sonner";
@@ -1,14 +0,0 @@
import {z} from "zod";
export const OrganizationFormSchema = z.object({
name: z.string().min(5, 'Name must be at least 5 characters long').max(40, 'Name must be at most 40 characters long'),
slug: z.string()
.regex(/^[a-zA-Z0-9_-]*$/, 'Slug can only contain letters, numbers, underscores, and hyphens')
.min(5, 'Slug must be at least 5 characters long')
.max(20, 'Slug must be at most 20 characters long'),
users: z.array(z.string()),
});
export type OrganizationFormType = z.infer<typeof OrganizationFormSchema>;
@@ -1,12 +0,0 @@
import { z } from "zod";
export const OrganizationSchema = z.object({
name: z.string().min(5, "Name must be at least 5 characters long").max(40, "Name must be at most 40 characters long"),
// slug: z
// .string()
// .regex(/^[a-zA-Z0-9_-]*$/, "Slug can only contain letters, numbers, underscores, and hyphens")
// .min(5, "Slug must be at least 5 characters long")
// .max(20, "Slug must be at most 20 characters long"),
});
export type OrganizationSchema = z.infer<typeof OrganizationSchema>;
@@ -1,20 +0,0 @@
"use client"
import {buttonVariants} from "@/components/ui/button";
import {GearIcon} from "@radix-ui/react-icons";
import Link from "next/link";
import {usePathname} from "next/navigation";
export type EditButtonSettings = {}
export const EditButtonSettings= (props:EditButtonSettings) => {
const pathname = usePathname();
return(
<Link className={buttonVariants({variant: "outline"})}
href={`${pathname}/edit/`}
>
<GearIcon className="w-7 h-7"/>
</Link>
)
}
@@ -1,6 +1,6 @@
"use server"; "use server";
import {ActionError, userAction} from "@/lib/safe-actions/actions"; import {ActionError, userAction} from "@/lib/safe-actions/actions";
import {AgentSchema} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.schema"; import {AgentSchema} from "@/features/agents/agents.schema";
import {z} from "zod"; import {z} from "zod";
import {eq, and, ne, count} from "drizzle-orm"; import {eq, and, ne, count} from "drizzle-orm";
import {db} from "@/db"; import {db} from "@/db";
@@ -1,7 +1,7 @@
import { z } from "zod"; import { z } from "zod";
export const AgentSchema = z.object({ export const AgentSchema = z.object({
name: z.string(), name: z.string().nonempty("Name is required"),
description: z.string(), description: z.string(),
}); });
@@ -0,0 +1,42 @@
"use client";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {AgentForm} from "@/features/agents/components/agent.form";
import {useState} from "react";
import {Button} from "@/components/ui/button";
import {Plus} from "lucide-react";
import {AgentType} from "@/features/agents/agents.schema";
type AgentDialogProps = {
children?: React.ReactNode;
agent?: AgentType & { id: string };
};
export const AgentDialog = ({children, agent}: AgentDialogProps) => {
const [open, setOpen] = useState(false);
const isEdit = !!agent;
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{children ? children : <Button><Plus className="mr-2 h-4 w-4"/> Create Agent</Button>}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{isEdit ? `Edit ${agent.name}` : "Create new agent"}</DialogTitle>
</DialogHeader>
<AgentForm
onSuccess={() => setOpen(false)}
defaultValues={agent}
agentId={agent?.id}
/>
</DialogContent>
</Dialog>
);
};
@@ -1,6 +1,5 @@
"use client"; "use client";
import {Card, CardContent} from "@/components/ui/card";
import { import {
FormControl, FormControl,
FormDescription, FormDescription,
@@ -16,13 +15,14 @@ import {Button} from "@/components/ui/button";
import {useRouter} from "next/navigation"; import {useRouter} from "next/navigation";
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 {AgentSchema, AgentType} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.schema"; import {AgentSchema, AgentType} from "@/features/agents/agents.schema";
import {toast} from "sonner"; import {toast} from "sonner";
import {createAgentAction, updateAgentAction} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.action"; import {createAgentAction, updateAgentAction} from "@/features/agents/agents.action";
export type agentFormProps = { export type agentFormProps = {
defaultValues?: AgentType; defaultValues?: AgentType;
agentId?: string; agentId?: string;
onSuccess?: (data: any) => void;
}; };
export const AgentForm = (props: agentFormProps) => { export const AgentForm = (props: agentFormProps) => {
@@ -52,7 +52,12 @@ export const AgentForm = (props: agentFormProps) => {
} }
toast.success(`Success ${isCreate ? "creating" : "updating"} agent`); toast.success(`Success ${isCreate ? "creating" : "updating"} agent`);
router.refresh(); router.refresh();
router.push(`/dashboard/agents/${data.id}`);
if (props.onSuccess) {
props.onSuccess(data);
} else {
router.push(`/dashboard/agents/${data.id}`);
}
}, },
}); });
@@ -0,0 +1,50 @@
"use client";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {OrganizationForm} from "@/features/organization/components/organization.form";
import {useState} from "react";
import {Button, buttonVariants} from "@/components/ui/button";
import {GearIcon} from "@radix-ui/react-icons";
import {OrganizationWithMembers} from "@/db/schema/03_organization";
import {User} from "@/db/schema/02_user";
import {User as BetterAuthUser} from "better-auth";
type EditOrganizationDialogProps = {
children?: React.ReactNode;
organization: OrganizationWithMembers;
users: User[];
currentUser: BetterAuthUser;
};
export const EditOrganizationDialog = ({children, organization, users, currentUser}: EditOrganizationDialogProps) => {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{children ? children : (
<Button variant="outline">
<GearIcon className="w-4 h-4 mr-2"/> Edit
</Button>
)}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit {organization.name}</DialogTitle>
</DialogHeader>
<OrganizationForm
onSuccess={() => setOpen(false)}
defaultValues={organization}
users={users}
currentUser={currentUser}
/>
</DialogContent>
</Dialog>
);
};
@@ -16,13 +16,13 @@ import {useMutation} from "@tanstack/react-query";
import {useRouter} from "next/navigation"; import {useRouter} from "next/navigation";
import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select"; import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
import { import {
OrganizationFormSchema, UpdateOrganizationSchema,
OrganizationFormType UpdateOrganizationType
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema"; } from "@/features/organization/organization.schema";
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization"; import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
import { import {
updateOrganizationAction updateOrganizationAction
} from "@/components/wrappers/dashboard/organization/organization.action"; } from "@/features/organization/organization.action";
import {toast} from "sonner"; import {toast} from "sonner";
import {User as BetterAuthUser} from "better-auth"; import {User as BetterAuthUser} from "better-auth";
import {User} from "@/db/schema/02_user"; import {User} from "@/db/schema/02_user";
@@ -32,6 +32,7 @@ export type organizationFormProps = {
defaultValues?: OrganizationWithMembers; defaultValues?: OrganizationWithMembers;
users: User[]; users: User[];
currentUser: BetterAuthUser; currentUser: BetterAuthUser;
onSuccess?: (data: any) => void;
}; };
export const OrganizationForm = (props: organizationFormProps) => { export const OrganizationForm = (props: organizationFormProps) => {
@@ -64,12 +65,12 @@ export const OrganizationForm = (props: organizationFormProps) => {
}; };
const form = useZodForm({ const form = useZodForm({
schema: OrganizationFormSchema, schema: UpdateOrganizationSchema,
defaultValues: formattedDefaultValues, defaultValues: formattedDefaultValues,
}); });
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (values: OrganizationFormType) => updateOrganizationAction({ mutationFn: (values: UpdateOrganizationType) => updateOrganizationAction({
data: values, data: values,
organizationId: props.defaultValues?.id ?? "" organizationId: props.defaultValues?.id ?? ""
}), }),
@@ -78,7 +79,11 @@ export const OrganizationForm = (props: organizationFormProps) => {
toast.success(result.data.actionSuccess?.message || "Organization updated successfully."); toast.success(result.data.actionSuccess?.message || "Organization updated successfully.");
refetch() refetch()
refetchActiveOrga() refetchActiveOrga()
router.push("/dashboard/settings"); if (props.onSuccess) {
props.onSuccess(result.data.value);
} else {
router.push("/dashboard/settings");
}
} else { } else {
// @ts-ignore // @ts-ignore
const errorMsg = result?.data?.actionError?.message || result?.data?.actionError?.messageParams?.message || "Failed to update the organization."; const errorMsg = result?.data?.actionError?.message || result?.data?.actionError?.messageParams?.message || "Failed to update the organization.";
@@ -1,12 +1,9 @@
"use server"; "use server";
import {userAction} from "@/lib/safe-actions/actions"; import {userAction} from "@/lib/safe-actions/actions";
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema"; import {CreateOrganizationSchema, UpdateOrganizationSchema} from "@/features/organization/organization.schema";
import {ServerActionResult} from "@/types/action-type"; import {ServerActionResult} from "@/types/action-type";
import {z} from "zod"; import {z} from "zod";
import {
OrganizationFormSchema
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
import {db} from "@/db"; import {db} from "@/db";
import {and, eq, inArray, or} from "drizzle-orm"; import {and, eq, inArray, or} from "drizzle-orm";
import {auth, checkSlugOrganization, createOrganization} from "@/lib/auth/auth"; import {auth, checkSlugOrganization, createOrganization} from "@/lib/auth/auth";
@@ -14,7 +11,7 @@ import {slugify} from "@/utils/slugify";
import {Organization} from "@/db/schema/03_organization"; import {Organization} from "@/db/schema/03_organization";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
export const createOrganizationAction = userAction.schema(OrganizationSchema).action(async ({parsedInput}): Promise<ServerActionResult<Organization>> => { export const createOrganizationAction = userAction.schema(CreateOrganizationSchema).action(async ({parsedInput}): Promise<ServerActionResult<Organization>> => {
try { try {
const slug = slugify(parsedInput.name); const slug = slugify(parsedInput.name);
if (!await checkSlugOrganization(slug)) { if (!await checkSlugOrganization(slug)) {
@@ -69,7 +66,7 @@ export const createOrganizationAction = userAction.schema(OrganizationSchema).ac
export const updateOrganizationAction = userAction export const updateOrganizationAction = userAction
.schema( .schema(
z.object({ z.object({
data: OrganizationFormSchema, data: UpdateOrganizationSchema,
organizationId: z.string(), organizationId: z.string(),
}) })
) )
@@ -0,0 +1,17 @@
import { z } from "zod";
export const CreateOrganizationSchema = z.object({
name: z.string().min(5, "Name must be at least 5 characters long").max(40, "Name must be at most 40 characters long"),
});
export const UpdateOrganizationSchema = z.object({
name: z.string().min(5, 'Name must be at least 5 characters long').max(40, 'Name must be at most 40 characters long'),
slug: z.string()
.regex(/^[a-zA-Z0-9_-]*$/, 'Slug can only contain letters, numbers, underscores, and hyphens')
.min(5, 'Slug must be at least 5 characters long')
.max(20, 'Slug must be at most 20 characters long'),
users: z.array(z.string()),
});
export type CreateOrganizationType = z.infer<typeof CreateOrganizationSchema>;
export type UpdateOrganizationType = z.infer<typeof UpdateOrganizationSchema>;
@@ -0,0 +1,51 @@
"use client";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {ProjectForm} from "@/features/projects/components/project.form";
import {useState} from "react";
import {Button} from "@/components/ui/button";
import {Plus} from "lucide-react";
import {DatabaseWith} from "@/db/schema/07_database";
import {Organization} from "@/db/schema/03_organization";
import {ProjectWith} from "@/db/schema/06_project";
type ProjectDialogProps = {
children?: React.ReactNode;
databases: DatabaseWith[];
organization: Organization;
project?: ProjectWith;
};
export const ProjectDialog = ({children, databases, organization, project}: ProjectDialogProps) => {
const [open, setOpen] = useState(false);
const isEdit = !!project;
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{children ? children : <Button><Plus className="mr-2 h-4 w-4"/> Create Project</Button>}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{isEdit ? `Edit ${project.name}` : "Create new project"}</DialogTitle>
</DialogHeader>
<ProjectForm
onSuccess={() => setOpen(false)}
databases={databases}
organization={organization}
defaultValues={project ? {
...project,
databases: project.databases.map(db => db.id)
} : undefined}
projectId={project?.id}
/>
</DialogContent>
</Dialog>
);
};
@@ -1,13 +1,12 @@
"use client"; "use client";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form"; import { FormControl, FormDescription, 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 { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { ProjectSchema, ProjectType } from "@/components/wrappers/dashboard/projects/project-form/project-form.schema"; import { ProjectSchema, ProjectType } from "@/features/projects/projects.schema";
import { createProjectAction, updateProjectAction } from "@/components/wrappers/dashboard/projects/project-form/project-form.action"; import { createProjectAction, updateProjectAction } from "@/features/projects/projects.action";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { MultiSelect } from "@/components/wrappers/common/multiselect/multi-select"; import { MultiSelect } from "@/components/wrappers/common/multiselect/multi-select";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -19,15 +18,22 @@ export type projectFormProps = {
databases: DatabaseWith[]; databases: DatabaseWith[];
organization: Organization; organization: Organization;
projectId?: string; projectId?: string;
onSuccess?: (data: any) => void;
}; };
export const ProjectForm = (props: projectFormProps) => { export const ProjectForm = (props: projectFormProps) => {
const router = useRouter(); const router = useRouter();
const isCreate = !Boolean(props.defaultValues); const isCreate = !Boolean(props.defaultValues);
const formatDatabasesList = (databases: DatabaseWith[]) => { const formatDatabasesList = (databases: DatabaseWith[]) => {
return databases.map((database) => ({ return databases.map((database) => ({
value: database.id, value: database.id,
label: `${database.name} (${database.agentDatabaseId}) | ${database.agent?.name}`, label: `${database.name} | ${database.agent?.name}`,
})); }));
}; };
@@ -65,7 +71,11 @@ export const ProjectForm = (props: projectFormProps) => {
if (project.data.success) { if (project.data.success) {
project.data.actionSuccess && toast.success(project.data.actionSuccess.message); project.data.actionSuccess && toast.success(project.data.actionSuccess.message);
router.refresh(); router.refresh();
router.push(`/dashboard/projects/${project.data.value!.id}`); if (props.onSuccess) {
props.onSuccess(project.data.value);
} else {
router.push(`/dashboard/projects/${project.data.value!.id}`);
}
} else { } else {
project.data.actionError && toast.error(project.data.actionError.message || "Unknown error occurred."); project.data.actionError && toast.error(project.data.actionError.message || "Unknown error occurred.");
router.refresh(); router.refresh();
@@ -1,7 +1,7 @@
"use server"; "use server";
import {userAction} from "@/lib/safe-actions/actions"; import {userAction} from "@/lib/safe-actions/actions";
import {ProjectSchema} from "@/components/wrappers/dashboard/projects/project-form/project-form.schema"; import {ProjectSchema} from "@/features/projects/projects.schema";
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";
@@ -119,13 +119,11 @@ export const updateProjectAction = userAction
.where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databasesToRemove)).execute(); .where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databasesToRemove)).execute();
} }
// const slug = slugify(parsedInput.data.name);
const [updatedProject] = await db const [updatedProject] = await db
.update(drizzleDb.schemas.project) .update(drizzleDb.schemas.project)
.set({ .set({
name: parsedInput.data.name, name: parsedInput.data.name,
// slug: slug,
}) })
.where(eq(drizzleDb.schemas.project.id, parsedInput.projectId)) .where(eq(drizzleDb.schemas.project.id, parsedInput.projectId))
.returning(); .returning();
@@ -1,7 +1,7 @@
import { z } from "zod"; import { z } from "zod";
export const ProjectSchema = z.object({ export const ProjectSchema = z.object({
name: z.string(), name: z.string().nonempty("Name is required"),
databases: z.array(z.string()), databases: z.array(z.string()),
}); });