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
@@ -16,7 +16,7 @@ seed-pocket:
|
||||
@docker compose -f docker-compose.func.yml stop pocket-id >/dev/null 2>&1 || true
|
||||
@docker compose -f docker-compose.func.yml rm -f -s pocket-id >/dev/null 2>&1 || true
|
||||
@docker volume rm portabase-dev-func_pocket-id-data >/dev/null 2>&1 || true
|
||||
@docker compose -f docker-compose.func.yml run --rm -v ./seeds/pocket-id/portabase.zip:/tmp/portabase.zip pocket-id ./pocket-id import --yes --path /tmp/portabase.zip >/dev/null
|
||||
@docker compose -f docker-compose.func.yml run --rm -v $$(pwd)/seeds/pocket-id/portabase.zip:/tmp/portabase.zip pocket-id ./pocket-id import --yes --path /tmp/portabase.zip >/dev/null
|
||||
@docker compose -f docker-compose.func.yml up -d pocket-id
|
||||
@sleep 2
|
||||
@docker compose -f docker-compose.func.yml exec pocket-id ./pocket-id one-time-access-token admin
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {logger} from "@/lib/logger";
|
||||
@@ -12,7 +12,7 @@ export function withAgentCheck(handler: Function) {
|
||||
const agentId = (await context.params).agentId;
|
||||
|
||||
const agent = await db.query.agent.findFirst({
|
||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
||||
where: and(eq(drizzleDb.schemas.agent.id, agentId), eq(drizzleDb.schemas.agent.isArchived, false)),
|
||||
});
|
||||
|
||||
if (!agent) {
|
||||
|
||||
@@ -70,10 +70,10 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
|
||||
if (hasSuccessfulStorage && backup.status !== "success") {
|
||||
await db
|
||||
.update(drizzleDb.schemas.backup)
|
||||
.set({
|
||||
.set(withUpdatedAt({
|
||||
status: "success",
|
||||
fileSize: fileSize,
|
||||
})
|
||||
}))
|
||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {db} from "@/db";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
||||
import {logger} from "@/lib/logger";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
|
||||
const log = logger.child({module: "api/agent/restore"});
|
||||
|
||||
@@ -34,7 +35,7 @@ export async function POST(
|
||||
}
|
||||
|
||||
const agent = await db.query.agent.findFirst({
|
||||
where: eq(drizzleDb.schemas.agent.id, agentId)
|
||||
where: and(eq(drizzleDb.schemas.agent.id, agentId), eq(drizzleDb.schemas.agent.isArchived, false)),
|
||||
})
|
||||
if (!agent) {
|
||||
return NextResponse.json({error: "Agent not found"}, {status: 404})
|
||||
@@ -62,7 +63,7 @@ export async function POST(
|
||||
|
||||
await db
|
||||
.update(drizzleDb.schemas.restoration)
|
||||
.set({status: body.status as RestorationStatus})
|
||||
.set(withUpdatedAt({status: body.status as RestorationStatus}))
|
||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||
|
||||
await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore");
|
||||
|
||||
@@ -183,7 +183,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
} else {
|
||||
await dbClient
|
||||
.update(drizzleDb.schemas.restoration)
|
||||
.set({status: "failed"})
|
||||
.set(withUpdatedAt({status: "failed"}))
|
||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||
|
||||
const errorMessage = "Failed to get backup URL";
|
||||
@@ -194,14 +194,14 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
log.error({error: err, name: "handleDatabases"}, "Restoration crashed unexpectedly");
|
||||
await dbClient
|
||||
.update(drizzleDb.schemas.restoration)
|
||||
.set({status: "failed"})
|
||||
.set(withUpdatedAt({status: "failed"}))
|
||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||
continue;
|
||||
}
|
||||
|
||||
await dbClient
|
||||
.update(drizzleDb.schemas.restoration)
|
||||
.set({status: "ongoing"})
|
||||
.set(withUpdatedAt({status: "ongoing"}))
|
||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||
}
|
||||
const storages = await getDatabaseStorageChannels(databaseUpdated.id)
|
||||
|
||||
@@ -3,7 +3,7 @@ import {handleDatabases} from "./helpers";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {db} from "@/db";
|
||||
import {EDbmsSchema} from "@/db/schema/types";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {isUuidv4} from "@/utils/verify-uuid";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {logger} from "@/lib/logger";
|
||||
@@ -46,7 +46,7 @@ export async function POST(
|
||||
}
|
||||
|
||||
const agent = await db.query.agent.findFirst({
|
||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
||||
where: and(eq(drizzleDb.schemas.agent.id, agentId), eq(drizzleDb.schemas.agent.isArchived, false)),
|
||||
})
|
||||
|
||||
if (!agent) {
|
||||
|
||||
+20
-19
@@ -15,8 +15,9 @@
|
||||
"release": "release-it"
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-auth/passkey": "^1.5.6",
|
||||
"@better-auth/sso": "^1.5.6",
|
||||
"@better-auth/core": "1.6.2",
|
||||
"@better-auth/passkey": "^1.6.2",
|
||||
"@better-auth/sso": "^1.6.2",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
@@ -48,22 +49,22 @@
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-email/components": "^0.0.41",
|
||||
"@t3-oss/env-nextjs": "^0.13.11",
|
||||
"@tanstack/react-query": "^5.95.2",
|
||||
"@tanstack/react-query": "^5.97.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@types/nodemailer": "^6.4.23",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@zenstackhq/runtime": "2.14.2",
|
||||
"argon2": "^0.43.1",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-auth": "1.5.6",
|
||||
"better-auth": "1.6.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dockerode": "^4.0.10",
|
||||
"dotenv": "^16.6.1",
|
||||
"drizzle-orm": "^0.43.1",
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"drizzle-zod": "0.8.3",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"googleapis": "^170.1.0",
|
||||
"input-otp": "^1.4.2",
|
||||
@@ -75,18 +76,18 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"node-cron": "^4.2.1",
|
||||
"node-forge": "^1.4.0",
|
||||
"nodemailer": "^7.0.13",
|
||||
"nodemailer": "8.0.5",
|
||||
"npm-check-updates": "^18.3.1",
|
||||
"pg": "^8.20.0",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"prettier": "^3.8.1",
|
||||
"react": "^19.2.4",
|
||||
"prettier": "^3.8.2",
|
||||
"react": "^19.2.5",
|
||||
"react-day-picker": "9.7.0",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-dropzone": "^14.4.1",
|
||||
"react-email": "^4.3.2",
|
||||
"react-hook-form": "^7.72.0",
|
||||
"react-hook-form": "^7.72.1",
|
||||
"react-qr-code": "^2.0.18",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-twc": "^1.5.1",
|
||||
@@ -101,33 +102,33 @@
|
||||
"uuid": "^11.1.0",
|
||||
"vaul": "^1.1.2",
|
||||
"ws": "^8.20.0",
|
||||
"zod": "^3.25.76"
|
||||
"zod": "4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/react": "^6.0.2",
|
||||
"@playwright/test": "1.58.2",
|
||||
"@react-email/preview-server": "4.3.2",
|
||||
"@react-email/render": "^2.0.4",
|
||||
"@react-email/render": "^2.0.6",
|
||||
"@release-it/bumper": "^7.0.5",
|
||||
"@release-it/conventional-changelog": "^10.0.6",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"@types/node": "^22.19.15",
|
||||
"@types/node": "^22.19.17",
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@zenstackhq/openapi": "^2.22.1",
|
||||
"@zenstackhq/tanstack-query": "^2.22.2",
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"baseline-browser-mapping": "^2.10.17",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"esbuild": "^0.27.4",
|
||||
"esbuild": "^0.27.7",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-next": "^16.2.1",
|
||||
"eslint-config-next": "^16.2.3",
|
||||
"eslint-plugin-tailwindcss": "^3.18.2",
|
||||
"framer-motion": "^12.34.3",
|
||||
"framer-motion": "^12.38.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss": "^8.5.9",
|
||||
"release-it": "^19.2.4",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"tsx": "^4.21.0",
|
||||
|
||||
Generated
+1703
-1841
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ import z from "zod";
|
||||
import { db } from "@/db";
|
||||
import {action} from "@/lib/safe-actions/actions";
|
||||
|
||||
//todo: to be continued...
|
||||
//TODO: to be continued...
|
||||
export const forgotPasswordAction = action
|
||||
.schema(
|
||||
z.object({
|
||||
|
||||
@@ -44,7 +44,7 @@ export function BreadCrumbsWrapper() {
|
||||
)
|
||||
}
|
||||
|
||||
const FORBIDDEN_LINKS = ["organization", "dashboard", "database", "admin", "settings", "notifications", "storages"];
|
||||
const FORBIDDEN_LINKS = ["organization", "dashboard", "database", "admin", "notifications", "storages"];
|
||||
|
||||
|
||||
export function BreadCrumbs({}: BreadCrumbsProps) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {Plus} from "lucide-react";
|
||||
import {CircleSlash2, Plus} from "lucide-react";
|
||||
import {forwardRef, HTMLAttributes} from "react";
|
||||
|
||||
type EmptyStatePlaceholderProps = {
|
||||
@@ -8,25 +8,32 @@ type EmptyStatePlaceholderProps = {
|
||||
onClick?: () => void;
|
||||
text: string;
|
||||
className?: string;
|
||||
state?: string
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const EmptyStatePlaceholder = forwardRef<HTMLDivElement, EmptyStatePlaceholderProps>(({
|
||||
url,
|
||||
onClick,
|
||||
text,
|
||||
className,
|
||||
...props
|
||||
}, ref) => {
|
||||
url,
|
||||
onClick,
|
||||
text,
|
||||
state,
|
||||
className,
|
||||
...props
|
||||
}, ref) => {
|
||||
const Container = (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full 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-4",
|
||||
"transition-colors text-muted-foreground text-center space-y-4",
|
||||
state != "empty" && "hover:bg-muted/50 hover:text-primary",
|
||||
(onClick || url) && "cursor-pointer"
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Plus className="w-5 h-5 lg:w-6 lg:h-6" />
|
||||
{state == "empty" ?
|
||||
<CircleSlash2 className="w-5 h-5 lg:w-6 lg:h-6"/>
|
||||
:
|
||||
<Plus className="w-5 h-5 lg:w-6 lg:h-6"/>
|
||||
}
|
||||
<p className="text-sm">{text}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const ChannelsOrganizationSchema = z.object({
|
||||
organizations: z.array(z.string())
|
||||
organizations: z.array(z.string().uuid())
|
||||
});
|
||||
|
||||
export type ChannelsOrganizationType = z.infer<typeof ChannelsOrganizationSchema>;
|
||||
|
||||
+3
-2
@@ -5,6 +5,7 @@ import { eq } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
|
||||
export const updateEmailSettingsAction = userAction
|
||||
.schema(
|
||||
@@ -18,9 +19,9 @@ export const updateEmailSettingsAction = userAction
|
||||
|
||||
const [updatedSettings] = await db
|
||||
.update(drizzleDb.schemas.setting)
|
||||
.set({
|
||||
.set(withUpdatedAt({
|
||||
...data,
|
||||
})
|
||||
}))
|
||||
.where(eq(drizzleDb.schemas.setting.name, name))
|
||||
.returning();
|
||||
|
||||
|
||||
+3
-2
@@ -10,6 +10,7 @@ import {z} from "zod";
|
||||
import {
|
||||
DefaultNotificationSchema
|
||||
} from "@/components/wrappers/dashboard/admin/settings/notification/settings-notification.schema";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
|
||||
export const updateNotificationSettingsAction = userAction
|
||||
.schema(
|
||||
@@ -24,9 +25,9 @@ export const updateNotificationSettingsAction = userAction
|
||||
try {
|
||||
const [updatedSettings] = await db
|
||||
.update(drizzleDb.schemas.setting)
|
||||
.set({
|
||||
.set(withUpdatedAt({
|
||||
defaultNotificationChannelId: data.notificationChannelId ?? null,
|
||||
})
|
||||
}))
|
||||
.where(eq(drizzleDb.schemas.setting.name, name))
|
||||
.returning();
|
||||
return {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {ServerActionResult} from "@/types/action-type";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {z} from "zod";
|
||||
import {DefaultStorageSchema} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage.schema";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
|
||||
export const updateStorageSettingsAction = userAction
|
||||
.schema(
|
||||
@@ -22,10 +23,10 @@ export const updateStorageSettingsAction = userAction
|
||||
try {
|
||||
const [updatedSettings] = await db
|
||||
.update(drizzleDb.schemas.setting)
|
||||
.set({
|
||||
.set(withUpdatedAt({
|
||||
defaultStorageChannelId: data.storageChannelId,
|
||||
encryption: data.encryption,
|
||||
})
|
||||
}))
|
||||
.where(eq(drizzleDb.schemas.setting.name, name))
|
||||
.returning();
|
||||
return {
|
||||
|
||||
+3
-1
@@ -6,6 +6,7 @@ import { S3FormSchema, StorageSwitchSchema } from "@/components/wrappers/dashboa
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
|
||||
export const updateS3SettingsAction = userAction
|
||||
.schema(
|
||||
@@ -19,7 +20,7 @@ export const updateS3SettingsAction = userAction
|
||||
|
||||
const [updatedSettings] = await db
|
||||
.update(drizzleDb.schemas.setting)
|
||||
.set({ ...data })
|
||||
.set(withUpdatedAt({ ...data }))
|
||||
.where(eq(drizzleDb.schemas.setting.name, name))
|
||||
.returning();
|
||||
|
||||
@@ -40,6 +41,7 @@ export const updateStorageSettingsAction = userAction
|
||||
|
||||
const [updatedSettings] = await db
|
||||
.update(drizzleDb.schemas.setting)
|
||||
// @ts-ignore
|
||||
.set({ ...data })
|
||||
.where(eq(drizzleDb.schemas.setting.name, name))
|
||||
.returning();
|
||||
|
||||
@@ -15,6 +15,7 @@ import {useAgentUpdateCheck} from "@/features/agents/hooks/use-agent-update-chec
|
||||
|
||||
export type agentCardProps = {
|
||||
data: AgentWith;
|
||||
organizationView?: boolean;
|
||||
};
|
||||
|
||||
export const AgentCard = (props: agentCardProps) => {
|
||||
@@ -34,7 +35,7 @@ export const AgentCard = (props: agentCardProps) => {
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/dashboard/agents/${agent.id}`}
|
||||
href={props.organizationView ? `/dashboard/settings/agents/${agent.id}`: `/dashboard/agents/${agent.id}`}
|
||||
className="group block transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl"
|
||||
>
|
||||
<Card className="flex flex-row items-center p-4 gap-5 transition-all border-border/50 bg-card hover:bg-accent/50 hover:border-primary/50 group-hover:shadow-md overflow-hidden">
|
||||
|
||||
@@ -9,8 +9,10 @@ import {deleteAgentAction} from "@/components/wrappers/dashboard/agent/button-de
|
||||
import {useIsMobile} from "@/hooks/use-mobile";
|
||||
|
||||
export type ButtonDeleteAgentProps = {
|
||||
text?: string;
|
||||
agentId: string;
|
||||
text?: string,
|
||||
agentId: string,
|
||||
organizationId?: string
|
||||
organizationIds?: string[]
|
||||
};
|
||||
|
||||
export const ButtonDeleteAgent = (props: ButtonDeleteAgentProps) => {
|
||||
@@ -18,11 +20,11 @@ export const ButtonDeleteAgent = (props: ButtonDeleteAgentProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => deleteAgentAction(props.agentId),
|
||||
mutationFn: () => deleteAgentAction({agentId: props.agentId, organizationId: props.organizationId, organizationIds: props.organizationIds}),
|
||||
onSuccess: async (result: any) => {
|
||||
if (result.data?.success) {
|
||||
toast.success(result.data.actionSuccess.message);
|
||||
router.push("/dashboard/agents");
|
||||
router.push(props.organizationId ? "/dashboard/settings?tab=agents" : "/dashboard/agents");
|
||||
} else {
|
||||
toast.error(result.data.actionError.message || "Unknown error occurred.");
|
||||
}
|
||||
@@ -35,7 +37,7 @@ export const ButtonDeleteAgent = (props: ButtonDeleteAgentProps) => {
|
||||
description="Are you sure you want to remove this agent? This action cannot be undone."
|
||||
button={{
|
||||
main: {
|
||||
text: props.text ? !isMobile ? props.text: "" : "",
|
||||
text: props.text ? !isMobile ? props.text : "" : "",
|
||||
variant: "outline",
|
||||
icon: <Trash2 color="red"/>,
|
||||
},
|
||||
|
||||
+121
-37
@@ -3,51 +3,135 @@
|
||||
import {z} from "zod";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {and, eq, inArray} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {Agent} from "@/db/schema/08_agent";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {zString} from "@/lib/zod";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
|
||||
export const deleteAgentAction = userAction.schema(z.string()).action(async ({parsedInput}): Promise<ServerActionResult<Agent>> => {
|
||||
try {
|
||||
export const deleteAgentAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
agentId: zString(),
|
||||
organizationId: zString().optional(),
|
||||
organizationIds: z.array(z.string()).optional()
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<Agent>> => {
|
||||
const {agentId, organizationId, organizationIds} = parsedInput;
|
||||
|
||||
// const deletedAgent: Agent[] = await db.delete(drizzleDb.schemas.agent).where(eq(drizzleDb.schemas.agent.id, parsedInput)).returning();
|
||||
try {
|
||||
let projectIds: string[] = [];
|
||||
|
||||
const uuid = uuidv4();
|
||||
const uuid = uuidv4();
|
||||
if (organizationId) {
|
||||
await db
|
||||
.delete(drizzleDb.schemas.organizationAgent)
|
||||
.where(
|
||||
and(
|
||||
eq(drizzleDb.schemas.organizationAgent.organizationId, organizationId),
|
||||
eq(drizzleDb.schemas.organizationAgent.agentId, agentId)
|
||||
)
|
||||
);
|
||||
|
||||
const updatedAgent = await db
|
||||
.update(drizzleDb.schemas.agent)
|
||||
.set({
|
||||
isArchived: true,
|
||||
slug: uuid,
|
||||
})
|
||||
.where(eq(drizzleDb.schemas.agent.id, parsedInput))
|
||||
.returning();
|
||||
const organization = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.id, organizationId),
|
||||
with: {
|
||||
projects: true,
|
||||
}
|
||||
});
|
||||
|
||||
projectIds = organization?.projects?.map(project => project.id) ?? [];
|
||||
|
||||
|
||||
if (!updatedAgent[0]) {
|
||||
throw new Error("Agent not found or update failed");
|
||||
} else if (organizationIds) {
|
||||
|
||||
const organizationsToRemoveDetails = await db.query.organization.findMany({
|
||||
where: inArray(drizzleDb.schemas.organization.id, organizationIds),
|
||||
with: {
|
||||
projects: true
|
||||
}
|
||||
});
|
||||
|
||||
projectIds = organizationsToRemoveDetails.flatMap(org =>
|
||||
org.projects.map(project => project.id)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (projectIds?.length > 0) {
|
||||
const databases = await db.query.database.findMany({
|
||||
where: (db, {inArray}) => inArray(db.projectId, projectIds),
|
||||
columns: {id: true}
|
||||
});
|
||||
|
||||
const databaseIds = databases.map(d => d.id);
|
||||
|
||||
await db
|
||||
.update(drizzleDb.schemas.database)
|
||||
.set(withUpdatedAt({
|
||||
backupPolicy: null,
|
||||
projectId: null
|
||||
}))
|
||||
.where(inArray(drizzleDb.schemas.database.projectId, projectIds))
|
||||
.execute();
|
||||
|
||||
await db.delete(drizzleDb.schemas.retentionPolicy)
|
||||
.where(inArray(drizzleDb.schemas.retentionPolicy.databaseId, databaseIds))
|
||||
.execute();
|
||||
|
||||
await db.delete(drizzleDb.schemas.alertPolicy)
|
||||
.where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databaseIds))
|
||||
.execute();
|
||||
|
||||
await db.delete(drizzleDb.schemas.storagePolicy)
|
||||
.where(inArray(drizzleDb.schemas.storagePolicy.databaseId, databaseIds))
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
||||
const updatedAgent = await db
|
||||
.update(drizzleDb.schemas.agent)
|
||||
.set(withUpdatedAt({
|
||||
isArchived: true,
|
||||
slug: uuid,
|
||||
deletedAt: new Date()
|
||||
}))
|
||||
.where(eq(drizzleDb.schemas.agent.id, agentId))
|
||||
.returning();
|
||||
|
||||
|
||||
if (!updatedAgent[0]) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Agent not found or update failed",
|
||||
status: 404,
|
||||
messageParams: {agentId: agentId},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: updatedAgent[0],
|
||||
actionSuccess: {
|
||||
message: "Agent has been successfully deleted.",
|
||||
messageParams: {projectId: agentId},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to delete agent.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {agentId: agentId},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: updatedAgent[0],
|
||||
actionSuccess: {
|
||||
message: "Agent has been successfully deleted.",
|
||||
messageParams: {projectId: parsedInput},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to delete agent.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {projectId: parsedInput},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,9 +55,7 @@ export const UploadBackupZone = ({onSuccessAction, database}: UploadRetentionZon
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const acceptDbImportFiles = getFileHeadersBasedOnDbms(database.dbms)
|
||||
console.log(acceptDbImportFiles)
|
||||
|
||||
const fileKindDescription = Object.values(acceptDbImportFiles)
|
||||
.flat()
|
||||
|
||||
@@ -10,6 +10,7 @@ import {useIsMobile} from "@/hooks/use-mobile";
|
||||
|
||||
export type DeleteOrganizationButtonProps = {
|
||||
organizationSlug: string;
|
||||
disabled?: boolean
|
||||
};
|
||||
|
||||
export const DeleteOrganizationButton = (props: DeleteOrganizationButtonProps) => {
|
||||
@@ -51,6 +52,7 @@ export const DeleteOrganizationButton = (props: DeleteOrganizationButtonProps) =
|
||||
main: {
|
||||
text: !isMobile ? "Delete Organization" : "",
|
||||
variant: "outline",
|
||||
disabled: props.disabled,
|
||||
icon: <Trash2 color="red"/>,
|
||||
},
|
||||
confirm: {
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {Agent} from "@/db/schema/08_agent";
|
||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||
import {AgentCard} from "@/components/wrappers/dashboard/agent/agent-card/agent-card";
|
||||
import {AgentDialog} from "@/features/agents/components/agent.dialog";
|
||||
|
||||
export type OrganizationAgentsTabProps = {
|
||||
organization: OrganizationWithMembers;
|
||||
agents: Agent[];
|
||||
};
|
||||
|
||||
export const OrganizationAgentsTab = ({
|
||||
organization,
|
||||
agents,
|
||||
}: OrganizationAgentsTabProps) => {
|
||||
|
||||
const hasAgent = agents.length > 0;
|
||||
return (
|
||||
<div className="flex flex-col gap-y-6 h-full py-4">
|
||||
<div className="h-full flex flex-col gap-y-6">
|
||||
<div className={cn("hidden flex-row justify-between items-start", hasAgent && "flex")}>
|
||||
<div className="max-w-2xl ">
|
||||
<h3 className="text-xl font-semibold text-balance mb-1">
|
||||
Agent Settings
|
||||
</h3>
|
||||
</div>
|
||||
<AgentDialog organization={organization} typeTrigger="create"/>
|
||||
</div>
|
||||
{hasAgent ? (
|
||||
<CardsWithPagination data={agents} organizationView={true} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1}/>
|
||||
) : (
|
||||
<AgentDialog organization={organization} typeTrigger="empty"/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-5
@@ -29,11 +29,6 @@ export const OrganizationNotifiersTab = ({
|
||||
Notification Settings
|
||||
</h3>
|
||||
</div>
|
||||
{/*<NotifierAddEditModal*/}
|
||||
{/* organization={organization}*/}
|
||||
{/* open={isAddModalOpen}*/}
|
||||
{/* onOpenChangeAction={setIsAddModalOpen}*/}
|
||||
{/*/>*/}
|
||||
<ChannelAddEditModal
|
||||
kind={kind}
|
||||
organization={organization}
|
||||
|
||||
@@ -16,22 +16,32 @@ import {
|
||||
import {
|
||||
OrganizationStoragesTab
|
||||
} from "@/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-storages-tab";
|
||||
import {
|
||||
OrganizationAgentsTab
|
||||
} from "@/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-agents-tab";
|
||||
import {Agent} from "@/db/schema/08_agent";
|
||||
|
||||
export type OrganizationTabsProps = {
|
||||
organization: OrganizationWithMembers;
|
||||
notificationChannels: NotificationChannel[];
|
||||
storageChannels: StorageChannel[];
|
||||
activeMember: MemberWithUser
|
||||
activeMember: MemberWithUser;
|
||||
agents: Agent[]
|
||||
};
|
||||
|
||||
export const OrganizationTabs = ({activeMember, organization, notificationChannels, storageChannels}: OrganizationTabsProps) => {
|
||||
export const OrganizationTabs = ({
|
||||
activeMember,
|
||||
organization,
|
||||
notificationChannels,
|
||||
storageChannels,
|
||||
agents
|
||||
}: OrganizationTabsProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [tab, setTab] = useState<string>(() => searchParams.get("tab") ?? "users");
|
||||
|
||||
const {
|
||||
canManageUsers,
|
||||
canManageNotifications,
|
||||
canManageStorages
|
||||
} = useOrganizationPermissions(activeMember);
|
||||
@@ -71,6 +81,12 @@ export const OrganizationTabs = ({activeMember, organization, notificationChanne
|
||||
>
|
||||
Storages
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
className="w-full"
|
||||
value="agents"
|
||||
>
|
||||
Agents
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-full" value="users">
|
||||
<SettingsOrganizationMembersTable organization={organization}/>
|
||||
@@ -87,6 +103,12 @@ export const OrganizationTabs = ({activeMember, organization, notificationChanne
|
||||
storageChannels={storageChannels}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent className="h-full" value="agents">
|
||||
<OrganizationAgentsTab
|
||||
organization={organization}
|
||||
agents={agents}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
:
|
||||
<SettingsOrganizationMembersTable organization={organization}/>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE "organization_agents" (
|
||||
"organization_id" uuid NOT NULL,
|
||||
"agent_id" uuid NOT NULL,
|
||||
CONSTRAINT "organization_agents_organization_id_agent_id_unique" UNIQUE("organization_id","agent_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "agents" ADD COLUMN "organization_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "organization_agents" ADD CONSTRAINT "organization_agents_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "organization_agents" ADD CONSTRAINT "organization_agents_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "agents" ADD CONSTRAINT "agents_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- Custom SQL migration file, put your code below! --
|
||||
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
org RECORD;
|
||||
proj RECORD;
|
||||
db RECORD;
|
||||
BEGIN
|
||||
FOR org IN SELECT id FROM organization LOOP
|
||||
FOR proj IN
|
||||
SELECT id FROM projects WHERE organization_id = org.id
|
||||
LOOP
|
||||
FOR db IN
|
||||
SELECT agent_id FROM databases WHERE project_id = proj.id
|
||||
LOOP
|
||||
IF db.agent_id IS NOT NULL THEN
|
||||
INSERT INTO organization_agents (
|
||||
organization_id,
|
||||
agent_id
|
||||
)
|
||||
VALUES (
|
||||
org.id,
|
||||
db.agent_id
|
||||
)
|
||||
ON CONFLICT (organization_id, agent_id) DO NOTHING;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END $$;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "organization_agents" ADD COLUMN "updated_at" timestamp;--> statement-breakpoint
|
||||
ALTER TABLE "organization_agents" ADD COLUMN "created_at" timestamp DEFAULT now() NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "organization_agents" ADD COLUMN "deleted_at" timestamp;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "two_factor" ADD COLUMN "verified" boolean NOT NULL;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -344,6 +344,34 @@
|
||||
"when": 1774886139855,
|
||||
"tag": "0048_yellow_eddie_brock",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 49,
|
||||
"version": "7",
|
||||
"when": 1775585355435,
|
||||
"tag": "0049_chief_terrax",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 50,
|
||||
"version": "7",
|
||||
"when": 1775761395545,
|
||||
"tag": "0050_dark_saracen",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 51,
|
||||
"version": "7",
|
||||
"when": 1775762288699,
|
||||
"tag": "0051_young_senator_kelly",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 52,
|
||||
"version": "7",
|
||||
"when": 1775803959723,
|
||||
"tag": "0052_cute_punisher",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -97,6 +97,7 @@ export const twoFactor = pgTable("two_factor", {
|
||||
id: uuid().defaultRandom().primaryKey(),
|
||||
secret: text("secret").notNull(),
|
||||
backupCodes: text("backup_codes").notNull(),
|
||||
verified: boolean("verified").notNull(),
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {pgTable, text, boolean, timestamp, uuid, integer, pgEnum} from "drizzle-orm/pg-core";
|
||||
import {Agent, agent} from "./08_agent";
|
||||
import {Agent, agent, AgentWith} from "./08_agent";
|
||||
import {Project, project} from "./06_project";
|
||||
import {relations} from "drizzle-orm";
|
||||
import {dbmsEnum, statusEnum} from "./types";
|
||||
@@ -123,7 +123,7 @@ export type RetentionPolicy = z.infer<typeof retentionPolicySchema>;
|
||||
|
||||
|
||||
export type DatabaseWith = Database & {
|
||||
agent?: Agent | null;
|
||||
agent?: Agent | AgentWith | null;
|
||||
project?: Project | null;
|
||||
backups?: Backup[] | null;
|
||||
restorations?: Restoration[] | null;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {boolean, pgTable, text, timestamp, uuid, integer} from "drizzle-orm/pg-core";
|
||||
import {boolean, pgTable, text, timestamp, uuid, integer, unique} from "drizzle-orm/pg-core";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
import {Database, database} from "@/db/schema/07_database";
|
||||
import {relations} from "drizzle-orm";
|
||||
import {timestamps} from "@/db/schema/00_common";
|
||||
import {organization} from "@/db/schema/03_organization";
|
||||
|
||||
export const agent = pgTable("agents", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
@@ -14,22 +15,56 @@ export const agent = pgTable("agents", {
|
||||
description: text("description").notNull(),
|
||||
isArchived: boolean("is_archived").default(false),
|
||||
lastContact: timestamp("last_contact"),
|
||||
organizationId: uuid("organization_id").references(() => organization.id, {onDelete: "cascade"}),
|
||||
...timestamps
|
||||
});
|
||||
|
||||
|
||||
export const organizationAgent = pgTable(
|
||||
"organization_agents",
|
||||
{
|
||||
organizationId: uuid('organization_id')
|
||||
.notNull()
|
||||
.references(() => organization.id, {onDelete: 'cascade'}),
|
||||
agentId: uuid('agent_id')
|
||||
.notNull()
|
||||
.references(() => agent.id, {onDelete: 'cascade'}),
|
||||
...timestamps
|
||||
},
|
||||
(t) => [unique().on(t.organizationId, t.agentId)]
|
||||
|
||||
);
|
||||
|
||||
export const agentSchema = createSelectSchema(agent);
|
||||
export type Agent = z.infer<typeof agentSchema>;
|
||||
|
||||
|
||||
export const agentRelations = relations(agent, ({many}) => ({
|
||||
databases: many(database),
|
||||
organizations: many(organizationAgent),
|
||||
}));
|
||||
|
||||
export const organizationAgentRelations = relations(organizationAgent, ({one}) => ({
|
||||
organization: one(organization, {
|
||||
fields: [organizationAgent.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
agent: one(agent, {
|
||||
fields: [organizationAgent.agentId],
|
||||
references: [agent.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
export type AgentWith = Agent & {
|
||||
databases?: Database[] | null;
|
||||
organizations: {
|
||||
organizationId: string;
|
||||
agentId: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type AgentWithDatabases = Agent & {
|
||||
databases: Database[] | [];
|
||||
};
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ import {organization} from "@/db/schema/03_organization";
|
||||
import {relations} from "drizzle-orm";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
import {database} from "@/db/schema/07_database";
|
||||
|
||||
|
||||
export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3', 'google-drive']);
|
||||
|
||||
@@ -32,7 +30,6 @@ export const organizationStorageChannel = pgTable(
|
||||
(t) => [unique().on(t.organizationId, t.storageChannelId)]
|
||||
);
|
||||
|
||||
|
||||
export const storageChannelRelations = relations(storageChannel, ({many}) => ({
|
||||
organizations: many(organizationStorageChannel),
|
||||
}));
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import {boolean, pgTable, uuid} from "drizzle-orm/pg-core";
|
||||
import {timestamps} from "@/db/schema/00_common";
|
||||
import {relations} from "drizzle-orm";
|
||||
import {Backup, Database, database, Restoration, RetentionPolicy} from "@/db/schema/07_database";
|
||||
import {database} from "@/db/schema/07_database";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
import {StorageChannel, storageChannel} from "@/db/schema/12_storage-channel";
|
||||
import {Agent} from "@/db/schema/08_agent";
|
||||
import {Project} from "@/db/schema/06_project";
|
||||
import {AlertPolicy} from "@/db/schema/10_alert-policy";
|
||||
|
||||
export const storagePolicy = pgTable('storage_policy', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import {and, desc, eq, sql} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
import {Agent, agent, organizationAgent} from "@/db/schema/08_agent";
|
||||
import {Database, database} from "@/db/schema/07_database";
|
||||
|
||||
export async function getOrganizationAgents(organizationId: string) {
|
||||
|
||||
return await db
|
||||
.select({
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
organizationId: agent.organizationId,
|
||||
slug: agent.slug,
|
||||
healthErrorCount: agent.healthErrorCount,
|
||||
description: agent.description,
|
||||
isArchived: agent.isArchived,
|
||||
lastContact: agent.lastContact,
|
||||
version: agent.version,
|
||||
updatedAt: agent.updatedAt,
|
||||
createdAt: agent.createdAt,
|
||||
deletedAt: agent.deletedAt,
|
||||
databases: sql<Database[]>`
|
||||
COALESCE(
|
||||
json_agg(${database}.*) FILTER (WHERE ${database}.id IS NOT NULL),
|
||||
'[]'
|
||||
)
|
||||
`,
|
||||
})
|
||||
.from(organizationAgent)
|
||||
.innerJoin(
|
||||
agent,
|
||||
eq(organizationAgent.agentId, agent.id)
|
||||
)
|
||||
.leftJoin(database, eq(database.agentId, agent.id))
|
||||
.groupBy(agent.id)
|
||||
.orderBy(desc(agent.createdAt))
|
||||
.where(
|
||||
and(
|
||||
eq(organizationAgent.organizationId, organizationId),
|
||||
eq(agent.isArchived, false)
|
||||
)
|
||||
) as unknown as Agent[];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"use server"
|
||||
import {db} from "@/db";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
import {AgentWith} from "@/db/schema/08_agent";
|
||||
|
||||
export async function getOrganizationAvailableDatabases(
|
||||
organizationId: string,
|
||||
projectId?: string
|
||||
) {
|
||||
|
||||
const availableDatabases = (
|
||||
await db.query.database.findMany({
|
||||
where: (db, { eq, or, isNull }) =>
|
||||
projectId
|
||||
? or(isNull(db.projectId), eq(db.projectId, projectId))
|
||||
: isNull(db.projectId),
|
||||
with: {
|
||||
agent: {
|
||||
with: {
|
||||
organizations: true
|
||||
}
|
||||
},
|
||||
project: true,
|
||||
backups: true,
|
||||
restorations: true,
|
||||
},
|
||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
||||
})
|
||||
) as DatabaseWith[];
|
||||
|
||||
return availableDatabases.filter(db => {
|
||||
const agent = db.agent as AgentWith;
|
||||
if (agent?.isArchived) return false;
|
||||
return (
|
||||
agent?.organizationId === organizationId ||
|
||||
agent?.organizations?.some(org => org.organizationId === organizationId)
|
||||
);
|
||||
})
|
||||
}
|
||||
@@ -10,18 +10,30 @@ import {getHealthLast12hLogs} from "@/db/services/healthcheck";
|
||||
|
||||
const verifySlugUniqueness = async (slug: string, agentId?: string) => {
|
||||
const conditions = agentId ? and(eq(drizzleDb.schemas.agent.slug, slug), ne(drizzleDb.schemas.agent.id, agentId)) : eq(drizzleDb.schemas.agent.slug, slug);
|
||||
|
||||
const [countResult] = await db.select({count: count()}).from(drizzleDb.schemas.agent).where(conditions);
|
||||
|
||||
if (countResult.count > 0) {
|
||||
throw new ActionError("Slug already exists");
|
||||
}
|
||||
};
|
||||
|
||||
export const createAgentAction = userAction.schema(AgentSchema).action(async ({parsedInput}) => {
|
||||
const slug = slugify(parsedInput.name);
|
||||
export const createAgentAction = userAction.schema(
|
||||
z.object({
|
||||
organizationId: z.string().optional(),
|
||||
data: AgentSchema,
|
||||
})
|
||||
).action(async ({parsedInput}) => {
|
||||
const slug = slugify(parsedInput.data.name);
|
||||
await verifySlugUniqueness(slug);
|
||||
const [createdAgent] = await db.insert(drizzleDb.schemas.agent).values({...parsedInput, slug: slug}).returning();
|
||||
|
||||
const [createdAgent] = await db.insert(drizzleDb.schemas.agent).values({...parsedInput.data, slug: slug, organizationId: parsedInput.organizationId}).returning();
|
||||
|
||||
if (createdAgent && parsedInput.organizationId){
|
||||
await db.insert(drizzleDb.schemas.organizationAgent).values({
|
||||
organizationId: parsedInput.organizationId,
|
||||
agentId: createdAgent.id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
data: createdAgent,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
export const AgentSchema = z.object({
|
||||
name: z.string().nonempty("Name is required"),
|
||||
description: z.string(),
|
||||
|
||||
});
|
||||
|
||||
export type AgentType = z.infer<typeof AgentSchema>;
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"use server"
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {db} from "@/db";
|
||||
import {and, eq, inArray} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {AgentWith} from "@/db/schema/08_agent";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
|
||||
|
||||
export const updateAgentOrganizationsAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
data: z.array(z.string()),
|
||||
id: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput , ctx}): Promise<ServerActionResult<null>> => {
|
||||
try {
|
||||
const organizationsIds = parsedInput.data;
|
||||
const agentId = parsedInput.id;
|
||||
|
||||
const agent = await db.query.agent.findFirst({
|
||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
||||
with: {
|
||||
organizations: true,
|
||||
databases: true
|
||||
}
|
||||
}) as AgentWith;
|
||||
|
||||
|
||||
if (!agent) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Agent not found.",
|
||||
status: 404,
|
||||
cause: "not_found",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const existingItemIds = agent.organizations.map((organization) => organization.organizationId);
|
||||
|
||||
const organizationsToAdd = organizationsIds.filter((id) => !existingItemIds.includes(id));
|
||||
const organizationsToRemove = existingItemIds.filter((id) => !organizationsIds.includes(id));
|
||||
|
||||
if (organizationsToAdd.length > 0) {
|
||||
for (const organizationToAdd of organizationsToAdd) {
|
||||
await db.insert(drizzleDb.schemas.organizationAgent).values({
|
||||
organizationId: organizationToAdd,
|
||||
agentId: agentId
|
||||
});
|
||||
}
|
||||
}
|
||||
if (organizationsToRemove.length > 0) {
|
||||
await db.delete(drizzleDb.schemas.organizationAgent).where(and(inArray(drizzleDb.schemas.organizationAgent.organizationId, organizationsToRemove), eq(drizzleDb.schemas.organizationAgent.agentId,agentId))).execute();
|
||||
|
||||
const organizationsToRemoveDetails = await db.query.organization.findMany({
|
||||
where: inArray(drizzleDb.schemas.organization.id, organizationsToRemove),
|
||||
with: {
|
||||
projects: true
|
||||
}
|
||||
});
|
||||
|
||||
const projectIds = organizationsToRemoveDetails.flatMap(org =>
|
||||
org.projects.map(project => project.id)
|
||||
);
|
||||
|
||||
if (projectIds.length > 0) {
|
||||
const databases = await db.query.database.findMany({
|
||||
where: (db, { inArray }) => inArray(db.projectId, projectIds),
|
||||
columns: { id: true }
|
||||
});
|
||||
|
||||
const databaseIds = databases.map(d => d.id);
|
||||
|
||||
await db
|
||||
.update(drizzleDb.schemas.database)
|
||||
.set(withUpdatedAt({
|
||||
backupPolicy: null,
|
||||
projectId: null
|
||||
}))
|
||||
.where(inArray(drizzleDb.schemas.database.projectId, projectIds))
|
||||
.execute();
|
||||
|
||||
await db.delete(drizzleDb.schemas.retentionPolicy)
|
||||
.where(inArray(drizzleDb.schemas.retentionPolicy.databaseId, databaseIds))
|
||||
.execute();
|
||||
|
||||
await db.delete(drizzleDb.schemas.alertPolicy)
|
||||
.where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databaseIds))
|
||||
.execute();
|
||||
|
||||
await db.delete(drizzleDb.schemas.storagePolicy)
|
||||
.where(inArray(drizzleDb.schemas.storagePolicy.databaseId, databaseIds))
|
||||
.execute();
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: null,
|
||||
actionSuccess: {
|
||||
message: "Agent organizations has been successfully updated.",
|
||||
messageParams: {agentId: agentId},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error updating agent organizations:", error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to update agent organizations.",
|
||||
status: 500,
|
||||
cause: "server_error",
|
||||
messageParams: {message: "Error updating the agent organizations"},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {Form, FormControl, FormField, FormItem, useZodForm} from "@/components/ui/form";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
|
||||
import {toast} from "sonner";
|
||||
import {AgentWith} from "@/db/schema/08_agent";
|
||||
import {AgentOrganizationSchema, AgentOrganizationType} from "@/features/agents/components/agent-organizations.schema";
|
||||
import {updateAgentOrganizationsAction} from "@/features/agents/components/agent-organizations.action";
|
||||
|
||||
|
||||
type AgentOrganisationFormProps = {
|
||||
organizations?: OrganizationWithMembers[];
|
||||
defaultValues?: AgentWith
|
||||
};
|
||||
|
||||
export const AgentOrganisationForm = ({
|
||||
organizations,
|
||||
defaultValues,
|
||||
}: AgentOrganisationFormProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const defaultOrganizationIds = defaultValues?.organizations?.map(organization => organization.organizationId) ?? []
|
||||
|
||||
|
||||
const form = useZodForm({
|
||||
schema: AgentOrganizationSchema,
|
||||
// @ts-ignore
|
||||
defaultValues: {
|
||||
organizations: defaultOrganizationIds
|
||||
},
|
||||
});
|
||||
|
||||
const formatOrganizationsList = (organizations: OrganizationWithMembers[]) => {
|
||||
return organizations
|
||||
.map((organization) => ({
|
||||
value: organization.id,
|
||||
label: `${organization.name}`,
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: AgentOrganizationType) => {
|
||||
|
||||
const payload = {
|
||||
data: values.organizations,
|
||||
id: defaultValues?.id ?? ""
|
||||
};
|
||||
|
||||
const result = await updateAgentOrganizationsAction(payload)
|
||||
const inner = result?.data;
|
||||
|
||||
if (inner?.success) {
|
||||
toast.success(inner.actionSuccess?.message);
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.error(inner?.actionError?.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`organizations`}
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<MultiSelect
|
||||
options={formatOrganizationsList(organizations ?? [])}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value ?? []}
|
||||
placeholder="Select organization(s)"
|
||||
variant="inverted"
|
||||
animation={0}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<div className="flex gap-2 justify-end">
|
||||
<ButtonWithLoading isPending={mutation.isPending}>
|
||||
Save
|
||||
</ButtonWithLoading>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const AgentOrganizationSchema = z.object({
|
||||
organizations: z.array(z.string().uuid())
|
||||
});
|
||||
|
||||
export type AgentOrganizationType = z.infer<typeof AgentOrganizationSchema>;
|
||||
@@ -10,19 +10,24 @@ import {
|
||||
import {AgentForm} from "@/features/agents/components/agent.form";
|
||||
import {Button, buttonVariants} from "@/components/ui/button";
|
||||
import {Plus} from "lucide-react";
|
||||
import {AgentType} from "@/features/agents/agents.schema";
|
||||
import {GearIcon} from "@radix-ui/react-icons";
|
||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||
import {useState} from "react";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {AgentOrganisationForm} from "@/features/agents/components/agent-organizations.form";
|
||||
import {AgentWith} from "@/db/schema/08_agent";
|
||||
|
||||
type AgentDialogProps = {
|
||||
agent?: AgentType & { id: string };
|
||||
agent?: AgentWith;
|
||||
typeTrigger: "edit" | "empty" | "create";
|
||||
organization?: OrganizationWithMembers;
|
||||
adminView?: boolean,
|
||||
organizations?: OrganizationWithMembers[];
|
||||
};
|
||||
|
||||
|
||||
export const AgentDialog = ({agent, typeTrigger}: AgentDialogProps) => {
|
||||
export const AgentDialog = ({agent, typeTrigger, organization, adminView, organizations}: AgentDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const isEdit = !!agent;
|
||||
const router = useRouter();
|
||||
@@ -36,7 +41,7 @@ export const AgentDialog = ({agent, typeTrigger}: AgentDialogProps) => {
|
||||
</div>
|
||||
);
|
||||
case "empty":
|
||||
return <EmptyStatePlaceholder text="Create new Agent"/>;
|
||||
return <EmptyStatePlaceholder className="h-full" text="Create new Agent"/>;
|
||||
case "create":
|
||||
return <Button><Plus className="mr-2 h-4 w-4"/> Create Agent</Button>;
|
||||
default:
|
||||
@@ -53,14 +58,45 @@ export const AgentDialog = ({agent, typeTrigger}: AgentDialogProps) => {
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEdit ? `Edit ${agent.name}` : "Create new agent"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<AgentForm
|
||||
onSuccess={() => {
|
||||
setOpen(false)
|
||||
router.refresh()
|
||||
}}
|
||||
defaultValues={agent}
|
||||
agentId={agent?.id}
|
||||
/>
|
||||
<>
|
||||
{adminView ?
|
||||
<Tabs className="flex flex-col flex-1" defaultValue="configuration">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="configuration">Configuration</TabsTrigger>
|
||||
<TabsTrigger value="organizations">Organizations</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-full justify-between" value="configuration">
|
||||
<AgentForm
|
||||
organization={organization}
|
||||
onSuccess={() => {
|
||||
setOpen(false)
|
||||
router.refresh()
|
||||
}}
|
||||
defaultValues={agent}
|
||||
agentId={agent?.id}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent className="h-full justify-between" value="organizations">
|
||||
<AgentOrganisationForm
|
||||
defaultValues={agent}
|
||||
organizations={organizations}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
:
|
||||
<>
|
||||
<AgentForm
|
||||
organization={organization}
|
||||
onSuccess={() => {
|
||||
setOpen(false)
|
||||
router.refresh()
|
||||
}}
|
||||
defaultValues={agent}
|
||||
agentId={agent?.id}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -18,11 +18,14 @@ import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import {AgentSchema, AgentType} from "@/features/agents/agents.schema";
|
||||
import {toast} from "sonner";
|
||||
import {createAgentAction, updateAgentAction} from "@/features/agents/agents.action";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
export type agentFormProps = {
|
||||
defaultValues?: AgentType;
|
||||
agentId?: string;
|
||||
onSuccess?: (data: any) => void;
|
||||
organization?: OrganizationWithMembers;
|
||||
|
||||
};
|
||||
|
||||
export const AgentForm = (props: agentFormProps) => {
|
||||
@@ -40,7 +43,10 @@ export const AgentForm = (props: agentFormProps) => {
|
||||
mutationFn: async (values: AgentType) => {
|
||||
|
||||
const createAgent = isCreate
|
||||
? await createAgentAction(values)
|
||||
? await createAgentAction({
|
||||
organizationId: props.organization?.id ?? undefined,
|
||||
data: values
|
||||
})
|
||||
: await updateAgentAction({
|
||||
id: props.agentId ?? "-",
|
||||
data: values,
|
||||
@@ -61,7 +67,7 @@ export const AgentForm = (props: agentFormProps) => {
|
||||
if (props.onSuccess) {
|
||||
props.onSuccess(data);
|
||||
} else {
|
||||
router.push(`/dashboard/agents/${data.id}`);
|
||||
router.push(props.organization ?`/dashboard/settings/agents/${data.id}` : `/dashboard/agents/${data.id}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ export type OrganizationPermissions = {
|
||||
isAdmin: boolean;
|
||||
isMember: boolean;
|
||||
|
||||
canManageAgents: boolean;
|
||||
canManageSettings: boolean;
|
||||
canManageUsers: boolean;
|
||||
canManageNotifications: boolean;
|
||||
@@ -31,6 +32,7 @@ export const computeOrganizationPermissions = (
|
||||
isMember,
|
||||
|
||||
canManageSettings: isOwner || isAdmin,
|
||||
canManageAgents: isOwner || isAdmin,
|
||||
canManageUsers: isOwner || isAdmin,
|
||||
canManageNotifications: isOwner || isAdmin,
|
||||
canManageStorages: isOwner || isAdmin,
|
||||
|
||||
@@ -30,7 +30,7 @@ export const auth = betterAuth({
|
||||
onAPIError: {
|
||||
errorURL: "/error",
|
||||
onError: (error, ctx) => {
|
||||
//todo: capture errors in a monitoring service
|
||||
//TODO: capture errors in a monitoring service
|
||||
},
|
||||
},
|
||||
database: drizzleAdapter(db, {
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ import {cleaningHealthcheckLogsJob, cleaningJob, healthcheckAgentAndDatabaseJob,
|
||||
import { generateRSAKeys, getOrCreateMasterKey } from "@/utils/rsa-keys";
|
||||
import { StorageProviderKind } from "@/features/storages/types";
|
||||
import {logger} from "@/lib/logger";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
|
||||
const log = logger.child({module: "init"});
|
||||
|
||||
@@ -103,7 +104,7 @@ async function createSettingsIfNotExist() {
|
||||
if (!finalSystemSetting.defaultStorageChannelId) {
|
||||
await tx
|
||||
.update(drizzleDb.schemas.setting)
|
||||
.set({ defaultStorageChannelId: localStorage.id })
|
||||
.set(withUpdatedAt({ defaultStorageChannelId: localStorage.id }))
|
||||
.where(eq(drizzleDb.schemas.setting.id, finalSystemSetting.id));
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user