mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Working on notification system.
This commit is contained in:
Binary file not shown.
@@ -4,7 +4,7 @@ import {db} from "@/db";
|
|||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page} from "@/features/layout/page";
|
import {Page} from "@/features/layout/page";
|
||||||
import {OrganizationManagement} from "@/components/wrappers/dashboard/admin/organization/organization-management";
|
import {OrganizationManagement} from "@/components/wrappers/dashboard/admin/organizations/organization/organization-management";
|
||||||
import {buildOrganizationWithMembers} from "@/utils/common";
|
import {buildOrganizationWithMembers} from "@/utils/common";
|
||||||
import {isUUID} from "@/utils/text";
|
import {isUUID} from "@/utils/text";
|
||||||
import {user} from "@/db/schema/02_user";
|
import {user} from "@/db/schema/02_user";
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/
|
|||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {
|
import {
|
||||||
AdminOrganizationAddModal
|
AdminOrganizationAddModal
|
||||||
} from "@/components/wrappers/dashboard/admin/organization/admin-organization-add-modal";
|
} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-organization-add-modal";
|
||||||
import {AdminOrganizationList} from "@/components/wrappers/dashboard/admin/organization/admin-orgnization-list";
|
import {AdminOrganizationList} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-orgnization-list";
|
||||||
import {isNull} from "drizzle-orm";
|
import {isNull} from "drizzle-orm";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {AdminTabs} from "@/components/wrappers/dashboard/admin/admin-tabs";
|
|
||||||
import {db} from "@/db";
|
|
||||||
import {isNull} from "drizzle-orm";
|
|
||||||
import {Metadata} from "next";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Admin",
|
|
||||||
};
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
|
||||||
|
|
||||||
const users = await db.query.user.findMany({
|
|
||||||
where: (fields) => isNull(fields.deletedAt),
|
|
||||||
with: {
|
|
||||||
accounts: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const organizations = await db.query.organization.findMany({
|
|
||||||
where: (fields) => isNull(fields.deletedAt),
|
|
||||||
with: {
|
|
||||||
members: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const settings = await db.query.setting.findFirst({
|
|
||||||
where: (fields, {eq}) => eq(fields.name, "system"),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>Administration Panel</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent>
|
|
||||||
<AdminTabs
|
|
||||||
organizations={organizations}
|
|
||||||
settings={settings!}
|
|
||||||
users={users}/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,18 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {SettingsStorageSection} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage-section";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const settings = await db.query.setting.findFirst({
|
||||||
|
where: (fields, {eq}) => eq(fields.name, "system"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
@@ -12,7 +22,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="flex flex-col gap-5">
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
<SettingsStorageSection settings={settings}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {PageParams} from "@/types/next";
|
|||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||||
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/columns-users";
|
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/users/columns-users";
|
||||||
import {isNull} from "drizzle-orm";
|
import {isNull} from "drizzle-orm";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {
|
||||||
|
NotificationChannelsSection
|
||||||
|
} from "@/components/wrappers/dashboard/admin/notifications/channels/notification-channels-section";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {notificationChannel, NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
|
import {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal";
|
||||||
|
import {desc, isNull} from "drizzle-orm";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Notification Channels",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const notificationChannels = await db.query.notificationChannel.findMany({
|
||||||
|
with: {
|
||||||
|
organizations: true
|
||||||
|
},
|
||||||
|
orderBy: desc(notificationChannel.createdAt)
|
||||||
|
}) as NotificationChannelWith[]
|
||||||
|
|
||||||
|
const organizations = await db.query.organization.findMany({
|
||||||
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
|
with: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle>Notification channels</PageTitle>
|
||||||
|
<PageActions>
|
||||||
|
<NotifierAddEditModal adminView={false}/>
|
||||||
|
</PageActions>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<NotificationChannelsSection organizations={organizations} notificationChannels={notificationChannels}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {NotificationLogsList} from "@/components/wrappers/dashboard/admin/notifications/logs/notification-logs-list";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
import {getNotificationHistory} from "@/db/services/notification-log";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Activity logs",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const notificationLogs = await getNotificationHistory()
|
||||||
|
|
||||||
|
if (!notificationLogs) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle>Activity logs</PageTitle>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<NotificationLogsList notificationLogs={notificationLogs} />
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -44,7 +44,7 @@ export function BreadCrumbsWrapper() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const FORBIDDEN_LINKS = ["organization", "dashboard", "database"];
|
const FORBIDDEN_LINKS = ["organization", "dashboard", "database", "admin", "settings", "notifications"];
|
||||||
|
|
||||||
|
|
||||||
export function BreadCrumbs({}: BreadCrumbsProps) {
|
export function BreadCrumbs({}: BreadCrumbsProps) {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ interface CardsWithPaginationProps<T> {
|
|||||||
cardsPerPage?: number;
|
cardsPerPage?: number;
|
||||||
numberOfColumns?: number;
|
numberOfColumns?: number;
|
||||||
maxVisiblePages?: number;
|
maxVisiblePages?: number;
|
||||||
// extendedProps?: any;
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
|
||||||
import {SettingsEmailTab} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/settings-email-tab";
|
|
||||||
import {SettingsStorageTab} from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/settings-storage-tab";
|
|
||||||
import {User, UserWithAccounts} from "@/db/schema/02_user";
|
|
||||||
import {Setting} from "@/db/schema/01_setting";
|
|
||||||
import {useEffect, useState} from "react";
|
|
||||||
import {useRouter, useSearchParams} from "next/navigation";
|
|
||||||
import {AdminUsersTable} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/admin-user-table";
|
|
||||||
import {
|
|
||||||
AdminOrganizationsTable
|
|
||||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-organizations-tab/admin-organizations-table";
|
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
|
||||||
|
|
||||||
export type AdminTabsProps = {
|
|
||||||
users: UserWithAccounts[];
|
|
||||||
settings: Setting;
|
|
||||||
organizations: OrganizationWithMembers[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AdminTabs = ({users, settings, organizations}: AdminTabsProps) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
const [tab, setTab] = useState<string>(() => searchParams.get("tab") ?? "users");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const newTab = searchParams.get("tab") ?? "users";
|
|
||||||
setTab(newTab);
|
|
||||||
}, [searchParams]);
|
|
||||||
|
|
||||||
const handleChangeTab = (value: string) => {
|
|
||||||
router.push(`?tab=${value}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tabs className="h-full" value={tab} onValueChange={handleChangeTab}>
|
|
||||||
<TabsList className="w-full">
|
|
||||||
<TabsTrigger className="w-full" value="users">
|
|
||||||
Users
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger className="w-full" value="organizations">
|
|
||||||
Organizations
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger className="w-full" value="email">
|
|
||||||
Email
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger className="w-full" value="storage">
|
|
||||||
Storage
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent className="h-full" value="users">
|
|
||||||
<AdminUsersTable users={users}/>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent className="h-full" value="organizations">
|
|
||||||
<AdminOrganizationsTable organizations={organizations}/>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="email">
|
|
||||||
<SettingsEmailTab settings={settings}/>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="storage">
|
|
||||||
<SettingsStorageTab settings={settings}/>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
"use client"
|
||||||
|
import {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
|
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||||
|
import {NotifierCard} from "@/components/wrappers/dashboard/common/notifier/notifier-card/notifier-card";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||||
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
|
|
||||||
|
type NotificationChannelsSectionProps = {
|
||||||
|
notificationChannels: NotificationChannelWith[]
|
||||||
|
organizations: OrganizationWithMembers[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotificationChannelsSection = ({
|
||||||
|
organizations,
|
||||||
|
notificationChannels
|
||||||
|
}: NotificationChannelsSectionProps) => {
|
||||||
|
|
||||||
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const hasNotifiers = notificationChannels.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full">
|
||||||
|
{hasNotifiers ? (
|
||||||
|
<div className="h-full">
|
||||||
|
<CardsWithPagination
|
||||||
|
data={notificationChannels}
|
||||||
|
cardItem={NotifierCard}
|
||||||
|
cardsPerPage={8}
|
||||||
|
numberOfColumns={2}
|
||||||
|
adminView={true}
|
||||||
|
organizations={organizations}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<EmptyStatePlaceholder
|
||||||
|
text="No notification channels configured yet"
|
||||||
|
onClick={() => setIsAddModalOpen(true)}
|
||||||
|
className="h-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
"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 {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
|
import {
|
||||||
|
NotificationChannelsOrganizationSchema,
|
||||||
|
NotificationChannelsOrganizationType
|
||||||
|
} from "@/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization.schema";
|
||||||
|
import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
|
||||||
|
import {
|
||||||
|
updateNotificationChannelsOrganizationAction
|
||||||
|
} from "@/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization.action";
|
||||||
|
import {toast} from "sonner";
|
||||||
|
|
||||||
|
|
||||||
|
type NotifierChannelOrganisationFormProps = {
|
||||||
|
organizations?: OrganizationWithMembers[];
|
||||||
|
defaultValues?: NotificationChannelWith
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NotifierChannelOrganisationForm = ({
|
||||||
|
organizations,
|
||||||
|
defaultValues,
|
||||||
|
}: NotifierChannelOrganisationFormProps) => {
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
||||||
|
console.log("defaultValues", defaultValues);
|
||||||
|
console.log("organizations", organizations);
|
||||||
|
|
||||||
|
const defaultOrganizationIds = defaultValues?.organizations?.map(organization => organization.organizationId) ?? []
|
||||||
|
|
||||||
|
|
||||||
|
const form = useZodForm({
|
||||||
|
schema: NotificationChannelsOrganizationSchema,
|
||||||
|
// @ts-ignore
|
||||||
|
defaultValues: {
|
||||||
|
organizations: defaultOrganizationIds
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatOrganizationsList = (organizations: OrganizationWithMembers[]) => {
|
||||||
|
return organizations
|
||||||
|
.map((organization) => ({
|
||||||
|
value: organization.id,
|
||||||
|
label: `${organization.name}`,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const mutationUpdateNotificationChannelOrganizations = useMutation({
|
||||||
|
mutationFn: async (values: NotificationChannelsOrganizationType) => {
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
data: values.organizations,
|
||||||
|
notificationChannelId: defaultValues?.id ?? ""
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await updateNotificationChannelsOrganizationAction(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 mutationUpdateNotificationChannelOrganizations.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={mutationUpdateNotificationChannelOrganizations.isPending}>
|
||||||
|
Save
|
||||||
|
</ButtonWithLoading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
"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 {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
|
|
||||||
|
|
||||||
|
export const updateNotificationChannelsOrganizationAction = userAction
|
||||||
|
.schema(
|
||||||
|
z.object({
|
||||||
|
data: z.array(z.string()),
|
||||||
|
notificationChannelId: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<null>> => {
|
||||||
|
try {
|
||||||
|
const organizationsIds = parsedInput.data;
|
||||||
|
|
||||||
|
const notificationChannel = await db.query.notificationChannel.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.notificationChannel.id, parsedInput.notificationChannelId),
|
||||||
|
with: {
|
||||||
|
organizations: true,
|
||||||
|
}
|
||||||
|
}) as NotificationChannelWith;
|
||||||
|
|
||||||
|
|
||||||
|
if (!notificationChannel) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
actionError: {
|
||||||
|
message: "Notification channel not found.",
|
||||||
|
status: 404,
|
||||||
|
cause: "not_found",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingItemIds = notificationChannel.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.organizationNotificationChannel).values({
|
||||||
|
organizationId: organizationToAdd,
|
||||||
|
notificationChannelId: parsedInput.notificationChannelId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (organizationsToRemove.length > 0) {
|
||||||
|
await db.delete(drizzleDb.schemas.organizationNotificationChannel).where(and(inArray(drizzleDb.schemas.organizationNotificationChannel.organizationId, organizationsToRemove), eq(drizzleDb.schemas.organizationNotificationChannel.notificationChannelId, parsedInput.notificationChannelId))).execute();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: null,
|
||||||
|
actionSuccess: {
|
||||||
|
message: "Notification channel organizations has been successfully updated.",
|
||||||
|
messageParams: {notificationChannelId: parsedInput.notificationChannelId},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating notification channel:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
actionError: {
|
||||||
|
message: "Failed to update notification channel.",
|
||||||
|
status: 500,
|
||||||
|
cause: "server_error",
|
||||||
|
messageParams: {message: "Error updating the notification channel"},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const NotificationChannelsOrganizationSchema = z.object({
|
||||||
|
organizations: z.array(z.string())
|
||||||
|
});
|
||||||
|
|
||||||
|
export type NotificationChannelsOrganizationType = z.infer<typeof NotificationChannelsOrganizationSchema>;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import {Mail, MessageSquare, Webhook} from "lucide-react";
|
||||||
|
|
||||||
|
export const getNotificationChannelIcon = (type: string) => {
|
||||||
|
const Icon = notificationTypes.find((t) => t.value === type)?.icon
|
||||||
|
return Icon ? <Icon className="h-4 w-4"/> : null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const notificationTypes = [
|
||||||
|
{value: "smtp", label: "Email", icon: Mail},
|
||||||
|
{value: "slack", label: "Slack", icon: MessageSquare},
|
||||||
|
{value: "webhook", label: "Webhook", icon: Webhook},
|
||||||
|
]
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {ColumnDef} from "@tanstack/react-table";
|
||||||
|
import {NotificationLogWithRelations} from "@/db/services/notification-log";
|
||||||
|
import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers";
|
||||||
|
import {humanReadableDate} from "@/utils/date-formatting";
|
||||||
|
import {CheckCircle2, XCircle} from "lucide-react";
|
||||||
|
import {Badge} from "@/components/ui/badge";
|
||||||
|
import {NotificationLogModal} from "@/components/wrappers/dashboard/admin/notifications/logs/notification-log-modal";
|
||||||
|
|
||||||
|
|
||||||
|
export function notificationLogsColumns(): ColumnDef<NotificationLogWithRelations>[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: "success",
|
||||||
|
header: "Status",
|
||||||
|
cell: ({row}) => {
|
||||||
|
const status = row.original.success ? "delivered" : "failed";
|
||||||
|
return(
|
||||||
|
<Badge variant="outline" className={`gap-1.5 ${getStatusColor(status)}`}>
|
||||||
|
{getStatusIcon(row.original.success)}
|
||||||
|
<span className="capitalize">{status}</span>
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "channel",
|
||||||
|
header: "Channel",
|
||||||
|
cell: ({row}) => {
|
||||||
|
const channel = row.original.channel
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-md bg-secondary border border-border">
|
||||||
|
{getNotificationChannelIcon(channel?.provider ?? "")}
|
||||||
|
</div>
|
||||||
|
{channel?.name}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "sentAt",
|
||||||
|
header: "Sent At",
|
||||||
|
cell: ({row}) => {
|
||||||
|
const sentAt = humanReadableDate(row.original.sentAt)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{sentAt}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "policy",
|
||||||
|
header: "Event Kinds",
|
||||||
|
cell: ({row}) => {
|
||||||
|
const eventKinds = row.original.policy?.eventKinds ?? [];
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{eventKinds.map((eventKind, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-2">
|
||||||
|
<Badge>{eventKind}</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "details",
|
||||||
|
header: "Details",
|
||||||
|
cell: ({row}) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<NotificationLogModal notificationLog={row.original} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const getStatusIcon = (status: boolean) => {
|
||||||
|
switch (status) {
|
||||||
|
case true:
|
||||||
|
return <CheckCircle2 className="h-4 w-4"/>
|
||||||
|
case false:
|
||||||
|
return <XCircle className="h-4 w-4"/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case "delivered":
|
||||||
|
return "bg-green-100 dark:bg-green-100/10"
|
||||||
|
case "failed":
|
||||||
|
return "bg-red-100 dark:bg-red-100/10"
|
||||||
|
case "pending":
|
||||||
|
return "bg-orange-100 dark:bg-orange-100/10"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"use client"
|
||||||
|
import {Eye} from "lucide-react";
|
||||||
|
|
||||||
|
import {useState} from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
import {Separator} from "@/components/ui/separator";
|
||||||
|
import {NotificationLogWithRelations} from "@/db/services/notification-log";
|
||||||
|
|
||||||
|
type NotificationLogModalProps = {
|
||||||
|
notificationLog: NotificationLogWithRelations;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotificationLogModal = ({notificationLog}: NotificationLogModalProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" onClick={() => setOpen(true)} className="relative">
|
||||||
|
<Eye/>
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Notification details</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Details of the notification sent
|
||||||
|
</DialogDescription>
|
||||||
|
<Separator className="mt-3 mb-3"/>
|
||||||
|
|
||||||
|
<div className="space-y-2 mb-4">
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold">Title:</span>{" "}
|
||||||
|
<span>{notificationLog.content.title}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold">Message:</span>{" "}
|
||||||
|
<span>{notificationLog.content.message}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{notificationLog.payload && (
|
||||||
|
<>
|
||||||
|
<Separator className="mb-3" />
|
||||||
|
<div className="text-sm font-mono bg-gray-100 dark:bg-gray-800 dark:text-gray-200 p-4 rounded-md overflow-auto">
|
||||||
|
{JSON.stringify(notificationLog.payload, null, 2)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"use client"
|
||||||
|
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||||
|
import {useRouter} from "next/navigation";
|
||||||
|
import {
|
||||||
|
notificationLogsColumns,
|
||||||
|
} from "@/components/wrappers/dashboard/admin/notifications/logs/columns";
|
||||||
|
import {NotificationLogWithRelations} from "@/db/services/notification-log";
|
||||||
|
|
||||||
|
|
||||||
|
type NotificationsLogsListProps = {
|
||||||
|
notificationLogs: NotificationLogWithRelations[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotificationLogsList = (props: NotificationsLogsListProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable
|
||||||
|
enableSelect={false}
|
||||||
|
columns={notificationLogsColumns()}
|
||||||
|
data={props.notificationLogs}
|
||||||
|
enablePagination
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
import {AdminOrganizationList} from "@/components/wrappers/dashboard/admin/organization/admin-orgnization-list";
|
import {AdminOrganizationList} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-orgnization-list";
|
||||||
|
|
||||||
export type AdminOrganizationsTableProps = {
|
export type AdminOrganizationsTableProps = {
|
||||||
organizations: OrganizationWithMembers[];
|
organizations: OrganizationWithMembers[];
|
||||||
-1
@@ -12,7 +12,6 @@ import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with
|
|||||||
import {UserWithAccounts} from "@/db/schema/02_user";
|
import {UserWithAccounts} from "@/db/schema/02_user";
|
||||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||||
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
||||||
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/button-delete-use";
|
|
||||||
import {Organization} from "@/db/schema/03_organization";
|
import {Organization} from "@/db/schema/03_organization";
|
||||||
|
|
||||||
export const organizationsColumnsAdmin: ColumnDef<Organization>[] = [
|
export const organizationsColumnsAdmin: ColumnDef<Organization>[] = [
|
||||||
+1
-1
@@ -6,7 +6,7 @@ import { useMutation } from "@tanstack/react-query";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
||||||
import { OrganizationSchema } from "@/components/wrappers/dashboard/admin/organization/organization.schema";
|
import { OrganizationSchema } from "@/components/wrappers/dashboard/admin/organizations/organization/organization.schema";
|
||||||
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { authClient } from "@/lib/auth/auth-client";
|
import { authClient } from "@/lib/auth/auth-client";
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { AdminOrganizationList } from "@/components/wrappers/dashboard/admin/organization/admin-orgnization-list";
|
import { AdminOrganizationList } from "@/components/wrappers/dashboard/admin/organizations/organization/admin-orgnization-list";
|
||||||
import { AdminOrganizationAddModal } from "@/components/wrappers/dashboard/admin/organization/admin-organization-add-modal";
|
import { AdminOrganizationAddModal } from "@/components/wrappers/dashboard/admin/organizations/organization/admin-organization-add-modal";
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
|
|
||||||
type AdminOrganizationSectionProps = {
|
type AdminOrganizationSectionProps = {
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { DataTable } from "@/components/wrappers/common/table/data-table";
|
import { DataTable } from "@/components/wrappers/common/table/data-table";
|
||||||
import { organizationsListColumns } from "@/components/wrappers/dashboard/admin/organization/table-colums";
|
import { organizationsListColumns } from "@/components/wrappers/dashboard/admin/organizations/organization/table-colums";
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
|
|
||||||
type AdminOrganizationListProps = {
|
type AdminOrganizationListProps = {
|
||||||
+2
-2
@@ -7,11 +7,11 @@ import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodFo
|
|||||||
import {
|
import {
|
||||||
AddMemberSchema,
|
AddMemberSchema,
|
||||||
AddMemberSchemaType
|
AddMemberSchemaType
|
||||||
} from "@/components/wrappers/dashboard/admin/organization/organization.schema";
|
} from "@/components/wrappers/dashboard/admin/organizations/organization/organization.schema";
|
||||||
import {SearchInput} from "@/components/ui/search-input";
|
import {SearchInput} from "@/components/ui/search-input";
|
||||||
import {
|
import {
|
||||||
addMemberOrganizationAction
|
addMemberOrganizationAction
|
||||||
} from "@/components/wrappers/dashboard/admin/organization/details/add-member.action";
|
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/add-member.action";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {OrganizationWithMembers, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||||
import {User} from "@/db/schema/02_user";
|
import {User} from "@/db/schema/02_user";
|
||||||
+19
-9
@@ -1,11 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import {
|
||||||
import { OrganizationAddMemberForm } from "@/components/wrappers/dashboard/admin/organization/details/organization-add-member-form";
|
Dialog,
|
||||||
import { useState } from "react";
|
DialogContent,
|
||||||
import { UserPlus } from "lucide-react";
|
DialogDescription,
|
||||||
import { Button } from "@/components/ui/button";
|
DialogHeader,
|
||||||
import {OrganizationWithMembers, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
DialogTitle,
|
||||||
|
DialogTrigger
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
OrganizationAddMemberForm
|
||||||
|
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/organization-add-member-form";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {UserPlus} from "lucide-react";
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
import { OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||||
import {User} from "@/db/schema/02_user";
|
import {User} from "@/db/schema/02_user";
|
||||||
|
|
||||||
type OrganizationAddMemberModalProps = {
|
type OrganizationAddMemberModalProps = {
|
||||||
@@ -13,13 +22,13 @@ type OrganizationAddMemberModalProps = {
|
|||||||
organization: OrganizationWithMembersAndUsers;
|
organization: OrganizationWithMembersAndUsers;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OrganizationAddMemberModal = ({ users, organization }: OrganizationAddMemberModalProps) => {
|
export const OrganizationAddMemberModal = ({users, organization}: OrganizationAddMemberModalProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>
|
<Button>
|
||||||
<UserPlus className="w-4 h-4 mr-2" />
|
<UserPlus className="w-4 h-4 mr-2"/>
|
||||||
Add member
|
Add member
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@@ -28,7 +37,8 @@ export const OrganizationAddMemberModal = ({ users, organization }: Organization
|
|||||||
<DialogTitle>Add member to your organization</DialogTitle>
|
<DialogTitle>Add member to your organization</DialogTitle>
|
||||||
<DialogDescription>Select a user to add to your organization</DialogDescription>
|
<DialogDescription>Select a user to add to your organization</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<OrganizationAddMemberForm users={users} organization={organization} onSuccessAction={() => setOpen(!open)} />
|
<OrganizationAddMemberForm users={users} organization={organization}
|
||||||
|
onSuccessAction={() => setOpen(!open)}/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
+2
-2
@@ -12,12 +12,12 @@ import {Button} from "@/components/ui/button";
|
|||||||
import {MoreHorizontal, Settings, Trash2} from "lucide-react";
|
import {MoreHorizontal, Settings, Trash2} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
OrganizationDeleteMemberModal
|
OrganizationDeleteMemberModal
|
||||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-delete-member-modal";
|
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/organization-delete-member-modal";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {authClient} from "@/lib/auth/auth-client";
|
import {authClient} from "@/lib/auth/auth-client";
|
||||||
import {
|
import {
|
||||||
OrganizationMemberChangeRoleModal
|
OrganizationMemberChangeRoleModal
|
||||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-member-change-role";
|
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/organization-member-change-role";
|
||||||
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||||
|
|
||||||
type OrganizationMemberCardProps = {
|
type OrganizationMemberCardProps = {
|
||||||
+1
-1
@@ -22,7 +22,7 @@ import {updateMemberRoleAction} from "@/components/wrappers/dashboard/settings/u
|
|||||||
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
|
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
|
||||||
import {
|
import {
|
||||||
updateMemberRoleAdminAction
|
updateMemberRoleAdminAction
|
||||||
} from "@/components/wrappers/dashboard/admin/organization/details/role-member.action";
|
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/role-member.action";
|
||||||
|
|
||||||
type OrganizationMemberChangeRoleModalProps = {
|
type OrganizationMemberChangeRoleModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodFo
|
|||||||
import {
|
import {
|
||||||
UpdateOrganizationSchema,
|
UpdateOrganizationSchema,
|
||||||
UpdateOrganizationSchemaType
|
UpdateOrganizationSchemaType
|
||||||
} from "@/components/wrappers/dashboard/admin/organization/organization.schema";
|
} from "@/components/wrappers/dashboard/admin/organizations/organization/organization.schema";
|
||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||||
import {Input} from "@/components/ui/input";
|
import {Input} from "@/components/ui/input";
|
||||||
import {authClient} from "@/lib/auth/auth-client";
|
import {authClient} from "@/lib/auth/auth-client";
|
||||||
+3
-3
@@ -6,17 +6,17 @@ import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
|||||||
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||||
import {
|
import {
|
||||||
UpdateOrganizationForm
|
UpdateOrganizationForm
|
||||||
} from "@/components/wrappers/dashboard/admin/organization/details/update-organization-form";
|
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/update-organization-form";
|
||||||
import {
|
import {
|
||||||
OrganizationMemberCard
|
OrganizationMemberCard
|
||||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-member-card";
|
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/organization-member-card";
|
||||||
import {useRouter, useSearchParams} from "next/navigation";
|
import {useRouter, useSearchParams} from "next/navigation";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {capitalizeFirstLetter} from "@/utils/text";
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
import {User} from "@/db/schema/02_user";
|
import {User} from "@/db/schema/02_user";
|
||||||
import {
|
import {
|
||||||
OrganizationAddMemberModal
|
OrganizationAddMemberModal
|
||||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-add-member-modal";
|
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/organization-add-member-modal";
|
||||||
import {cn} from "@/lib/utils";
|
import {cn} from "@/lib/utils";
|
||||||
|
|
||||||
type OrganizationManagementProps = {
|
type OrganizationManagementProps = {
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import {ColumnDef} from "@tanstack/react-table";
|
import {ColumnDef} from "@tanstack/react-table";
|
||||||
import {ButtonDeleteOrganization} from "@/components/wrappers/dashboard/admin/organization/button-delete-organization";
|
import {ButtonDeleteOrganization} from "@/components/wrappers/dashboard/admin/organizations/organization/button-delete-organization";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {Settings} from "lucide-react";
|
import {Settings} from "lucide-react";
|
||||||
import {buttonVariants} from "@/components/ui/button";
|
import {buttonVariants} from "@/components/ui/button";
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/settings/email/email-form/email-form.schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ import {TooltipProvider} from "@/components/ui/tooltip";
|
|||||||
import {
|
import {
|
||||||
EmailFormSchema,
|
EmailFormSchema,
|
||||||
EmailFormType
|
EmailFormType
|
||||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
} from "@/components/wrappers/dashboard/admin/settings/email/email-form/email-form.schema";
|
||||||
import {PasswordInput} from "@/components/ui/password-input";
|
import {PasswordInput} from "@/components/ui/password-input";
|
||||||
import {
|
import {
|
||||||
updateEmailSettingsAction
|
updateEmailSettingsAction
|
||||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.action";
|
} from "@/components/wrappers/dashboard/admin/settings/email/email-form/email-form.action";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import {EmailForm} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form";
|
import {EmailForm} from "@/components/wrappers/dashboard/admin/settings/email/email-form/email-form";
|
||||||
import {Setting} from "@/db/schema/01_setting";
|
import {Setting} from "@/db/schema/01_setting";
|
||||||
import {EmailFormType} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
import {EmailFormType} from "@/components/wrappers/dashboard/admin/settings/email/email-form/email-form.schema";
|
||||||
|
|
||||||
|
|
||||||
export type SettingsEmailSectionProps = {
|
export type SettingsEmailSectionProps = {
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import {toast} from "sonner";
|
|||||||
|
|
||||||
export type AdminSettingsTabProps = {};
|
export type AdminSettingsTabProps = {};
|
||||||
|
|
||||||
export const AdminSettingsTab = (props: AdminSettingsTabProps) => {
|
export const AdminSettingsSection = (props: AdminSettingsTabProps) => {
|
||||||
|
|
||||||
const handleDownloadKey = async () => {
|
const handleDownloadKey = async () => {
|
||||||
|
|
||||||
+7
-7
@@ -1,8 +1,9 @@
|
|||||||
|
"use client"
|
||||||
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
||||||
import {Info, ShieldCheck} from "lucide-react";
|
import {Info, ShieldCheck} from "lucide-react";
|
||||||
import {Switch} from "@/components/ui/switch";
|
import {Switch} from "@/components/ui/switch";
|
||||||
import {Label} from "@/components/ui/label";
|
import {Label} from "@/components/ui/label";
|
||||||
import {StorageS3Form} from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/storage-s3-form";
|
import {StorageS3Form} from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/storage-s3-form";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||||
import {useMutation} from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
@@ -11,15 +12,15 @@ import {toast} from "sonner";
|
|||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {
|
import {
|
||||||
updateStorageSettingsAction
|
updateStorageSettingsAction
|
||||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.action";
|
} from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.action";
|
||||||
import {Setting} from "@/db/schema/01_setting";
|
import {Setting} from "@/db/schema/01_setting";
|
||||||
import {S3FormType} from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.schema";
|
import {S3FormType} from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.schema";
|
||||||
|
|
||||||
export type SettingsStorageTabProps = {
|
export type SettingsStorageSectionProps = {
|
||||||
settings: Setting;
|
settings: Setting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
export const SettingsStorageSection = (props: SettingsStorageSectionProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
@@ -62,8 +63,7 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full py-4">
|
<div className="flex flex-col h-full">
|
||||||
<h1>Settings for Portabase storage</h1>
|
|
||||||
<Alert className="mt-3">
|
<Alert className="mt-3">
|
||||||
<Info className="h-4 w-4"/>
|
<Info className="h-4 w-4"/>
|
||||||
<AlertTitle>Informations</AlertTitle>
|
<AlertTitle>Informations</AlertTitle>
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { S3FormSchema, StorageSwitchSchema } from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.schema";
|
import { S3FormSchema, StorageSwitchSchema } from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {userAction} from "@/lib/safe-actions/actions";
|
import {userAction} from "@/lib/safe-actions/actions";
|
||||||
+2
-2
@@ -8,10 +8,10 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
import { S3FormSchema, S3FormType } from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.schema";
|
import { S3FormSchema, S3FormType } from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.schema";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.action";
|
import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.action";
|
||||||
import {PasswordInput} from "@/components/ui/password-input";
|
import {PasswordInput} from "@/components/ui/password-input";
|
||||||
|
|
||||||
export type S3FormProps = {
|
export type S3FormProps = {
|
||||||
-30
@@ -1,30 +0,0 @@
|
|||||||
"use server";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { db } from "@/db";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
import {userAction} from "@/lib/safe-actions/actions";
|
|
||||||
|
|
||||||
export const updateEmailSettingsAction = userAction
|
|
||||||
.schema(
|
|
||||||
z.object({
|
|
||||||
name: z.string(),
|
|
||||||
data: EmailFormSchema,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.action(async ({ parsedInput }) => {
|
|
||||||
const { name, data } = parsedInput;
|
|
||||||
|
|
||||||
const [updatedSettings] = await db
|
|
||||||
.update(drizzleDb.schemas.setting)
|
|
||||||
.set({
|
|
||||||
...data,
|
|
||||||
})
|
|
||||||
.where(eq(drizzleDb.schemas.setting.name, name))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: updatedSettings,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const EmailFormSchema = z.object({
|
|
||||||
smtpPassword: z.string(),
|
|
||||||
smtpFrom: z.string(),
|
|
||||||
smtpHost: z.string(),
|
|
||||||
smtpPort: z.string(),
|
|
||||||
smtpUser: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type EmailFormType = z.infer<typeof EmailFormSchema>;
|
|
||||||
-208
@@ -1,208 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {Card, CardContent} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
useZodForm
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import {Input} from "@/components/ui/input";
|
|
||||||
import {Form} from "@/components/ui/form";
|
|
||||||
import {Button} from "@/components/ui/button";
|
|
||||||
import {useMutation} from "@tanstack/react-query";
|
|
||||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
|
||||||
|
|
||||||
import {
|
|
||||||
EmailFormSchema,
|
|
||||||
EmailFormType
|
|
||||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
|
||||||
import {PasswordInput} from "@/components/ui/password-input";
|
|
||||||
import {
|
|
||||||
updateEmailSettingsAction
|
|
||||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.action";
|
|
||||||
import {toast} from "sonner";
|
|
||||||
import {useRouter} from "next/navigation";
|
|
||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
|
||||||
import {Send} from "lucide-react";
|
|
||||||
import {sendEmail} from "@/lib/email/email-helper";
|
|
||||||
import {render} from "@react-email/render";
|
|
||||||
import TestEmailSettings from "@/components/emails/email-settings-test";
|
|
||||||
import {cn} from "@/lib/utils";
|
|
||||||
|
|
||||||
export type EmailFormProps = {
|
|
||||||
defaultValues?: EmailFormType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EmailForm = (props: EmailFormProps) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const form = useZodForm({
|
|
||||||
schema: EmailFormSchema,
|
|
||||||
defaultValues: props.defaultValues,
|
|
||||||
});
|
|
||||||
const isDirty = form.formState.isDirty;
|
|
||||||
|
|
||||||
|
|
||||||
const mutation = useMutation({
|
|
||||||
mutationFn: async (values: EmailFormType) => {
|
|
||||||
const updateEmailSettings = await updateEmailSettingsAction({name: "system", data: values});
|
|
||||||
const data = updateEmailSettings?.data?.data;
|
|
||||||
if (updateEmailSettings?.serverError || !data) {
|
|
||||||
toast.error(updateEmailSettings?.serverError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
toast.success(`Success updating email informations`);
|
|
||||||
router.refresh();
|
|
||||||
form.reset(data)
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const mutationSendEmailTest = useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
if (!props.defaultValues?.smtpUser || !props.defaultValues?.smtpFrom) {
|
|
||||||
toast.error("SMTP is not configured");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const email = await sendEmail({
|
|
||||||
to: props.defaultValues.smtpUser,
|
|
||||||
|
|
||||||
subject: "Portabase",
|
|
||||||
html: await render(TestEmailSettings(), {}),
|
|
||||||
from: props.defaultValues.smtpFrom,
|
|
||||||
});
|
|
||||||
if (email.response) {
|
|
||||||
toast.success("Test Email Successfully sent !");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Card>
|
|
||||||
<CardContent>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
className="flex flex-col gap-4"
|
|
||||||
onSubmit={async (values) => {
|
|
||||||
await mutation.mutateAsync(values);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="smtpFrom"
|
|
||||||
defaultValue=""
|
|
||||||
render={({field}) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>From Email *</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={"exemple@portabase.com"} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>{"The email from where the email will be send"}</FormDescription>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="smtpHost"
|
|
||||||
defaultValue=""
|
|
||||||
render={({field}) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Server Host *</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={"ssl0.ovh.net"} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>{"Your email server host"}</FormDescription>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="smtpPort"
|
|
||||||
defaultValue=""
|
|
||||||
render={({field}) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Server Port *</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={"465"} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>{"Your email server port (send)"}</FormDescription>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="smtpPassword"
|
|
||||||
defaultValue=""
|
|
||||||
render={({field}) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<PasswordInput placeholder="Password" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>{"Your email server password"}</FormDescription>
|
|
||||||
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="smtpUser"
|
|
||||||
defaultValue=""
|
|
||||||
render={({field}) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>User Email *</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={"exemple@portabase.com"} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>{"The email server user"}</FormDescription>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex justify-between gap-4 mt-5">
|
|
||||||
{props.defaultValues?.smtpFrom && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div>
|
|
||||||
<ButtonWithLoading
|
|
||||||
type="button"
|
|
||||||
disabled={isDirty || mutationSendEmailTest.isPending}
|
|
||||||
isPending={mutationSendEmailTest.isPending}
|
|
||||||
onClick={async () => {
|
|
||||||
await mutationSendEmailTest.mutateAsync();
|
|
||||||
}}
|
|
||||||
icon={<Send/>}
|
|
||||||
size="default"
|
|
||||||
>
|
|
||||||
Send email test
|
|
||||||
</ButtonWithLoading>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
|
|
||||||
{isDirty && (
|
|
||||||
<TooltipContent className={cn(!isDirty && "hidden")}>
|
|
||||||
You must save changes before testing the email settings.
|
|
||||||
</TooltipContent>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button>Save</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import {EmailForm} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form";
|
|
||||||
import {Send} from "lucide-react";
|
|
||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
|
||||||
import {useMutation} from "@tanstack/react-query";
|
|
||||||
import {sendEmail} from "@/lib/email/email-helper";
|
|
||||||
import {render} from "@react-email/render";
|
|
||||||
import {toast} from "sonner";
|
|
||||||
import {Setting} from "@/db/schema/01_setting";
|
|
||||||
import {EmailFormType} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
|
||||||
import TestEmailSettings from "@/components/emails/email-settings-test"
|
|
||||||
|
|
||||||
|
|
||||||
export type SettingsEmailTabProps = {
|
|
||||||
settings: Setting;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SettingsEmailTab = (props: SettingsEmailTabProps) => {
|
|
||||||
const mutation = useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
if (!props.settings.smtpUser || !props.settings.smtpFrom) {
|
|
||||||
toast.error("SMTP is not configured");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const email = await sendEmail({
|
|
||||||
to: props.settings.smtpUser,
|
|
||||||
subject: "Portabase",
|
|
||||||
html: await render(TestEmailSettings(), {}),
|
|
||||||
from: props.settings.smtpFrom,
|
|
||||||
});
|
|
||||||
if (email.response) {
|
|
||||||
toast.success("Test Email Successfully sent !");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSendMailTest = async () => {
|
|
||||||
await mutation.mutateAsync();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col h-full py-4">
|
|
||||||
<div className="flex gap-4 h-fit justify-between">
|
|
||||||
<h1>Settings for Portabase email setup</h1>
|
|
||||||
{props.settings.smtpFrom && (
|
|
||||||
<ButtonWithLoading
|
|
||||||
isPending={mutation.isPending}
|
|
||||||
onClick={async () => {
|
|
||||||
await handleSendMailTest();
|
|
||||||
}}
|
|
||||||
icon={<Send/>}
|
|
||||||
size="default"
|
|
||||||
>Send email test</ButtonWithLoading>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-5">
|
|
||||||
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings as EmailFormType : undefined}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import {UserWithAccounts} from "@/db/schema/02_user";
|
import {UserWithAccounts} from "@/db/schema/02_user";
|
||||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||||
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/columns-users";
|
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/users/columns-users";
|
||||||
|
|
||||||
export type AdminUsersTableProps = {
|
export type AdminUsersTableProps = {
|
||||||
users: UserWithAccounts[];
|
users: UserWithAccounts[];
|
||||||
+1
-1
@@ -9,7 +9,7 @@ import {useState} from "react";
|
|||||||
import {UserWithAccounts} from "@/db/schema/02_user";
|
import {UserWithAccounts} from "@/db/schema/02_user";
|
||||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||||
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
||||||
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/button-delete-use";
|
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/users/button-delete-use";
|
||||||
import {formatLocalizedDate} from "@/utils/date-formatting";
|
import {formatLocalizedDate} from "@/utils/date-formatting";
|
||||||
|
|
||||||
export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
||||||
@@ -8,33 +8,47 @@ import {
|
|||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {NotifierForm} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form";
|
import {NotifierForm} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form";
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
import {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
import {useIsMobile} from "@/hooks/use-mobile";
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
NotifierChannelOrganisationForm
|
||||||
|
} from "@/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization-form";
|
||||||
|
|
||||||
type OrganizationNotifierAddModalProps = {
|
type OrganizationNotifierAddModalProps = {
|
||||||
notificationChannel?: NotificationChannel
|
notificationChannel?: NotificationChannelWith
|
||||||
organization?: OrganizationWithMembers;
|
organization?: OrganizationWithMembers;
|
||||||
open: boolean;
|
open?: boolean;
|
||||||
onOpenChangeAction: (open: boolean) => void;
|
onOpenChangeAction?: (open: boolean) => void;
|
||||||
|
adminView?: boolean;
|
||||||
|
organizations?: OrganizationWithMembers[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const NotifierAddEditModal = ({
|
export const NotifierAddEditModal = ({
|
||||||
organization,
|
organization,
|
||||||
notificationChannel,
|
notificationChannel,
|
||||||
open,
|
open = false,
|
||||||
onOpenChangeAction
|
onOpenChangeAction,
|
||||||
|
adminView,
|
||||||
|
organizations
|
||||||
}: OrganizationNotifierAddModalProps) => {
|
}: OrganizationNotifierAddModalProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const [openInternal, setOpen] = useState(open);
|
||||||
|
|
||||||
const isCreate = !Boolean(notificationChannel);
|
const isCreate = !Boolean(notificationChannel);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChangeAction}>
|
<Dialog open={openInternal} onOpenChange={(state) => {
|
||||||
|
onOpenChangeAction?.(state);
|
||||||
|
setOpen(state);
|
||||||
|
}}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
{isCreate ?
|
{isCreate ?
|
||||||
<Button>
|
<Button>
|
||||||
<Plus/>{!isMobile && `Add notification channel` }
|
<Plus/>{!isMobile && `Add notification channel`}
|
||||||
</Button>
|
</Button>
|
||||||
:
|
:
|
||||||
<Button
|
<Button
|
||||||
@@ -52,11 +66,52 @@ export const NotifierAddEditModal = ({
|
|||||||
Configure your notification channel preferences and event triggers.
|
Configure your notification channel preferences and event triggers.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{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">
|
||||||
<NotifierForm
|
<NotifierForm
|
||||||
|
adminView={adminView}
|
||||||
defaultValues={notificationChannel}
|
defaultValues={notificationChannel}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
onSuccessAction={() => onOpenChangeAction(false)}
|
onSuccessAction={() => {
|
||||||
|
onOpenChangeAction?.(false)
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent className="h-full justify-between" value="organizations">
|
||||||
|
|
||||||
|
<NotifierChannelOrganisationForm
|
||||||
|
defaultValues={notificationChannel}
|
||||||
|
organizations={organizations}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
:
|
||||||
|
<NotifierForm
|
||||||
|
adminView={adminView}
|
||||||
|
defaultValues={notificationChannel}
|
||||||
|
organization={organization}
|
||||||
|
onSuccessAction={() => {
|
||||||
|
onOpenChangeAction?.(false)
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
+13
-4
@@ -2,7 +2,7 @@
|
|||||||
import {useMutation} from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal";
|
import {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal";
|
||||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
import {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
import {Switch} from "@/components/ui/switch";
|
import {Switch} from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
updateNotificationChannelAction
|
updateNotificationChannelAction
|
||||||
@@ -12,11 +12,18 @@ import {useState} from "react";
|
|||||||
import {Organization, OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {Organization, OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
|
|
||||||
export type EditNotifierButtonProps = {
|
export type EditNotifierButtonProps = {
|
||||||
notificationChannel: NotificationChannel;
|
notificationChannel: NotificationChannelWith;
|
||||||
organization? : OrganizationWithMembers;
|
organization?: OrganizationWithMembers;
|
||||||
|
organizations?: OrganizationWithMembers[];
|
||||||
|
adminView?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditNotifierButton = ({notificationChannel, organization}: EditNotifierButtonProps) => {
|
export const EditNotifierButton = ({
|
||||||
|
organizations,
|
||||||
|
adminView = false,
|
||||||
|
notificationChannel,
|
||||||
|
organization
|
||||||
|
}: EditNotifierButtonProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
|
|
||||||
@@ -53,6 +60,8 @@ export const EditNotifierButton = ({notificationChannel, organization}: EditNoti
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<NotifierAddEditModal
|
<NotifierAddEditModal
|
||||||
|
organizations={organizations}
|
||||||
|
adminView={adminView}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
notificationChannel={notificationChannel}
|
notificationChannel={notificationChannel}
|
||||||
open={isAddModalOpen}
|
open={isAddModalOpen}
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {Card} from "@/components/ui/card";
|
import {Card} from "@/components/ui/card";
|
||||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
import {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
import {Mail, MessageSquare, Pencil, Trash2, Webhook} from "lucide-react";
|
|
||||||
import {Badge} from "@/components/ui/badge";
|
import {Badge} from "@/components/ui/badge";
|
||||||
import {Switch} from "@/components/ui/switch";
|
|
||||||
import {
|
import {
|
||||||
DeleteNotifierButton
|
DeleteNotifierButton
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-card/button-delete-notifier";
|
} from "@/components/wrappers/dashboard/common/notifier/notifier-card/button-delete-notifier";
|
||||||
import {EditNotifierButton} from "@/components/wrappers/dashboard/common/notifier/notifier-card/button-edit-notifier";
|
import {EditNotifierButton} from "@/components/wrappers/dashboard/common/notifier/notifier-card/button-edit-notifier";
|
||||||
import {Organization, OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
|
import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers";
|
||||||
|
|
||||||
export type NotifierCardProps = {
|
export type NotifierCardProps = {
|
||||||
data: NotificationChannel;
|
data: NotificationChannelWith;
|
||||||
organization?: OrganizationWithMembers;
|
organization?: OrganizationWithMembers;
|
||||||
|
organizations?: OrganizationWithMembers[];
|
||||||
|
adminView?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NotifierCard = (props: NotifierCardProps) => {
|
export const NotifierCard = (props: NotifierCardProps) => {
|
||||||
@@ -25,7 +26,7 @@ export const NotifierCard = (props: NotifierCardProps) => {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
className="flex h-10 w-10 items-center justify-center rounded-md bg-secondary border border-border">
|
className="flex h-10 w-10 items-center justify-center rounded-md bg-secondary border border-border">
|
||||||
{getIcon(data.provider)}
|
{getNotificationChannelIcon(data.provider)}
|
||||||
</div>
|
</div>
|
||||||
<div className={`h-2 w-2 rounded-full ${data.enabled ? "bg-green-600" : "bg-muted"}`}/>
|
<div className={`h-2 w-2 rounded-full ${data.enabled ? "bg-green-600" : "bg-muted"}`}/>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,6 +42,8 @@ export const NotifierCard = (props: NotifierCardProps) => {
|
|||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<EditNotifierButton
|
<EditNotifierButton
|
||||||
|
organizations={props.organizations}
|
||||||
|
adminView={props.adminView}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
notificationChannel={data}/>
|
notificationChannel={data}/>
|
||||||
<DeleteNotifierButton
|
<DeleteNotifierButton
|
||||||
@@ -54,14 +57,3 @@ export const NotifierCard = (props: NotifierCardProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const getIcon = (type: string) => {
|
|
||||||
const Icon = notificationTypes.find((t) => t.value === type)?.icon
|
|
||||||
return Icon ? <Icon className="h-4 w-4"/> : null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const notificationTypes = [
|
|
||||||
{value: "smtp", label: "Email", icon: Mail},
|
|
||||||
{value: "slack", label: "Slack", icon: MessageSquare},
|
|
||||||
{value: "webhook", label: "Webhook", icon: Webhook},
|
|
||||||
]
|
|
||||||
+1
-1
@@ -7,7 +7,7 @@ export const NotificationChannelFormSchema = z.object({
|
|||||||
.min(5, "Name must be at least 5 characters long")
|
.min(5, "Name must be at least 5 characters long")
|
||||||
.max(40, "Name must be at most 40 characters long"),
|
.max(40, "Name must be at most 40 characters long"),
|
||||||
|
|
||||||
provider: z.enum(["curl", "slack", "smtp", "webhook"], {
|
provider: z.enum(["slack", "smtp"], {
|
||||||
required_error: "Provider is required",
|
required_error: "Provider is required",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
NotifierSlackForm
|
NotifierSlackForm
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-slack.form";
|
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-slack.form";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
import {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
import {
|
import {
|
||||||
NotifierTestChannelButton
|
NotifierTestChannelButton
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-test-channel-button";
|
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-test-channel-button";
|
||||||
@@ -30,13 +30,16 @@ import {useEffect} from "react";
|
|||||||
type NotifierFormProps = {
|
type NotifierFormProps = {
|
||||||
onSuccessAction?: () => void;
|
onSuccessAction?: () => void;
|
||||||
organization?: OrganizationWithMembers;
|
organization?: OrganizationWithMembers;
|
||||||
defaultValues?: NotificationChannel
|
defaultValues?: NotificationChannelWith
|
||||||
|
adminView?: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NotifierForm = ({onSuccessAction, organization, defaultValues}: NotifierFormProps) => {
|
export const NotifierForm = ({onSuccessAction, organization, defaultValues, adminView}: NotifierFormProps) => {
|
||||||
|
|
||||||
const isCreate = !Boolean(defaultValues);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isCreate = !Boolean(defaultValues);
|
||||||
|
|
||||||
|
|
||||||
const form = useZodForm({
|
const form = useZodForm({
|
||||||
schema: NotificationChannelFormSchema,
|
schema: NotificationChannelFormSchema,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -47,7 +50,7 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
|||||||
form.reset(defaultValues ? {...defaultValues} : {});
|
form.reset(defaultValues ? {...defaultValues} : {});
|
||||||
}, [defaultValues]);
|
}, [defaultValues]);
|
||||||
|
|
||||||
const mutationCreateOrganisation = useMutation({
|
const mutationAddNotificationChannel = useMutation({
|
||||||
mutationFn: async (values: NotificationChannelFormType) => {
|
mutationFn: async (values: NotificationChannelFormType) => {
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -73,12 +76,14 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
|||||||
|
|
||||||
const provider = form.watch("provider");
|
const provider = form.watch("provider");
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
await mutationCreateOrganisation.mutateAsync(values);
|
await mutationAddNotificationChannel.mutateAsync(values);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -136,7 +141,8 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
|||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
{defaultValues && (
|
{defaultValues && (
|
||||||
<NotifierTestChannelButton organizationId={organization?.id} notificationChannel={defaultValues}/>
|
<NotifierTestChannelButton organizationId={organization?.id}
|
||||||
|
notificationChannel={defaultValues}/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -150,7 +156,7 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<ButtonWithLoading isPending={mutationCreateOrganisation.isPending}>
|
<ButtonWithLoading isPending={mutationAddNotificationChannel.isPending}>
|
||||||
{isCreate ? "Add" : "Save"} Channel
|
{isCreate ? "Add" : "Save"} Channel
|
||||||
</ButtonWithLoading>
|
</ButtonWithLoading>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +165,3 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
|||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
Layers,
|
Layers,
|
||||||
ChartArea,
|
ChartArea,
|
||||||
ShieldHalf,
|
ShieldHalf,
|
||||||
Building, UserRoundCog, Mail, PackageOpen
|
Building, UserRoundCog, Mail, PackageOpen, Logs, Megaphone, Blocks
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {SidebarGroupItem, SidebarMenuCustomBase} from "@/components/wrappers/dashboard/common/sidebar/menu-sidebar";
|
import {SidebarGroupItem, SidebarMenuCustomBase} from "@/components/wrappers/dashboard/common/sidebar/menu-sidebar";
|
||||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||||
@@ -69,6 +69,17 @@ export const SidebarMenuCustomMain = () => {
|
|||||||
details: true,
|
details: true,
|
||||||
type: "item"
|
type: "item"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Notifications",
|
||||||
|
url: "/notifications",
|
||||||
|
icon: Megaphone,
|
||||||
|
details: true,
|
||||||
|
type: "collapse",
|
||||||
|
submenu: [
|
||||||
|
{ title: "Channels", url: "/notifications/channels", icon: Blocks, type: "item" },
|
||||||
|
{ title: "Activity Logs", url: "/notifications/logs", icon: Logs, type: "item" },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Access management",
|
title: "Access management",
|
||||||
url: "/admin",
|
url: "/admin",
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import { UserSchema, UserType } from "@/components/wrappers/dashboard/profile/us
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { updateUserAction } from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
|
import { updateUserAction } from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
|
||||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||||
import {sessionsColumns} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/sessions/table-columns";
|
import {sessionsColumns} from "@/components/wrappers/dashboard/admin/users/sessions/table-columns";
|
||||||
import {accountsColumns} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/accounts/table-columns";
|
import {accountsColumns} from "@/components/wrappers/dashboard/admin/users/accounts/table-columns";
|
||||||
import {Account, Session} from "better-auth";
|
import {Account, Session} from "better-auth";
|
||||||
|
|
||||||
export type UserFormProps = {
|
export type UserFormProps = {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import {boolean, jsonb, pgEnum, pgTable, primaryKey, unique, uuid, varchar} from "drizzle-orm/pg-core";
|
import {boolean, jsonb, pgEnum, pgTable, primaryKey, unique, uuid, varchar} from "drizzle-orm/pg-core";
|
||||||
import {timestamps} from "@/db/schema/00_common";
|
import {timestamps} from "@/db/schema/00_common";
|
||||||
import {organization} from "@/db/schema/03_organization";
|
import {MemberWithUser, Organization, organization} from "@/db/schema/03_organization";
|
||||||
import {relations} from "drizzle-orm";
|
import {relations} from "drizzle-orm";
|
||||||
import {createSelectSchema} from "drizzle-zod";
|
import {createSelectSchema} from "drizzle-zod";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
|
import {OrganizationInvitation} from "@/db/schema/05_invitation";
|
||||||
|
|
||||||
|
|
||||||
export const providerKindEnum = pgEnum('provider_kind', ['slack', 'smtp']);
|
export const providerKindEnum = pgEnum('provider_kind', ['slack', 'smtp']);
|
||||||
@@ -48,3 +49,11 @@ export const organizationNotificationChannelRelations = relations(organizationNo
|
|||||||
|
|
||||||
export const notificationChannelSchema = createSelectSchema(notificationChannel);
|
export const notificationChannelSchema = createSelectSchema(notificationChannel);
|
||||||
export type NotificationChannel = z.infer<typeof notificationChannelSchema>;
|
export type NotificationChannel = z.infer<typeof notificationChannelSchema>;
|
||||||
|
|
||||||
|
|
||||||
|
export type NotificationChannelWith = NotificationChannel & {
|
||||||
|
organizations: {
|
||||||
|
organizationId: string;
|
||||||
|
notificationChannelId: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import {pgTable, uuid, timestamp, jsonb, varchar, boolean, text, pgEnum} from 'drizzle-orm/pg-core';
|
import {pgTable, uuid, timestamp, jsonb, varchar, boolean, text, pgEnum} from 'drizzle-orm/pg-core';
|
||||||
import {notificationChannel, providerKindEnum} from "@/db/schema/09_notification-channel";
|
import {notificationChannel} from "@/db/schema/09_notification-channel";
|
||||||
import {alertPolicy} from "@/db/schema/10_alert-policy";
|
import {alertPolicy} from "@/db/schema/10_alert-policy";
|
||||||
import {organization} from "@/db/schema/03_organization";
|
import {organization} from "@/db/schema/03_organization";
|
||||||
import {timestamps} from "@/db/schema/00_common";
|
import {timestamps} from "@/db/schema/00_common";
|
||||||
|
import {createSelectSchema} from "drizzle-zod";
|
||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
|
||||||
export const levelEnum = pgEnum('level', ['critical', 'warning', 'info']);
|
export const levelEnum = pgEnum('level', ['critical', 'warning', 'info']);
|
||||||
|
|
||||||
@@ -12,17 +15,18 @@ export const notificationLog = pgTable('notification_log', {
|
|||||||
|
|
||||||
channelId: uuid('channel_id')
|
channelId: uuid('channel_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => notificationChannel.id, { onDelete: 'restrict' }),
|
.references(() => notificationChannel.id, {onDelete: 'restrict'}),
|
||||||
policyId: uuid('policy_id')
|
policyId: uuid('policy_id')
|
||||||
.references(() => alertPolicy.id, { onDelete: 'restrict' }),
|
.references(() => alertPolicy.id, {onDelete: 'restrict'}),
|
||||||
organizationId: uuid('organization_id')
|
organizationId: uuid('organization_id')
|
||||||
.references(() => organization.id, { onDelete: 'set null' }),
|
.references(() => organization.id, {onDelete: 'set null'}),
|
||||||
|
|
||||||
title: varchar('title', { length: 255 }).notNull(),
|
title: varchar('title', {length: 255}).notNull(),
|
||||||
message: text('message').notNull(),
|
message: text('message').notNull(),
|
||||||
level: levelEnum('level').notNull(),
|
level: levelEnum('level').notNull(),
|
||||||
payload: jsonb('payload'),
|
payload: jsonb('payload'),
|
||||||
|
|
||||||
|
|
||||||
success: boolean('success').notNull(),
|
success: boolean('success').notNull(),
|
||||||
error: text('error'),
|
error: text('error'),
|
||||||
providerResponse: jsonb('provider_response'),
|
providerResponse: jsonb('provider_response'),
|
||||||
@@ -30,3 +34,8 @@ export const notificationLog = pgTable('notification_log', {
|
|||||||
|
|
||||||
...timestamps
|
...timestamps
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const notificationLogSchema = createSelectSchema(notificationLog);
|
||||||
|
export type NotificationLog = z.infer<typeof notificationLogSchema>;
|
||||||
|
|
||||||
|
export type NotificationLevel = (typeof levelEnum.enumValues)[number];
|
||||||
|
|||||||
@@ -1,24 +1,45 @@
|
|||||||
import {desc, eq, and, gte, lte} from 'drizzle-orm';
|
import {and, desc, eq, gte, lte} from 'drizzle-orm';
|
||||||
import {
|
import {NotificationLevel, notificationLog} from "@/db/schema/11_notification-log";
|
||||||
notificationLog
|
import {notificationChannel} from "@/db/schema/09_notification-channel";
|
||||||
} from "@/db/schema/11_notification-log";
|
|
||||||
import {
|
|
||||||
notificationChannel,
|
|
||||||
} from "@/db/schema/09_notification-channel";
|
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {alertPolicy} from "@/db/schema/10_alert-policy";
|
import {alertPolicy} from "@/db/schema/10_alert-policy";
|
||||||
|
import {Json} from "drizzle-zod";
|
||||||
|
|
||||||
|
export type NotificationLogWithRelations = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
level: NotificationLevel;
|
||||||
|
success: boolean;
|
||||||
|
error: string | null;
|
||||||
|
sentAt: Date;
|
||||||
|
payload: Json | null;
|
||||||
|
content: {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
},
|
||||||
|
channel: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
provider: string;
|
||||||
|
} | null;
|
||||||
|
policy: {
|
||||||
|
id: string;
|
||||||
|
eventKinds: string[];
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
export async function getNotificationHistory(filters?: {
|
export async function getNotificationHistory(
|
||||||
|
filters?: {
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
policyId?: string;
|
policyId?: string;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
level?: string;
|
level?: NotificationLevel;
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
from?: Date;
|
from?: Date;
|
||||||
to?: Date;
|
to?: Date;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}) {
|
}
|
||||||
|
): Promise<NotificationLogWithRelations[]> {
|
||||||
const where = [];
|
const where = [];
|
||||||
if (filters?.channelId) where.push(eq(notificationLog.channelId, filters.channelId));
|
if (filters?.channelId) where.push(eq(notificationLog.channelId, filters.channelId));
|
||||||
if (filters?.policyId) where.push(eq(notificationLog.policyId, filters.policyId));
|
if (filters?.policyId) where.push(eq(notificationLog.policyId, filters.policyId));
|
||||||
@@ -28,7 +49,7 @@ export async function getNotificationHistory(filters?: {
|
|||||||
if (filters?.from) where.push(gte(notificationLog.sentAt, filters.from));
|
if (filters?.from) where.push(gte(notificationLog.sentAt, filters.from));
|
||||||
if (filters?.to) where.push(lte(notificationLog.sentAt, filters.to));
|
if (filters?.to) where.push(lte(notificationLog.sentAt, filters.to));
|
||||||
|
|
||||||
return await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
id: notificationLog.id,
|
id: notificationLog.id,
|
||||||
title: notificationLog.title,
|
title: notificationLog.title,
|
||||||
@@ -36,14 +57,19 @@ export async function getNotificationHistory(filters?: {
|
|||||||
success: notificationLog.success,
|
success: notificationLog.success,
|
||||||
error: notificationLog.error,
|
error: notificationLog.error,
|
||||||
sentAt: notificationLog.sentAt,
|
sentAt: notificationLog.sentAt,
|
||||||
|
payload: notificationLog.payload,
|
||||||
|
content: {
|
||||||
|
title: notificationLog.title,
|
||||||
|
message: notificationLog.message,
|
||||||
|
},
|
||||||
channel: {
|
channel: {
|
||||||
id: notificationLog.id,
|
id: notificationChannel.id,
|
||||||
name: notificationChannel.name,
|
name: notificationChannel.name,
|
||||||
provider: notificationChannel.provider,
|
provider: notificationChannel.provider,
|
||||||
},
|
},
|
||||||
policy: {
|
policy: {
|
||||||
id: alertPolicy.id,
|
id: alertPolicy.id,
|
||||||
eventKind: alertPolicy.eventKind,
|
eventKinds: alertPolicy.eventKinds,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.from(notificationLog)
|
.from(notificationLog)
|
||||||
@@ -52,4 +78,9 @@ export async function getNotificationHistory(filters?: {
|
|||||||
.where(and(...where))
|
.where(and(...where))
|
||||||
.orderBy(desc(notificationLog.sentAt))
|
.orderBy(desc(notificationLog.sentAt))
|
||||||
.limit(filters?.limit || 100);
|
.limit(filters?.limit || 100);
|
||||||
|
|
||||||
|
return rows.map(row => ({
|
||||||
|
...row,
|
||||||
|
payload: row.payload as Json,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user