mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
feat: org agent management (#259)
* feat: org agent management * feat: org agent management * fix: adding migrations for legacy and refactoring. * fix: refactoring * fix: delete agent * fix: refactoring --------- Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
This commit is contained in:
co-authored by
charles-gauthereau
parent
a620d7a9f7
commit
e38519aec2
@@ -7,7 +7,7 @@ import {
|
||||
} from "@/features/layout/page";
|
||||
import { db } from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {eq, isNull} from "drizzle-orm";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ButtonDeleteAgent } from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
|
||||
import { capitalizeFirstLetter } from "@/utils/text";
|
||||
@@ -15,7 +15,7 @@ import { generateEdgeKey } from "@/utils/edge_key";
|
||||
import { getServerUrl } from "@/utils/get-server-url";
|
||||
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 }>,
|
||||
@@ -26,13 +26,30 @@ export default async function RoutePage(
|
||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
||||
with: {
|
||||
databases: true,
|
||||
organizations: true,
|
||||
},
|
||||
});
|
||||
|
||||
const organizations = await db.query.organization.findMany({
|
||||
where: (fields) => isNull(fields.deletedAt),
|
||||
with: {
|
||||
members: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (!agent) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const isOwnerByAnOrganization = agent.organizationId
|
||||
|
||||
if (isOwnerByAnOrganization){
|
||||
notFound();
|
||||
}
|
||||
|
||||
const organizationIds = agent.organizations.map(org => org.organizationId)
|
||||
|
||||
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
|
||||
|
||||
return (
|
||||
@@ -45,12 +62,14 @@ export default async function RoutePage(
|
||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||
<div className="flex items-center gap-2">
|
||||
<AgentDialog
|
||||
agent={agent as AgentType & { id: string }}
|
||||
agent={agent}
|
||||
typeTrigger={"edit"}
|
||||
adminView={true}
|
||||
organizations={organizations}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"} />
|
||||
<ButtonDeleteAgent organizationIds={organizationIds} agentId={agentId} text={"Delete Agent"} />
|
||||
</div>
|
||||
</div>
|
||||
</PageTitle>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/
|
||||
import {notFound} from "next/navigation";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {desc, eq, not} from "drizzle-orm";
|
||||
import {and, desc, eq, isNull, not} from "drizzle-orm";
|
||||
import {Metadata} from "next";
|
||||
import {AgentDialog} from "@/features/agents/components/agent.dialog";
|
||||
|
||||
@@ -16,13 +16,13 @@ export const metadata: Metadata = {
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
const agents = await db.query.agent.findMany({
|
||||
where: not(eq(drizzleDb.schemas.agent.isArchived, true)),
|
||||
where: and(not(eq(drizzleDb.schemas.agent.isArchived, true)),isNull(drizzleDb.schemas.agent.organizationId)),
|
||||
with: {
|
||||
databases: true
|
||||
},
|
||||
orderBy: (fields) => desc(fields.lastContact),
|
||||
});
|
||||
|
||||
|
||||
if (!agents) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
@@ -6,16 +6,15 @@ import {
|
||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||
import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
|
||||
import {notFound, redirect} from "next/navigation";
|
||||
|
||||
import {db} from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import * as drizzleDb from "@/db";
|
||||
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";
|
||||
import {isUuidv4} from "@/utils/verify-uuid";
|
||||
import {getOrganizationAvailableDatabases} from "@/db/services/database";
|
||||
|
||||
|
||||
export default async function RoutePage(props: PageParams<{
|
||||
@@ -56,19 +55,7 @@ export default async function RoutePage(props: PageParams<{
|
||||
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 availableDatabases = await getOrganizationAvailableDatabases(organization.id, proj.id)
|
||||
const isMember = activeMember?.role === "member";
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,6 +9,7 @@ import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-pl
|
||||
import {Metadata} from "next";
|
||||
import {ProjectDialog} from "@/features/projects/components/project.dialog";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
import {getOrganizationAvailableDatabases} from "@/db/services/database";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Projects",
|
||||
@@ -35,18 +36,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
});
|
||||
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[];
|
||||
const availableDatabases = await getOrganizationAvailableDatabases(organization.id)
|
||||
|
||||
|
||||
return (
|
||||
@@ -71,7 +61,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
pageSizeOptions={[12, 24, 48]}
|
||||
/>
|
||||
) : isMember ? (
|
||||
<EmptyStatePlaceholder text="No project available"/>
|
||||
<EmptyStatePlaceholder state={"empty"} text="No project available"/>
|
||||
) : (
|
||||
<ProjectDialog databases={availableDatabases} organization={organization} isEmpty={true}/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {
|
||||
Page,
|
||||
PageContent,
|
||||
PageDescription,
|
||||
PageTitle,
|
||||
} from "@/features/layout/page";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {notFound} from "next/navigation";
|
||||
import {ButtonDeleteAgent} from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import {generateEdgeKey} from "@/utils/edge_key";
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
import {AgentContentPage} from "@/components/wrappers/dashboard/agent/agent-content";
|
||||
import {AgentDialog} from "@/features/agents/components/agent.dialog";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||
|
||||
|
||||
export default async function RoutePage(
|
||||
props: PageParams<{ agentId: string }>,
|
||||
) {
|
||||
const {agentId} = await props.params;
|
||||
|
||||
const organization = await getOrganization({});
|
||||
const user = await currentUser();
|
||||
const activeMember = await getActiveMember();
|
||||
|
||||
if (!organization || !activeMember || !user) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
|
||||
|
||||
const {canManageAgents} = computeOrganizationPermissions(activeMember);
|
||||
|
||||
if (!canManageAgents){
|
||||
notFound();
|
||||
}
|
||||
|
||||
const agent = await db.query.agent.findFirst({
|
||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
||||
with: {
|
||||
databases: true,
|
||||
organizations: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (!agent) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const hasAccess =
|
||||
agent.organizationId === organization.id ||
|
||||
agent.organizations.some(org => org.organizationId === organization.id);
|
||||
|
||||
if (!hasAccess) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const isOwned = agent.organizationId
|
||||
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="justify-between gap-2 sm:flex">
|
||||
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||
<div className="min-w-full md:min-w-fit ">
|
||||
{capitalizeFirstLetter(agent.name)}
|
||||
</div>
|
||||
{isOwned && (
|
||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||
<div className="flex items-center gap-2">
|
||||
<AgentDialog
|
||||
agent={agent}
|
||||
typeTrigger={"edit"}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonDeleteAgent organizationId={organization.id ?? null} agentId={agentId} text={"Delete Agent"}/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PageTitle>
|
||||
</div>
|
||||
|
||||
{agent.description && (
|
||||
<PageDescription className="mt-5 sm:mt-0">
|
||||
{agent.description}
|
||||
</PageDescription>
|
||||
)}
|
||||
<PageContent className="flex flex-col w-full h-full justify-between gap-6">
|
||||
<AgentContentPage agent={agent} edgeKey={edgeKey}/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function RoutePage() {
|
||||
redirect("/dashboard/settings?tab=agents");
|
||||
}
|
||||
@@ -1,95 +1,114 @@
|
||||
import { PageParams } from "@/types/next";
|
||||
import {PageParams} from "@/types/next";
|
||||
import {
|
||||
Page,
|
||||
PageActions,
|
||||
PageContent,
|
||||
PageHeader,
|
||||
PageTitle,
|
||||
Page,
|
||||
PageContent,
|
||||
PageHeader,
|
||||
PageTitle,
|
||||
} from "@/features/layout/page";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
import { getActiveMember, getOrganization } from "@/lib/auth/auth";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Metadata } from "next";
|
||||
import { OrganizationTabs } from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
||||
import { getOrganizationChannels } from "@/db/services/notification-channel";
|
||||
import { computeOrganizationPermissions } from "@/lib/acl/organization-acl";
|
||||
import { getOrganizationStorageChannels } from "@/db/services/storage-channel";
|
||||
import { DeleteOrganizationButton } from "@/components/wrappers/dashboard/organization/delete-organization-button";
|
||||
import { EditOrganizationDialog } from "@/features/organization/components/edit-organization.dialog";
|
||||
import { db } from "@/db";
|
||||
import { isNull } from "drizzle-orm";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {notFound} from "next/navigation";
|
||||
import {Metadata} from "next";
|
||||
import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
||||
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
||||
import {DeleteOrganizationButton} from "@/components/wrappers/dashboard/organization/delete-organization-button";
|
||||
import {EditOrganizationDialog} from "@/features/organization/components/edit-organization.dialog";
|
||||
import {db} from "@/db";
|
||||
import {isNull} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {getOrganizationAgents} from "@/db/services/agent";
|
||||
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Settings",
|
||||
title: "Settings",
|
||||
};
|
||||
|
||||
export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||
const organization = await getOrganization({});
|
||||
const user = await currentUser();
|
||||
const activeMember = await getActiveMember();
|
||||
const organization = await getOrganization({});
|
||||
const user = await currentUser();
|
||||
const activeMember = await getActiveMember();
|
||||
|
||||
if (!organization || !activeMember || !user) {
|
||||
notFound();
|
||||
}
|
||||
if (!organization || !activeMember || !user) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const notificationChannels = await getOrganizationChannels(organization.id);
|
||||
const storageChannels = await getOrganizationStorageChannels(organization.id);
|
||||
const permissions = computeOrganizationPermissions(activeMember);
|
||||
const notificationChannels = await getOrganizationChannels(organization.id);
|
||||
const storageChannels = await getOrganizationStorageChannels(organization.id);
|
||||
const agents = await getOrganizationAgents(organization.id);
|
||||
const permissions = computeOrganizationPermissions(activeMember);
|
||||
|
||||
const users = await db.query.user.findMany({
|
||||
where: (fields) => isNull(fields.deletedAt),
|
||||
});
|
||||
const users = await db.query.user.findMany({
|
||||
where: (fields) => isNull(fields.deletedAt),
|
||||
});
|
||||
|
||||
const organizationWithMembers = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.id, organization.id),
|
||||
with: {
|
||||
members: {
|
||||
const organizationWithMembers = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.id, organization.id),
|
||||
with: {
|
||||
user: true,
|
||||
projects: true,
|
||||
members: {
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (!organizationWithMembers) notFound();
|
||||
if (!organizationWithMembers) notFound();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||
<div className="min-w-full md:min-w-fit ">Organization settings</div>
|
||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||
<div className="flex items-center gap-2">
|
||||
{permissions.canManageSettings &&
|
||||
organization.slug !== "default" && (
|
||||
<EditOrganizationDialog
|
||||
organization={organizationWithMembers}
|
||||
users={users}
|
||||
currentUser={user}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{permissions.canManageDangerZone &&
|
||||
organization.slug !== "default" && (
|
||||
<DeleteOrganizationButton
|
||||
organizationSlug={organization.slug}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<OrganizationTabs
|
||||
activeMember={activeMember}
|
||||
organization={organization}
|
||||
notificationChannels={notificationChannels}
|
||||
storageChannels={storageChannels}
|
||||
/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||
<div className="min-w-full md:min-w-fit ">Organization settings</div>
|
||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||
<div className="flex items-center gap-2">
|
||||
{permissions.canManageSettings &&
|
||||
organization.slug !== "default" && (
|
||||
<EditOrganizationDialog
|
||||
organization={organizationWithMembers}
|
||||
users={users}
|
||||
currentUser={user}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{permissions.canManageDangerZone &&
|
||||
organization.slug !== "default" && (
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<DeleteOrganizationButton
|
||||
disabled={organizationWithMembers.projects.length > 0}
|
||||
organizationSlug={organization.slug}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</TooltipTrigger>
|
||||
{organizationWithMembers.projects.length > 0 && (
|
||||
<TooltipContent>
|
||||
<p>Your organization has some projects associated with it. Please delete them before deleting the organization.</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<OrganizationTabs
|
||||
activeMember={activeMember}
|
||||
organization={organization}
|
||||
notificationChannels={notificationChannels}
|
||||
storageChannels={storageChannels}
|
||||
agents={agents}
|
||||
/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user