mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
feat: backend storage
This commit is contained in:
@@ -8,6 +8,8 @@ import {db} from "@/db";
|
|||||||
import {notificationChannel, NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
import {notificationChannel, NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
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 {desc, isNull} from "drizzle-orm";
|
import {desc, isNull} from "drizzle-orm";
|
||||||
|
import {ChannelsSection} from "@/components/wrappers/dashboard/admin/channels/channels-section";
|
||||||
|
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Notification Channels",
|
title: "Notification Channels",
|
||||||
@@ -35,11 +37,14 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>Notification channels</PageTitle>
|
<PageTitle>Notification channels</PageTitle>
|
||||||
<PageActions>
|
<PageActions>
|
||||||
<NotifierAddEditModal adminView={false}/>
|
{/*<NotifierAddEditModal adminView={false}/>*/}
|
||||||
|
<ChannelAddEditModal kind="notification" adminView={false}/>
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<NotificationChannelsSection organizations={organizations} notificationChannels={notificationChannels}/>
|
{/*<NotificationChannelsSection organizations={organizations} notificationChannels={notificationChannels}/>*/}
|
||||||
|
<ChannelsSection kind="notification" organizations={organizations}
|
||||||
|
channels={notificationChannels}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal";
|
||||||
|
import {ChannelsSection} from "@/components/wrappers/dashboard/admin/channels/channels-section";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {desc, isNull} from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Storage Channels",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const storageChannels = await db.query.storageChannel.findMany({
|
||||||
|
with: {
|
||||||
|
organizations: true
|
||||||
|
},
|
||||||
|
orderBy: desc(drizzleDb.schemas.storageChannel.createdAt)
|
||||||
|
}) as StorageChannelWith[]
|
||||||
|
|
||||||
|
const organizations = await db.query.organization.findMany({
|
||||||
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
|
with: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle>Storage channels</PageTitle>
|
||||||
|
<PageActions>
|
||||||
|
<ChannelAddEditModal kind={"storage"} adminView={false}/>
|
||||||
|
</PageActions>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<ChannelsSection kind={"storage"} organizations={organizations} channels={storageChannels}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,13 +11,6 @@ import {Metadata} from "next";
|
|||||||
import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
||||||
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||||
import {capitalizeFirstLetter} from "@/utils/text";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {buttonVariants} from "@/components/ui/button";
|
|
||||||
import {GearIcon} from "@radix-ui/react-icons";
|
|
||||||
import {
|
|
||||||
ButtonDeleteProject
|
|
||||||
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const AuthLogoSection = () => {
|
|||||||
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="absolute bottom-10 right-5 text-sm text-muted-foreground" style={style}>
|
<span className="absolute bottom-8 right-5 text-sm text-muted-foreground" style={style}>
|
||||||
v{env.NEXT_PUBLIC_PROJECT_VERSION}
|
v{env.NEXT_PUBLIC_PROJECT_VERSION}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+34
-28
@@ -6,44 +6,51 @@ import {
|
|||||||
Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger
|
Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
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 {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
import {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
import {useIsMobile} from "@/hooks/use-mobile";
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||||
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
import {ChannelForm} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form";
|
||||||
|
import {ChannelKind, getChannelTextBasedOnKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||||
import {
|
import {
|
||||||
NotifierChannelOrganisationForm
|
ChannelOrganisationForm
|
||||||
} from "@/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization-form";
|
} from "@/components/wrappers/dashboard/admin/channels/organization/channels-organization-form";
|
||||||
|
|
||||||
type OrganizationNotifierAddModalProps = {
|
type ChannelAddModalProps = {
|
||||||
notificationChannel?: NotificationChannelWith
|
channel?: NotificationChannelWith | StorageChannelWith
|
||||||
organization?: OrganizationWithMembers;
|
organization?: OrganizationWithMembers;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onOpenChangeAction?: (open: boolean) => void;
|
onOpenChangeAction?: (open: boolean) => void;
|
||||||
adminView?: boolean;
|
adminView?: boolean;
|
||||||
organizations?: OrganizationWithMembers[]
|
organizations?: OrganizationWithMembers[]
|
||||||
trigger?: boolean;
|
trigger?: boolean;
|
||||||
|
kind: ChannelKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const NotifierAddEditModal = ({
|
export const ChannelAddEditModal = ({
|
||||||
organization,
|
organization,
|
||||||
notificationChannel,
|
channel,
|
||||||
open = false,
|
open = false,
|
||||||
onOpenChangeAction,
|
onOpenChangeAction,
|
||||||
adminView,
|
adminView,
|
||||||
organizations ,
|
organizations,
|
||||||
trigger= true
|
trigger = true,
|
||||||
}: OrganizationNotifierAddModalProps) => {
|
kind
|
||||||
|
}: ChannelAddModalProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [openInternal, setOpen] = useState(open);
|
const [openInternal, setOpen] = useState(open);
|
||||||
|
|
||||||
const isCreate = !Boolean(notificationChannel);
|
const isCreate = !Boolean(channel);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpen(open);
|
setOpen(open);
|
||||||
},[open])
|
}, [open])
|
||||||
|
|
||||||
|
|
||||||
|
const channelText = getChannelTextBasedOnKind(kind)
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -55,7 +62,7 @@ export const NotifierAddEditModal = ({
|
|||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
{isCreate ?
|
{isCreate ?
|
||||||
<Button>
|
<Button>
|
||||||
<Plus/>{!isMobile && `Add notification channel`}
|
<Plus/>{!isMobile && `Add ${channelText} channel`}
|
||||||
</Button>
|
</Button>
|
||||||
:
|
:
|
||||||
<Button
|
<Button
|
||||||
@@ -70,9 +77,9 @@ export const NotifierAddEditModal = ({
|
|||||||
|
|
||||||
<DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>
|
<DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle> {isCreate ? "Add" : "Edit"} Notification Channel</DialogTitle>
|
<DialogTitle> {isCreate ? "Add" : "Edit"} {channelText} Channel</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Configure your notification channel preferences and event triggers.
|
Configure your {channelText.toLowerCase()} channel preferences.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -86,9 +93,10 @@ export const NotifierAddEditModal = ({
|
|||||||
<TabsTrigger value="organizations">Organizations</TabsTrigger>
|
<TabsTrigger value="organizations">Organizations</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent className="h-full justify-between" value="configuration">
|
<TabsContent className="h-full justify-between" value="configuration">
|
||||||
<NotifierForm
|
<ChannelForm
|
||||||
|
kind={kind}
|
||||||
adminView={adminView}
|
adminView={adminView}
|
||||||
defaultValues={notificationChannel}
|
defaultValues={channel}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
onSuccessAction={() => {
|
onSuccessAction={() => {
|
||||||
onOpenChangeAction?.(false)
|
onOpenChangeAction?.(false)
|
||||||
@@ -97,30 +105,28 @@ export const NotifierAddEditModal = ({
|
|||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent className="h-full justify-between" value="organizations">
|
<TabsContent className="h-full justify-between" value="organizations">
|
||||||
|
<ChannelOrganisationForm
|
||||||
<NotifierChannelOrganisationForm
|
defaultValues={channel}
|
||||||
defaultValues={notificationChannel}
|
kind={kind}
|
||||||
organizations={organizations}
|
organizations={organizations}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
:
|
:
|
||||||
<NotifierForm
|
<>
|
||||||
|
<ChannelForm
|
||||||
|
kind={kind}
|
||||||
adminView={adminView}
|
adminView={adminView}
|
||||||
defaultValues={notificationChannel}
|
defaultValues={channel}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
onSuccessAction={() => {
|
onSuccessAction={() => {
|
||||||
onOpenChangeAction?.(false)
|
onOpenChangeAction?.(false)
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
+18
-7
@@ -5,20 +5,31 @@ import {useRouter} from "next/navigation";
|
|||||||
import {Trash2} from "lucide-react";
|
import {Trash2} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
removeNotificationChannelAction,
|
removeNotificationChannelAction,
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.action";
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/action";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
|
import {ChannelKind, getChannelTextBasedOnKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||||
|
import {
|
||||||
|
removeStorageChannelAction
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/action";
|
||||||
|
|
||||||
export type DeleteNotifierButtonProps = {
|
export type DeleteChannelButtonProps = {
|
||||||
notificationChannelId: string;
|
channelId: string;
|
||||||
|
kind: ChannelKind;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteNotifierButton = ({notificationChannelId, organizationId}: DeleteNotifierButtonProps) => {
|
export const DeleteChannelButton = ({channelId, organizationId, kind}: DeleteChannelButtonProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const channelText = getChannelTextBasedOnKind(kind)
|
||||||
|
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const result = await removeNotificationChannelAction({organizationId, notificationChannelId})
|
|
||||||
|
const result = kind === "notification" ? await removeNotificationChannelAction({
|
||||||
|
organizationId,
|
||||||
|
notificationChannelId: channelId
|
||||||
|
}) : await removeStorageChannelAction({organizationId, id: channelId})
|
||||||
const inner = result?.data;
|
const inner = result?.data;
|
||||||
|
|
||||||
if (inner?.success) {
|
if (inner?.success) {
|
||||||
@@ -32,8 +43,8 @@ export const DeleteNotifierButton = ({notificationChannelId, organizationId}: De
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonWithConfirm
|
<ButtonWithConfirm
|
||||||
title="Delete Notifier"
|
title={`Delete ${channelText.toLowerCase()} channel`}
|
||||||
description="Are you sure you want to remove this notifier ? This action cannot be undone and will delete all alert policies related to this channel !"
|
description={`Are you sure you want to remove this ${channelText.toLowerCase()} channel ? This action cannot be undone and will delete all alert policies related to this channel !`}
|
||||||
button={{
|
button={{
|
||||||
main: {
|
main: {
|
||||||
size: "icon",
|
size: "icon",
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
"use client";
|
||||||
|
import {useMutation} from "@tanstack/react-query";
|
||||||
|
import {useRouter} from "next/navigation";
|
||||||
|
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
|
import {Switch} from "@/components/ui/switch";
|
||||||
|
import {toast} from "sonner";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
import {ChannelKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||||
|
import {
|
||||||
|
updateNotificationChannelAction
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/action";
|
||||||
|
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
|
||||||
|
import {
|
||||||
|
updateStorageChannelAction
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/action";
|
||||||
|
|
||||||
|
export type EditChannelButtonProps = {
|
||||||
|
channel: NotificationChannelWith | StorageChannelWith;
|
||||||
|
organization?: OrganizationWithMembers;
|
||||||
|
organizations?: OrganizationWithMembers[];
|
||||||
|
adminView?: boolean;
|
||||||
|
kind: ChannelKind;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditChannelButton = ({
|
||||||
|
organizations,
|
||||||
|
adminView = false,
|
||||||
|
channel,
|
||||||
|
organization,
|
||||||
|
kind
|
||||||
|
}: EditChannelButtonProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async (value: boolean) => {
|
||||||
|
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
data: {
|
||||||
|
name: channel.name,
|
||||||
|
provider: channel.provider,
|
||||||
|
config: channel.config as Record<string, any>,
|
||||||
|
enabled: value
|
||||||
|
},
|
||||||
|
id: channel.id
|
||||||
|
};
|
||||||
|
|
||||||
|
let result: any;
|
||||||
|
if (kind == "notification") {
|
||||||
|
// @ts-ignore
|
||||||
|
result = await updateNotificationChannelAction(payload)
|
||||||
|
} else if (kind == "storage") {
|
||||||
|
// @ts-ignore
|
||||||
|
result = await updateStorageChannelAction(payload)
|
||||||
|
} else {
|
||||||
|
toast.error("An error occurred while updating storage channel")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const inner = result?.data;
|
||||||
|
|
||||||
|
if (inner?.success) {
|
||||||
|
toast.success(inner.actionSuccess?.message);
|
||||||
|
router.refresh();
|
||||||
|
} else {
|
||||||
|
toast.error(inner?.actionError?.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Switch checked={channel.enabled} onCheckedChange={async () => {
|
||||||
|
await mutation.mutateAsync(!channel.enabled)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ChannelAddEditModal
|
||||||
|
kind={kind}
|
||||||
|
organizations={organizations}
|
||||||
|
adminView={adminView}
|
||||||
|
organization={organization}
|
||||||
|
channel={channel}
|
||||||
|
open={isAddModalOpen}
|
||||||
|
onOpenChangeAction={setIsAddModalOpen}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+25
-17
@@ -1,26 +1,31 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {Card} from "@/components/ui/card";
|
import {Card} from "@/components/ui/card";
|
||||||
import {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
import {Badge} from "@/components/ui/badge";
|
import {Badge} from "@/components/ui/badge";
|
||||||
import {
|
|
||||||
DeleteNotifierButton
|
|
||||||
} 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 {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers";
|
|
||||||
import {truncateWords} from "@/utils/text";
|
import {truncateWords} from "@/utils/text";
|
||||||
import {useIsMobile} from "@/hooks/use-mobile";
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
import {
|
||||||
|
EditChannelButton
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-card/button-edit-channel";
|
||||||
|
import {ChannelKind, getChannelIcon} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||||
|
import {
|
||||||
|
DeleteChannelButton
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-card/button-delete-channel";
|
||||||
|
|
||||||
export type NotifierCardProps = {
|
export type ChannelCardProps = {
|
||||||
data: NotificationChannelWith;
|
data: NotificationChannelWith | StorageChannelWith;
|
||||||
organization?: OrganizationWithMembers;
|
organization?: OrganizationWithMembers;
|
||||||
organizations?: OrganizationWithMembers[];
|
organizations?: OrganizationWithMembers[];
|
||||||
adminView?: boolean;
|
adminView?: boolean;
|
||||||
|
kind?: ChannelKind;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NotifierCard = (props: NotifierCardProps) => {
|
|
||||||
const {data, organization} = props;
|
export const ChannelCard = (props: ChannelCardProps) => {
|
||||||
|
const {data, organization, kind} = props;
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -29,7 +34,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">
|
||||||
{getNotificationChannelIcon(data.provider)}
|
{getChannelIcon(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>
|
||||||
@@ -42,19 +47,22 @@ export const NotifierCard = (props: NotifierCardProps) => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{kind && (
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<EditNotifierButton
|
<EditChannelButton
|
||||||
organizations={props.organizations}
|
organizations={props.organizations}
|
||||||
adminView={props.adminView}
|
adminView={props.adminView}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
notificationChannel={data}/>
|
channel={data}
|
||||||
<DeleteNotifierButton
|
kind={kind}
|
||||||
|
/>
|
||||||
|
<DeleteChannelButton
|
||||||
|
kind={kind}
|
||||||
organizationId={organization?.id}
|
organizationId={organization?.id}
|
||||||
notificationChannelId={data.id}
|
channelId={data.id}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
|
||||||
|
export const ChannelFormSchema = z.object({
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(5, "Name must be at least 5 characters long")
|
||||||
|
.max(40, "Name must be at most 40 characters long"),
|
||||||
|
config: z.record(z.union([z.string(), z.number(), z.boolean(), z.null(), z.undefined()])).optional(),
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const NotificationChannelFormSchema = ChannelFormSchema.extend({
|
||||||
|
provider: z.enum(
|
||||||
|
["slack", "smtp", "discord", "telegram", "gotify", "ntfy", "webhook"],
|
||||||
|
{required_error: "Provider is required"}
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const StorageChannelFormSchema = ChannelFormSchema.extend({
|
||||||
|
provider: z.enum(["local", "s3"], {required_error: "Provider is required"}),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export type NotificationChannelFormType = z.infer<typeof NotificationChannelFormSchema>;
|
||||||
|
export type StorageChannelFormType = z.infer<typeof StorageChannelFormSchema>;
|
||||||
+55
-72
@@ -5,61 +5,50 @@ import {useMutation} from "@tanstack/react-query";
|
|||||||
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 {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 {
|
import {
|
||||||
NotificationChannelFormSchema, NotificationChannelFormType
|
addStorageChannelAction, updateStorageChannelAction
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.schema";
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/action";
|
||||||
import {
|
|
||||||
addNotificationChannelAction, updateNotificationChannelAction
|
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.action";
|
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
import {
|
|
||||||
NotifierSmtpForm
|
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-smtp.form";
|
|
||||||
import {
|
|
||||||
NotifierSlackForm
|
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-slack.form";
|
|
||||||
import {
|
|
||||||
NotifierDiscordForm
|
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-discord.form";
|
|
||||||
import {
|
|
||||||
NotifierTelegramForm
|
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-telegram.form";
|
|
||||||
import {
|
|
||||||
NotifierGotifyForm
|
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-gotify.form";
|
|
||||||
import {
|
|
||||||
NotifierNtfyForm
|
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-ntfy.form";
|
|
||||||
import {
|
|
||||||
NotifierWebhookForm
|
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-webhook.form";
|
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
import {
|
|
||||||
NotifierTestChannelButton
|
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-test-channel-button";
|
|
||||||
import {useEffect} from "react";
|
import {useEffect} from "react";
|
||||||
import {notificationTypes} from "@/components/wrappers/dashboard/admin/notifications/helpers";
|
|
||||||
import {cn} from "@/lib/utils";
|
import {cn} from "@/lib/utils";
|
||||||
import {Card} from "@/components/ui/card";
|
import {Card} from "@/components/ui/card";
|
||||||
import {ArrowLeft} from "lucide-react";
|
import {ArrowLeft} from "lucide-react";
|
||||||
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
import {
|
||||||
|
NotificationChannelFormSchema, NotificationChannelFormType, StorageChannelFormSchema, StorageChannelFormType
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.schema";
|
||||||
|
import {
|
||||||
|
ChannelKind,
|
||||||
|
renderChannelForm
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||||
|
import {storageTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/storage";
|
||||||
|
import {notificationTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/notification";
|
||||||
|
import {
|
||||||
|
ChannelTestButton
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button";
|
||||||
|
import {
|
||||||
|
addNotificationChannelAction, updateNotificationChannelAction
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/action";
|
||||||
|
|
||||||
type NotifierFormProps = {
|
type NotifierFormProps = {
|
||||||
onSuccessAction?: () => void;
|
onSuccessAction?: () => void;
|
||||||
organization?: OrganizationWithMembers;
|
organization?: OrganizationWithMembers;
|
||||||
defaultValues?: NotificationChannelWith
|
defaultValues?: NotificationChannelWith | StorageChannelWith
|
||||||
adminView?: boolean
|
adminView?: boolean
|
||||||
|
kind: ChannelKind
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NotifierForm = ({onSuccessAction, organization, defaultValues}: NotifierFormProps) => {
|
export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}: NotifierFormProps) => {
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isCreate = !Boolean(defaultValues);
|
const isCreate = !Boolean(defaultValues);
|
||||||
|
|
||||||
|
|
||||||
const form = useZodForm({
|
const form = useZodForm({
|
||||||
schema: NotificationChannelFormSchema,
|
schema: kind == "notification" ? NotificationChannelFormSchema : StorageChannelFormSchema,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
defaultValues: {...defaultValues},
|
defaultValues: {...defaultValues},
|
||||||
});
|
});
|
||||||
@@ -69,16 +58,28 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
|||||||
}, [defaultValues]);
|
}, [defaultValues]);
|
||||||
|
|
||||||
const mutationAddNotificationChannel = useMutation({
|
const mutationAddNotificationChannel = useMutation({
|
||||||
mutationFn: async (values: NotificationChannelFormType) => {
|
mutationFn: async (values: NotificationChannelFormType | StorageChannelFormType) => {
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
data: values,
|
data: values,
|
||||||
...(organization && {organizationId: organization.id}),
|
...(organization && {organizationId: organization.id}),
|
||||||
...((defaultValues && {notificationChannelId: defaultValues.id}))
|
...((defaultValues && {id: defaultValues.id}))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
let result: any;
|
||||||
|
|
||||||
|
if (kind === "notification") {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const result = isCreate ? await addNotificationChannelAction(payload) : await updateNotificationChannelAction(payload);
|
result = isCreate ? await addNotificationChannelAction(payload) : await updateNotificationChannelAction(payload);
|
||||||
|
} else if (kind === "storage") {
|
||||||
|
// @ts-ignore
|
||||||
|
result = isCreate ? await addStorageChannelAction(payload) : await updateStorageChannelAction(payload);
|
||||||
|
} else {
|
||||||
|
toast.error("An error occurred");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const inner = result?.data;
|
const inner = result?.data;
|
||||||
|
|
||||||
if (inner?.success) {
|
if (inner?.success) {
|
||||||
@@ -93,12 +94,16 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
|||||||
});
|
});
|
||||||
|
|
||||||
const provider = form.watch("provider");
|
const provider = form.watch("provider");
|
||||||
const selectedProviderDetails = notificationTypes.find(t => t.value === provider);
|
|
||||||
|
|
||||||
|
const channelTypes = kind == "notification" ? notificationTypes : storageTypes
|
||||||
|
|
||||||
|
const selectedProviderDetails = channelTypes.find(t => t.value === provider);
|
||||||
|
|
||||||
if (isCreate && !provider) {
|
if (isCreate && !provider) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4 py-4">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4 py-4">
|
||||||
{notificationTypes.map((type) => {
|
{channelTypes.map((type) => {
|
||||||
const Icon = type.icon;
|
const Icon = type.icon;
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -132,7 +137,8 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 mb-2 p-3 bg-secondary/30 rounded-lg border border-border">
|
<div className="flex items-center gap-3 mb-2 p-3 bg-secondary/30 rounded-lg border border-border">
|
||||||
{selectedProviderDetails && (
|
{selectedProviderDetails && (
|
||||||
<div className="h-10 w-10 bg-background rounded-full flex items-center justify-center border border-border shadow-sm">
|
<div
|
||||||
|
className="h-10 w-10 bg-background rounded-full flex items-center justify-center border border-border shadow-sm">
|
||||||
<selectedProviderDetails.icon className="h-5 w-5"/>
|
<selectedProviderDetails.icon className="h-5 w-5"/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -163,7 +169,8 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Channel Name</FormLabel>
|
<FormLabel>Channel Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder={`My ${selectedProviderDetails?.label} Channel`} value={field.value ?? ""}/>
|
<Input {...field} placeholder={`My ${selectedProviderDetails?.label} Channel`}
|
||||||
|
value={field.value ?? ""}/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -174,44 +181,20 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="provider"
|
name="provider"
|
||||||
render={({field}) => (
|
render={({field}) => (
|
||||||
<input type="hidden" {...field} value={field.value || ""} />
|
<input type="hidden" {...field} value={field.value || ""}/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{provider === "smtp" && (
|
{renderChannelForm(provider, form)}
|
||||||
<NotifierSmtpForm form={form}/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{provider === "slack" && (
|
|
||||||
<NotifierSlackForm form={form}/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{provider === "discord" && (
|
|
||||||
<NotifierDiscordForm form={form}/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{provider === "telegram" && (
|
|
||||||
<NotifierTelegramForm form={form}/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{provider === "gotify" && (
|
|
||||||
<NotifierGotifyForm form={form}/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{provider === "ntfy" && (
|
|
||||||
<NotifierNtfyForm form={form}/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{provider === "webhook" && (
|
|
||||||
<NotifierWebhookForm form={form}/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex justify-between mt-4">
|
<div className="flex justify-between mt-4">
|
||||||
<div>
|
<div>
|
||||||
{defaultValues && (
|
{defaultValues && (
|
||||||
<NotifierTestChannelButton organizationId={organization?.id}
|
<ChannelTestButton
|
||||||
notificationChannel={defaultValues}/>
|
kind={kind}
|
||||||
|
organizationId={organization?.id}
|
||||||
|
channel={defaultValues}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
"use client"
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||||
|
import {useMutation} from "@tanstack/react-query";
|
||||||
|
import {dispatchNotification} from "@/features/notifications/dispatch";
|
||||||
|
import {EventPayload} from "@/features/notifications/types";
|
||||||
|
import {Send, ShieldCheck} from "lucide-react";
|
||||||
|
import {toast} from "sonner";
|
||||||
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
|
import {cn} from "@/lib/utils";
|
||||||
|
import {StorageChannel} from "@/db/schema/12_storage-channel";
|
||||||
|
import {ChannelKind, getChannelTextBasedOnKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||||
|
|
||||||
|
type NotifierTestChannelButtonProps = {
|
||||||
|
channel: NotificationChannel | StorageChannel;
|
||||||
|
organizationId?: string;
|
||||||
|
kind: ChannelKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestChannelButtonProps) => {
|
||||||
|
const channelText = getChannelTextBasedOnKind(kind)
|
||||||
|
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (kind === "notification") {
|
||||||
|
const payload: EventPayload = {
|
||||||
|
title: 'Test Channel',
|
||||||
|
message: `We are testing channel ${channel.name}`,
|
||||||
|
level: 'info',
|
||||||
|
};
|
||||||
|
const result = await dispatchNotification(payload, undefined, channel.id, organizationId);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(result.message);
|
||||||
|
} else {
|
||||||
|
toast.error("An error occurred while testing the notification channel");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error("Not yet supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="default"
|
||||||
|
onClick={() => mutation.mutateAsync()}
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
className="bg-green-600 hover:bg-green-700 text-white font-medium shadow-sm transition-all"
|
||||||
|
>
|
||||||
|
{mutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={cn(" h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white", !isMobile && "mr-2")}/>
|
||||||
|
{!isMobile && `Sending...`}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-row justify-center items-center">
|
||||||
|
{kind === "notification" ?
|
||||||
|
<>
|
||||||
|
<Send className={cn("h-4 w-4", !isMobile && "mr-2")}/>{!isMobile && ` Test Channel`}
|
||||||
|
</>
|
||||||
|
:
|
||||||
|
<>
|
||||||
|
<ShieldCheck className={cn("h-4 w-4", !isMobile && "mr-2")}/>{!isMobile && ` Test Storage`}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
+8
-9
@@ -6,11 +6,11 @@ import * as drizzleDb from "@/db";
|
|||||||
import {userAction} from "@/lib/safe-actions/actions";
|
import {userAction} from "@/lib/safe-actions/actions";
|
||||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {
|
|
||||||
NotificationChannelFormSchema
|
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.schema";
|
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
import {
|
||||||
|
NotificationChannelFormSchema
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.schema";
|
||||||
|
|
||||||
|
|
||||||
export const addNotificationChannelAction = userAction.schema(
|
export const addNotificationChannelAction = userAction.schema(
|
||||||
@@ -20,7 +20,6 @@ export const addNotificationChannelAction = userAction.schema(
|
|||||||
})
|
})
|
||||||
).action(async ({parsedInput}): Promise<ServerActionResult<NotificationChannel>> => {
|
).action(async ({parsedInput}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||||
const {organizationId, data} = parsedInput;
|
const {organizationId, data} = parsedInput;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [channel] = await db
|
const [channel] = await db
|
||||||
.insert(drizzleDb.schemas.notificationChannel)
|
.insert(drizzleDb.schemas.notificationChannel)
|
||||||
@@ -128,11 +127,11 @@ export const removeNotificationChannelAction = userAction.schema(
|
|||||||
|
|
||||||
export const updateNotificationChannelAction = userAction.schema(
|
export const updateNotificationChannelAction = userAction.schema(
|
||||||
z.object({
|
z.object({
|
||||||
notificationChannelId: z.string(),
|
id: z.string(),
|
||||||
data: NotificationChannelFormSchema
|
data: NotificationChannelFormSchema
|
||||||
})
|
})
|
||||||
).action(async ({parsedInput}): Promise<ServerActionResult<NotificationChannel>> => {
|
).action(async ({parsedInput}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||||
const {notificationChannelId, data} = parsedInput;
|
const {id, data} = parsedInput;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [channel] = await db
|
const [channel] = await db
|
||||||
@@ -143,7 +142,7 @@ export const updateNotificationChannelAction = userAction.schema(
|
|||||||
config: data.config,
|
config: data.config,
|
||||||
enabled: data.enabled ?? true,
|
enabled: data.enabled ?? true,
|
||||||
}))
|
}))
|
||||||
.where(eq(drizzleDb.schemas.notificationChannel.id, notificationChannelId))
|
.where(eq(drizzleDb.schemas.notificationChannel.id, id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -154,7 +153,7 @@ export const updateNotificationChannelAction = userAction.schema(
|
|||||||
},
|
},
|
||||||
actionSuccess: {
|
actionSuccess: {
|
||||||
message: `Notification channel "${channel.name}" has been successfully updated.`,
|
message: `Notification channel "${channel.name}" has been successfully updated.`,
|
||||||
messageParams: {notificationChannelId: channel.id},
|
messageParams: {id: channel.id},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -165,7 +164,7 @@ export const updateNotificationChannelAction = userAction.schema(
|
|||||||
message: "Failed to update notification channel.",
|
message: "Failed to update notification channel.",
|
||||||
status: 500,
|
status: 500,
|
||||||
cause: error instanceof Error ? error.message : "Unknown error",
|
cause: error instanceof Error ? error.message : "Unknown error",
|
||||||
messageParams: {notificationChannelId: ""},
|
messageParams: {id: ""},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import {z} from "zod";
|
||||||
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {userAction} from "@/lib/safe-actions/actions";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {and, eq} from "drizzle-orm";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
import {
|
||||||
|
StorageChannelFormSchema
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.schema";
|
||||||
|
import {StorageChannel} from "@/db/schema/12_storage-channel";
|
||||||
|
|
||||||
|
|
||||||
|
export const addStorageChannelAction = userAction.schema(
|
||||||
|
z.object({
|
||||||
|
organizationId: z.string().optional(),
|
||||||
|
data: StorageChannelFormSchema
|
||||||
|
})
|
||||||
|
).action(async ({parsedInput}): Promise<ServerActionResult<StorageChannel>> => {
|
||||||
|
const {organizationId, data} = parsedInput;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [channel] = await db
|
||||||
|
.insert(drizzleDb.schemas.storageChannel)
|
||||||
|
.values({
|
||||||
|
provider: data.provider,
|
||||||
|
name: data.name,
|
||||||
|
config: data.config,
|
||||||
|
enabled: data.enabled ?? true,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (organizationId) {
|
||||||
|
await db.insert(drizzleDb.schemas.organizationStorageChannel).values({
|
||||||
|
organizationId,
|
||||||
|
storageChannelId: channel.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: {
|
||||||
|
...channel,
|
||||||
|
config: channel.config as JSON
|
||||||
|
},
|
||||||
|
actionSuccess: {
|
||||||
|
message: "Storage channel has been successfully created.",
|
||||||
|
messageParams: {id: channel.id},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
actionError: {
|
||||||
|
message: "Failed to create storage channel.",
|
||||||
|
status: 500,
|
||||||
|
cause: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
messageParams: {id: ""},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const removeStorageChannelAction = userAction.schema(
|
||||||
|
z.object({
|
||||||
|
organizationId: z.string().optional(),
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
).action(async ({parsedInput}): Promise<ServerActionResult<StorageChannel>> => {
|
||||||
|
const {organizationId, id} = parsedInput;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (organizationId) {
|
||||||
|
await db
|
||||||
|
.delete(drizzleDb.schemas.organizationNotificationChannel)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(drizzleDb.schemas.organizationStorageChannel.organizationId, organizationId),
|
||||||
|
eq(drizzleDb.schemas.organizationStorageChannel.storageChannelId, id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [deletedChannel] = await db
|
||||||
|
.delete(drizzleDb.schemas.storageChannel)
|
||||||
|
.where(eq(drizzleDb.schemas.storageChannel.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!deletedChannel) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
actionError: {
|
||||||
|
message: "Storage channel not found.",
|
||||||
|
status: 404,
|
||||||
|
messageParams: {id: id},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: {
|
||||||
|
...deletedChannel,
|
||||||
|
config: deletedChannel.config as JSON
|
||||||
|
},
|
||||||
|
actionSuccess: {
|
||||||
|
message: "Storage channel has been successfully removed.",
|
||||||
|
messageParams: {id: id},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
actionError: {
|
||||||
|
message: "Failed to remove storage channel.",
|
||||||
|
status: 500,
|
||||||
|
cause: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
messageParams: {id: id},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const updateStorageChannelAction = userAction.schema(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
data: StorageChannelFormSchema
|
||||||
|
})
|
||||||
|
).action(async ({parsedInput}): Promise<ServerActionResult<StorageChannel>> => {
|
||||||
|
const {id, data} = parsedInput;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [channel] = await db
|
||||||
|
.update(drizzleDb.schemas.storageChannel)
|
||||||
|
.set(withUpdatedAt({
|
||||||
|
provider: data.provider,
|
||||||
|
name: data.name,
|
||||||
|
config: data.config,
|
||||||
|
enabled: data.enabled ?? true,
|
||||||
|
}))
|
||||||
|
.where(eq(drizzleDb.schemas.storageChannel.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: {
|
||||||
|
...channel,
|
||||||
|
config: channel.config as JSON
|
||||||
|
},
|
||||||
|
actionSuccess: {
|
||||||
|
message: `Storage channel "${channel.name}" has been successfully updated.`,
|
||||||
|
messageParams: {id: channel.id},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
actionError: {
|
||||||
|
message: "Failed to update storage channel.",
|
||||||
|
status: 500,
|
||||||
|
cause: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
messageParams: {id: ""},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"use client"
|
||||||
|
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
|
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||||
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
import {ChannelCard} from "@/components/wrappers/dashboard/admin/channels/channel/channel-card/channel-card";
|
||||||
|
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
|
||||||
|
import {ChannelKind, getChannelTextBasedOnKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||||
|
|
||||||
|
type ChannelsSectionProps = {
|
||||||
|
channels: NotificationChannelWith[] | StorageChannelWith[]
|
||||||
|
organizations: OrganizationWithMembers[]
|
||||||
|
kind: ChannelKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChannelsSection = ({
|
||||||
|
organizations,
|
||||||
|
channels,
|
||||||
|
kind
|
||||||
|
}: ChannelsSectionProps) => {
|
||||||
|
|
||||||
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
|
const channelText = getChannelTextBasedOnKind(kind)
|
||||||
|
const hasChannels = channels.length > 0
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full">
|
||||||
|
<ChannelAddEditModal kind={kind} open={isAddModalOpen} onOpenChangeAction={setIsAddModalOpen}
|
||||||
|
adminView={false}
|
||||||
|
trigger={false}/>
|
||||||
|
{hasChannels ? (
|
||||||
|
<div className="h-full">
|
||||||
|
<CardsWithPagination
|
||||||
|
data={channels}
|
||||||
|
cardItem={ChannelCard}
|
||||||
|
cardsPerPage={8}
|
||||||
|
numberOfColumns={2}
|
||||||
|
adminView={true}
|
||||||
|
organizations={organizations}
|
||||||
|
kind={kind}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<EmptyStatePlaceholder
|
||||||
|
text={`No ${channelText} channels configured yet`}
|
||||||
|
onClick={() => {
|
||||||
|
setIsAddModalOpen(true)
|
||||||
|
}}
|
||||||
|
className="h-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import {UseFormReturn} from "react-hook-form";
|
||||||
|
import {
|
||||||
|
NotifierSmtpForm
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/smtp.form";
|
||||||
|
import {
|
||||||
|
NotifierSlackForm
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/slack.form";
|
||||||
|
import {
|
||||||
|
NotifierDiscordForm
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/discord.form";
|
||||||
|
import {
|
||||||
|
NotifierTelegramForm
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/telegram.form";
|
||||||
|
import {
|
||||||
|
NotifierGotifyForm
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/gotify.form";
|
||||||
|
import {
|
||||||
|
NotifierNtfyForm
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/ntfy.form";
|
||||||
|
import {
|
||||||
|
NotifierWebhookForm
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/webhook.form";
|
||||||
|
import {notificationTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/notification";
|
||||||
|
import {storageTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/storage";
|
||||||
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
|
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
import {ForwardRefExoticComponent, JSX, RefAttributes, SVGProps} from "react";
|
||||||
|
import {LucideProps} from "lucide-react";
|
||||||
|
|
||||||
|
export type ChannelKind = "notification" | "storage";
|
||||||
|
|
||||||
|
export function getChannelTextBasedOnKind(kind: ChannelKind) {
|
||||||
|
switch (kind) {
|
||||||
|
case "notification":
|
||||||
|
return "Notification";
|
||||||
|
case "storage":
|
||||||
|
return "Storage";
|
||||||
|
default:
|
||||||
|
return "Notification";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type ProviderIconTypes = {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
icon: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>
|
||||||
|
} | {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
icon: (props: SVGProps<SVGSVGElement>) => JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerIcons: ProviderIconTypes[] = [
|
||||||
|
...notificationTypes,
|
||||||
|
...storageTypes,
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
export const getChannelIcon = (type: string) => {
|
||||||
|
const Icon = providerIcons.find((t) => t.value === type)?.icon
|
||||||
|
return Icon ? <Icon className="h-4 w-4"/> : null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const renderChannelForm = (provider: string | undefined, form: UseFormReturn<any>) => {
|
||||||
|
switch (provider) {
|
||||||
|
case "smtp":
|
||||||
|
return <NotifierSmtpForm form={form}/>;
|
||||||
|
case "slack":
|
||||||
|
return <NotifierSlackForm form={form}/>;
|
||||||
|
case "discord":
|
||||||
|
return <NotifierDiscordForm form={form}/>;
|
||||||
|
case "telegram":
|
||||||
|
return <NotifierTelegramForm form={form}/>;
|
||||||
|
case "gotify":
|
||||||
|
return <NotifierGotifyForm form={form}/>;
|
||||||
|
case "ntfy":
|
||||||
|
return <NotifierNtfyForm form={form}/>;
|
||||||
|
case "webhook":
|
||||||
|
return <NotifierWebhookForm form={form}/>;
|
||||||
|
case "local":
|
||||||
|
return <></>
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,483 @@
|
|||||||
|
import {Mail} from "lucide-react";
|
||||||
|
import type { SVGProps } from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const notificationTypes = [
|
||||||
|
{value: "smtp", label: "Email", icon: Mail},
|
||||||
|
{value: "slack", label: "Slack", icon: SlackIcon},
|
||||||
|
{value: "discord", label: "Discord", icon: DiscordIcon},
|
||||||
|
{value: "telegram", label: "Telegram", icon: TelegramIcon},
|
||||||
|
{value: "gotify", label: "Gotify", icon: GotifyIcon},
|
||||||
|
{value: "ntfy", label: "ntfy.sh", icon: NtfyIcon},
|
||||||
|
{value: "webhook", label: "Webhook", icon: WebhookIcon},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
export function SlackIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (<svg xmlns="http://www.w3.org/2000/svg" width={256} height={256} viewBox="0 0 256 256" {...props}><path fill="#e01e5a" d="M53.841 161.32c0 14.832-11.987 26.82-26.819 26.82S.203 176.152.203 161.32c0-14.831 11.987-26.818 26.82-26.818H53.84zm13.41 0c0-14.831 11.987-26.818 26.819-26.818s26.819 11.987 26.819 26.819v67.047c0 14.832-11.987 26.82-26.82 26.82c-14.83 0-26.818-11.988-26.818-26.82z"></path><path fill="#36c5f0" d="M94.07 53.638c-14.832 0-26.82-11.987-26.82-26.819S79.239 0 94.07 0s26.819 11.987 26.819 26.819v26.82zm0 13.613c14.832 0 26.819 11.987 26.819 26.819s-11.987 26.819-26.82 26.819H26.82C11.987 120.889 0 108.902 0 94.069c0-14.83 11.987-26.818 26.819-26.818z"></path><path fill="#2eb67d" d="M201.55 94.07c0-14.832 11.987-26.82 26.818-26.82s26.82 11.988 26.82 26.82s-11.988 26.819-26.82 26.819H201.55zm-13.41 0c0 14.832-11.988 26.819-26.82 26.819c-14.831 0-26.818-11.987-26.818-26.82V26.82C134.502 11.987 146.489 0 161.32 0s26.819 11.987 26.819 26.819z"></path><path fill="#ecb22e" d="M161.32 201.55c14.832 0 26.82 11.987 26.82 26.818s-11.988 26.82-26.82 26.82c-14.831 0-26.818-11.988-26.818-26.82V201.55zm0-13.41c-14.831 0-26.818-11.988-26.818-26.82c0-14.831 11.987-26.818 26.819-26.818h67.25c14.832 0 26.82 11.987 26.82 26.819s-11.988 26.819-26.82 26.819z"></path></svg>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DiscordIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (<svg xmlns="http://www.w3.org/2000/svg" width={256} height={199} viewBox="0 0 256 199" {...props}><path fill="#5865f2" d="M216.856 16.597A208.5 208.5 0 0 0 164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046q-29.538-4.442-58.533 0c-1.832-4.4-4.55-9.933-6.846-14.046a207.8 207.8 0 0 0-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161 161 0 0 0 79.735 175.3a136.4 136.4 0 0 1-21.846-10.632a109 109 0 0 0 5.356-4.237c42.122 19.702 87.89 19.702 129.51 0a132 132 0 0 0 5.355 4.237a136 136 0 0 1-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848c21.142-6.58 42.646-16.637 64.815-33.213c5.316-56.288-9.08-105.09-38.056-148.36M85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2s23.236 11.804 23.015 26.2c.02 14.375-10.148 26.18-23.015 26.18m85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2c0 14.375-10.148 26.18-23.015 26.18"></path></svg>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TelegramIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (<svg xmlns="http://www.w3.org/2000/svg" width={256} height={256} viewBox="0 0 256 256" {...props}><defs><linearGradient id="SVGuySfwdaH" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stopColor="#2aabee"></stop><stop offset="100%" stopColor="#229ed9"></stop></linearGradient></defs><path fill="url(#SVGuySfwdaH)" d="M128 0C94.06 0 61.48 13.494 37.5 37.49A128.04 128.04 0 0 0 0 128c0 33.934 13.5 66.514 37.5 90.51C61.48 242.506 94.06 256 128 256s66.52-13.494 90.5-37.49c24-23.996 37.5-56.576 37.5-90.51s-13.5-66.514-37.5-90.51C194.52 13.494 161.94 0 128 0"></path><path fill="#fff" d="M57.94 126.648q55.98-24.384 74.64-32.152c35.56-14.786 42.94-17.354 47.76-17.441c1.06-.017 3.42.245 4.96 1.49c1.28 1.05 1.64 2.47 1.82 3.467c.16.996.38 3.266.2 5.038c-1.92 20.24-10.26 69.356-14.5 92.026c-1.78 9.592-5.32 12.808-8.74 13.122c-7.44.684-13.08-4.912-20.28-9.63c-11.26-7.386-17.62-11.982-28.56-19.188c-12.64-8.328-4.44-12.906 2.76-20.386c1.88-1.958 34.64-31.748 35.26-34.45c.08-.338.16-1.598-.6-2.262c-.74-.666-1.84-.438-2.64-.258c-1.14.256-19.12 12.152-54 35.686c-5.1 3.508-9.72 5.218-13.88 5.128c-4.56-.098-13.36-2.584-19.9-4.708c-8-2.606-14.38-3.984-13.82-8.41c.28-2.304 3.46-4.662 9.52-7.072"></path></svg>);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function NtfyIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
|
||||||
|
return ( <svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
id="Layer_1"
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<style>{".st1{fill:#fff}"}</style>
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_1_"
|
||||||
|
x1={115.045}
|
||||||
|
x2={142.93}
|
||||||
|
y1={512.747}
|
||||||
|
y2={491.741}
|
||||||
|
gradientTransform="matrix(15.8584 0 0 -15.8584 -1816.246 8200.18)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset={0}
|
||||||
|
style={{
|
||||||
|
stopColor: "#348878",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={1}
|
||||||
|
style={{
|
||||||
|
stopColor: "#56bda8",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
d="M441.8 64.8H84.7c-24.5 0-44.6 19-44.6 42.1l.3 285.3-6.7 38.4 95.6-28.8h312.4c24.5 0 44.6-19 44.6-42.1V106.9c.1-23.2-20-42.1-44.5-42.1"
|
||||||
|
style={{
|
||||||
|
fill: "url(#SVGID_1_)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M78.1 32.7C40.9 32.7 9 62 9 99.2l.4 311.2-9.4 69 127.1-33.8H443c37.2 0 69.1-29.3 69.1-66.5V99.2c0-37.2-31.9-66.5-69-66.5zm0 46.9H443c13.1 0 22.1 9.5 22.1 19.6V379c0 10-9 19.8-22.1 19.6H120.2l-64.6 19.5.7-3.8-.4-315.1c0-10.1 9.1-19.6 22.2-19.6"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M116.4 310.9v-34.8l73.1-38.7c2.4-1.2 4.6-2.1 6.8-2.7 2.3-.8 4.5-1.4 6.3-1.8l6-.6V230q-3-.3-6-1.2-2.85-.6-6.3-1.5c-2.2-.8-4.5-1.8-6.8-3l-73.1-38.7v-35.1l113 61.5v37.4z"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M110.5 139.7v50.4l76.6 40.6.5.2-.5.2-76.6 40.6v50.1l8.2-4.5L235.1 254v-46.4zm11.8 21.7 101.2 55.1V245l-101.2 55.1v-19.4l69.6-36.8.1-.1c1.9-1 3.8-1.7 5.8-2.3h.1l.2-.1c2.1-.7 3.9-1.2 5.5-1.6l11.1-1.1v-15.2l-5.4-.5c-1.7-.2-3.5-.5-5.1-1.1h-.2l-.2-.1c-2-.4-3.9-.9-5.9-1.4-2-.7-4-1.6-5.9-2.6l-.1-.1-69.6-36.7zm152.3 137.4h120.7v32.6H274.6z"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M268.7 292.1v46h132.4v-46zm11.7 13.4h109v19.2h-109z"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
</svg>)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function GotifyIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
id="Layer_2"
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
viewBox="46.6 113.46 377.2 307.81"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<style>
|
||||||
|
{
|
||||||
|
".st0{fill:#ddcba2}.st0,.st1,.st2,.st4,.st5{stroke:#000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10}.st1{fill:#71caee}.st2{fill:#fff}.st4{fill:#f0f0f0}.st5{fill:none}"
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<switch>
|
||||||
|
<foreignObject
|
||||||
|
width={1}
|
||||||
|
height={1}
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/"
|
||||||
|
/>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M317.7 376.2c6.2-1.7 15.8 0 19.5 3.2 5.5 4.8 4.9 20.9 1.1 29-.8 1.7-1.8 3.4-3.2 4.4s-3.4 1.2-4.7 0-1.7-3.3-1.9-5.2c-.2-3.1-.2-6.2.3-9.2.2-1.3.2-2.9-.2-4.2-.6-2.2-2.5-3.1-4.5-3.5-3.4-.8-14.3-.7-19.5-.7-.8 0 1.5-9.1 2-9.7 2.6-2.9 7.5-3.1 11.1-4.1z"
|
||||||
|
className="st0"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m258.9 119.7-9-2.7c-4.6-1.4-9.2-2.8-14-2.5-2.8.2-6.1 1.3-6.9 4-.6 2-1.6 7.3-1.3 7.9 1.5 3.4 13.9 6.7 18.3 6.7"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path d="M392.6 177.9c-1.4 1.4-2.2 3.5-2.5 5.5-.2 1.4-.1 3 .5 4.3s1.8 2.3 3.1 3c1.3.6 2.8.9 4.3.9 1.1 0 2.3-.1 3.1-.9.6-.7.8-1.6.9-2.5.2-2.3-.1-4.7-.9-6.9-.4-1.1-.9-2.3-1.8-3.1-1.7-1.8-4.5-2.2-6.4-.5-.1 0-.2.1-.3.2" />
|
||||||
|
<path
|
||||||
|
d="M358.5 164.2c-1-1 0-2.7 1-3.7 5.8-5.2 15.1-4.6 21.8-.6 10.9 6.6 15.6 19.9 17.2 32.5.6 5.2.9 10.6-.5 15.7s-4.6 9.9-9.3 12.1c-1.1.5-2.3.9-3.4.5s-1.9-1.8-1.2-2.8c-9.4-13.6-19-26.8-20.9-43.2-.5-4.1-1.8-7.4-4.7-10.5z"
|
||||||
|
className="st2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M134.7 328.4c-5.1-3.1-9.9-6.6-14.3-10.6-1.3-1.2-2.6-2.5-2.6-4.3 0-1.2.6-2.2 1.2-3.2.8-1.4 1.7-2.8 2.5-4.1 1.1-1.8 2.9-3.9 4.9-3.2.9.3 1.5 1.1 2 1.8 2.4 3.3 4.9 6.6 7.3 9.8 1.5 2 3.7 4.3 6.1 3.5"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M209.6 133c33.2-18 77.8-19.6 111.5-8.7 24.3 7.9 43.4 26.7 53.3 50 8.7 20.6 10.5 43.6 8.1 65.7-4.4 40.2-20.2 77.9-40.3 112.6-11.1 19-21.8 36-40.5 48.5-36.8 24.6-87.2 22.1-128.4 11.5-19.9-5.1-39.7-17.3-47.2-37.3-4.8-12.8-4.2-27.6 1.5-40 11.6-24.8 43.2-38.4 45.6-67.9.7-8.7-1.6-17.3-3.6-25.7-5.6-23.4-8.9-45.8 1.4-68.7 8.1-17.7 21.9-31 38.6-40z"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M189.8 151.4c-5.4-5.2-11.9-8.8-19-10.3-2.2-.5-4.7-.7-6.9.7-1.8 1.2-3.1 3.3-4.2 5.3-1.6 3-3 6.2-4.1 9.4-.4 1.2-.6 2.5 0 3.5.3.6.9.9 1.5 1.2 8.1 4.2 16.8 7.1 25.5 9.8"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M183.7 158.7c-2.5-1.8-16.8-12.1-18.7-4.8-.4 1.6.5 3.9 1.5 4.8"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M264.5 174.9c-.5.5-.9 1-1.3 1.6-9 11.6-12 27.9-9.3 42.1 1.7 9 5.9 17.9 13.2 23.4 19.3 14.6 51.5 13.5 68.4-1.5 24.4-21.7 13-67.6-14-78.8-17.6-7.2-43.7-1.6-57 13.2zm117.6 62.2c1.4-.1 2.9-.1 4.3.1.3 0 .7.1 1 .4.2.3.4.7.5 1.1 1 3.9.5 8.2.1 12.4-.1.9-.2 1.8-.6 2.6-1 2.1-3.1 2.7-4.7 2.7-.1 0-.2 0-.3-.1-.3-.2-.3-.7-.2-1.2.3-5.9-.1-11.9-.1-18q0 .15 0 0z"
|
||||||
|
className="st2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M378.7 236.8c-1.4.4-2.5 2-2.8 4.4-.5 4.4-.7 8.9-.5 13.4 0 .9.1 1.9.5 2.4.2.3.5.4.8.4 1.6.3 4.1-.6 5.6-1 0 0 0-5.2-.1-8s-.1-6.1-.2-8.9v-2.2c.1-.7-2.6-.7-3.3-.5z"
|
||||||
|
className="st2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M358.3 231.8c-.3 2.2.1 4.7 1.7 7.4 2.6 4.4 7 6.1 11.9 5.8 8.9-.6 25.3-5.4 27.5-15.7.6-3-.3-6.1-2.2-8.5-6.2-7.8-17.8-5.7-25.6-2-5.9 2.7-12.4 7-13.3 13z"
|
||||||
|
className="st0"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M386.4 208.6c2.2 1.4 3.7 3.8 4 7 .3 3.6-1.4 7.5-5 8.8-2.9 1.1-6.2.6-9.1-.4s-5.8-2.8-6.8-5.7c-.7-2-.3-4.3.7-6.1 1.1-1.8 2.8-3.2 4.7-4.1 3.9-1.8 8.4-1.6 11.5.5z"
|
||||||
|
style={{
|
||||||
|
fill: "#888e93",
|
||||||
|
stroke: "#000",
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeLinecap: "round",
|
||||||
|
strokeMiterlimit: 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M414.7 262.6c2.4.6 4.8 2.1 5.6 4.4s.1 4.9-1.6 6.7-4.2 2.5-6.6 2.5c-.8 0-1.7-.1-2.4-.5-2.5-1.1-3.5-4-4.2-6.6-1.8-6.8 3.6-7.8 9.2-6.5z"
|
||||||
|
className="st0"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M267.1 284.7c2.3-4.5 141.3-36.2 144.7-31.6 3.4 4.5 15.8 88.2 9 90.4-6.8 2.3-119.8 37.3-126.6 35s-29.4-89.3-27.1-93.8z"
|
||||||
|
className="st4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M294.2 378.5s54.3-74.6 59.9-76.9c5.7-2.3 67.3 41.3 67.3 41.3"
|
||||||
|
className="st5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M267 287.7s86 38.8 91.6 36.6c5.7-2.3 53.1-71.2 53.1-71.2"
|
||||||
|
className="st4"
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_1_"
|
||||||
|
x1={131.119}
|
||||||
|
x2={103.259}
|
||||||
|
y1={373.573}
|
||||||
|
y2={396.951}
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset={0}
|
||||||
|
style={{
|
||||||
|
stopColor: "#71caee",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.041}
|
||||||
|
style={{
|
||||||
|
stopColor: "#83cae2",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.121}
|
||||||
|
style={{
|
||||||
|
stopColor: "#9fcace",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.211}
|
||||||
|
style={{
|
||||||
|
stopColor: "#b6cbbe",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.314}
|
||||||
|
style={{
|
||||||
|
stopColor: "#c7cbb1",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.438}
|
||||||
|
style={{
|
||||||
|
stopColor: "#d4cba8",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.606}
|
||||||
|
style={{
|
||||||
|
stopColor: "#dbcba3",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={1}
|
||||||
|
style={{
|
||||||
|
stopColor: "#ddcba2",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
d="M132.8 375.6c-3.5 3.8-7.3 7.8-13 9.2-4.6 1.2-10 .2-13.6-2.3-1.4-1-2.6-2.2-4-3.2-1.5-1-3.4-1.7-5.3-1.3-2.7.5-4.1 3.1-3.6 5.3 2 8.8 17 15.6 27.5 15.5 9 0 19-4.6 21.4-11.8"
|
||||||
|
style={{
|
||||||
|
fill: "url(#SVGID_1_)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M132.8 375.6c-3.5 3.8-7.3 7.8-13 9.2-4.6 1.2-10 .2-13.6-2.3-1.4-1-2.6-2.2-4-3.2-1.5-1-3.4-1.7-5.3-1.3-2.7.5-4.1 3.1-3.6 5.3 2 8.8 17 15.6 27.5 15.5 9 0 19-4.6 21.4-11.8"
|
||||||
|
className="st5"
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_2_"
|
||||||
|
x1={265.033}
|
||||||
|
x2={275.301}
|
||||||
|
y1={280.295}
|
||||||
|
y2={302.314}
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset={0}
|
||||||
|
style={{
|
||||||
|
stopColor: "#71caee",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.041}
|
||||||
|
style={{
|
||||||
|
stopColor: "#83cae2",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.121}
|
||||||
|
style={{
|
||||||
|
stopColor: "#9fcace",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.211}
|
||||||
|
style={{
|
||||||
|
stopColor: "#b6cbbe",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.314}
|
||||||
|
style={{
|
||||||
|
stopColor: "#c7cbb1",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.438}
|
||||||
|
style={{
|
||||||
|
stopColor: "#d4cba8",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.606}
|
||||||
|
style={{
|
||||||
|
stopColor: "#dbcba3",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={1}
|
||||||
|
style={{
|
||||||
|
stopColor: "#ddcba2",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
d="M261.9 283.5c-.1 4.2 4.3 7.3 8.4 7.6s8.2-1.3 12.2-2.6c1.4-.4 2.9-.8 4.2-.2 1.8.9 2.7 4.1 1.8 5.9s-3.4 3.5-5.3 4.4c-6.5 3-12.9 3.6-19.9 2-5.3-1.2-11.3-4.3-13-13.5"
|
||||||
|
style={{
|
||||||
|
fill: "url(#SVGID_2_)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M261.9 283.5c-.1 4.2 4.3 7.3 8.4 7.6s8.2-1.3 12.2-2.6c1.4-.4 2.9-.8 4.2-.2 1.8.9 2.7 4.1 1.8 5.9s-3.4 3.5-5.3 4.4c-6.5 3-12.9 3.6-19.9 2-5.3-1.2-11.3-4.3-13-13.5"
|
||||||
|
className="st5"
|
||||||
|
/>
|
||||||
|
<path d="M318.4 198.4c-2-.3-4.1.1-5.9 1.3-3.2 2.1-4.7 6.2-4.7 9.9 0 1.9.4 3.8 1.4 5.3 1.2 1.7 3.1 2.9 5.2 3.4 3.4.8 8.2.7 10.5-2.5 1-1.5 1.4-3.3 1.5-5.1.5-5.7-1.8-11.4-8-12.3" />
|
||||||
|
<path
|
||||||
|
d="M320.4 203.3c.9.3 1.7.8 2.1 1.7.4.8.4 1.7.3 2.5-.1 1-.6 2-1.5 2.7-.7.5-1.7.7-2.6.5s-1.7-.8-2.2-1.6c-1.1-1.6-.9-4.4.9-5.5.9-.4 2-.6 3-.3"
|
||||||
|
style={{
|
||||||
|
fill: "#fff",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_3_"
|
||||||
|
x1={-18.456}
|
||||||
|
x2={110.382}
|
||||||
|
y1={252.286}
|
||||||
|
y2={252.286}
|
||||||
|
gradientTransform="rotate(-5.297 -26.409 -856.73)scale(.99997)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset={0}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.477}
|
||||||
|
style={{
|
||||||
|
stopColor: "#575756",
|
||||||
|
stopOpacity: 0.2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.995}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0.001911551,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={1}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
d="M81.6 222.4s56.5 68.4 123.9 42.4l6.6-29.7C150 272.2 82.8 214.3 82.8 214.3z"
|
||||||
|
style={{
|
||||||
|
fill: "url(#SVGID_3_)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_4_"
|
||||||
|
x1={110.198}
|
||||||
|
x2={283.737}
|
||||||
|
y1={262.493}
|
||||||
|
y2={262.493}
|
||||||
|
gradientTransform="matrix(.9991 .04164 -.04164 .9991 -36.323 1.051)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset={0}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.477}
|
||||||
|
style={{
|
||||||
|
stopColor: "#575756",
|
||||||
|
stopOpacity: 0.2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.995}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0.001911551,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={1}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
d="M64.2 234.9s63.1 101.5 157.8 78.9l14.2-38.5c-89.5 38.3-168.9-51-168.9-51z"
|
||||||
|
style={{
|
||||||
|
fill: "url(#SVGID_4_)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_5_"
|
||||||
|
x1={218.967}
|
||||||
|
x2={428.745}
|
||||||
|
y1={263.594}
|
||||||
|
y2={263.594}
|
||||||
|
gradientTransform="rotate(9.265 -38.832 -826.002)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset={0}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.477}
|
||||||
|
style={{
|
||||||
|
stopColor: "#575756",
|
||||||
|
stopOpacity: 0.2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.995}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0.001911551,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={1}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
d="M46.6 251.5s61.1 130.9 178 117.5l22.6-44.2c-113 33-195.3-85.7-195.3-85.7z"
|
||||||
|
style={{
|
||||||
|
fill: "url(#SVGID_5_)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</switch>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WebhookIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={2500}
|
||||||
|
height={2448}
|
||||||
|
fillRule="evenodd"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeMiterlimit={2}
|
||||||
|
clipRule="evenodd"
|
||||||
|
viewBox="-6.74 -6.74 223.55 218.799"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#7547db"
|
||||||
|
d="M111.134 71.906a21 21 0 0 1-1.324.042c-11.314 0-20.5-9.186-20.5-20.5s9.186-20.5 20.5-20.5 20.5 9.186 20.5 20.5a20.4 20.4 0 0 1-4.582 12.913l22.66 41.655a51 51 0 0 1 17.422-3.068c28.148 0 51 22.852 51 51s-22.852 51-51 51a51.2 51.2 0 0 1-28.036-8.373 7.303 7.303 0 0 1-1.359-11.105c.015-.016.031-.033.061-.036a7.665 7.665 0 0 1 9.736-1.228 36.06 36.06 0 0 0 19.598 5.792c19.897 0 36.05-16.153 36.05-36.05s-16.153-36.05-36.05-36.05c-6.89 0-16.669 3.014-22.548 6.745a2.637 2.637 0 0 1-3.899-.845zm-9.412 88.614a51 51 0 0 1-6.054 16.62c-14.074 24.378-45.29 32.743-69.667 18.669S-6.74 150.518 7.333 126.14a51.2 51.2 0 0 1 21.27-20.094 7.3 7.3 0 0 1 6.167-.211 7.3 7.3 0 0 1 4.13 4.587c.006.022.012.043 0 .07a7.665 7.665 0 0 1-3.804 9.047 36.05 36.05 0 0 0-14.816 14.076c-9.948 17.23-4.036 39.297 13.195 49.246 17.231 9.948 39.298 4.035 49.246-13.196 3.445-5.967 5.724-15.943 5.433-22.9a2.632 2.632 0 0 1 2.681-2.953l59.054 1.499q.292-.59.626-1.168c5.657-9.798 18.205-13.161 28.003-7.504 9.8 5.658 13.161 18.206 7.504 28.004s-18.205 13.16-28.004 7.503a20.4 20.4 0 0 1-8.892-10.424zM76.97 90.555A51 51 0 0 1 65.601 77c-14.074-24.377-5.71-55.594 18.667-69.667S139.862 1.624 153.935 26a51.2 51.2 0 0 1 6.768 28.466 7.306 7.306 0 0 1-8.938 6.73c-.022-.005-.044-.011-.062-.035a7.664 7.664 0 0 1-5.932-7.817 36.04 36.04 0 0 0-4.783-19.87c-9.948-17.23-32.014-23.143-49.245-13.195S68.599 52.294 78.548 69.525c3.445 5.967 10.945 12.929 17.115 16.155a2.635 2.635 0 0 1 1.218 3.799l-30.826 50.393q.366.548.7 1.125c5.656 9.799 2.294 22.347-7.505 28.004s-22.346 2.295-28.003-7.504-2.295-22.346 7.503-28.003a20.4 20.4 0 0 1 13.474-2.488z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import {Server} from "lucide-react";
|
||||||
|
|
||||||
|
export const storageTypes = [
|
||||||
|
{value: "local", label: "Local", icon: Server},
|
||||||
|
{value: "s3", label: "s3", icon: Server},
|
||||||
|
]
|
||||||
+22
-18
@@ -6,26 +6,30 @@ import {Form, FormControl, FormField, FormItem, useZodForm} from "@/components/u
|
|||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
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 {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";
|
import {toast} from "sonner";
|
||||||
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
import {
|
||||||
|
ChannelsOrganizationSchema, ChannelsOrganizationType
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/organization/channels-organization.schema";
|
||||||
|
import {
|
||||||
|
updateNotificationChannelsOrganizationAction, updateStorageChannelsOrganizationAction
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/organization/channels-organization.action";
|
||||||
|
import {ChannelKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||||
|
|
||||||
|
|
||||||
type NotifierChannelOrganisationFormProps = {
|
type ChannelOrganisationFormProps = {
|
||||||
organizations?: OrganizationWithMembers[];
|
organizations?: OrganizationWithMembers[];
|
||||||
defaultValues?: NotificationChannelWith
|
defaultValues?: NotificationChannelWith | StorageChannelWith
|
||||||
|
kind: ChannelKind
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NotifierChannelOrganisationForm = ({
|
export const ChannelOrganisationForm = ({
|
||||||
organizations,
|
organizations,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
}: NotifierChannelOrganisationFormProps) => {
|
kind
|
||||||
|
}: ChannelOrganisationFormProps) => {
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -33,7 +37,7 @@ export const NotifierChannelOrganisationForm = ({
|
|||||||
|
|
||||||
|
|
||||||
const form = useZodForm({
|
const form = useZodForm({
|
||||||
schema: NotificationChannelsOrganizationSchema,
|
schema: ChannelsOrganizationSchema,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
organizations: defaultOrganizationIds
|
organizations: defaultOrganizationIds
|
||||||
@@ -49,15 +53,15 @@ export const NotifierChannelOrganisationForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const mutationUpdateNotificationChannelOrganizations = useMutation({
|
const mutationUpdateChannelOrganizations = useMutation({
|
||||||
mutationFn: async (values: NotificationChannelsOrganizationType) => {
|
mutationFn: async (values: ChannelsOrganizationType) => {
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
data: values.organizations,
|
data: values.organizations,
|
||||||
notificationChannelId: defaultValues?.id ?? ""
|
id: defaultValues?.id ?? ""
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await updateNotificationChannelsOrganizationAction(payload)
|
const result = kind === "notification" ? await updateNotificationChannelsOrganizationAction(payload) : await updateStorageChannelsOrganizationAction(payload)
|
||||||
const inner = result?.data;
|
const inner = result?.data;
|
||||||
|
|
||||||
if (inner?.success) {
|
if (inner?.success) {
|
||||||
@@ -76,7 +80,7 @@ export const NotifierChannelOrganisationForm = ({
|
|||||||
form={form}
|
form={form}
|
||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
await mutationUpdateNotificationChannelOrganizations.mutateAsync(values);
|
await mutationUpdateChannelOrganizations.mutateAsync(values);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -100,7 +104,7 @@ export const NotifierChannelOrganisationForm = ({
|
|||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<div className="flex gap-2 justify-end">
|
<div className="flex gap-2 justify-end">
|
||||||
<ButtonWithLoading isPending={mutationUpdateNotificationChannelOrganizations.isPending}>
|
<ButtonWithLoading isPending={mutationUpdateChannelOrganizations.isPending}>
|
||||||
Save
|
Save
|
||||||
</ButtonWithLoading>
|
</ButtonWithLoading>
|
||||||
</div>
|
</div>
|
||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
"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";
|
||||||
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
|
||||||
|
|
||||||
|
export const updateNotificationChannelsOrganizationAction = userAction
|
||||||
|
.schema(
|
||||||
|
z.object({
|
||||||
|
data: z.array(z.string()),
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.action(async ({parsedInput , ctx}): Promise<ServerActionResult<null>> => {
|
||||||
|
try {
|
||||||
|
const organizationsIds = parsedInput.data;
|
||||||
|
const notificationChannelId = parsedInput.id;
|
||||||
|
|
||||||
|
const notificationChannel = await db.query.notificationChannel.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.notificationChannel.id, 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: notificationChannelId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (organizationsToRemove.length > 0) {
|
||||||
|
await db.delete(drizzleDb.schemas.organizationNotificationChannel).where(and(inArray(drizzleDb.schemas.organizationNotificationChannel.organizationId, organizationsToRemove), eq(drizzleDb.schemas.organizationNotificationChannel.notificationChannelId,notificationChannelId))).execute();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: null,
|
||||||
|
actionSuccess: {
|
||||||
|
message: "Notification channel organizations has been successfully updated.",
|
||||||
|
messageParams: {notificationChannelId: 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"},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const updateStorageChannelsOrganizationAction = userAction
|
||||||
|
.schema(
|
||||||
|
z.object({
|
||||||
|
data: z.array(z.string()),
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<null>> => {
|
||||||
|
try {
|
||||||
|
const organizationsIds = parsedInput.data;
|
||||||
|
const storageChannelId = parsedInput.id;
|
||||||
|
|
||||||
|
const storageChannel = await db.query.storageChannel.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.storageChannel.id, storageChannelId),
|
||||||
|
with: {
|
||||||
|
organizations: true,
|
||||||
|
}
|
||||||
|
}) as StorageChannelWith;
|
||||||
|
|
||||||
|
|
||||||
|
if (!storageChannel) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
actionError: {
|
||||||
|
message: "Storage channel not found.",
|
||||||
|
status: 404,
|
||||||
|
cause: "not_found",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingItemIds = storageChannel.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.organizationStorageChannel).values({
|
||||||
|
organizationId: organizationToAdd,
|
||||||
|
storageChannelId: storageChannelId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organizationsToRemove.length > 0) {
|
||||||
|
await db.delete(drizzleDb.schemas.organizationStorageChannel).where(and(inArray(drizzleDb.schemas.organizationStorageChannel.organizationId, organizationsToRemove), eq(drizzleDb.schemas.organizationStorageChannel.storageChannelId, storageChannelId))).execute();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: null,
|
||||||
|
actionSuccess: {
|
||||||
|
message: "Storage channel organizations has been successfully updated.",
|
||||||
|
messageParams: {storageChannelId: storageChannelId},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating storage channel:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
actionError: {
|
||||||
|
message: "Failed to update storage channel.",
|
||||||
|
status: 500,
|
||||||
|
cause: "server_error",
|
||||||
|
messageParams: {message: "Error updating the storage channel"},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const ChannelsOrganizationSchema = z.object({
|
||||||
|
organizations: z.array(z.string())
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ChannelsOrganizationType = z.infer<typeof ChannelsOrganizationSchema>;
|
||||||
-50
@@ -1,50 +0,0 @@
|
|||||||
"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";
|
|
||||||
import {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal";
|
|
||||||
|
|
||||||
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">
|
|
||||||
<NotifierAddEditModal open={isAddModalOpen} onOpenChangeAction={setIsAddModalOpen} adminView={false} trigger={false}/>
|
|
||||||
{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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-79
@@ -1,79 +0,0 @@
|
|||||||
"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
@@ -1,7 +0,0 @@
|
|||||||
import {z} from "zod";
|
|
||||||
|
|
||||||
export const NotificationChannelsOrganizationSchema = z.object({
|
|
||||||
organizations: z.array(z.string())
|
|
||||||
});
|
|
||||||
|
|
||||||
export type NotificationChannelsOrganizationType = z.infer<typeof NotificationChannelsOrganizationSchema>;
|
|
||||||
@@ -1,487 +0,0 @@
|
|||||||
import {Mail} from "lucide-react";
|
|
||||||
import type { SVGProps } from 'react';
|
|
||||||
|
|
||||||
export const getNotificationChannelIcon = (type: string) => {
|
|
||||||
const Icon = notificationTypes.find((t) => t.value === type)?.icon
|
|
||||||
return Icon ? <Icon className="h-4 w-4"/> : null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const notificationTypes = [
|
|
||||||
{value: "smtp", label: "Email", icon: Mail},
|
|
||||||
{value: "slack", label: "Slack", icon: SlackIcon},
|
|
||||||
{value: "discord", label: "Discord", icon: DiscordIcon},
|
|
||||||
{value: "telegram", label: "Telegram", icon: TelegramIcon},
|
|
||||||
{value: "gotify", label: "Gotify", icon: GotifyIcon},
|
|
||||||
{value: "ntfy", label: "ntfy.sh", icon: NtfyIcon},
|
|
||||||
{value: "webhook", label: "Webhook", icon: WebhookIcon},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
export function SlackIcon(props: SVGProps<SVGSVGElement>) {
|
|
||||||
return (<svg xmlns="http://www.w3.org/2000/svg" width={256} height={256} viewBox="0 0 256 256" {...props}><path fill="#e01e5a" d="M53.841 161.32c0 14.832-11.987 26.82-26.819 26.82S.203 176.152.203 161.32c0-14.831 11.987-26.818 26.82-26.818H53.84zm13.41 0c0-14.831 11.987-26.818 26.819-26.818s26.819 11.987 26.819 26.819v67.047c0 14.832-11.987 26.82-26.82 26.82c-14.83 0-26.818-11.988-26.818-26.82z"></path><path fill="#36c5f0" d="M94.07 53.638c-14.832 0-26.82-11.987-26.82-26.819S79.239 0 94.07 0s26.819 11.987 26.819 26.819v26.82zm0 13.613c14.832 0 26.819 11.987 26.819 26.819s-11.987 26.819-26.82 26.819H26.82C11.987 120.889 0 108.902 0 94.069c0-14.83 11.987-26.818 26.819-26.818z"></path><path fill="#2eb67d" d="M201.55 94.07c0-14.832 11.987-26.82 26.818-26.82s26.82 11.988 26.82 26.82s-11.988 26.819-26.82 26.819H201.55zm-13.41 0c0 14.832-11.988 26.819-26.82 26.819c-14.831 0-26.818-11.987-26.818-26.82V26.82C134.502 11.987 146.489 0 161.32 0s26.819 11.987 26.819 26.819z"></path><path fill="#ecb22e" d="M161.32 201.55c14.832 0 26.82 11.987 26.82 26.818s-11.988 26.82-26.82 26.82c-14.831 0-26.818-11.988-26.818-26.82V201.55zm0-13.41c-14.831 0-26.818-11.988-26.818-26.82c0-14.831 11.987-26.818 26.819-26.818h67.25c14.832 0 26.82 11.987 26.82 26.819s-11.988 26.819-26.82 26.819z"></path></svg>);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DiscordIcon(props: SVGProps<SVGSVGElement>) {
|
|
||||||
return (<svg xmlns="http://www.w3.org/2000/svg" width={256} height={199} viewBox="0 0 256 199" {...props}><path fill="#5865f2" d="M216.856 16.597A208.5 208.5 0 0 0 164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046q-29.538-4.442-58.533 0c-1.832-4.4-4.55-9.933-6.846-14.046a207.8 207.8 0 0 0-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161 161 0 0 0 79.735 175.3a136.4 136.4 0 0 1-21.846-10.632a109 109 0 0 0 5.356-4.237c42.122 19.702 87.89 19.702 129.51 0a132 132 0 0 0 5.355 4.237a136 136 0 0 1-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848c21.142-6.58 42.646-16.637 64.815-33.213c5.316-56.288-9.08-105.09-38.056-148.36M85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2s23.236 11.804 23.015 26.2c.02 14.375-10.148 26.18-23.015 26.18m85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2c0 14.375-10.148 26.18-23.015 26.18"></path></svg>);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TelegramIcon(props: SVGProps<SVGSVGElement>) {
|
|
||||||
return (<svg xmlns="http://www.w3.org/2000/svg" width={256} height={256} viewBox="0 0 256 256" {...props}><defs><linearGradient id="SVGuySfwdaH" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stopColor="#2aabee"></stop><stop offset="100%" stopColor="#229ed9"></stop></linearGradient></defs><path fill="url(#SVGuySfwdaH)" d="M128 0C94.06 0 61.48 13.494 37.5 37.49A128.04 128.04 0 0 0 0 128c0 33.934 13.5 66.514 37.5 90.51C61.48 242.506 94.06 256 128 256s66.52-13.494 90.5-37.49c24-23.996 37.5-56.576 37.5-90.51s-13.5-66.514-37.5-90.51C194.52 13.494 161.94 0 128 0"></path><path fill="#fff" d="M57.94 126.648q55.98-24.384 74.64-32.152c35.56-14.786 42.94-17.354 47.76-17.441c1.06-.017 3.42.245 4.96 1.49c1.28 1.05 1.64 2.47 1.82 3.467c.16.996.38 3.266.2 5.038c-1.92 20.24-10.26 69.356-14.5 92.026c-1.78 9.592-5.32 12.808-8.74 13.122c-7.44.684-13.08-4.912-20.28-9.63c-11.26-7.386-17.62-11.982-28.56-19.188c-12.64-8.328-4.44-12.906 2.76-20.386c1.88-1.958 34.64-31.748 35.26-34.45c.08-.338.16-1.598-.6-2.262c-.74-.666-1.84-.438-2.64-.258c-1.14.256-19.12 12.152-54 35.686c-5.1 3.508-9.72 5.218-13.88 5.128c-4.56-.098-13.36-2.584-19.9-4.708c-8-2.606-14.38-3.984-13.82-8.41c.28-2.304 3.46-4.662 9.52-7.072"></path></svg>);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function NtfyIcon(props: SVGProps<SVGSVGElement>) {
|
|
||||||
|
|
||||||
return ( <svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlSpace="preserve"
|
|
||||||
id="Layer_1"
|
|
||||||
x={0}
|
|
||||||
y={0}
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<style>{".st1{fill:#fff}"}</style>
|
|
||||||
<linearGradient
|
|
||||||
id="SVGID_1_"
|
|
||||||
x1={115.045}
|
|
||||||
x2={142.93}
|
|
||||||
y1={512.747}
|
|
||||||
y2={491.741}
|
|
||||||
gradientTransform="matrix(15.8584 0 0 -15.8584 -1816.246 8200.18)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop
|
|
||||||
offset={0}
|
|
||||||
style={{
|
|
||||||
stopColor: "#348878",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={1}
|
|
||||||
style={{
|
|
||||||
stopColor: "#56bda8",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
<path
|
|
||||||
d="M441.8 64.8H84.7c-24.5 0-44.6 19-44.6 42.1l.3 285.3-6.7 38.4 95.6-28.8h312.4c24.5 0 44.6-19 44.6-42.1V106.9c.1-23.2-20-42.1-44.5-42.1"
|
|
||||||
style={{
|
|
||||||
fill: "url(#SVGID_1_)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M78.1 32.7C40.9 32.7 9 62 9 99.2l.4 311.2-9.4 69 127.1-33.8H443c37.2 0 69.1-29.3 69.1-66.5V99.2c0-37.2-31.9-66.5-69-66.5zm0 46.9H443c13.1 0 22.1 9.5 22.1 19.6V379c0 10-9 19.8-22.1 19.6H120.2l-64.6 19.5.7-3.8-.4-315.1c0-10.1 9.1-19.6 22.2-19.6"
|
|
||||||
className="st1"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M116.4 310.9v-34.8l73.1-38.7c2.4-1.2 4.6-2.1 6.8-2.7 2.3-.8 4.5-1.4 6.3-1.8l6-.6V230q-3-.3-6-1.2-2.85-.6-6.3-1.5c-2.2-.8-4.5-1.8-6.8-3l-73.1-38.7v-35.1l113 61.5v37.4z"
|
|
||||||
className="st1"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M110.5 139.7v50.4l76.6 40.6.5.2-.5.2-76.6 40.6v50.1l8.2-4.5L235.1 254v-46.4zm11.8 21.7 101.2 55.1V245l-101.2 55.1v-19.4l69.6-36.8.1-.1c1.9-1 3.8-1.7 5.8-2.3h.1l.2-.1c2.1-.7 3.9-1.2 5.5-1.6l11.1-1.1v-15.2l-5.4-.5c-1.7-.2-3.5-.5-5.1-1.1h-.2l-.2-.1c-2-.4-3.9-.9-5.9-1.4-2-.7-4-1.6-5.9-2.6l-.1-.1-69.6-36.7zm152.3 137.4h120.7v32.6H274.6z"
|
|
||||||
className="st1"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M268.7 292.1v46h132.4v-46zm11.7 13.4h109v19.2h-109z"
|
|
||||||
className="st1"
|
|
||||||
/>
|
|
||||||
</svg>)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function GotifyIcon(props: SVGProps<SVGSVGElement>) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlSpace="preserve"
|
|
||||||
id="Layer_2"
|
|
||||||
x={0}
|
|
||||||
y={0}
|
|
||||||
viewBox="46.6 113.46 377.2 307.81"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<style>
|
|
||||||
{
|
|
||||||
".st0{fill:#ddcba2}.st0,.st1,.st2,.st4,.st5{stroke:#000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10}.st1{fill:#71caee}.st2{fill:#fff}.st4{fill:#f0f0f0}.st5{fill:none}"
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<switch>
|
|
||||||
<foreignObject
|
|
||||||
width={1}
|
|
||||||
height={1}
|
|
||||||
x={0}
|
|
||||||
y={0}
|
|
||||||
requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/"
|
|
||||||
/>
|
|
||||||
<g>
|
|
||||||
<path
|
|
||||||
d="M317.7 376.2c6.2-1.7 15.8 0 19.5 3.2 5.5 4.8 4.9 20.9 1.1 29-.8 1.7-1.8 3.4-3.2 4.4s-3.4 1.2-4.7 0-1.7-3.3-1.9-5.2c-.2-3.1-.2-6.2.3-9.2.2-1.3.2-2.9-.2-4.2-.6-2.2-2.5-3.1-4.5-3.5-3.4-.8-14.3-.7-19.5-.7-.8 0 1.5-9.1 2-9.7 2.6-2.9 7.5-3.1 11.1-4.1z"
|
|
||||||
className="st0"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="m258.9 119.7-9-2.7c-4.6-1.4-9.2-2.8-14-2.5-2.8.2-6.1 1.3-6.9 4-.6 2-1.6 7.3-1.3 7.9 1.5 3.4 13.9 6.7 18.3 6.7"
|
|
||||||
className="st1"
|
|
||||||
/>
|
|
||||||
<path d="M392.6 177.9c-1.4 1.4-2.2 3.5-2.5 5.5-.2 1.4-.1 3 .5 4.3s1.8 2.3 3.1 3c1.3.6 2.8.9 4.3.9 1.1 0 2.3-.1 3.1-.9.6-.7.8-1.6.9-2.5.2-2.3-.1-4.7-.9-6.9-.4-1.1-.9-2.3-1.8-3.1-1.7-1.8-4.5-2.2-6.4-.5-.1 0-.2.1-.3.2" />
|
|
||||||
<path
|
|
||||||
d="M358.5 164.2c-1-1 0-2.7 1-3.7 5.8-5.2 15.1-4.6 21.8-.6 10.9 6.6 15.6 19.9 17.2 32.5.6 5.2.9 10.6-.5 15.7s-4.6 9.9-9.3 12.1c-1.1.5-2.3.9-3.4.5s-1.9-1.8-1.2-2.8c-9.4-13.6-19-26.8-20.9-43.2-.5-4.1-1.8-7.4-4.7-10.5z"
|
|
||||||
className="st2"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M134.7 328.4c-5.1-3.1-9.9-6.6-14.3-10.6-1.3-1.2-2.6-2.5-2.6-4.3 0-1.2.6-2.2 1.2-3.2.8-1.4 1.7-2.8 2.5-4.1 1.1-1.8 2.9-3.9 4.9-3.2.9.3 1.5 1.1 2 1.8 2.4 3.3 4.9 6.6 7.3 9.8 1.5 2 3.7 4.3 6.1 3.5"
|
|
||||||
className="st1"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M209.6 133c33.2-18 77.8-19.6 111.5-8.7 24.3 7.9 43.4 26.7 53.3 50 8.7 20.6 10.5 43.6 8.1 65.7-4.4 40.2-20.2 77.9-40.3 112.6-11.1 19-21.8 36-40.5 48.5-36.8 24.6-87.2 22.1-128.4 11.5-19.9-5.1-39.7-17.3-47.2-37.3-4.8-12.8-4.2-27.6 1.5-40 11.6-24.8 43.2-38.4 45.6-67.9.7-8.7-1.6-17.3-3.6-25.7-5.6-23.4-8.9-45.8 1.4-68.7 8.1-17.7 21.9-31 38.6-40z"
|
|
||||||
className="st1"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M189.8 151.4c-5.4-5.2-11.9-8.8-19-10.3-2.2-.5-4.7-.7-6.9.7-1.8 1.2-3.1 3.3-4.2 5.3-1.6 3-3 6.2-4.1 9.4-.4 1.2-.6 2.5 0 3.5.3.6.9.9 1.5 1.2 8.1 4.2 16.8 7.1 25.5 9.8"
|
|
||||||
className="st1"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M183.7 158.7c-2.5-1.8-16.8-12.1-18.7-4.8-.4 1.6.5 3.9 1.5 4.8"
|
|
||||||
className="st1"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M264.5 174.9c-.5.5-.9 1-1.3 1.6-9 11.6-12 27.9-9.3 42.1 1.7 9 5.9 17.9 13.2 23.4 19.3 14.6 51.5 13.5 68.4-1.5 24.4-21.7 13-67.6-14-78.8-17.6-7.2-43.7-1.6-57 13.2zm117.6 62.2c1.4-.1 2.9-.1 4.3.1.3 0 .7.1 1 .4.2.3.4.7.5 1.1 1 3.9.5 8.2.1 12.4-.1.9-.2 1.8-.6 2.6-1 2.1-3.1 2.7-4.7 2.7-.1 0-.2 0-.3-.1-.3-.2-.3-.7-.2-1.2.3-5.9-.1-11.9-.1-18q0 .15 0 0z"
|
|
||||||
className="st2"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M378.7 236.8c-1.4.4-2.5 2-2.8 4.4-.5 4.4-.7 8.9-.5 13.4 0 .9.1 1.9.5 2.4.2.3.5.4.8.4 1.6.3 4.1-.6 5.6-1 0 0 0-5.2-.1-8s-.1-6.1-.2-8.9v-2.2c.1-.7-2.6-.7-3.3-.5z"
|
|
||||||
className="st2"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M358.3 231.8c-.3 2.2.1 4.7 1.7 7.4 2.6 4.4 7 6.1 11.9 5.8 8.9-.6 25.3-5.4 27.5-15.7.6-3-.3-6.1-2.2-8.5-6.2-7.8-17.8-5.7-25.6-2-5.9 2.7-12.4 7-13.3 13z"
|
|
||||||
className="st0"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M386.4 208.6c2.2 1.4 3.7 3.8 4 7 .3 3.6-1.4 7.5-5 8.8-2.9 1.1-6.2.6-9.1-.4s-5.8-2.8-6.8-5.7c-.7-2-.3-4.3.7-6.1 1.1-1.8 2.8-3.2 4.7-4.1 3.9-1.8 8.4-1.6 11.5.5z"
|
|
||||||
style={{
|
|
||||||
fill: "#888e93",
|
|
||||||
stroke: "#000",
|
|
||||||
strokeWidth: 2,
|
|
||||||
strokeLinecap: "round",
|
|
||||||
strokeMiterlimit: 10,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M414.7 262.6c2.4.6 4.8 2.1 5.6 4.4s.1 4.9-1.6 6.7-4.2 2.5-6.6 2.5c-.8 0-1.7-.1-2.4-.5-2.5-1.1-3.5-4-4.2-6.6-1.8-6.8 3.6-7.8 9.2-6.5z"
|
|
||||||
className="st0"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M267.1 284.7c2.3-4.5 141.3-36.2 144.7-31.6 3.4 4.5 15.8 88.2 9 90.4-6.8 2.3-119.8 37.3-126.6 35s-29.4-89.3-27.1-93.8z"
|
|
||||||
className="st4"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M294.2 378.5s54.3-74.6 59.9-76.9c5.7-2.3 67.3 41.3 67.3 41.3"
|
|
||||||
className="st5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M267 287.7s86 38.8 91.6 36.6c5.7-2.3 53.1-71.2 53.1-71.2"
|
|
||||||
className="st4"
|
|
||||||
/>
|
|
||||||
<linearGradient
|
|
||||||
id="SVGID_1_"
|
|
||||||
x1={131.119}
|
|
||||||
x2={103.259}
|
|
||||||
y1={373.573}
|
|
||||||
y2={396.951}
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop
|
|
||||||
offset={0}
|
|
||||||
style={{
|
|
||||||
stopColor: "#71caee",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.041}
|
|
||||||
style={{
|
|
||||||
stopColor: "#83cae2",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.121}
|
|
||||||
style={{
|
|
||||||
stopColor: "#9fcace",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.211}
|
|
||||||
style={{
|
|
||||||
stopColor: "#b6cbbe",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.314}
|
|
||||||
style={{
|
|
||||||
stopColor: "#c7cbb1",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.438}
|
|
||||||
style={{
|
|
||||||
stopColor: "#d4cba8",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.606}
|
|
||||||
style={{
|
|
||||||
stopColor: "#dbcba3",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={1}
|
|
||||||
style={{
|
|
||||||
stopColor: "#ddcba2",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
<path
|
|
||||||
d="M132.8 375.6c-3.5 3.8-7.3 7.8-13 9.2-4.6 1.2-10 .2-13.6-2.3-1.4-1-2.6-2.2-4-3.2-1.5-1-3.4-1.7-5.3-1.3-2.7.5-4.1 3.1-3.6 5.3 2 8.8 17 15.6 27.5 15.5 9 0 19-4.6 21.4-11.8"
|
|
||||||
style={{
|
|
||||||
fill: "url(#SVGID_1_)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M132.8 375.6c-3.5 3.8-7.3 7.8-13 9.2-4.6 1.2-10 .2-13.6-2.3-1.4-1-2.6-2.2-4-3.2-1.5-1-3.4-1.7-5.3-1.3-2.7.5-4.1 3.1-3.6 5.3 2 8.8 17 15.6 27.5 15.5 9 0 19-4.6 21.4-11.8"
|
|
||||||
className="st5"
|
|
||||||
/>
|
|
||||||
<linearGradient
|
|
||||||
id="SVGID_2_"
|
|
||||||
x1={265.033}
|
|
||||||
x2={275.301}
|
|
||||||
y1={280.295}
|
|
||||||
y2={302.314}
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop
|
|
||||||
offset={0}
|
|
||||||
style={{
|
|
||||||
stopColor: "#71caee",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.041}
|
|
||||||
style={{
|
|
||||||
stopColor: "#83cae2",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.121}
|
|
||||||
style={{
|
|
||||||
stopColor: "#9fcace",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.211}
|
|
||||||
style={{
|
|
||||||
stopColor: "#b6cbbe",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.314}
|
|
||||||
style={{
|
|
||||||
stopColor: "#c7cbb1",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.438}
|
|
||||||
style={{
|
|
||||||
stopColor: "#d4cba8",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.606}
|
|
||||||
style={{
|
|
||||||
stopColor: "#dbcba3",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={1}
|
|
||||||
style={{
|
|
||||||
stopColor: "#ddcba2",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
<path
|
|
||||||
d="M261.9 283.5c-.1 4.2 4.3 7.3 8.4 7.6s8.2-1.3 12.2-2.6c1.4-.4 2.9-.8 4.2-.2 1.8.9 2.7 4.1 1.8 5.9s-3.4 3.5-5.3 4.4c-6.5 3-12.9 3.6-19.9 2-5.3-1.2-11.3-4.3-13-13.5"
|
|
||||||
style={{
|
|
||||||
fill: "url(#SVGID_2_)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M261.9 283.5c-.1 4.2 4.3 7.3 8.4 7.6s8.2-1.3 12.2-2.6c1.4-.4 2.9-.8 4.2-.2 1.8.9 2.7 4.1 1.8 5.9s-3.4 3.5-5.3 4.4c-6.5 3-12.9 3.6-19.9 2-5.3-1.2-11.3-4.3-13-13.5"
|
|
||||||
className="st5"
|
|
||||||
/>
|
|
||||||
<path d="M318.4 198.4c-2-.3-4.1.1-5.9 1.3-3.2 2.1-4.7 6.2-4.7 9.9 0 1.9.4 3.8 1.4 5.3 1.2 1.7 3.1 2.9 5.2 3.4 3.4.8 8.2.7 10.5-2.5 1-1.5 1.4-3.3 1.5-5.1.5-5.7-1.8-11.4-8-12.3" />
|
|
||||||
<path
|
|
||||||
d="M320.4 203.3c.9.3 1.7.8 2.1 1.7.4.8.4 1.7.3 2.5-.1 1-.6 2-1.5 2.7-.7.5-1.7.7-2.6.5s-1.7-.8-2.2-1.6c-1.1-1.6-.9-4.4.9-5.5.9-.4 2-.6 3-.3"
|
|
||||||
style={{
|
|
||||||
fill: "#fff",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<linearGradient
|
|
||||||
id="SVGID_3_"
|
|
||||||
x1={-18.456}
|
|
||||||
x2={110.382}
|
|
||||||
y1={252.286}
|
|
||||||
y2={252.286}
|
|
||||||
gradientTransform="rotate(-5.297 -26.409 -856.73)scale(.99997)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop
|
|
||||||
offset={0}
|
|
||||||
style={{
|
|
||||||
stopColor: "#333",
|
|
||||||
stopOpacity: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.477}
|
|
||||||
style={{
|
|
||||||
stopColor: "#575756",
|
|
||||||
stopOpacity: 0.2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.995}
|
|
||||||
style={{
|
|
||||||
stopColor: "#333",
|
|
||||||
stopOpacity: 0.001911551,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={1}
|
|
||||||
style={{
|
|
||||||
stopColor: "#333",
|
|
||||||
stopOpacity: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
<path
|
|
||||||
d="M81.6 222.4s56.5 68.4 123.9 42.4l6.6-29.7C150 272.2 82.8 214.3 82.8 214.3z"
|
|
||||||
style={{
|
|
||||||
fill: "url(#SVGID_3_)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<linearGradient
|
|
||||||
id="SVGID_4_"
|
|
||||||
x1={110.198}
|
|
||||||
x2={283.737}
|
|
||||||
y1={262.493}
|
|
||||||
y2={262.493}
|
|
||||||
gradientTransform="matrix(.9991 .04164 -.04164 .9991 -36.323 1.051)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop
|
|
||||||
offset={0}
|
|
||||||
style={{
|
|
||||||
stopColor: "#333",
|
|
||||||
stopOpacity: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.477}
|
|
||||||
style={{
|
|
||||||
stopColor: "#575756",
|
|
||||||
stopOpacity: 0.2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.995}
|
|
||||||
style={{
|
|
||||||
stopColor: "#333",
|
|
||||||
stopOpacity: 0.001911551,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={1}
|
|
||||||
style={{
|
|
||||||
stopColor: "#333",
|
|
||||||
stopOpacity: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
<path
|
|
||||||
d="M64.2 234.9s63.1 101.5 157.8 78.9l14.2-38.5c-89.5 38.3-168.9-51-168.9-51z"
|
|
||||||
style={{
|
|
||||||
fill: "url(#SVGID_4_)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<linearGradient
|
|
||||||
id="SVGID_5_"
|
|
||||||
x1={218.967}
|
|
||||||
x2={428.745}
|
|
||||||
y1={263.594}
|
|
||||||
y2={263.594}
|
|
||||||
gradientTransform="rotate(9.265 -38.832 -826.002)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop
|
|
||||||
offset={0}
|
|
||||||
style={{
|
|
||||||
stopColor: "#333",
|
|
||||||
stopOpacity: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.477}
|
|
||||||
style={{
|
|
||||||
stopColor: "#575756",
|
|
||||||
stopOpacity: 0.2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={0.995}
|
|
||||||
style={{
|
|
||||||
stopColor: "#333",
|
|
||||||
stopOpacity: 0.001911551,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset={1}
|
|
||||||
style={{
|
|
||||||
stopColor: "#333",
|
|
||||||
stopOpacity: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
<path
|
|
||||||
d="M46.6 251.5s61.1 130.9 178 117.5l22.6-44.2c-113 33-195.3-85.7-195.3-85.7z"
|
|
||||||
style={{
|
|
||||||
fill: "url(#SVGID_5_)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</switch>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WebhookIcon(props: SVGProps<SVGSVGElement>) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width={2500}
|
|
||||||
height={2448}
|
|
||||||
fillRule="evenodd"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeMiterlimit={2}
|
|
||||||
clipRule="evenodd"
|
|
||||||
viewBox="-6.74 -6.74 223.55 218.799"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="#7547db"
|
|
||||||
d="M111.134 71.906a21 21 0 0 1-1.324.042c-11.314 0-20.5-9.186-20.5-20.5s9.186-20.5 20.5-20.5 20.5 9.186 20.5 20.5a20.4 20.4 0 0 1-4.582 12.913l22.66 41.655a51 51 0 0 1 17.422-3.068c28.148 0 51 22.852 51 51s-22.852 51-51 51a51.2 51.2 0 0 1-28.036-8.373 7.303 7.303 0 0 1-1.359-11.105c.015-.016.031-.033.061-.036a7.665 7.665 0 0 1 9.736-1.228 36.06 36.06 0 0 0 19.598 5.792c19.897 0 36.05-16.153 36.05-36.05s-16.153-36.05-36.05-36.05c-6.89 0-16.669 3.014-22.548 6.745a2.637 2.637 0 0 1-3.899-.845zm-9.412 88.614a51 51 0 0 1-6.054 16.62c-14.074 24.378-45.29 32.743-69.667 18.669S-6.74 150.518 7.333 126.14a51.2 51.2 0 0 1 21.27-20.094 7.3 7.3 0 0 1 6.167-.211 7.3 7.3 0 0 1 4.13 4.587c.006.022.012.043 0 .07a7.665 7.665 0 0 1-3.804 9.047 36.05 36.05 0 0 0-14.816 14.076c-9.948 17.23-4.036 39.297 13.195 49.246 17.231 9.948 39.298 4.035 49.246-13.196 3.445-5.967 5.724-15.943 5.433-22.9a2.632 2.632 0 0 1 2.681-2.953l59.054 1.499q.292-.59.626-1.168c5.657-9.798 18.205-13.161 28.003-7.504 9.8 5.658 13.161 18.206 7.504 28.004s-18.205 13.16-28.004 7.503a20.4 20.4 0 0 1-8.892-10.424zM76.97 90.555A51 51 0 0 1 65.601 77c-14.074-24.377-5.71-55.594 18.667-69.667S139.862 1.624 153.935 26a51.2 51.2 0 0 1 6.768 28.466 7.306 7.306 0 0 1-8.938 6.73c-.022-.005-.044-.011-.062-.035a7.664 7.664 0 0 1-5.932-7.817 36.04 36.04 0 0 0-4.783-19.87c-9.948-17.23-32.014-23.143-49.245-13.195S68.599 52.294 78.548 69.525c3.445 5.967 10.945 12.929 17.115 16.155a2.635 2.635 0 0 1 1.218 3.799l-30.826 50.393q.366.548.7 1.125c5.656 9.799 2.294 22.347-7.505 28.004s-22.346 2.295-28.003-7.504-2.295-22.346 7.503-28.003a20.4 20.4 0 0 1 13.474-2.488z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import {ColumnDef} from "@tanstack/react-table";
|
import {ColumnDef} from "@tanstack/react-table";
|
||||||
import {NotificationLogWithRelations} from "@/db/services/notification-log";
|
import {NotificationLogWithRelations} from "@/db/services/notification-log";
|
||||||
import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers";
|
|
||||||
import {humanReadableDate} from "@/utils/date-formatting";
|
import {humanReadableDate} from "@/utils/date-formatting";
|
||||||
import {CheckCircle2, XCircle} from "lucide-react";
|
import {CheckCircle2, XCircle} from "lucide-react";
|
||||||
import {Badge} from "@/components/ui/badge";
|
import {Badge} from "@/components/ui/badge";
|
||||||
import {NotificationLogModal} from "@/components/wrappers/dashboard/admin/notifications/logs/notification-log-modal";
|
import {NotificationLogModal} from "@/components/wrappers/dashboard/admin/notifications/logs/notification-log-modal";
|
||||||
|
import {getChannelIcon} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||||
|
|
||||||
|
|
||||||
export function notificationLogsColumns(): ColumnDef<NotificationLogWithRelations>[] {
|
export function notificationLogsColumns(): ColumnDef<NotificationLogWithRelations>[] {
|
||||||
@@ -33,7 +33,7 @@ export function notificationLogsColumns(): ColumnDef<NotificationLogWithRelation
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<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">
|
||||||
{getNotificationChannelIcon(channel?.provider ?? "")}
|
{getChannelIcon(channel?.provider ?? "")}
|
||||||
</div>
|
</div>
|
||||||
{channel?.name}
|
{channel?.name}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
-72
@@ -1,72 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import {useMutation} from "@tanstack/react-query";
|
|
||||||
import {useRouter} from "next/navigation";
|
|
||||||
import {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal";
|
|
||||||
import {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
|
||||||
import {Switch} from "@/components/ui/switch";
|
|
||||||
import {
|
|
||||||
updateNotificationChannelAction
|
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.action";
|
|
||||||
import {toast} from "sonner";
|
|
||||||
import {useState} from "react";
|
|
||||||
import {Organization, OrganizationWithMembers} from "@/db/schema/03_organization";
|
|
||||||
|
|
||||||
export type EditNotifierButtonProps = {
|
|
||||||
notificationChannel: NotificationChannelWith;
|
|
||||||
organization?: OrganizationWithMembers;
|
|
||||||
organizations?: OrganizationWithMembers[];
|
|
||||||
adminView?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EditNotifierButton = ({
|
|
||||||
organizations,
|
|
||||||
adminView = false,
|
|
||||||
notificationChannel,
|
|
||||||
organization
|
|
||||||
}: EditNotifierButtonProps) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
|
||||||
|
|
||||||
|
|
||||||
const mutation = useMutation({
|
|
||||||
mutationFn: async (value: boolean) => {
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
data: {
|
|
||||||
name: notificationChannel.name,
|
|
||||||
provider: notificationChannel.provider,
|
|
||||||
config: notificationChannel.config as Record<string, any>,
|
|
||||||
enabled: value
|
|
||||||
},
|
|
||||||
notificationChannelId: notificationChannel.id
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await updateNotificationChannelAction(payload);
|
|
||||||
const inner = result?.data;
|
|
||||||
|
|
||||||
if (inner?.success) {
|
|
||||||
toast.success(inner.actionSuccess?.message);
|
|
||||||
router.refresh();
|
|
||||||
} else {
|
|
||||||
toast.error(inner?.actionError?.message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Switch checked={notificationChannel.enabled} onCheckedChange={async () => {
|
|
||||||
await mutation.mutateAsync(!notificationChannel.enabled)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<NotifierAddEditModal
|
|
||||||
organizations={organizations}
|
|
||||||
adminView={adminView}
|
|
||||||
organization={organization}
|
|
||||||
notificationChannel={notificationChannel}
|
|
||||||
open={isAddModalOpen}
|
|
||||||
onOpenChangeAction={setIsAddModalOpen}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
import {z} from "zod";
|
|
||||||
|
|
||||||
|
|
||||||
export const NotificationChannelFormSchema = z.object({
|
|
||||||
name: z
|
|
||||||
.string()
|
|
||||||
.min(5, "Name must be at least 5 characters long")
|
|
||||||
.max(40, "Name must be at most 40 characters long"),
|
|
||||||
|
|
||||||
provider: z.enum(["slack", "smtp", "discord", "telegram", "gotify", "ntfy", "webhook"], {
|
|
||||||
required_error: "Provider is required",
|
|
||||||
}),
|
|
||||||
|
|
||||||
config: z.record(z.union([z.string(), z.number(), z.boolean(), z.null(), z.undefined()])).optional(),
|
|
||||||
|
|
||||||
|
|
||||||
enabled: z.boolean().default(true),
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export type NotificationChannelFormType = z.infer<typeof NotificationChannelFormSchema>;
|
|
||||||
-61
@@ -1,61 +0,0 @@
|
|||||||
"use client"
|
|
||||||
import {Button} from "@/components/ui/button";
|
|
||||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
|
||||||
import {useMutation} from "@tanstack/react-query";
|
|
||||||
import {dispatchNotification} from "@/features/notifications/dispatch";
|
|
||||||
import {EventPayload} from "@/features/notifications/types";
|
|
||||||
import {Send} from "lucide-react";
|
|
||||||
import {toast} from "sonner";
|
|
||||||
import {useIsMobile} from "@/hooks/use-mobile";
|
|
||||||
import {cn} from "@/lib/utils";
|
|
||||||
|
|
||||||
type NotifierTestChannelButtonProps = {
|
|
||||||
notificationChannel: NotificationChannel;
|
|
||||||
organizationId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NotifierTestChannelButton = ({notificationChannel, organizationId}: NotifierTestChannelButtonProps) => {
|
|
||||||
|
|
||||||
const isMobile = useIsMobile()
|
|
||||||
const mutation = useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
|
|
||||||
const payload: EventPayload = {
|
|
||||||
title: 'Test Channel',
|
|
||||||
message: `We are testing channel ${notificationChannel.name}`,
|
|
||||||
level: 'info',
|
|
||||||
// data: {host: 'db-prod-01', error: 'connection timeout'},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await dispatchNotification(payload, undefined, notificationChannel.id, organizationId);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
toast.success(result.message);
|
|
||||||
} else {
|
|
||||||
toast.error("An error occurred while testing the notification channel");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="default"
|
|
||||||
onClick={() => mutation.mutateAsync()}
|
|
||||||
disabled={mutation.isPending}
|
|
||||||
className="bg-green-600 hover:bg-green-700 text-white font-medium shadow-sm transition-all"
|
|
||||||
>
|
|
||||||
{mutation.isPending ? (
|
|
||||||
<>
|
|
||||||
<div className={cn(" h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white", !isMobile && "mr-2")}/>
|
|
||||||
{!isMobile && `Sending...`}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-row justify-center items-center">
|
|
||||||
<Send className={cn("h-4 w-4", !isMobile && "mr-2")}/>{!isMobile && ` Test Channel`}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
Layers,
|
Layers,
|
||||||
ChartArea,
|
ChartArea,
|
||||||
ShieldHalf,
|
ShieldHalf,
|
||||||
Building, UserRoundCog, Mail, PackageOpen, Logs, Megaphone, Blocks
|
Building, UserRoundCog, Mail, PackageOpen, Logs, Megaphone, Blocks, Warehouse
|
||||||
} 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";
|
||||||
@@ -80,6 +80,16 @@ export const SidebarMenuCustomMain = () => {
|
|||||||
{ title: "Activity Logs", url: "/notifications/logs", icon: Logs, type: "item" },
|
{ title: "Activity Logs", url: "/notifications/logs", icon: Logs, type: "item" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Storages",
|
||||||
|
url: "/storages",
|
||||||
|
icon: Warehouse,
|
||||||
|
details: true,
|
||||||
|
type: "collapse",
|
||||||
|
submenu: [
|
||||||
|
{ title: "Channels", url: "/storages/channels", icon: Blocks, type: "item" },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Access management",
|
title: "Access management",
|
||||||
url: "/admin",
|
url: "/admin",
|
||||||
|
|||||||
+12
-5
@@ -1,11 +1,11 @@
|
|||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import {NotifierCard} from "@/components/wrappers/dashboard/common/notifier/notifier-card/notifier-card";
|
|
||||||
import {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal";
|
|
||||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {cn} from "@/lib/utils";
|
import {cn} from "@/lib/utils";
|
||||||
|
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
|
||||||
|
import {ChannelCard} from "@/components/wrappers/dashboard/admin/channels/channel/channel-card/channel-card";
|
||||||
|
|
||||||
export type OrganizationNotifiersTabProps = {
|
export type OrganizationNotifiersTabProps = {
|
||||||
organization: OrganizationWithMembers;
|
organization: OrganizationWithMembers;
|
||||||
@@ -19,7 +19,7 @@ export const OrganizationNotifiersTab = ({
|
|||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
|
|
||||||
const hasNotifiers = notificationChannels.length > 0;
|
const hasNotifiers = notificationChannels.length > 0;
|
||||||
|
const kind="notification"
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-6 h-full py-4">
|
<div className="flex flex-col gap-y-6 h-full py-4">
|
||||||
<div className="h-full flex flex-col gap-y-6">
|
<div className="h-full flex flex-col gap-y-6">
|
||||||
@@ -29,7 +29,13 @@ export const OrganizationNotifiersTab = ({
|
|||||||
Notification Settings
|
Notification Settings
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<NotifierAddEditModal
|
{/*<NotifierAddEditModal*/}
|
||||||
|
{/* organization={organization}*/}
|
||||||
|
{/* open={isAddModalOpen}*/}
|
||||||
|
{/* onOpenChangeAction={setIsAddModalOpen}*/}
|
||||||
|
{/*/>*/}
|
||||||
|
<ChannelAddEditModal
|
||||||
|
kind={kind}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
open={isAddModalOpen}
|
open={isAddModalOpen}
|
||||||
onOpenChangeAction={setIsAddModalOpen}
|
onOpenChangeAction={setIsAddModalOpen}
|
||||||
@@ -39,10 +45,11 @@ export const OrganizationNotifiersTab = ({
|
|||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<CardsWithPagination
|
<CardsWithPagination
|
||||||
data={notificationChannels}
|
data={notificationChannels}
|
||||||
cardItem={NotifierCard}
|
cardItem={ChannelCard}
|
||||||
cardsPerPage={8}
|
cardsPerPage={8}
|
||||||
numberOfColumns={2}
|
numberOfColumns={2}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
|
kind={kind}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
+3
-1
@@ -12,6 +12,7 @@ import * as notificationChannel from "./schema/09_notification-channel";
|
|||||||
import * as organizationNotificationChannel from "./schema/09_notification-channel";
|
import * as organizationNotificationChannel from "./schema/09_notification-channel";
|
||||||
import * as alertPolicy from "./schema/10_alert-policy";
|
import * as alertPolicy from "./schema/10_alert-policy";
|
||||||
import * as notificationLog from "./schema/11_notification-log";
|
import * as notificationLog from "./schema/11_notification-log";
|
||||||
|
import * as storageChannel from "./schema/12_storage-channel";
|
||||||
|
|
||||||
|
|
||||||
import {Pool} from "pg";
|
import {Pool} from "pg";
|
||||||
@@ -40,7 +41,8 @@ export const schemas = {
|
|||||||
...notificationChannel,
|
...notificationChannel,
|
||||||
...organizationNotificationChannel,
|
...organizationNotificationChannel,
|
||||||
...alertPolicy,
|
...alertPolicy,
|
||||||
...notificationLog
|
...notificationLog,
|
||||||
|
...storageChannel
|
||||||
};
|
};
|
||||||
|
|
||||||
export const db = drizzle({
|
export const db = drizzle({
|
||||||
|
|||||||
@@ -141,6 +141,13 @@
|
|||||||
"when": 1767782077775,
|
"when": 1767782077775,
|
||||||
"tag": "0019_overjoyed_butterfly",
|
"tag": "0019_overjoyed_butterfly",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 20,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1768248015695,
|
||||||
|
"tag": "0020_thankful_sunspot",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import {member, OrganizationMember} from "@/db/schema/04_member";
|
|||||||
import {User} from "@/db/schema/02_user";
|
import {User} from "@/db/schema/02_user";
|
||||||
import {timestamps} from "@/db/schema/00_common";
|
import {timestamps} from "@/db/schema/00_common";
|
||||||
import {NotificationChannel, organizationNotificationChannel} from "@/db/schema/09_notification-channel";
|
import {NotificationChannel, organizationNotificationChannel} from "@/db/schema/09_notification-channel";
|
||||||
|
import {organizationStorageChannel, StorageChannel} from "@/db/schema/12_storage-channel";
|
||||||
|
|
||||||
export const organization = pgTable("organization", {
|
export const organization = pgTable("organization", {
|
||||||
id: uuid("id").defaultRandom().primaryKey(),
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
@@ -24,6 +25,7 @@ export const organizationRelations = relations(organization, ({many}) => ({
|
|||||||
invitations: many(invitation),
|
invitations: many(invitation),
|
||||||
projects: many(project),
|
projects: many(project),
|
||||||
notificationChannels: many(organizationNotificationChannel),
|
notificationChannels: many(organizationNotificationChannel),
|
||||||
|
storageChannels: many(organizationStorageChannel),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
@@ -48,5 +50,6 @@ export type OrganizationWith = Organization & {
|
|||||||
members?: MemberWithUser[] | null;
|
members?: MemberWithUser[] | null;
|
||||||
invitations?: OrganizationInvitation[] | null;
|
invitations?: OrganizationInvitation[] | null;
|
||||||
notificationChannels?: NotificationChannel[] | null;
|
notificationChannels?: NotificationChannel[] | null;
|
||||||
|
storageChannels?: StorageChannel[] | null;
|
||||||
projects?: Project[] | null;
|
projects?: Project[] | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import {boolean, jsonb, pgEnum, pgTable, unique, uuid, varchar} from "drizzle-orm/pg-core";
|
||||||
|
import {timestamps} from "@/db/schema/00_common";
|
||||||
|
import {organization} from "@/db/schema/03_organization";
|
||||||
|
import {relations} from "drizzle-orm";
|
||||||
|
import {createSelectSchema} from "drizzle-zod";
|
||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
|
||||||
|
export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3']);
|
||||||
|
|
||||||
|
export const storageChannel = pgTable('storage_channel', {
|
||||||
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
|
provider: providerStorageKindEnum('provider').notNull(),
|
||||||
|
name: varchar('name', {length: 255}).notNull(),
|
||||||
|
config: jsonb('config').notNull(),
|
||||||
|
enabled: boolean('enabled').default(false).notNull(),
|
||||||
|
...timestamps
|
||||||
|
});
|
||||||
|
|
||||||
|
export const organizationStorageChannel = pgTable(
|
||||||
|
"organization_storage_channels",
|
||||||
|
{
|
||||||
|
organizationId: uuid('organization_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => organization.id, {onDelete: 'cascade'}),
|
||||||
|
storageChannelId: uuid('storage_channel_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => storageChannel.id, {onDelete: 'cascade'}),
|
||||||
|
},
|
||||||
|
(t) => [unique().on(t.organizationId, t.storageChannelId)]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export const storageChannelRelations = relations(storageChannel, ({many}) => ({
|
||||||
|
organizations: many(organizationStorageChannel),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const organizationStorageChannelRelations = relations(organizationStorageChannel, ({one}) => ({
|
||||||
|
organization: one(organization, {
|
||||||
|
fields: [organizationStorageChannel.organizationId],
|
||||||
|
references: [organization.id],
|
||||||
|
}),
|
||||||
|
storageChannel: one(storageChannel, {
|
||||||
|
fields: [organizationStorageChannel.storageChannelId],
|
||||||
|
references: [storageChannel.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const storageChannelSchema = createSelectSchema(storageChannel);
|
||||||
|
export type StorageChannel = z.infer<typeof storageChannelSchema>;
|
||||||
|
|
||||||
|
|
||||||
|
export type StorageChannelWith = StorageChannel & {
|
||||||
|
organizations: {
|
||||||
|
organizationId: string;
|
||||||
|
storageChannelId: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
"use server";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
|
import {dispatchViaProvider} from "./providers";
|
||||||
|
import type {EventPayload, DispatchResult, EventKind} from "./types";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {notificationLog} from "@/db/schema/11_notification-log";
|
||||||
|
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||||
|
import {Json} from "drizzle-zod";
|
||||||
|
|
||||||
|
export async function dispatchStorage(
|
||||||
|
payload: EventPayload,
|
||||||
|
policyId?: string,
|
||||||
|
channelId?: string,
|
||||||
|
organizationId?: string
|
||||||
|
): Promise<DispatchResult> {
|
||||||
|
try {
|
||||||
|
let channel: NotificationChannel | null = null;
|
||||||
|
|
||||||
|
if (policyId) {
|
||||||
|
const policyDb = await db.query.alertPolicy.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.alertPolicy.id, policyId),
|
||||||
|
with: {
|
||||||
|
notificationChannel: true
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!policyDb || !policyDb.notificationChannel) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
channelId: "",
|
||||||
|
provider: null,
|
||||||
|
error: "Policy or associated channel not found",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!policyDb.enabled || !policyDb.notificationChannel.enabled) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
channelId: policyDb.notificationChannel.id,
|
||||||
|
provider: policyDb.notificationChannel.provider as any,
|
||||||
|
error: "Policy or channel is disabled",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
channel = {
|
||||||
|
...policyDb.notificationChannel,
|
||||||
|
config: policyDb.notificationChannel.config as Json,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelId) {
|
||||||
|
const fetchedChannel = await db.query.notificationChannel.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.notificationChannel.id, channelId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fetchedChannel) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
channelId: channelId,
|
||||||
|
provider: null,
|
||||||
|
error: "Channel not found",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
channel = {
|
||||||
|
...fetchedChannel,
|
||||||
|
config: fetchedChannel.config as Json,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
channelId: channelId || "",
|
||||||
|
provider: null,
|
||||||
|
error: "No valid channel to dispatch notification",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!channel.enabled) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
channelId: channelId || "",
|
||||||
|
provider: null,
|
||||||
|
error: "Channel not active",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await dispatchViaProvider(
|
||||||
|
channel.provider,
|
||||||
|
channel.config,
|
||||||
|
{...payload, timestamp: payload.timestamp || new Date()},
|
||||||
|
channel.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const [log] = await db
|
||||||
|
.insert(notificationLog)
|
||||||
|
.values({
|
||||||
|
channelId: channel.id,
|
||||||
|
policyId: policyId || null,
|
||||||
|
organizationId: organizationId || null,
|
||||||
|
|
||||||
|
provider: channel.provider,
|
||||||
|
providerName: channel.name,
|
||||||
|
event: payload.event as EventKind,
|
||||||
|
|
||||||
|
title: payload.title,
|
||||||
|
message: payload.message,
|
||||||
|
level: payload.level,
|
||||||
|
payload: payload.data || null,
|
||||||
|
success: result.success,
|
||||||
|
error: result.success ? null : result.error,
|
||||||
|
providerResponse: result.response || null,
|
||||||
|
})
|
||||||
|
.returning({id: notificationLog.id});
|
||||||
|
|
||||||
|
return {...result, channelId: channel.id};
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
channelId: channelId || "",
|
||||||
|
provider: null,
|
||||||
|
error: err?.message || "Unexpected error during dispatch",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user