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
@@ -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}/>
|
||||
|
||||
Reference in New Issue
Block a user