mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
104
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9bfae9b9d | ||
|
|
2fca928cf0 | ||
|
|
8f3a362a3a | ||
|
|
62a6189788 | ||
|
|
2a6d631719 | ||
|
|
4942591d7f | ||
|
|
1760c1b852 | ||
|
|
0e7ebb79db | ||
|
|
727d9eb57c | ||
|
|
228ff75468 | ||
|
|
80018cac08 | ||
|
|
b2c19e7ed0 | ||
|
|
a64a80910d | ||
|
|
4eeeef3427 | ||
|
|
657586ddff | ||
|
|
797b287899 | ||
|
|
db2d95f2c5 | ||
|
|
16a8f259ab | ||
|
|
ee1cd8d08d | ||
|
|
e53046324c | ||
|
|
dd71977317 | ||
|
|
bee0da1454 | ||
|
|
b60feaa447 | ||
|
|
743e54614a | ||
|
|
bd96713af4 | ||
|
|
8f7f9b457a | ||
|
|
4c0d68eab8 | ||
|
|
8211407cf2 | ||
|
|
5fb6c7d36a | ||
|
|
30b1cf4493 | ||
|
|
f4789ac5fe | ||
|
|
5fd826ae71 | ||
|
|
22ce08f06c | ||
|
|
696c88f03f | ||
|
|
9d5d87e47b | ||
|
|
e32539e02c | ||
|
|
46ba931a6c | ||
|
|
4bac5143d6 | ||
|
|
bcb1674491 | ||
|
|
3cbd9677c2 | ||
|
|
b216ee1a80 | ||
|
|
32ec482449 | ||
|
|
5074748055 | ||
|
|
6fa11f834b | ||
|
|
25c7702127 | ||
|
|
b546556d08 | ||
|
|
379a85f6f6 | ||
|
|
92d2c14d6a | ||
|
|
a6c1f7d3f2 | ||
|
|
6fa6187cc4 | ||
|
|
848e7a1609 | ||
|
|
3e8c0a2896 | ||
|
|
11900b1500 | ||
|
|
85044fe3e0 | ||
|
|
c0499d40ea | ||
|
|
8ee44ffea1 | ||
|
|
5668a1411f | ||
|
|
6db60f111d | ||
|
|
3d695709d5 | ||
|
|
5c682addc9 | ||
|
|
24810cc818 | ||
|
|
8caeb11da5 | ||
|
|
5e07b446fc | ||
|
|
f29444a219 | ||
|
|
bce1c5d9b8 | ||
|
|
5b980fb382 | ||
|
|
e63e62d3e0 | ||
|
|
53f82792d8 | ||
|
|
760388307c | ||
|
|
eebd8500ee | ||
|
|
12112afa37 | ||
|
|
424fa581b9 | ||
|
|
cddb0a7e05 | ||
|
|
83ef0ee74e | ||
|
|
21b317e4f1 | ||
|
|
7d818206b0 | ||
|
|
0216c40bfe | ||
|
|
ecf80280f1 | ||
|
|
6e9568b4fb | ||
|
|
8d968e7d63 | ||
|
|
bb264f0b9b | ||
|
|
be4f770c35 | ||
|
|
17d955dd69 | ||
|
|
3f31e85c06 | ||
|
|
5a0c230ead | ||
|
|
e10350268d | ||
|
|
969a48bef2 | ||
|
|
8158934cb9 | ||
|
|
670f6a1974 | ||
|
|
dd1f791b3e | ||
|
|
91f25bc25d | ||
|
|
d8e9e2dac3 | ||
|
|
67a89a12ac | ||
|
|
faaa4033bd | ||
|
|
f3eedf6f30 | ||
|
|
29718d2a0c | ||
|
|
c7e6bd8b72 | ||
|
|
a67ff12786 | ||
|
|
c92e671622 | ||
|
|
2bd25353da | ||
|
|
e1bee19c18 | ||
|
|
0ec49af72a | ||
|
|
6d4eb836ab | ||
|
|
da5c65d5a6 |
+8
-15
@@ -1,37 +1,30 @@
|
||||
# Environnement
|
||||
NODE_ENV=production
|
||||
NODE_ENV=development
|
||||
|
||||
# Base de données
|
||||
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
||||
DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public
|
||||
|
||||
# Projet
|
||||
PROJECT_NAME="Portabase"
|
||||
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
||||
PROJECT_URL=http://app.portabase.io
|
||||
PROJECT_URL=http://localhost:8887
|
||||
PROJECT_SECRET=
|
||||
|
||||
# SMTP (email)
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_PORT=
|
||||
SMTP_USER=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM=
|
||||
#SMTP_SECURE="true"
|
||||
#Port 587 -> secure: false
|
||||
#Port 465 -> secure: true
|
||||
#By default : true
|
||||
|
||||
# Google
|
||||
AUTH_GOOGLE_ID=
|
||||
AUTH_GOOGLE_SECRET=
|
||||
AUTH_GOOGLE_METHOD=
|
||||
|
||||
# S3
|
||||
S3_ENDPOINT=http://app.s3.portabase.io
|
||||
S3_ACCESS_KEY=
|
||||
S3_SECRET_KEY=
|
||||
S3_BUCKET_NAME=portabase
|
||||
S3_PORT=9000
|
||||
S3_USE_SSL=true
|
||||
|
||||
# Storage Type (s3, local)
|
||||
STORAGE_TYPE=local
|
||||
|
||||
# Retention
|
||||
RETENTION_CRON="* * * * *"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: portabase
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -7,6 +7,10 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: 'solucetechnologies/portabase'
|
||||
image_name2:
|
||||
required: false
|
||||
type: string
|
||||
default: 'portabase/portabase'
|
||||
add_latest:
|
||||
required: false
|
||||
type: boolean
|
||||
@@ -24,6 +28,10 @@ on:
|
||||
required: true
|
||||
DOCKER_PASSWORD:
|
||||
required: true
|
||||
DOCKER_USERNAME_2:
|
||||
required: true
|
||||
DOCKER_PASSWORD_2:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
@@ -37,27 +45,53 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set tags
|
||||
id: set-tags
|
||||
run: |
|
||||
REF_NAME=${GITHUB_REF#refs/tags/}
|
||||
TAGS="${{ inputs.image_name }}:$REF_NAME"
|
||||
TAGS2="${{ inputs.image_name2 }}:$REF_NAME"
|
||||
if [[ "${{ inputs.add_latest }}" == "true" ]]; then
|
||||
TAGS="$TAGS,${{ inputs.image_name }}:latest"
|
||||
TAGS2="$TAGS2,${{ inputs.image_name2 }}:latest"
|
||||
fi
|
||||
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
||||
echo "tags2=$TAGS2" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push Docker image
|
||||
- name: Set up QEMU (enables multi-arch emulation)
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub account 1
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push to solucetechnologies/portabase
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.dockerfile }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.set-tags.outputs.tags }}
|
||||
target: ${{ inputs.target }}
|
||||
|
||||
- name: Log in to Docker Hub account 2
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME_2 }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD_2 }}
|
||||
|
||||
- name: Build and push to portabase/portabase
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.dockerfile }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.set-tags.outputs.tags2 }}
|
||||
target: ${{ inputs.target }}
|
||||
|
||||
@@ -17,6 +17,8 @@ jobs:
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME_2: ${{ secrets.DOCKER_USERNAME_2 }}
|
||||
DOCKER_PASSWORD_2: ${{ secrets.DOCKER_PASSWORD_2 }}
|
||||
|
||||
github_release:
|
||||
needs: docker_publish
|
||||
|
||||
@@ -18,6 +18,8 @@ jobs:
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME_2: ${{ secrets.DOCKER_USERNAME_2 }}
|
||||
DOCKER_PASSWORD_2: ${{ secrets.DOCKER_PASSWORD_2 }}
|
||||
|
||||
github_release:
|
||||
needs: docker_publish
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
.idea
|
||||
.vscode
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
@@ -41,3 +42,5 @@ public/uploads/*
|
||||
private/uploads/*
|
||||
|
||||
/.env
|
||||
|
||||
certificates
|
||||
Generated
-8
@@ -1,8 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
Generated
-8
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/portabase.iml" filepath="$PROJECT_DIR$/.idea/portabase.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
-12
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/backend" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"WillLuke.nextjs.hasPrompted": true
|
||||
}
|
||||
+2
-2
@@ -26,5 +26,5 @@ keywords:
|
||||
- web-ui
|
||||
- agent
|
||||
license: Apache-2.0
|
||||
version: 1.1.12-rc.1
|
||||
date-released: "2026-01-11"
|
||||
version: 1.2.4-rc.5
|
||||
date-released: "2026-01-28"
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
[](LICENSE)
|
||||
[](https://hub.docker.com/r/solucetechnologies/portabase)
|
||||
[](https://github.com/Portabase/portabase)
|
||||
[](https://www.buymeacoffee.com/portabase)
|
||||
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://www.mysql.com/)
|
||||
[](https://mariadb.org/)
|
||||
[](https://www.mongodb.com/)
|
||||
[](https://github.com/Portabase/portabase)
|
||||
[](https://github.com/Portabase/portabase)
|
||||
|
||||
@@ -61,6 +63,7 @@ You have 4 ways to install Portabase:
|
||||
|
||||
[](https://github.com/Portabase/portabase/graphs/contributors)
|
||||
|
||||
[](https://www.buymeacoffee.com/portabase)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import {CardAuth} from "@/features/layout/card-auth";
|
||||
export default async function GuardPage() {
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get("better-auth.two_factor")?.value;
|
||||
|
||||
const token = cookieStore.get("better-auth.two_factor")?.value || cookieStore.get("__Secure-better-auth.two_factor")?.value;
|
||||
|
||||
if (!token) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {db} from "@/db";
|
||||
import {notFound} from "next/navigation";
|
||||
import {SettingsTabs} from "@/components/wrappers/dashboard/admin/settings/settings-tabs";
|
||||
import {desc, isNull} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
const settings = await db.query.setting.findFirst({
|
||||
where: (fields, {eq}) => eq(fields.name, "system"),
|
||||
});
|
||||
|
||||
const storageChannels = await db.query.storageChannel.findMany({
|
||||
with: {
|
||||
organizations: true
|
||||
},
|
||||
where: isNull(drizzleDb.schemas.storageChannel.organizationId),
|
||||
orderBy: desc(drizzleDb.schemas.storageChannel.createdAt)
|
||||
}) as StorageChannelWith[]
|
||||
|
||||
if (!settings || !storageChannels ) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader className="flex flex-col">
|
||||
<div className="flex justify-between">
|
||||
<PageTitle className="mb-3">System settings</PageTitle>
|
||||
</div>
|
||||
</PageHeader>
|
||||
<PageContent className="flex flex-col gap-5">
|
||||
<SettingsTabs storageChannels={storageChannels} settings={settings} />
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { PageParams } from "@/types/next";
|
||||
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
||||
import { AgentForm } from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
|
||||
import { notFound } from "next/navigation";
|
||||
import { db } from "@/db";
|
||||
|
||||
export default async function RoutePage(
|
||||
props: PageParams<{
|
||||
agentId: string;
|
||||
}>
|
||||
) {
|
||||
const { agentId } = await props.params;
|
||||
|
||||
const agent = await db.query.agent.findFirst({
|
||||
where: (fields, { eq }) => eq(fields.id, agentId),
|
||||
});
|
||||
|
||||
if (!agent) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>Edit {agent.name}</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<AgentForm defaultValues={agent} agentId={agent.id} />
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import {generateEdgeKey} from "@/utils/edge_key";
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
import {AgentContentPage} from "@/components/wrappers/dashboard/agent/agent-content";
|
||||
import {AgentDialog} from "@/features/agents/components/agent.dialog";
|
||||
import {AgentType} from "@/features/agents/agents.schema";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{ agentId: string }>) {
|
||||
|
||||
@@ -39,10 +41,11 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
||||
</div>
|
||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link className={buttonVariants({variant: "outline"})}
|
||||
href={`/dashboard/agents/${agent.id}/edit`}>
|
||||
<GearIcon className="w-7 h-7"/>
|
||||
</Link>
|
||||
<AgentDialog agent={agent as AgentType & { id: string }}>
|
||||
<div className={buttonVariants({variant: "outline", className: "cursor-pointer"})}>
|
||||
<GearIcon className="w-7 h-7"/>
|
||||
</div>
|
||||
</AgentDialog>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {AgentForm} from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
|
||||
import {Metadata} from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Agent",
|
||||
};
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>Create new agent</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<AgentForm/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import {PageParams} from "@/types/next";
|
||||
import {AgentCard} from "@/components/wrappers/dashboard/agent/agent-card/agent-card";
|
||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {notFound} from "next/navigation";
|
||||
import {db} from "@/db";
|
||||
@@ -10,6 +9,7 @@ import * as drizzleDb from "@/db";
|
||||
import {desc, eq, not} from "drizzle-orm";
|
||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||
import {Metadata} from "next";
|
||||
import {AgentDialog} from "@/features/agents/components/agent.dialog";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Agents",
|
||||
@@ -36,9 +36,9 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
<PageTitle>Agents</PageTitle>
|
||||
{agents.length > 0 && (
|
||||
<PageActions>
|
||||
<Link href={"/dashboard/agents/new"}>
|
||||
<AgentDialog>
|
||||
<Button>+ Create Agent</Button>
|
||||
</Link>
|
||||
</AgentDialog>
|
||||
</PageActions>
|
||||
)}
|
||||
</PageHeader>
|
||||
@@ -46,12 +46,13 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
{agents.length > 0 ? (
|
||||
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1}/>
|
||||
) : (
|
||||
<EmptyStatePlaceholder
|
||||
url={"/dashboard/agents/new"}
|
||||
text={"Create new Agent"}
|
||||
/>
|
||||
<AgentDialog>
|
||||
<EmptyStatePlaceholder
|
||||
text={"Create new Agent"}
|
||||
/>
|
||||
</AgentDialog>
|
||||
)}
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {Metadata} from "next";
|
||||
import {
|
||||
NotificationChannelsSection
|
||||
} from "@/components/wrappers/dashboard/admin/notifications/channels/notification-channels-section";
|
||||
import {db} from "@/db";
|
||||
import {notificationChannel, NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal";
|
||||
import {notificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
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";
|
||||
import * as drizzleDb from "@/db";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Notification Channels",
|
||||
@@ -19,6 +18,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
with: {
|
||||
organizations: true
|
||||
},
|
||||
where: isNull(drizzleDb.schemas.notificationChannel.organizationId),
|
||||
orderBy: desc(notificationChannel.createdAt)
|
||||
}) as NotificationChannelWith[]
|
||||
|
||||
@@ -35,11 +35,14 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
<PageHeader>
|
||||
<PageTitle>Notification channels</PageTitle>
|
||||
<PageActions>
|
||||
<NotifierAddEditModal adminView={false}/>
|
||||
{/*<NotifierAddEditModal adminView={false}/>*/}
|
||||
<ChannelAddEditModal kind="notification" adminView={false}/>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<NotificationChannelsSection organizations={organizations} notificationChannels={notificationChannels}/>
|
||||
{/*<NotificationChannelsSection organizations={organizations} notificationChannels={notificationChannels}/>*/}
|
||||
<ChannelsSection kind="notification" organizations={organizations}
|
||||
channels={notificationChannels}/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {Metadata} from "next";
|
||||
import {ChannelsSection} from "@/components/wrappers/dashboard/admin/channels/channels-section";
|
||||
import {db} from "@/db";
|
||||
import {desc, eq, 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
|
||||
},
|
||||
where: isNull(drizzleDb.schemas.storageChannel.organizationId),
|
||||
orderBy: desc(drizzleDb.schemas.storageChannel.createdAt)
|
||||
}) as StorageChannelWith[]
|
||||
|
||||
const organizations = await db.query.organization.findMany({
|
||||
where: (fields) => isNull(fields.deletedAt),
|
||||
with: {
|
||||
members: true,
|
||||
},
|
||||
});
|
||||
|
||||
const settings = await db.query.setting.findFirst({
|
||||
where: eq(drizzleDb.schemas.setting.name, "system"),
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>Storage channels</PageTitle>
|
||||
<PageActions>
|
||||
<ChannelAddEditModal kind={"storage"} adminView={false}/>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<ChannelsSection defaultStorageChannelId={settings?.defaultStorageChannelId} kind={"storage"} organizations={organizations} channels={storageChannels}/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
+40
-11
@@ -2,7 +2,6 @@ import {PageParams} from "@/types/next";
|
||||
import {notFound, redirect} from "next/navigation";
|
||||
import {Page, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
||||
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
|
||||
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/database/database-tabs";
|
||||
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi";
|
||||
import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button";
|
||||
import {db} from "@/db";
|
||||
@@ -12,9 +11,13 @@ import {getOrganizationProjectDatabases} from "@/lib/services";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import {AlertPolicyModal} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy-modal";
|
||||
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||
import {ImportModal} from "@/components/wrappers/dashboard/database/import/import-modal";
|
||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
||||
import {ChannelPoliciesModal} from "@/components/wrappers/dashboard/database/channels-policy/policy-modal";
|
||||
import {HardDrive, Megaphone} from "lucide-react";
|
||||
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
||||
import {DatabaseContent} from "@/components/wrappers/dashboard/projects/database/database-content";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{
|
||||
projectId: string;
|
||||
@@ -39,7 +42,8 @@ export default async function RoutePage(props: PageParams<{
|
||||
with: {
|
||||
project: true,
|
||||
retentionPolicy: true,
|
||||
alertPolicies: true
|
||||
alertPolicies: true,
|
||||
storagePolicies: true
|
||||
}
|
||||
});
|
||||
|
||||
@@ -51,6 +55,11 @@ export default async function RoutePage(props: PageParams<{
|
||||
where: eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
|
||||
with: {
|
||||
restorations: true,
|
||||
storages: {
|
||||
with: {
|
||||
storageChannel: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: (b, {desc}) => [desc(b.createdAt)],
|
||||
});
|
||||
@@ -87,6 +96,10 @@ export default async function RoutePage(props: PageParams<{
|
||||
const organizationChannels = await getOrganizationChannels(organization.id);
|
||||
const activeOrganizationChannels = organizationChannels.filter(channel => channel.enabled);
|
||||
|
||||
const organizationStorageChannels = await getOrganizationStorageChannels(organization.id);
|
||||
const activeOrganizationStorageChannels = organizationStorageChannels.filter(channel => channel.enabled);
|
||||
|
||||
|
||||
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||
|
||||
const isMember = activeMember?.role === "member";
|
||||
@@ -102,12 +115,22 @@ export default async function RoutePage(props: PageParams<{
|
||||
{!isMember && (
|
||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Do not delete*/}
|
||||
{/*<EditButton/>*/}
|
||||
<RetentionPolicySheet database={dbItem}/>
|
||||
<CronButton database={dbItem}/>
|
||||
<AlertPolicyModal database={dbItem} notificationChannels={activeOrganizationChannels}
|
||||
organizationId={organization.id}/>
|
||||
<ChannelPoliciesModal
|
||||
database={dbItem}
|
||||
kind={"notification"}
|
||||
icon={<Megaphone/>}
|
||||
channels={activeOrganizationChannels}
|
||||
organizationId={organization.id}
|
||||
/>
|
||||
<ChannelPoliciesModal
|
||||
database={dbItem}
|
||||
icon={<HardDrive/>}
|
||||
kind={"storage"}
|
||||
channels={activeOrganizationStorageChannels}
|
||||
organizationId={organization.id}
|
||||
/>
|
||||
<ImportModal database={dbItem}/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -126,10 +149,16 @@ export default async function RoutePage(props: PageParams<{
|
||||
<PageContent className="flex flex-col w-full h-full">
|
||||
<DatabaseKpi successRate={successRate} database={dbItem} availableBackups={availableBackups}
|
||||
totalBackups={totalBackups}/>
|
||||
<DatabaseTabs activeMember={activeMember} settings={settings} database={dbItem}
|
||||
isAlreadyRestore={isAlreadyRestore}
|
||||
backups={backups}
|
||||
restorations={restorations}/>
|
||||
<BackupModalProvider>
|
||||
<DatabaseContent
|
||||
activeMember={activeMember}
|
||||
settings={settings}
|
||||
database={dbItem}
|
||||
isAlreadyRestore={isAlreadyRestore}
|
||||
restorations={restorations}
|
||||
backups={backups}
|
||||
/>
|
||||
</BackupModalProvider>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import {notFound} from "next/navigation";
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
|
||||
import * as drizzleDb from "@/db";
|
||||
|
||||
import {db} from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
|
||||
const {projectId} = await props.params;
|
||||
|
||||
const proj = await db.query.project.findFirst({
|
||||
where: eq(drizzleDb.schemas.project.id, projectId),
|
||||
with: {
|
||||
databases: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!proj) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.slug, "default"),
|
||||
});
|
||||
|
||||
if (!org) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const availableDatabases = (
|
||||
await db.query.database.findMany({
|
||||
where: (db, {or, eq, isNull}) => or(isNull(db.projectId), eq(db.projectId, proj.id)),
|
||||
with: {
|
||||
agent: true,
|
||||
project: true,
|
||||
backups: true,
|
||||
restorations: true,
|
||||
},
|
||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
||||
})
|
||||
)
|
||||
// .filter((db) => db.project !== null) as DatabaseWith[];
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>Edit {proj.name}</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<ProjectForm
|
||||
organization={org}
|
||||
databases={availableDatabases as DatabaseWith[]}
|
||||
defaultValues={{...proj, databases: proj.databases.map((db) => db.id)}}
|
||||
projectId={proj.id}
|
||||
/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
||||
import {Page, PageContent, PageTitle} from "@/features/layout/page";
|
||||
import {buttonVariants} from "@/components/ui/button";
|
||||
import {GearIcon} from "@radix-ui/react-icons";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ButtonDeleteProject
|
||||
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
|
||||
@@ -15,11 +14,10 @@ import {eq} from "drizzle-orm";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
|
||||
import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button";
|
||||
import {AlertPolicyModal} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy-modal";
|
||||
import {ImportModal} from "@/components/wrappers/dashboard/database/import/import-modal";
|
||||
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
|
||||
import {ProjectDialog} from "@/features/projects/components/project.dialog";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
import {ProjectWith} from "@/db/schema/06_project";
|
||||
|
||||
|
||||
export default async function RoutePage(props: PageParams<{
|
||||
projectId: string
|
||||
@@ -55,6 +53,19 @@ export default async function RoutePage(props: PageParams<{
|
||||
redirect("/dashboard/projects");
|
||||
}
|
||||
|
||||
const availableDatabases = (
|
||||
await db.query.database.findMany({
|
||||
where: (db, {or, eq, isNull}) => or(isNull(db.projectId), eq(db.projectId, proj.id)),
|
||||
with: {
|
||||
agent: true,
|
||||
project: true,
|
||||
backups: true,
|
||||
restorations: true,
|
||||
},
|
||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
||||
})
|
||||
) as DatabaseWith[];
|
||||
|
||||
const isMember = activeMember?.role === "member";
|
||||
|
||||
return (
|
||||
@@ -67,10 +78,15 @@ export default async function RoutePage(props: PageParams<{
|
||||
{!isMember && (
|
||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link className={buttonVariants({variant: "outline"})}
|
||||
href={`/dashboard/projects/${proj.id}/edit`}>
|
||||
<GearIcon className="w-7 h-7"/>
|
||||
</Link>
|
||||
<ProjectDialog
|
||||
databases={availableDatabases}
|
||||
organization={org}
|
||||
project={proj as ProjectWith}
|
||||
>
|
||||
<div className={buttonVariants({variant: "outline", className: "cursor-pointer"})}>
|
||||
<GearIcon className="w-7 h-7"/>
|
||||
</div>
|
||||
</ProjectDialog>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
||||
@@ -99,4 +115,4 @@ export default async function RoutePage(props: PageParams<{
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
|
||||
import {notFound} from "next/navigation";
|
||||
import {db} from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {getOrganization} from "@/lib/auth/auth";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
const organization = await getOrganization({});
|
||||
|
||||
if (!organization) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const availableDatabases = (
|
||||
await db.query.database.findMany({
|
||||
where: (db, {isNull}) => isNull(db.projectId),
|
||||
with: {
|
||||
agent: true,
|
||||
project: true,
|
||||
backups: true,
|
||||
restorations: true,
|
||||
},
|
||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
||||
})
|
||||
).filter((db) => db.project == null) as DatabaseWith[];
|
||||
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
|
||||
});
|
||||
|
||||
if (!org) notFound();
|
||||
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>Create new project</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<ProjectForm databases={availableDatabases} organization={org}/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import {PageParams} from "@/types/next";
|
||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||
import {Button} from "@/components/ui/button";
|
||||
@@ -10,6 +8,8 @@ import {notFound} from "next/navigation";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||
import {Metadata} from "next";
|
||||
import {ProjectDialog} from "@/features/projects/components/project.dialog";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Projects",
|
||||
@@ -36,6 +36,19 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
});
|
||||
const isMember = activeMember?.role === "member";
|
||||
|
||||
const availableDatabases = (
|
||||
await db.query.database.findMany({
|
||||
where: (db, {isNull}) => isNull(db.projectId),
|
||||
with: {
|
||||
agent: true,
|
||||
project: true,
|
||||
backups: true,
|
||||
restorations: true,
|
||||
},
|
||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
||||
})
|
||||
).filter((db) => db.project == null) as DatabaseWith[];
|
||||
|
||||
|
||||
return (
|
||||
<Page>
|
||||
@@ -43,9 +56,9 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
<PageTitle>Projects</PageTitle>
|
||||
{(projects.length > 0 && !isMember) && (
|
||||
<PageActions>
|
||||
<Link href={`/dashboard/projects/new`}>
|
||||
<ProjectDialog databases={availableDatabases} organization={organization}>
|
||||
<Button>+ Create Project</Button>
|
||||
</Link>
|
||||
</ProjectDialog>
|
||||
</PageActions>
|
||||
)}
|
||||
</PageHeader>
|
||||
@@ -62,9 +75,11 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
) : isMember ? (
|
||||
<EmptyStatePlaceholder text="No project available"/>
|
||||
) : (
|
||||
<EmptyStatePlaceholder url="/dashboard/projects/new" text="Create new Project"/>
|
||||
<ProjectDialog databases={availableDatabases} organization={organization}>
|
||||
<EmptyStatePlaceholder text="Create new Project"/>
|
||||
</ProjectDialog>
|
||||
)}
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {notFound} from "next/navigation";
|
||||
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/organization-form/organization-form";
|
||||
import {getOrganization} from "@/lib/auth/auth";
|
||||
import {db} from "@/db";
|
||||
import {isNull} from "drizzle-orm";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{
|
||||
|
||||
}>) {
|
||||
const user = await currentUser();
|
||||
|
||||
const organization = await getOrganization({});
|
||||
|
||||
const users = await db.query.user.findMany({
|
||||
where: (fields) => isNull(fields.deletedAt)
|
||||
});
|
||||
|
||||
if (!user || !users || !organization || organization.slug == "default") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return(
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>
|
||||
Edit {organization.name}
|
||||
</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<OrganizationForm currentUser={user} users={users} defaultValues={organization}/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -3,21 +3,19 @@ import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {notFound} from "next/navigation";
|
||||
import {
|
||||
DeleteOrganizationButton
|
||||
} from "@/components/wrappers/dashboard/organization/delete-organization/delete-organization-button";
|
||||
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/edit-button-settings/edit-button-settings";
|
||||
import {Metadata} from "next";
|
||||
import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
||||
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import Link from "next/link";
|
||||
import {buttonVariants} from "@/components/ui/button";
|
||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
||||
import {DeleteOrganizationButton} from "@/components/wrappers/dashboard/organization/delete-organization-button";
|
||||
import {EditOrganizationDialog} from "@/features/organization/components/edit-organization.dialog";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {GearIcon} from "@radix-ui/react-icons";
|
||||
import {
|
||||
ButtonDeleteProject
|
||||
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
|
||||
import {db} from "@/db";
|
||||
import {isNull} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Settings",
|
||||
@@ -28,13 +26,38 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||
const user = await currentUser();
|
||||
const activeMember = await getActiveMember()
|
||||
|
||||
if (!organization || !activeMember) {
|
||||
if (!organization || !activeMember || !user) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const notificationChannels = await getOrganizationChannels(organization.id)
|
||||
const storageChannels = await getOrganizationStorageChannels(organization.id)
|
||||
const permissions = computeOrganizationPermissions(activeMember);
|
||||
|
||||
// Fetch users for the form
|
||||
const users = await db.query.user.findMany({
|
||||
where: (fields) => isNull(fields.deletedAt)
|
||||
});
|
||||
|
||||
// Re-fetch organization with members to match type expectations if needed,
|
||||
// although getOrganization returns OrganizationWithMembers usually, let's verify or cast.
|
||||
// getOrganization returns Organization type from database schema, which includes relations if defined in auth lib.
|
||||
// However, OrganizationForm expects OrganizationWithMembers.
|
||||
// Let's ensure we have members.
|
||||
|
||||
const organizationWithMembers = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.id, organization.id),
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!organizationWithMembers) notFound();
|
||||
|
||||
|
||||
return (
|
||||
<Page>
|
||||
@@ -46,7 +69,15 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||
<div className="flex items-center gap-2">
|
||||
{permissions.canManageSettings && organization.slug !== "default" && (
|
||||
<EditButtonSettings/>
|
||||
<EditOrganizationDialog
|
||||
organization={organizationWithMembers}
|
||||
users={users}
|
||||
currentUser={user}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<GearIcon className="w-7 h-7"/>
|
||||
</Button>
|
||||
</EditOrganizationDialog>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -62,8 +93,9 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||
activeMember={activeMember}
|
||||
organization={organization}
|
||||
notificationChannels={notificationChannels}
|
||||
storageChannels={storageChannels}
|
||||
/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {AppSidebar} from "@/components/wrappers/dashboard/common/sidebar/app-sid
|
||||
import {Header} from "@/features/layout/Header";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
import {ThemeMetaUpdater} from "@/features/browser/theme-meta-updater";
|
||||
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
||||
|
||||
export default async function Layout({children}: { children: ReactNode }) {
|
||||
const user = await currentUser();
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import forge from "node-forge";
|
||||
import {EventPayload} from "@/features/notifications/types";
|
||||
import {dispatchNotification} from "@/features/notifications/dispatch";
|
||||
import {Database, DatabaseWith} from "@/db/schema/07_database";
|
||||
|
||||
|
||||
export async function decryptedDump(file: File, aesKeyHex: string, ivHex: string, fileExtension: string): Promise<File> {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import {isUuidv4} from "@/utils/verify-uuid";
|
||||
import {uploadLocalPrivate, uploadS3Private} from "@/features/upload/private/upload.action";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import {eventEmitter} from "../../../events/route";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {db} from "@/db";
|
||||
import {Backup} from "@/db/schema/07_database";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {env} from "@/env.mjs";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {decryptedDump, getFileExtension} from "./helpers";
|
||||
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
||||
import {storeBackupFiles} from "@/features/storages/helpers";
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
@@ -25,6 +24,7 @@ export async function POST(
|
||||
{status: 400}
|
||||
);
|
||||
}
|
||||
|
||||
eventEmitter.emit('modification', {update: true});
|
||||
|
||||
const agentId = (await params).agentId;
|
||||
@@ -34,6 +34,9 @@ export async function POST(
|
||||
const generatedId = formData.get("generatedId") as string | null;
|
||||
const method = formData.get("method") as string | null;
|
||||
|
||||
|
||||
|
||||
|
||||
if (!generatedId || !isUuidv4(generatedId)) {
|
||||
return NextResponse.json(
|
||||
{error: "generatedId is not a valid UUID"},
|
||||
@@ -56,7 +59,8 @@ export async function POST(
|
||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
||||
with: {
|
||||
project: true,
|
||||
alertPolicies: true
|
||||
alertPolicies: true,
|
||||
storagePolicies: true
|
||||
}
|
||||
});
|
||||
|
||||
@@ -112,6 +116,7 @@ export async function POST(
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{error: "File is required for successful backup"},
|
||||
@@ -125,36 +130,12 @@ export async function POST(
|
||||
const uuid = uuidv4();
|
||||
const fileName = `${uuid}${fileExtension}`;
|
||||
const buffer = Buffer.from(await decryptedFile.arrayBuffer());
|
||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||
if (!settings) {
|
||||
throw new Error("System settings not found.");
|
||||
}
|
||||
|
||||
let success: boolean, message: string, filePath: string;
|
||||
|
||||
const result =
|
||||
settings.storage === "local"
|
||||
? await uploadLocalPrivate(fileName, buffer)
|
||||
: await uploadS3Private(`${database.project?.slug}/${fileName}`, buffer, env.S3_BUCKET_NAME!);
|
||||
console.log(extension)
|
||||
|
||||
({success, message, filePath} = result);
|
||||
|
||||
if (!success) {
|
||||
return NextResponse.json(
|
||||
{error: message},
|
||||
{status: 500}
|
||||
);
|
||||
}
|
||||
|
||||
await db
|
||||
.update(drizzleDb.schemas.backup)
|
||||
.set(withUpdatedAt({
|
||||
file: fileName,
|
||||
fileSize: fileSizeBytes,
|
||||
status: 'success',
|
||||
}))
|
||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||
|
||||
const storageResults = await storeBackupFiles(backup, database, buffer, fileName)
|
||||
eventEmitter.emit('modification', {update: true});
|
||||
|
||||
await sendNotificationsBackupRestore(database, "success_backup");
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import {Body} from "./route";
|
||||
import {isUuidv4} from "@/utils/verify-uuid";
|
||||
import {getFileUrlPresignedLocal, getFileUrlPreSignedS3Action} from "@/features/upload/private/upload.action";
|
||||
import {Agent} from "@/db/schema/08_agent";
|
||||
import {Database} from "@/db/schema/07_database";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {db as dbClient} from "@/db";
|
||||
import {and, eq, inArray} from "drizzle-orm";
|
||||
import {EDbmsSchema} from "@/db/schema/types";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {SafeActionResult} from "next-safe-action";
|
||||
import {ZodString} from "zod";
|
||||
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import type {StorageInput} from "@/features/storages/types";
|
||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||
|
||||
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) {
|
||||
const databasesResponse = [];
|
||||
@@ -48,6 +46,12 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
{status: 500}
|
||||
);
|
||||
}
|
||||
|
||||
if (!dbmsEnumSchema.safeParse(db.dbms).success) {
|
||||
console.log(`Database type not available: ${db.dbms}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const [databaseCreated] = await dbClient
|
||||
.insert(drizzleDb.schemas.database)
|
||||
.values({
|
||||
@@ -75,10 +79,6 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
.returning();
|
||||
|
||||
|
||||
// const backup = await dbClient.query.backup.findFirst({
|
||||
// where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting"))
|
||||
// })
|
||||
|
||||
const activeBackup = await dbClient.query.backup.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
|
||||
@@ -88,7 +88,10 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
|
||||
|
||||
const restoration = await dbClient.query.restoration.findFirst({
|
||||
where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status, "waiting"))
|
||||
where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status, "waiting")),
|
||||
with: {
|
||||
backupStorage: true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -105,53 +108,38 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
restoreAction = true
|
||||
|
||||
|
||||
const backupToRestore = await dbClient.query.backup.findFirst({
|
||||
where: eq(drizzleDb.schemas.backup.id, restoration.backupId),
|
||||
with: {
|
||||
database: {
|
||||
with: {
|
||||
project: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const [settings] = await dbClient.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||
if (!settings) {
|
||||
return NextResponse.json(
|
||||
{error: "Unable to find settings"},
|
||||
{status: 500}
|
||||
);
|
||||
if (!restoration.backupStorage || restoration.backupStorage.status != "success" || !restoration.backupStorage.path) {
|
||||
restoreAction = false
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const fileName = backupToRestore?.file
|
||||
const input: StorageInput = {
|
||||
action: "get",
|
||||
data: {
|
||||
path: restoration.backupStorage.path,
|
||||
signedUrl: true,
|
||||
},
|
||||
metadata: {
|
||||
storageId: restoration.backupStorage.storageChannelId,
|
||||
fileKind: "backups"
|
||||
}
|
||||
};
|
||||
|
||||
let data: SafeActionResult<string, ZodString, readonly [], {
|
||||
_errors?: string[] | undefined;
|
||||
}, readonly [], ServerActionResult<string>, object> | undefined
|
||||
|
||||
try {
|
||||
const result = await dispatchStorage(input, undefined, restoration.backupStorage.storageChannelId);
|
||||
|
||||
if (settings.storage == "local") {
|
||||
data = await getFileUrlPresignedLocal({fileName: fileName!})
|
||||
} else if (settings.storage == "s3") {
|
||||
|
||||
data = await getFileUrlPreSignedS3Action(`backups/${backupToRestore?.database.project?.slug}/${fileName}`);
|
||||
}
|
||||
|
||||
if (data?.data?.success) {
|
||||
urlBackup = data.data.value ?? "";
|
||||
if (result.success) {
|
||||
urlBackup = result.url ?? "";
|
||||
} else {
|
||||
await dbClient
|
||||
.update(drizzleDb.schemas.restoration)
|
||||
.set({status: "failed"})
|
||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||
|
||||
// @ts-ignore
|
||||
const errorMessage = data?.data?.actionError?.message || "Failed to get presigned URL";
|
||||
const errorMessage = "Failed to get backup URL";
|
||||
console.error("Restoration failed: ", errorMessage);
|
||||
|
||||
continue;
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -160,9 +148,48 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
.update(drizzleDb.schemas.restoration)
|
||||
.set({status: "failed"})
|
||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// const fileName = backupToRestore?.file
|
||||
//
|
||||
// let data: SafeActionResult<string, ZodString, readonly [], {
|
||||
// _errors?: string[] | undefined;
|
||||
// }, readonly [], ServerActionResult<string>, object> | undefined
|
||||
//
|
||||
// try {
|
||||
//
|
||||
// if (settings.storage == "local") {
|
||||
// data = await getFileUrlPresignedLocal({fileName: fileName!})
|
||||
// } else if (settings.storage == "s3") {
|
||||
//
|
||||
// data = await getFileUrlPreSignedS3Action(`backups/${backupToRestore?.database.project?.slug}/${fileName}`);
|
||||
// }
|
||||
//
|
||||
// if (data?.data?.success) {
|
||||
// urlBackup = data.data.value ?? "";
|
||||
// } else {
|
||||
// await dbClient
|
||||
// .update(drizzleDb.schemas.restoration)
|
||||
// .set({status: "failed"})
|
||||
// .where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||
//
|
||||
// // @ts-ignore
|
||||
// const errorMessage = data?.data?.actionError?.message || "Failed to get presigned URL";
|
||||
// console.error("Restoration failed: ", errorMessage);
|
||||
//
|
||||
// continue;
|
||||
// }
|
||||
// } catch (err) {
|
||||
// console.error("Restoration crashed unexpectedly:", err);
|
||||
// await dbClient
|
||||
// .update(drizzleDb.schemas.restoration)
|
||||
// .set({status: "failed"})
|
||||
// .where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||
//
|
||||
// continue;
|
||||
// }
|
||||
await dbClient
|
||||
.update(drizzleDb.schemas.restoration)
|
||||
.set({status: "ongoing"})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action";
|
||||
import {handleDatabases} from "./helpers";
|
||||
import {eventEmitter} from "../../../events/route";
|
||||
import * as drizzleDb from "@/db";
|
||||
@@ -20,13 +19,6 @@ export type Body = {
|
||||
databases: databaseAgent[]
|
||||
}
|
||||
|
||||
// Function to test the get file url presigned local
|
||||
export async function GET(request: Request) {
|
||||
const url = await getFileUrlPresignedLocal({fileName: "d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump"})
|
||||
return Response.json({
|
||||
message: url
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
@@ -38,7 +30,6 @@ export async function POST(
|
||||
const lastContact = new Date();
|
||||
let message: string
|
||||
|
||||
|
||||
if (!isUuidv4(agentId)) {
|
||||
message = "agentId is not a valid uuid"
|
||||
return NextResponse.json(
|
||||
@@ -57,7 +48,6 @@ export async function POST(
|
||||
}
|
||||
const databasesResponse = await handleDatabases(body, agent, lastContact)
|
||||
|
||||
|
||||
await db
|
||||
.update(drizzleDb.schemas.agent)
|
||||
.set(withUpdatedAt({
|
||||
|
||||
@@ -1,29 +1,47 @@
|
||||
import * as fs from "node:fs";
|
||||
import {NextResponse} from "next/server";
|
||||
import path from "path";
|
||||
import type {StorageInput} from "@/features/storages/types";
|
||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||
import {Readable} from "node:stream";
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{params}: { params: Promise<{ fileName: string }> }
|
||||
) {
|
||||
|
||||
const {searchParams} = new URL(request.url);
|
||||
const token = searchParams.get('token');
|
||||
const expires = searchParams.get('expires');
|
||||
const fileName = (await params).fileName
|
||||
const pathFromUrl = searchParams.get('path');
|
||||
const storageId = searchParams.get('storageId');
|
||||
|
||||
const uploadsDir = "private/uploads/files/";
|
||||
const uploadPath = path.join(uploadsDir, fileName);
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
let filePath = null;
|
||||
if (fs.existsSync(uploadPath)) {
|
||||
filePath = uploadPath;
|
||||
} else {
|
||||
return NextResponse.json({error: "File not found"}, {status: 404})
|
||||
if (!pathFromUrl || !storageId) {
|
||||
return NextResponse.json({error: "Missing search params"}, {status: 404})
|
||||
}
|
||||
|
||||
const input: StorageInput = {
|
||||
action: "get",
|
||||
data: {
|
||||
path: pathFromUrl,
|
||||
signedUrl: true,
|
||||
},
|
||||
metadata: {
|
||||
storageId: storageId,
|
||||
fileKind: "backups",
|
||||
}
|
||||
};
|
||||
|
||||
console.debug(input);
|
||||
|
||||
const result = await dispatchStorage(input, undefined, storageId);
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json({error: "Enable to get file from privided storage channel, an error occurred !"})
|
||||
}
|
||||
|
||||
|
||||
const fileName = path.basename(pathFromUrl);
|
||||
|
||||
const crypto = require('crypto');
|
||||
const expectedToken = crypto.createHash('sha256').update(`${fileName}${expires}`).digest('hex');
|
||||
if (token !== expectedToken) {
|
||||
return NextResponse.json(
|
||||
@@ -40,7 +58,15 @@ export async function GET(
|
||||
);
|
||||
}
|
||||
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
if (!result.file || !Buffer.isBuffer(result.file)) {
|
||||
return NextResponse.json(
|
||||
{error: "Invalid file payload"},
|
||||
{status: 500}
|
||||
);
|
||||
}
|
||||
|
||||
const fileStream = Readable.from(result.file);
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
fileStream.on('data', (chunk) => controller.enqueue(chunk));
|
||||
@@ -0,0 +1,102 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import {auth} from "@/lib/auth/auth";
|
||||
import {headers} from "next/headers";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {StorageInput} from "@/features/storages/types";
|
||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||
import {Readable} from "node:stream";
|
||||
|
||||
export async function GET(
|
||||
req: Request,
|
||||
{params}: { params: Promise<{ fileName: string }> }
|
||||
) {
|
||||
const {searchParams} = new URL(req.url);
|
||||
const fileName = (await params).fileName;
|
||||
const storageId = searchParams.get('storageId');
|
||||
|
||||
if (!fileName) return NextResponse.json({error: "Missing file parameter"}, {status: 400});
|
||||
|
||||
const session = await auth.api.getSession({headers: await headers()});
|
||||
if (!session) return NextResponse.json({error: "Unauthorized"}, {status: 403});
|
||||
|
||||
|
||||
if (!storageId) {
|
||||
return NextResponse.json({error: "Missing storageId in search params"}, {status: 404})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// const settings = await db.query.setting.findFirst({
|
||||
// where: eq(drizzleDb.schemas.setting.name, "system"),
|
||||
// with: {
|
||||
// storageChannel: true
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// if (!settings || !settings.storageChannel) {
|
||||
// return NextResponse.json({error: "Unable to get settings or no default storage channel"});
|
||||
// }
|
||||
|
||||
|
||||
const ext = fileName.split(".").pop()?.toLowerCase();
|
||||
const contentType =
|
||||
ext === "png"
|
||||
? "image/png"
|
||||
: ext === "jpg" || ext === "jpeg"
|
||||
? "image/jpeg"
|
||||
: ext === "gif"
|
||||
? "image/gif"
|
||||
: ext === "webp"
|
||||
? "image/webp"
|
||||
: "application/octet-stream";
|
||||
|
||||
try {
|
||||
|
||||
const path = `images/${fileName}`;
|
||||
|
||||
const input: StorageInput = {
|
||||
action: "get",
|
||||
data: {
|
||||
path: path,
|
||||
},
|
||||
metadata: {
|
||||
storageId: storageId,
|
||||
fileKind: "images"
|
||||
}
|
||||
}
|
||||
|
||||
const result = await dispatchStorage(input, undefined, storageId);
|
||||
|
||||
if (!result.file || !Buffer.isBuffer(result.file)) {
|
||||
console.error(`An error occurred while getting file :`, result);
|
||||
return NextResponse.json(
|
||||
{error: "Invalid file payload"},
|
||||
{status: 500}
|
||||
);
|
||||
}
|
||||
|
||||
const fileStream = Readable.from(result.file);
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
fileStream.on('data', (chunk) => controller.enqueue(chunk));
|
||||
fileStream.on('end', () => controller.close());
|
||||
fileStream.on('error', (err) => controller.error(err));
|
||||
},
|
||||
});
|
||||
|
||||
return new NextResponse(stream, {
|
||||
headers: {
|
||||
'Content-Disposition': `inline; filename="${fileName}"`,
|
||||
"Cache-Control": "no-store",
|
||||
"Content-Type": contentType,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error("Error streaming image:", err);
|
||||
return NextResponse.json({error: "Error fetching file"}, {status: 500});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export async function GET(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
const code = url.searchParams.get("code");
|
||||
|
||||
const success = Boolean(code);
|
||||
|
||||
return new Response(
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>${success ? "Success" : "Failed"}</h1>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
{
|
||||
status: success ? 200 : 400,
|
||||
headers: {
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import {auth} from "@/lib/auth/auth";
|
||||
import {headers} from "next/headers";
|
||||
import {checkFileExistsInBucket, getObjectFromClient} from "@/utils/s3-file-management";
|
||||
import {env} from "@/env.mjs";
|
||||
import * as stream from "node:stream";
|
||||
import path from "path";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import fs from "fs/promises";
|
||||
|
||||
function nodeStreamToWebStream(nodeStream: stream.Readable) {
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
nodeStream.on("data", chunk => controller.enqueue(chunk));
|
||||
nodeStream.on("end", () => controller.close());
|
||||
nodeStream.on("error", err => controller.error(err));
|
||||
},
|
||||
cancel() {
|
||||
nodeStream.destroy();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const privateS3ImageDir = "images/";
|
||||
|
||||
|
||||
export async function GET(
|
||||
req: Request,
|
||||
{params}: { params: Promise<{ fileName: string }> }
|
||||
) {
|
||||
const fileName = (await params).fileName;
|
||||
if (!fileName) return NextResponse.json({error: "Missing file parameter"}, {status: 400});
|
||||
|
||||
const session = await auth.api.getSession({headers: await headers()});
|
||||
if (!session) return NextResponse.json({error: "Unauthorized"}, {status: 403});
|
||||
|
||||
const [settings] = await db
|
||||
.select()
|
||||
.from(drizzleDb.schemas.setting)
|
||||
.where(eq(drizzleDb.schemas.setting.name, "system"))
|
||||
.limit(1);
|
||||
|
||||
if (!settings) throw new Error("System settings not found.");
|
||||
|
||||
const storageType = settings.storage; // "local" or "s3"
|
||||
const ext = fileName.split(".").pop()?.toLowerCase();
|
||||
const contentType =
|
||||
ext === "png"
|
||||
? "image/png"
|
||||
: ext === "jpg" || ext === "jpeg"
|
||||
? "image/jpeg"
|
||||
: ext === "gif"
|
||||
? "image/gif"
|
||||
: ext === "webp"
|
||||
? "image/webp"
|
||||
: "application/octet-stream";
|
||||
|
||||
try {
|
||||
if (storageType === "local") {
|
||||
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
|
||||
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
const file = await fs.readFile(filePath);
|
||||
|
||||
return new NextResponse(file, {
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
"Cache-Control": "no-store",
|
||||
"Content-Disposition": `inline; filename="${fileName}"`,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// if not found locally, fallback to S3
|
||||
}
|
||||
}
|
||||
|
||||
const exists = await checkFileExistsInBucket({
|
||||
bucketName: env.S3_BUCKET_NAME!,
|
||||
fileName: `${privateS3ImageDir}${fileName}`,
|
||||
});
|
||||
if (!exists) return NextResponse.json({error: "File not found"}, {status: 404});
|
||||
|
||||
const nodeStream = await getObjectFromClient({
|
||||
bucketName: env.S3_BUCKET_NAME!,
|
||||
fileName: `${privateS3ImageDir}${fileName}`,
|
||||
});
|
||||
const webStream = nodeStreamToWebStream(nodeStream);
|
||||
|
||||
return new NextResponse(webStream, {
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
"Cache-Control": "no-store",
|
||||
"Content-Disposition": `inline; filename="${fileName}"`,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error streaming image:", err);
|
||||
return NextResponse.json({error: "Error fetching file"}, {status: 500});
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -6,6 +6,7 @@ import {ThemeProvider} from "@/features/theme/theme-provider";
|
||||
import {Toaster} from "@/components/ui/sonner";
|
||||
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
|
||||
import {ThemeMetaUpdaterRoot} from "@/features/browser/theme-meta-updater-root";
|
||||
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
||||
|
||||
|
||||
export type ProviderProps = PropsWithChildren<{}>;
|
||||
@@ -21,8 +22,8 @@ export const Providers = (props: ProviderProps) => {
|
||||
>
|
||||
<ThemeMetaUpdaterRoot/>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Toaster/>
|
||||
{props.children}
|
||||
<Toaster/>
|
||||
{props.children}
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
</Suspense>
|
||||
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
# context: .
|
||||
# dockerfile: docker/dockerfile/Dockerfile
|
||||
# target: prod
|
||||
image: solucetechnologies/portabase:1.1.4-rc.2
|
||||
image: solucetechnologies/portabase:latest
|
||||
ports:
|
||||
- '8887:80'
|
||||
environment:
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: portabase-dev
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
image: postgres:17-alpine
|
||||
ports:
|
||||
- "5433:5432"
|
||||
volumes:
|
||||
|
||||
+6
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "portabase",
|
||||
"version": "1.1.12-rc.1",
|
||||
"version": "1.2.4-rc.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 8887",
|
||||
@@ -62,6 +62,7 @@
|
||||
"drizzle-orm": "^0.43.1",
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"googleapis": "^170.1.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.553.0",
|
||||
"minio": "^8.0.5",
|
||||
@@ -74,6 +75,7 @@
|
||||
"nodemailer": "^7.0.3",
|
||||
"npm-check-updates": "^18.0.1",
|
||||
"pg": "^8.16.0",
|
||||
"prettier": "^3.8.0",
|
||||
"react": "^19.2.0",
|
||||
"react-day-picker": "9.7.0",
|
||||
"react-dom": "^19.2.0",
|
||||
@@ -85,9 +87,11 @@
|
||||
"react-twc": "^1.4.2",
|
||||
"react-use-measure": "^2.1.7",
|
||||
"recharts": "^2.15.3",
|
||||
"sharp": "^0.34.5",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"sonner": "^2.0.3",
|
||||
"swiper": "^12.0.3",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vaul": "^1.1.2",
|
||||
@@ -120,5 +124,5 @@
|
||||
"typescript": "^5.8.3",
|
||||
"zenstack": "2.14.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.27.0"
|
||||
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316"
|
||||
}
|
||||
|
||||
Generated
+296
-23
@@ -124,7 +124,7 @@ importers:
|
||||
version: 6.0.0
|
||||
better-auth:
|
||||
specifier: 1.4.5
|
||||
version: 1.4.5(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
version: 1.4.5(next@16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@@ -152,6 +152,9 @@ importers:
|
||||
embla-carousel-react:
|
||||
specifier: ^8.6.0
|
||||
version: 8.6.0(react@19.2.3)
|
||||
googleapis:
|
||||
specifier: ^170.1.0
|
||||
version: 170.1.0
|
||||
input-otp:
|
||||
specifier: ^1.4.2
|
||||
version: 1.4.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -166,10 +169,10 @@ importers:
|
||||
version: 12.24.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
next:
|
||||
specifier: 16.0.10
|
||||
version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
version: 16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
next-safe-action:
|
||||
specifier: ^7.10.8
|
||||
version: 7.10.8(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76)
|
||||
version: 7.10.8(next@16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76)
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -188,6 +191,9 @@ importers:
|
||||
pg:
|
||||
specifier: ^8.16.0
|
||||
version: 8.16.3
|
||||
prettier:
|
||||
specifier: ^3.8.0
|
||||
version: 3.8.0
|
||||
react:
|
||||
specifier: ^19.2.0
|
||||
version: 19.2.3
|
||||
@@ -221,6 +227,9 @@ importers:
|
||||
recharts:
|
||||
specifier: ^2.15.3
|
||||
version: 2.15.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
sharp:
|
||||
specifier: ^0.34.5
|
||||
version: 0.34.5
|
||||
socket.io:
|
||||
specifier: ^4.8.1
|
||||
version: 4.8.3
|
||||
@@ -230,6 +239,9 @@ importers:
|
||||
sonner:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
swiper:
|
||||
specifier: ^12.0.3
|
||||
version: 12.0.3
|
||||
tailwind-merge:
|
||||
specifier: ^3.3.0
|
||||
version: 3.4.0
|
||||
@@ -1729,6 +1741,10 @@ packages:
|
||||
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@prisma/client@6.7.0':
|
||||
resolution: {integrity: sha512-+k61zZn1XHjbZul8q6TdQLpuI/cvyfil87zqK2zpreNIXyXtpUv3+H/oM69hcsFcZXaokHJIzPAt5Z8C8eK2QA==}
|
||||
engines: {node: '>=18.18'}
|
||||
@@ -3424,6 +3440,10 @@ packages:
|
||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ajv-formats@2.1.1:
|
||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||
peerDependencies:
|
||||
@@ -3624,6 +3644,9 @@ packages:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
bignumber.js@9.3.1:
|
||||
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
|
||||
|
||||
binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3662,6 +3685,9 @@ packages:
|
||||
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
@@ -3918,6 +3944,10 @@ packages:
|
||||
damerau-levenshtein@1.0.8:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
|
||||
data-uri-to-buffer@4.0.1:
|
||||
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
data-view-buffer@1.0.2:
|
||||
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4155,6 +4185,9 @@ packages:
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
|
||||
effect@3.18.4:
|
||||
resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==}
|
||||
|
||||
@@ -4423,6 +4456,9 @@ packages:
|
||||
exsolve@1.0.8:
|
||||
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
|
||||
|
||||
extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
|
||||
fast-check@3.23.2:
|
||||
resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
@@ -4477,6 +4513,10 @@ packages:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
||||
engines: {node: ^12.20 || >= 14.13}
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@@ -4516,6 +4556,10 @@ packages:
|
||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||
engines: {node: '>=0.4.x'}
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
fraction.js@4.3.7:
|
||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||
|
||||
@@ -4569,6 +4613,14 @@ packages:
|
||||
functions-have-names@1.2.3:
|
||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||
|
||||
gaxios@7.1.3:
|
||||
resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
gcp-metadata@8.1.2:
|
||||
resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
generator-function@2.0.1:
|
||||
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4619,6 +4671,10 @@ packages:
|
||||
glob-to-regexp@0.4.1:
|
||||
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||
|
||||
glob@10.5.0:
|
||||
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||
hasBin: true
|
||||
|
||||
glob@11.1.0:
|
||||
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -4640,6 +4696,22 @@ packages:
|
||||
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
google-auth-library@10.5.0:
|
||||
resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
google-logging-utils@1.1.3:
|
||||
resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
googleapis-common@8.0.1:
|
||||
resolution: {integrity: sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
googleapis@170.1.0:
|
||||
resolution: {integrity: sha512-RLbc7yG6qzZqvAmGcgjvNIoZ7wpcCFxtc+HN+46etxDrlO4a8l5Cb7NxNQGhV91oRmL7mt56VoRoypAtEQEIKg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4647,6 +4719,10 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
gtoken@8.0.0:
|
||||
resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
has-bigints@1.1.0:
|
||||
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4701,6 +4777,10 @@ packages:
|
||||
resolution: {integrity: sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
@@ -4894,6 +4974,9 @@ packages:
|
||||
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
jackspeak@4.1.1:
|
||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -4929,6 +5012,9 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
json-bigint@1.0.0:
|
||||
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
|
||||
|
||||
json-buffer@3.0.1:
|
||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||
|
||||
@@ -4960,6 +5046,12 @@ packages:
|
||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
jwa@2.0.1:
|
||||
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
|
||||
|
||||
jws@4.0.1:
|
||||
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
@@ -5118,6 +5210,9 @@ packages:
|
||||
lowlight@1.17.0:
|
||||
resolution: {integrity: sha512-vmtBgYKD+QVNy7tIa7ulz5d//Il9R4MooOVh4nkOf9R9Cb/Dk5TXMSTieg/vDulkBkIWj59/BIlyFQxT9X1oAQ==}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@11.2.4:
|
||||
resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -5358,6 +5453,11 @@ packages:
|
||||
resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
|
||||
node-fetch-native@1.6.7:
|
||||
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
|
||||
|
||||
@@ -5370,6 +5470,10 @@ packages:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
node-fetch@3.3.2:
|
||||
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
node-forge@1.3.3:
|
||||
resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==}
|
||||
engines: {node: '>= 6.13.0'}
|
||||
@@ -5524,6 +5628,10 @@ packages:
|
||||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
path-scurry@2.0.1:
|
||||
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -5666,8 +5774,8 @@ packages:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
prettier@3.7.4:
|
||||
resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
|
||||
prettier@3.8.0:
|
||||
resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
@@ -5722,6 +5830,10 @@ packages:
|
||||
qr.js@0.0.0:
|
||||
resolution: {integrity: sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==}
|
||||
|
||||
qs@6.14.1:
|
||||
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
query-string@7.1.3:
|
||||
resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -5922,6 +6034,10 @@ packages:
|
||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
|
||||
rimraf@5.0.10:
|
||||
resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
|
||||
hasBin: true
|
||||
|
||||
rou3@0.7.12:
|
||||
resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==}
|
||||
|
||||
@@ -6230,6 +6346,10 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
swiper@12.0.3:
|
||||
resolution: {integrity: sha512-BHd6U1VPEIksrXlyXjMmRWO0onmdNPaTAFduzqR3pgjvi7KfmUCAm/0cj49u2D7B0zNjMw02TSeXfinC1hDCXg==}
|
||||
engines: {node: '>= 4.7.0'}
|
||||
|
||||
tailwind-merge@3.2.0:
|
||||
resolution: {integrity: sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==}
|
||||
|
||||
@@ -6430,6 +6550,9 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
url-template@2.0.8:
|
||||
resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==}
|
||||
|
||||
use-callback-ref@1.3.3:
|
||||
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -6547,6 +6670,10 @@ packages:
|
||||
web-encoding@1.1.5:
|
||||
resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==}
|
||||
|
||||
web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
@@ -8021,6 +8148,9 @@ snapshots:
|
||||
|
||||
'@phc/format@1.0.0': {}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3)':
|
||||
optionalDependencies:
|
||||
prisma: 6.7.0(typescript@5.9.3)
|
||||
@@ -9466,7 +9596,7 @@ snapshots:
|
||||
'@react-email/render@1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
html-to-text: 9.0.5
|
||||
prettier: 3.7.4
|
||||
prettier: 3.8.0
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
react-promise-suspense: 0.3.4
|
||||
@@ -9474,7 +9604,7 @@ snapshots:
|
||||
'@react-email/render@2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
html-to-text: 9.0.5
|
||||
prettier: 3.7.4
|
||||
prettier: 3.8.0
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
|
||||
@@ -10006,7 +10136,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 22.19.3
|
||||
tapable: 2.3.0
|
||||
webpack: 5.104.1(esbuild@0.27.2)
|
||||
webpack: 5.104.1(esbuild@0.25.10)
|
||||
transitivePeerDependencies:
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
@@ -10423,6 +10553,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
ajv-formats@2.1.1(ajv@8.17.1):
|
||||
optionalDependencies:
|
||||
ajv: 8.17.1
|
||||
@@ -10601,7 +10733,7 @@ snapshots:
|
||||
|
||||
bcryptjs@2.4.3: {}
|
||||
|
||||
better-auth@1.4.5(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
better-auth@1.4.5(next@16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
'@better-auth/core': 1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)
|
||||
'@better-auth/telemetry': 1.4.5(@better-auth/core@1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0))
|
||||
@@ -10617,7 +10749,7 @@ snapshots:
|
||||
nanostores: 1.1.0
|
||||
zod: 4.3.5
|
||||
optionalDependencies:
|
||||
next: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
next: 16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
|
||||
@@ -10630,6 +10762,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
zod: 4.3.5
|
||||
|
||||
bignumber.js@9.3.1: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
bl@4.1.0:
|
||||
@@ -10671,6 +10805,8 @@ snapshots:
|
||||
|
||||
buffer-crc32@1.0.0: {}
|
||||
|
||||
buffer-equal-constant-time@1.0.1: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
buffer@5.7.1:
|
||||
@@ -10950,6 +11086,8 @@ snapshots:
|
||||
|
||||
damerau-levenshtein@1.0.8: {}
|
||||
|
||||
data-uri-to-buffer@4.0.1: {}
|
||||
|
||||
data-view-buffer@1.0.2:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -11109,6 +11247,10 @@ snapshots:
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
effect@3.18.4:
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
@@ -11640,6 +11782,8 @@ snapshots:
|
||||
|
||||
exsolve@1.0.8: {}
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
fast-check@3.23.2:
|
||||
dependencies:
|
||||
pure-rand: 6.1.0
|
||||
@@ -11692,6 +11836,11 @@ snapshots:
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 3.3.3
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
dependencies:
|
||||
flat-cache: 4.0.1
|
||||
@@ -11729,6 +11878,10 @@ snapshots:
|
||||
|
||||
format@0.2.2: {}
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
dependencies:
|
||||
fetch-blob: 3.2.0
|
||||
|
||||
fraction.js@4.3.7: {}
|
||||
|
||||
framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
@@ -11773,6 +11926,23 @@ snapshots:
|
||||
|
||||
functions-have-names@1.2.3: {}
|
||||
|
||||
gaxios@7.1.3:
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
node-fetch: 3.3.2
|
||||
rimraf: 5.0.10
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
gcp-metadata@8.1.2:
|
||||
dependencies:
|
||||
gaxios: 7.1.3
|
||||
google-logging-utils: 1.1.3
|
||||
json-bigint: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
generator-function@2.0.1: {}
|
||||
|
||||
gensync@1.0.0-beta.2: {}
|
||||
@@ -11830,6 +12000,15 @@ snapshots:
|
||||
|
||||
glob-to-regexp@0.4.1: {}
|
||||
|
||||
glob@10.5.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
glob@11.1.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
@@ -11850,10 +12029,48 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
gopd: 1.2.0
|
||||
|
||||
google-auth-library@10.5.0:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
gaxios: 7.1.3
|
||||
gcp-metadata: 8.1.2
|
||||
google-logging-utils: 1.1.3
|
||||
gtoken: 8.0.0
|
||||
jws: 4.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
google-logging-utils@1.1.3: {}
|
||||
|
||||
googleapis-common@8.0.1:
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
gaxios: 7.1.3
|
||||
google-auth-library: 10.5.0
|
||||
qs: 6.14.1
|
||||
url-template: 2.0.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
googleapis@170.1.0:
|
||||
dependencies:
|
||||
google-auth-library: 10.5.0
|
||||
googleapis-common: 8.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
gtoken@8.0.0:
|
||||
dependencies:
|
||||
gaxios: 7.1.3
|
||||
jws: 4.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
has-bigints@1.1.0: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
@@ -11913,6 +12130,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
ignore@5.3.2: {}
|
||||
@@ -12093,6 +12317,12 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
set-function-name: 2.0.2
|
||||
|
||||
jackspeak@3.4.3:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jackspeak@4.1.1:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
@@ -12119,6 +12349,10 @@ snapshots:
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
json-bigint@1.0.0:
|
||||
dependencies:
|
||||
bignumber.js: 9.3.1
|
||||
|
||||
json-buffer@3.0.1: {}
|
||||
|
||||
json-parse-even-better-errors@2.3.1: {}
|
||||
@@ -12148,6 +12382,17 @@ snapshots:
|
||||
object.assign: 4.1.7
|
||||
object.values: 1.2.1
|
||||
|
||||
jwa@2.0.1:
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
jws@4.0.1:
|
||||
dependencies:
|
||||
jwa: 2.0.1
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
@@ -12282,6 +12527,8 @@ snapshots:
|
||||
fault: 1.0.4
|
||||
highlight.js: 10.4.1
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.2.4: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
@@ -12416,9 +12663,9 @@ snapshots:
|
||||
|
||||
neo-async@2.6.2: {}
|
||||
|
||||
next-safe-action@7.10.8(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76):
|
||||
next-safe-action@7.10.8(next@16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76):
|
||||
dependencies:
|
||||
next: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
next: 16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
optionalDependencies:
|
||||
@@ -12447,12 +12694,12 @@ snapshots:
|
||||
'@next/swc-linux-x64-musl': 15.5.2
|
||||
'@next/swc-win32-arm64-msvc': 15.5.2
|
||||
'@next/swc-win32-x64-msvc': 15.5.2
|
||||
sharp: 0.34.4
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
next@16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
'@next/env': 16.0.10
|
||||
'@swc/helpers': 0.5.15
|
||||
@@ -12460,7 +12707,7 @@ snapshots:
|
||||
postcss: 8.4.31
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3)
|
||||
styled-jsx: 5.1.6(@babel/core@7.26.10)(react@19.2.3)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.0.10
|
||||
'@next/swc-darwin-x64': 16.0.10
|
||||
@@ -12484,12 +12731,20 @@ snapshots:
|
||||
|
||||
node-cron@4.2.1: {}
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch-native@1.6.7: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
node-fetch@3.3.2:
|
||||
dependencies:
|
||||
data-uri-to-buffer: 4.0.1
|
||||
fetch-blob: 3.2.0
|
||||
formdata-polyfill: 4.0.10
|
||||
|
||||
node-forge@1.3.3: {}
|
||||
|
||||
node-gyp-build@4.8.4: {}
|
||||
@@ -12670,6 +12925,11 @@ snapshots:
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
|
||||
path-scurry@2.0.1:
|
||||
dependencies:
|
||||
lru-cache: 11.2.4
|
||||
@@ -12791,7 +13051,7 @@ snapshots:
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@3.7.4: {}
|
||||
prettier@3.8.0: {}
|
||||
|
||||
pretty-bytes@6.1.1: {}
|
||||
|
||||
@@ -12857,6 +13117,10 @@ snapshots:
|
||||
|
||||
qr.js@0.0.0: {}
|
||||
|
||||
qs@6.14.1:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
query-string@7.1.3:
|
||||
dependencies:
|
||||
decode-uri-component: 0.2.2
|
||||
@@ -13121,6 +13385,10 @@ snapshots:
|
||||
|
||||
reusify@1.1.0: {}
|
||||
|
||||
rimraf@5.0.10:
|
||||
dependencies:
|
||||
glob: 10.5.0
|
||||
|
||||
rou3@0.7.12: {}
|
||||
|
||||
run-parallel@1.2.0:
|
||||
@@ -13266,7 +13534,6 @@ snapshots:
|
||||
'@img/sharp-win32-arm64': 0.34.5
|
||||
'@img/sharp-win32-ia32': 0.34.5
|
||||
'@img/sharp-win32-x64': 0.34.5
|
||||
optional: true
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
@@ -13520,12 +13787,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.26.10
|
||||
|
||||
styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.3):
|
||||
styled-jsx@5.1.6(@babel/core@7.26.10)(react@19.2.3):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 19.2.3
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/core': 7.26.10
|
||||
|
||||
sucrase@3.35.1:
|
||||
dependencies:
|
||||
@@ -13556,6 +13823,8 @@ snapshots:
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
swiper@12.0.3: {}
|
||||
|
||||
tailwind-merge@3.2.0: {}
|
||||
|
||||
tailwind-merge@3.4.0: {}
|
||||
@@ -13611,14 +13880,14 @@ snapshots:
|
||||
ansi-escapes: 4.3.2
|
||||
supports-hyperlinks: 2.3.0
|
||||
|
||||
terser-webpack-plugin@5.3.16(esbuild@0.25.10)(webpack@5.104.1(esbuild@0.25.10)):
|
||||
terser-webpack-plugin@5.3.16(esbuild@0.25.10)(webpack@5.104.1(esbuild@0.27.2)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 4.3.3
|
||||
serialize-javascript: 6.0.2
|
||||
terser: 5.44.1
|
||||
webpack: 5.104.1(esbuild@0.27.2)
|
||||
webpack: 5.104.1(esbuild@0.25.10)
|
||||
optionalDependencies:
|
||||
esbuild: 0.25.10
|
||||
|
||||
@@ -13828,6 +14097,8 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
url-template@2.0.8: {}
|
||||
|
||||
use-callback-ref@1.3.3(@types/react@19.0.10)(react@19.0.0):
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
@@ -13965,11 +14236,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@zxing/text-encoding': 0.9.0
|
||||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webpack-sources@3.3.3: {}
|
||||
|
||||
webpack@5.104.1(esbuild@0.27.2):
|
||||
webpack@5.104.1(esbuild@0.25.10):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.8
|
||||
@@ -13993,7 +14266,7 @@ snapshots:
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 4.3.3
|
||||
tapable: 2.3.0
|
||||
terser-webpack-plugin: 5.3.16(esbuild@0.25.10)(webpack@5.104.1(esbuild@0.25.10))
|
||||
terser-webpack-plugin: 5.3.16(esbuild@0.25.10)(webpack@5.104.1(esbuild@0.27.2))
|
||||
watchpack: 2.5.0
|
||||
webpack-sources: 3.3.3
|
||||
transitivePeerDependencies:
|
||||
|
||||
@@ -1,2 +1,13 @@
|
||||
onlyBuiltDependencies:
|
||||
- '@prisma/client'
|
||||
- '@prisma/engines'
|
||||
- argon2
|
||||
- bcrypt
|
||||
- cpu-features
|
||||
- esbuild
|
||||
- prisma
|
||||
- protobufjs
|
||||
- sharp
|
||||
- ssh2
|
||||
- unrs-resolver
|
||||
- zenstack
|
||||
|
||||
@@ -54,11 +54,12 @@ function checkRouteExists(pathname: string) {
|
||||
/^\/api\/agent\/[^/]+\/status\/?$/,
|
||||
/^\/api\/agent\/[^/]+\/backup\/?$/,
|
||||
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
||||
/^\/api\/files\/[^/]+\/?$/,
|
||||
/^\/api\/images\/[^/]+\/?$/,
|
||||
/^\/api\/files\/images\/[^/]+\/?$/,
|
||||
/^\/api\/files\/backups\/?$/,
|
||||
/^\/api\/events\/?$/,
|
||||
/^\/api\/init\/?$/,
|
||||
/^\/api\/config\/?$/,
|
||||
/^\/api\/google\/drive\/callback\/?$/,
|
||||
];
|
||||
return routePatterns.some((pattern) => pattern.test(pathname));
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -46,7 +46,9 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: R
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
// @ts-ignore
|
||||
IconLeft: ({ className, children: _children, ...props }) => <ChevronLeftIcon className={cn("size-4", className)} {...props} />,
|
||||
// @ts-ignore
|
||||
IconRight: ({ className, children: _children, ...props }) => <ChevronRightIcon className={cn("size-4", className)} {...props} />,
|
||||
}}
|
||||
{...props}
|
||||
|
||||
@@ -58,7 +58,7 @@ function DialogContent({
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
"max-h-[90vh] overflow-y-auto",
|
||||
"max-h-[90vh] overflow-y-auto overflow-x-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -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}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {toast} from "sonner";
|
||||
|
||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Form} from "@/components/ui/form";
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import {useRouter} from "next/navigation";
|
||||
import {Info} from "lucide-react";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {toast} from "sonner";
|
||||
|
||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {CardContent, CardHeader} from "@/components/ui/card";
|
||||
import {Form} from "@/components/ui/form";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {TooltipProvider, TooltipTrigger, Tooltip, TooltipContent} from "@/components/ui/tooltip";
|
||||
import {RegisterSchema, RegisterType} from "@/components/wrappers/auth/register/register-form/register-form.schema";
|
||||
import {PasswordInput} from "@/components/ui/password-input";
|
||||
import {signUp} from "@/lib/auth/auth-client";
|
||||
import Link from "next/link";
|
||||
import {useZodForm} from "@/components/ui/form";
|
||||
import {CardAuth} from "@/features/layout/card-auth";
|
||||
import {FormField} from "@/components/ui/form";
|
||||
import {FormItem} from "@/components/ui/form";
|
||||
import {FormLabel} from "@/components/ui/form";
|
||||
import {FormControl} from "@/components/ui/form";
|
||||
import {FormMessage} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {PasswordInput} from "@/components/ui/password-input";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import {Info} from "lucide-react";
|
||||
|
||||
export type registerFormProps = {
|
||||
defaultValues?: RegisterType;
|
||||
@@ -26,9 +30,11 @@ export const RegisterForm = (props: registerFormProps) => {
|
||||
const form = useZodForm({
|
||||
schema: RegisterSchema,
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: RegisterType) => {
|
||||
// @ts-ignore
|
||||
await signUp.email(values, {
|
||||
onSuccess: () => {
|
||||
toast.success(`Account successfully created`);
|
||||
|
||||
@@ -1,48 +1,3 @@
|
||||
// "use client";
|
||||
//
|
||||
// import { Button } from "@/components/ui/button";
|
||||
// import { useState } from "react";
|
||||
// import { Loader2 } from "lucide-react";
|
||||
//
|
||||
// export type VariantButton = {
|
||||
// secondary: string;
|
||||
// default: string;
|
||||
// outline: string;
|
||||
// ghost: string;
|
||||
// link: string;
|
||||
// destructive: string;
|
||||
// };
|
||||
//
|
||||
// export type ButtonWithConfirmProps = {
|
||||
// icon?: any;
|
||||
// text: string;
|
||||
// variant?: keyof VariantButton;
|
||||
// className?: string;
|
||||
// onClick?: () => void;
|
||||
// isPending?: boolean;
|
||||
// };
|
||||
//
|
||||
// export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
||||
// const [isConfirming, setIsConfirming] = useState(false);
|
||||
//
|
||||
// return (
|
||||
// <Button
|
||||
// onClick={() => {
|
||||
// if (isConfirming && props.onClick) {
|
||||
// props.onClick();
|
||||
// } else {
|
||||
// setIsConfirming(true);
|
||||
// }
|
||||
// }}
|
||||
// variant={props.variant ? props.variant : "default"}
|
||||
// className={props.className}
|
||||
// >
|
||||
// {props.isPending && <Loader2 className="animate-spin mr-4" size={16} />}
|
||||
// {isConfirming ? "Are you sure ?" : `${props.text}`}
|
||||
// <>{props.icon ? props.icon : null}</>
|
||||
// </Button>
|
||||
// );
|
||||
// };
|
||||
"use client"
|
||||
import {Button, ButtonVariantsProps} from "@/components/ui/button";
|
||||
import {useState} from "react";
|
||||
@@ -57,6 +12,7 @@ export type ButtonWithConfirmProps = {
|
||||
button: {
|
||||
main: {
|
||||
className?: string;
|
||||
type?: "button" | "submit" | "reset" | undefined;
|
||||
text?: string;
|
||||
icon?: any;
|
||||
variant?: ButtonVariantsProps["variant"];
|
||||
@@ -100,6 +56,7 @@ export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
||||
)}
|
||||
role="button">
|
||||
<Button
|
||||
type={props.button.main.type}
|
||||
disabled={!!props.button.main.disabled}
|
||||
variant={props.button.main.variant ?? "default"}
|
||||
size={props.button.main.size ?? "default"}
|
||||
@@ -108,7 +65,8 @@ export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
||||
if (!props.button.main.disabled) setIsConfirming(true);
|
||||
}}
|
||||
>
|
||||
{props.button.main.icon}
|
||||
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
|
||||
{props.button.main.icon}
|
||||
{props.button.main.text && <span>{props.button.main.text}</span>}
|
||||
</Button>
|
||||
</span>
|
||||
|
||||
@@ -109,7 +109,6 @@ export const ButtonWithLoading = ({
|
||||
{isPending && <Loader2 className="mr-2 animate-spin" size={16} />}
|
||||
{children && children}
|
||||
<>{icon ? icon : null}</>
|
||||
{/*{icon && <span className="ml-2">{icon}</span>}*/}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import {cn} from "@/lib/utils";
|
||||
|
||||
export type ConnectionCircleProps = {
|
||||
date?: Date | null;
|
||||
};
|
||||
|
||||
export const ConnectionCircle = ({date}: ConnectionCircleProps) => {
|
||||
let style = "bg-gray-300 border-gray-400";
|
||||
|
||||
if (date instanceof Date && !isNaN(date.getTime())) {
|
||||
const now = Date.now();
|
||||
const timestamp = date.getTime();
|
||||
const interval_seconds = (now - timestamp) / 1000;
|
||||
|
||||
if (interval_seconds < 55) {
|
||||
style = "bg-green-400 border-green-600";
|
||||
} else if (interval_seconds <= 60) {
|
||||
style = "bg-orange-400 border-orange-600";
|
||||
} else {
|
||||
style = "bg-red-400 border-red-600";
|
||||
}
|
||||
} else {
|
||||
console.warn("Invalid date passed to ConnectionCircle:", date);
|
||||
}
|
||||
|
||||
return <div className={cn("w-5 h-5 rounded-full border-4", style)}/>;
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import {cn} from "@/lib/utils";
|
||||
|
||||
export type ConnectionIndicatorProps = {
|
||||
date?: Date | null;
|
||||
};
|
||||
|
||||
export const ConnectionIndicator = ({date}: ConnectionIndicatorProps) => {
|
||||
let style = "bg-gray-300";
|
||||
|
||||
if (date instanceof Date && !isNaN(date.getTime())) {
|
||||
const intervalSeconds = (Date.now() - date.getTime()) / 1000;
|
||||
|
||||
if (intervalSeconds < 55) {
|
||||
style = "bg-green-500";
|
||||
} else if (intervalSeconds <= 60) {
|
||||
style = "bg-orange-400";
|
||||
} else {
|
||||
style = "bg-red-500";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-3 h-3">
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
"absolute -inset-0.25 rounded-full opacity-60 animate-ping",
|
||||
style
|
||||
)}
|
||||
style={{
|
||||
animationDuration: "2s",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-3 h-3 rounded-full shadow-sm animate-pulse",
|
||||
style
|
||||
)}
|
||||
style={{
|
||||
animationDuration: "2s",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +1,22 @@
|
||||
import Link from "next/link";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {Plus} from "lucide-react";
|
||||
import {forwardRef} from "react";
|
||||
|
||||
type EmptyStatePlaceholderProps = {
|
||||
url?: string;
|
||||
onClick?: () => void;
|
||||
text: string;
|
||||
className?: string;
|
||||
};
|
||||
} & React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const EmptyStatePlaceholder = ({
|
||||
export const EmptyStatePlaceholder = forwardRef<HTMLDivElement, EmptyStatePlaceholderProps>(({
|
||||
url,
|
||||
onClick,
|
||||
text,
|
||||
className,
|
||||
}: EmptyStatePlaceholderProps) => {
|
||||
...props
|
||||
}, ref) => {
|
||||
const Container = (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -37,5 +39,7 @@ export const EmptyStatePlaceholder = ({
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={cn(className)}>{Container}</div>;
|
||||
};
|
||||
return <div className={cn(className)} ref={ref} {...props}>{Container}</div>;
|
||||
});
|
||||
|
||||
EmptyStatePlaceholder.displayName = "EmptyStatePlaceholder";
|
||||
@@ -15,7 +15,7 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
|
||||
* Variants for the multi-select component to handle different styles.
|
||||
* Uses class-variance-authority (cva) to define different styles based on "variant" prop.
|
||||
*/
|
||||
const multiSelectVariants = cva("m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300", {
|
||||
const multiSelectVariants = cva("m-1 transition ease-in-out duration-300", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-foreground/10 text-foreground bg-card hover:bg-card/80",
|
||||
@@ -177,13 +177,13 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
return (
|
||||
<Badge
|
||||
key={value}
|
||||
className={cn(isAnimating ? "animate-bounce" : "", multiSelectVariants({ variant }))}
|
||||
className={cn(isAnimating ? "animate-bounce" : "", "max-w-full", multiSelectVariants({ variant }))}
|
||||
style={{ animationDuration: `${animation}s` }}
|
||||
>
|
||||
{IconComponent && <IconComponent className="h-4 w-4 mr-2" />}
|
||||
{option?.label}
|
||||
<span className="truncate">{option?.label}</span>
|
||||
<XCircle
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
className="ml-2 h-4 w-4 cursor-pointer shrink-0"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
toggleOption(value);
|
||||
@@ -196,6 +196,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
<Badge
|
||||
className={cn(
|
||||
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
|
||||
"max-w-full",
|
||||
isAnimating ? "animate-bounce" : "",
|
||||
multiSelectVariants({ variant })
|
||||
)}
|
||||
@@ -203,7 +204,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
>
|
||||
{`+ ${selectedValues.length - maxCount} more`}
|
||||
<XCircle
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
className="ml-2 h-4 w-4 cursor-pointer shrink-0"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
clearExtraOptions();
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
"use client"
|
||||
|
||||
import {Pencil, Plus} from "lucide-react";
|
||||
|
||||
import {
|
||||
Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger
|
||||
} from "@/components/ui/dialog";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {useIsMobile} from "@/hooks/use-mobile";
|
||||
import {useEffect, useState} from "react";
|
||||
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 {
|
||||
ChannelOrganisationForm
|
||||
} from "@/components/wrappers/dashboard/admin/channels/organization/channels-organization-form";
|
||||
|
||||
type ChannelAddModalProps = {
|
||||
channel?: NotificationChannelWith | StorageChannelWith
|
||||
organization?: OrganizationWithMembers;
|
||||
open?: boolean;
|
||||
onOpenChangeAction?: (open: boolean) => void;
|
||||
adminView?: boolean;
|
||||
organizations?: OrganizationWithMembers[]
|
||||
trigger?: boolean;
|
||||
kind: ChannelKind;
|
||||
}
|
||||
|
||||
|
||||
export const ChannelAddEditModal = ({
|
||||
organization,
|
||||
channel,
|
||||
open = false,
|
||||
onOpenChangeAction,
|
||||
adminView,
|
||||
organizations,
|
||||
trigger = true,
|
||||
kind
|
||||
}: ChannelAddModalProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const [openInternal, setOpen] = useState(open);
|
||||
const isLocalSystem = channel?.provider == "local";
|
||||
|
||||
const isCreate = !Boolean(channel);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(open);
|
||||
}, [open])
|
||||
|
||||
|
||||
const channelText = getChannelTextBasedOnKind(kind)
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={openInternal} onOpenChange={(state) => {
|
||||
onOpenChangeAction?.(state);
|
||||
setOpen(state);
|
||||
}}>
|
||||
{trigger && (
|
||||
<DialogTrigger asChild>
|
||||
{isCreate ?
|
||||
<Button>
|
||||
<Plus/>{!isMobile && `Add ${channelText} channel`}
|
||||
</Button>
|
||||
:
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<Pencil className="h-4 w-4"/>
|
||||
</Button>
|
||||
}
|
||||
</DialogTrigger>
|
||||
)}
|
||||
<DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle> {isCreate ? "Add" : "Edit"} {channelText} Channel</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure your {channelText.toLowerCase()} channel preferences.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<>
|
||||
{!isLocalSystem ? (
|
||||
<>
|
||||
{adminView ?
|
||||
<Tabs className="flex flex-col flex-1" defaultValue="configuration">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="configuration">Configuration</TabsTrigger>
|
||||
<TabsTrigger value="organizations">Organizations</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-full justify-between" value="configuration">
|
||||
<ChannelForm
|
||||
kind={kind}
|
||||
adminView={adminView}
|
||||
defaultValues={channel}
|
||||
organization={organization}
|
||||
onSuccessAction={() => {
|
||||
onOpenChangeAction?.(false)
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent className="h-full justify-between" value="organizations">
|
||||
<ChannelOrganisationForm
|
||||
defaultValues={channel}
|
||||
kind={kind}
|
||||
organizations={organizations}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
:
|
||||
<>
|
||||
<ChannelForm
|
||||
kind={kind}
|
||||
adminView={adminView}
|
||||
defaultValues={channel}
|
||||
organization={organization}
|
||||
onSuccessAction={() => {
|
||||
onOpenChangeAction?.(false)
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
)
|
||||
|
||||
:
|
||||
|
||||
<>
|
||||
<ChannelOrganisationForm
|
||||
defaultValues={channel}
|
||||
kind={kind}
|
||||
organizations={organizations}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
</>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
+18
-7
@@ -5,20 +5,31 @@ import {useRouter} from "next/navigation";
|
||||
import {Trash2} from "lucide-react";
|
||||
import {
|
||||
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 {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 = {
|
||||
notificationChannelId: string;
|
||||
export type DeleteChannelButtonProps = {
|
||||
channelId: string;
|
||||
kind: ChannelKind;
|
||||
organizationId?: string;
|
||||
};
|
||||
|
||||
export const DeleteNotifierButton = ({notificationChannelId, organizationId}: DeleteNotifierButtonProps) => {
|
||||
export const DeleteChannelButton = ({channelId, organizationId, kind}: DeleteChannelButtonProps) => {
|
||||
const router = useRouter();
|
||||
const channelText = getChannelTextBasedOnKind(kind)
|
||||
|
||||
|
||||
const mutation = useMutation({
|
||||
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;
|
||||
|
||||
if (inner?.success) {
|
||||
@@ -32,8 +43,8 @@ export const DeleteNotifierButton = ({notificationChannelId, organizationId}: De
|
||||
|
||||
return (
|
||||
<ButtonWithConfirm
|
||||
title="Delete Notifier"
|
||||
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 !"
|
||||
title={`Delete ${channelText.toLowerCase()} 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={{
|
||||
main: {
|
||||
size: "icon",
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
"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 isLocalSystem = channel.provider == "local";
|
||||
|
||||
|
||||
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 (
|
||||
<>
|
||||
|
||||
{!isLocalSystem && (
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import {Card} from "@/components/ui/card";
|
||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {truncateWords} from "@/utils/text";
|
||||
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 ChannelCardProps = {
|
||||
data: NotificationChannelWith | StorageChannelWith;
|
||||
organization?: OrganizationWithMembers;
|
||||
organizations?: OrganizationWithMembers[];
|
||||
adminView?: boolean;
|
||||
kind?: ChannelKind;
|
||||
defaultStorageChannelId?: string | null | undefined
|
||||
|
||||
};
|
||||
|
||||
|
||||
export const ChannelCard = (props: ChannelCardProps) => {
|
||||
const {data, organization, kind, adminView, defaultStorageChannelId} = props;
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const isDefaultSystemStorage = defaultStorageChannelId === data.id;
|
||||
|
||||
const isOwned = data.organizationId ? true : !organization;
|
||||
const isLocalSystem = data.provider == "local";
|
||||
|
||||
return (
|
||||
<div className="block transition-all duration-200 rounded-xl">
|
||||
<Card className="flex flex-row justify-between p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="flex h-10 w-10 items-center justify-center rounded-md bg-secondary border border-border">
|
||||
{getChannelIcon(data.provider)}
|
||||
</div>
|
||||
<div className={`h-2 w-2 rounded-full ${data.enabled ? "bg-green-600" : "bg-muted"}`}/>
|
||||
</div>
|
||||
<div className="flex justify-start w-full">
|
||||
<div className="flex flex-col items-start md:flex-row md:items-center gap-2 ">
|
||||
<h3 className="font-medium text-foreground">{isMobile ? truncateWords(data.name, 2) : data.name}</h3>
|
||||
<Badge variant="secondary" className="text-xs font-mono">
|
||||
{data.provider}
|
||||
</Badge>
|
||||
{isDefaultSystemStorage && (
|
||||
<Badge variant="secondary" className="text-xs font-mono">
|
||||
default
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{kind && (
|
||||
<div className="flex items-center gap-2">
|
||||
{(isOwned) && (
|
||||
<>
|
||||
<EditChannelButton
|
||||
organizations={props.organizations}
|
||||
adminView={props.adminView}
|
||||
organization={organization}
|
||||
channel={data}
|
||||
kind={kind}
|
||||
/>
|
||||
{!isLocalSystem && (
|
||||
<DeleteChannelButton
|
||||
kind={kind}
|
||||
organizationId={organization?.id}
|
||||
channelId={data.id}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</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", "google-drive"], {required_error: "Provider is required"}),
|
||||
});
|
||||
|
||||
|
||||
export type NotificationChannelFormType = z.infer<typeof NotificationChannelFormSchema>;
|
||||
export type StorageChannelFormType = z.infer<typeof StorageChannelFormSchema>;
|
||||
+96
-78
@@ -5,61 +5,50 @@ import {useMutation} from "@tanstack/react-query";
|
||||
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {Input} from "@/components/ui/input";
|
||||
|
||||
import {
|
||||
NotificationChannelFormSchema, NotificationChannelFormType
|
||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.schema";
|
||||
import {
|
||||
addNotificationChannelAction, updateNotificationChannelAction
|
||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.action";
|
||||
addStorageChannelAction, updateStorageChannelAction
|
||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/action";
|
||||
import {toast} from "sonner";
|
||||
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 {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 {notificationTypes} from "@/components/wrappers/dashboard/admin/notifications/helpers";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {Card} from "@/components/ui/card";
|
||||
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 {storageProviders} from "@/components/wrappers/dashboard/admin/channels/helpers/storage";
|
||||
import {notificationProviders} 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 = {
|
||||
onSuccessAction?: () => void;
|
||||
organization?: OrganizationWithMembers;
|
||||
defaultValues?: NotificationChannelWith
|
||||
defaultValues?: NotificationChannelWith | StorageChannelWith
|
||||
adminView?: boolean
|
||||
kind: ChannelKind
|
||||
};
|
||||
|
||||
export const NotifierForm = ({onSuccessAction, organization, defaultValues}: NotifierFormProps) => {
|
||||
export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}: NotifierFormProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
const isCreate = !Boolean(defaultValues);
|
||||
|
||||
|
||||
const form = useZodForm({
|
||||
schema: NotificationChannelFormSchema,
|
||||
schema: kind == "notification" ? NotificationChannelFormSchema : StorageChannelFormSchema,
|
||||
// @ts-ignore
|
||||
defaultValues: {...defaultValues},
|
||||
});
|
||||
@@ -69,16 +58,28 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
||||
}, [defaultValues]);
|
||||
|
||||
const mutationAddNotificationChannel = useMutation({
|
||||
mutationFn: async (values: NotificationChannelFormType) => {
|
||||
mutationFn: async (values: NotificationChannelFormType | StorageChannelFormType) => {
|
||||
|
||||
const payload = {
|
||||
data: values,
|
||||
...(organization && {organizationId: organization.id}),
|
||||
...((defaultValues && {notificationChannelId: defaultValues.id}))
|
||||
...((defaultValues && {id: defaultValues.id}))
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const result = isCreate ? await addNotificationChannelAction(payload) : await updateNotificationChannelAction(payload);
|
||||
|
||||
let result: any;
|
||||
|
||||
if (kind === "notification") {
|
||||
// @ts-ignore
|
||||
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;
|
||||
|
||||
if (inner?.success) {
|
||||
@@ -93,29 +94,67 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
||||
});
|
||||
|
||||
const provider = form.watch("provider");
|
||||
const selectedProviderDetails = notificationTypes.find(t => t.value === provider);
|
||||
const channelTypes = kind == "notification" ? notificationProviders : storageProviders
|
||||
const selectedProviderDetails = channelTypes.find(t => t.value === provider);
|
||||
|
||||
if (isCreate && !provider) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4 py-4">
|
||||
{notificationTypes.map((type) => {
|
||||
{channelTypes.filter(p => p.value != "local").map((type) => {
|
||||
const Icon = type.icon;
|
||||
return (
|
||||
<Card
|
||||
key={type.value}
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-center gap-3 p-4 cursor-pointer hover:bg-accent/50 hover:border-primary/50 transition-all",
|
||||
"relative flex flex-col items-center justify-center gap-3 p-4 transition-all border",
|
||||
type.preview
|
||||
? "cursor-not-allowed bg-muted/60 border-border text-muted-foreground"
|
||||
: "cursor-pointer bg-background hover:bg-accent/40 hover:border-primary/50"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (type.preview) return;
|
||||
form.setValue("provider", type.value as any);
|
||||
form.setValue("config", {});
|
||||
}}
|
||||
>
|
||||
<div className="h-10 w-10 bg-secondary rounded-full flex items-center justify-center">
|
||||
<Icon className="h-6 w-6 text-foreground"/>
|
||||
|
||||
{type.preview && (
|
||||
<div
|
||||
className="absolute bottom-0 right-0 overflow-hidden w-20 h-20 pointer-events-none">
|
||||
<div
|
||||
className="absolute bottom-0 right-0 w-38 h-5 flex items-center justify-center
|
||||
transform -rotate-45 translate-x-14 -translate-y-4"
|
||||
style={{backgroundColor: "#FE6702"}}
|
||||
>
|
||||
<span className="text-white text-[7px] pr-3 font-medium text-center w-full">COMING SOON</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full flex items-center justify-center transition-colors",
|
||||
type.preview
|
||||
? "bg-muted"
|
||||
: "bg-accent text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={cn(
|
||||
"h-6 w-6",
|
||||
type.preview ? "text-muted-foreground" : "text-foreground"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-medium text-sm">{type.label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium text-sm text-center leading-tight",
|
||||
type.preview ? "text-muted-foreground" : "text-foreground"
|
||||
)}
|
||||
>
|
||||
{type.label}
|
||||
</span>
|
||||
</Card>
|
||||
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -132,7 +171,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">
|
||||
{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"/>
|
||||
</div>
|
||||
)}
|
||||
@@ -163,7 +203,8 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
||||
<FormItem>
|
||||
<FormLabel>Channel Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder={`My ${selectedProviderDetails?.label} Channel`} value={field.value ?? ""}/>
|
||||
<Input {...field} placeholder={`My ${selectedProviderDetails?.label} Channel`}
|
||||
value={field.value ?? ""}/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
@@ -174,44 +215,20 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
||||
control={form.control}
|
||||
name="provider"
|
||||
render={({field}) => (
|
||||
<input type="hidden" {...field} value={field.value || ""} />
|
||||
<input type="hidden" {...field} value={field.value || ""}/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{provider === "smtp" && (
|
||||
<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}/>
|
||||
)}
|
||||
|
||||
{renderChannelForm(provider, form)}
|
||||
|
||||
<div className="flex justify-between mt-4">
|
||||
<div>
|
||||
{defaultValues && (
|
||||
<NotifierTestChannelButton organizationId={organization?.id}
|
||||
notificationChannel={defaultValues}/>
|
||||
<ChannelTestButton
|
||||
kind={kind}
|
||||
organizationId={organization?.id}
|
||||
channel={defaultValues}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -233,4 +250,5 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
"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";
|
||||
import type {StorageInput} from "@/features/storages/types";
|
||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||
|
||||
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, check your configuration");
|
||||
}
|
||||
} else if (kind === "storage") {
|
||||
const input: StorageInput = {
|
||||
action: "ping",
|
||||
};
|
||||
const result = await dispatchStorage(input, undefined, channel.id);
|
||||
|
||||
if (result.success) {
|
||||
toast.success("Successfully connected to storage channel");
|
||||
} else {
|
||||
console.error(result);
|
||||
toast.error("An error occurred while testing the storage channel, check your configuration");
|
||||
}
|
||||
} 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>
|
||||
)
|
||||
}
|
||||
+9
-9
@@ -6,11 +6,11 @@ import * as drizzleDb from "@/db";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||
import {db} from "@/db";
|
||||
import {
|
||||
NotificationChannelFormSchema
|
||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.schema";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {
|
||||
NotificationChannelFormSchema
|
||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.schema";
|
||||
|
||||
|
||||
export const addNotificationChannelAction = userAction.schema(
|
||||
@@ -20,7 +20,6 @@ export const addNotificationChannelAction = userAction.schema(
|
||||
})
|
||||
).action(async ({parsedInput}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||
const {organizationId, data} = parsedInput;
|
||||
|
||||
try {
|
||||
const [channel] = await db
|
||||
.insert(drizzleDb.schemas.notificationChannel)
|
||||
@@ -29,6 +28,7 @@ export const addNotificationChannelAction = userAction.schema(
|
||||
name: data.name,
|
||||
config: data.config,
|
||||
enabled: data.enabled ?? true,
|
||||
organizationId: organizationId ?? null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -128,11 +128,11 @@ export const removeNotificationChannelAction = userAction.schema(
|
||||
|
||||
export const updateNotificationChannelAction = userAction.schema(
|
||||
z.object({
|
||||
notificationChannelId: z.string(),
|
||||
id: z.string(),
|
||||
data: NotificationChannelFormSchema
|
||||
})
|
||||
).action(async ({parsedInput}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||
const {notificationChannelId, data} = parsedInput;
|
||||
const {id, data} = parsedInput;
|
||||
|
||||
try {
|
||||
const [channel] = await db
|
||||
@@ -143,7 +143,7 @@ export const updateNotificationChannelAction = userAction.schema(
|
||||
config: data.config,
|
||||
enabled: data.enabled ?? true,
|
||||
}))
|
||||
.where(eq(drizzleDb.schemas.notificationChannel.id, notificationChannelId))
|
||||
.where(eq(drizzleDb.schemas.notificationChannel.id, id))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
@@ -154,7 +154,7 @@ export const updateNotificationChannelAction = userAction.schema(
|
||||
},
|
||||
actionSuccess: {
|
||||
message: `Notification channel "${channel.name}" has been successfully updated.`,
|
||||
messageParams: {notificationChannelId: channel.id},
|
||||
messageParams: {id: channel.id},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -165,7 +165,7 @@ export const updateNotificationChannelAction = userAction.schema(
|
||||
message: "Failed to update notification channel.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {notificationChannelId: ""},
|
||||
messageParams: {id: ""},
|
||||
},
|
||||
};
|
||||
}
|
||||
+33
-1
@@ -3,7 +3,6 @@ import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/compon
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
|
||||
|
||||
type NotifierNtfyFormProps = {
|
||||
form: UseFormReturn<any, any, any>
|
||||
}
|
||||
@@ -12,6 +11,7 @@ export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
|
||||
return (
|
||||
<>
|
||||
<Separator className="my-1"/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.ntfyTopic"
|
||||
@@ -25,6 +25,7 @@ export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.ntfyServerUrl"
|
||||
@@ -39,6 +40,7 @@ export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.ntfyToken"
|
||||
@@ -53,6 +55,36 @@ export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Separator className="my-1"/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.ntfyUsername"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Basic Auth Username (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="username"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.ntfyPassword"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Basic Auth Password (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="password" placeholder="password"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
"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,
|
||||
organizationId: organizationId ?? null
|
||||
})
|
||||
.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.organizationStorageChannel)
|
||||
.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: ""},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
import {UseFormReturn} from "react-hook-form";
|
||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {PasswordInput} from "@/components/ui/password-input";
|
||||
import {
|
||||
googleDriveRefreshTokenAction
|
||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive/helpers";
|
||||
import {toast} from "sonner";
|
||||
|
||||
type StorageGoogleDriveFormProps = {
|
||||
form: UseFormReturn<any>;
|
||||
};
|
||||
|
||||
export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
|
||||
const refreshToken = form.watch("config.refreshToken");
|
||||
const isConnected = Boolean(refreshToken);
|
||||
|
||||
const handleConnect = () => {
|
||||
const clientId = form.getValues("config.clientId");
|
||||
const clientSecret = form.getValues("config.clientSecret");
|
||||
const redirectUri = `${window.location.origin}/api/google/drive/callback`;
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
form.setError("config.clientId", {message: "Client ID and Secret are required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = encodeURIComponent("https://www.googleapis.com/auth/drive.file");
|
||||
const oauthUrl =
|
||||
`https://accounts.google.com/o/oauth2/v2/auth` +
|
||||
`?client_id=${clientId}` +
|
||||
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
||||
`&response_type=code` +
|
||||
`&scope=${scope}` +
|
||||
`&access_type=offline` +
|
||||
`&prompt=consent`;
|
||||
|
||||
const oauthWindow = window.open(oauthUrl, "_blank", "width=500,height=600");
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
if (!oauthWindow || oauthWindow.closed) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
if (oauthWindow.location.href.startsWith(redirectUri)) {
|
||||
const code = new URL(oauthWindow.location.href).searchParams.get("code");
|
||||
if (!code) return;
|
||||
|
||||
const result = await googleDriveRefreshTokenAction({
|
||||
code,
|
||||
clientId,
|
||||
clientSecret,
|
||||
redirectUri,
|
||||
});
|
||||
const inner = result?.data;
|
||||
|
||||
if (inner?.success) {
|
||||
toast.success(inner.actionSuccess?.message);
|
||||
|
||||
if (!inner.value) {
|
||||
form.setError("config", {message: "OAuth succeeded but no refresh token returned"});
|
||||
return;
|
||||
}
|
||||
|
||||
form.setValue("config.refreshToken", inner.value, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
} else {
|
||||
toast.error(inner?.actionError?.message);
|
||||
}
|
||||
|
||||
|
||||
oauthWindow.close();
|
||||
clearInterval(interval);
|
||||
}
|
||||
} catch {
|
||||
// Ignore cross-origin errors until redirect
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Separator className="my-1"/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.clientId"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="xxxx.apps.googleusercontent.com"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.clientSecret"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client Secret</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.folderId"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Folder ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<input type="hidden" {...form.register("config.refreshToken")} />
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button type="button" onClick={handleConnect}>
|
||||
{isConnected ? "Reconnect Google Drive" : "Connect Google Drive"}
|
||||
</Button>
|
||||
|
||||
{isConnected && (
|
||||
<span className="text-sm text-green-600 font-medium">
|
||||
Google Drive connected
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
"use server"
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
|
||||
|
||||
export const googleDriveRefreshTokenAction = userAction.schema(
|
||||
z.object({
|
||||
code: z.string(),
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
redirectUri: z.string(),
|
||||
})).action(async ({parsedInput}): Promise<ServerActionResult<string>> => {
|
||||
const {code, clientId, clientSecret, redirectUri} = parsedInput;
|
||||
|
||||
try {
|
||||
|
||||
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code: code,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: redirectUri,
|
||||
}),
|
||||
});
|
||||
|
||||
const tokens = await tokenRes.json();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: tokens.refresh_token,
|
||||
actionSuccess: {
|
||||
message: "Refresh token successfully fetched",
|
||||
messageParams: {code: code},
|
||||
},
|
||||
};
|
||||
}catch {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "An error occurred",
|
||||
status: 404,
|
||||
messageParams: {code: code},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import {UseFormReturn} from "react-hook-form";
|
||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import {PasswordInput} from "@/components/ui/password-input";
|
||||
import {Switch} from "@/components/ui/switch";
|
||||
|
||||
|
||||
type StorageS3FormProps = {
|
||||
form: UseFormReturn<any, any, any>
|
||||
}
|
||||
|
||||
export const StorageS3Form = ({form}: StorageS3FormProps) => {
|
||||
return (
|
||||
<>
|
||||
<Separator className="my-1"/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.endPointUrl"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Endpoind URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="s3.exemple.com"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.accessKey"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Access Key</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="A1b2C3d4E5f6G7h"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.secretKey"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Secret Key</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} placeholder="A1b2C3d4E5f6G7h"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.bucketName"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bucket name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="portabase-dev"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.port"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="number" placeholder="443" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.useSSL"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Use SSL</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
"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,
|
||||
defaultStorageChannelId?: string | null | undefined
|
||||
}
|
||||
|
||||
export const ChannelsSection = ({
|
||||
organizations,
|
||||
channels,
|
||||
kind,
|
||||
defaultStorageChannelId
|
||||
}: 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}
|
||||
defaultStorageChannelId={defaultStorageChannelId}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyStatePlaceholder
|
||||
text={`No ${channelText} channels configured yet`}
|
||||
onClick={() => {
|
||||
setIsAddModalOpen(true)
|
||||
}}
|
||||
className="h-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
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 {
|
||||
notificationProviders,
|
||||
} from "@/components/wrappers/dashboard/admin/channels/helpers/notification";
|
||||
import {storageProviders} from "@/components/wrappers/dashboard/admin/channels/helpers/storage";
|
||||
import {ForwardRefExoticComponent, JSX, RefAttributes, SVGProps} from "react";
|
||||
import {LucideProps} from "lucide-react";
|
||||
import {
|
||||
StorageS3Form
|
||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/s3.form";
|
||||
import {
|
||||
StorageGoogleDriveForm
|
||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive.form";
|
||||
|
||||
export type ChannelKind = "notification" | "storage";
|
||||
|
||||
export function getChannelTextBasedOnKind(kind: ChannelKind) {
|
||||
switch (kind) {
|
||||
case "notification":
|
||||
return "Notification";
|
||||
case "storage":
|
||||
return "Storage";
|
||||
default:
|
||||
return "Notification";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type ProviderIconTypes = {
|
||||
value: string
|
||||
label: string
|
||||
icon: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>
|
||||
preview?: boolean
|
||||
} | {
|
||||
value: string
|
||||
label: string
|
||||
icon: (props: SVGProps<SVGSVGElement>) => JSX.Element
|
||||
preview?: boolean
|
||||
}
|
||||
|
||||
export const providerIcons: ProviderIconTypes[] = [
|
||||
...notificationProviders,
|
||||
...storageProviders,
|
||||
];
|
||||
|
||||
|
||||
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 "s3":
|
||||
return <StorageS3Form form={form}/>
|
||||
case "google-drive":
|
||||
return <StorageGoogleDriveForm form={form}/>
|
||||
case "local":
|
||||
return <></>
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,532 @@
|
||||
import {Mail} from "lucide-react";
|
||||
import type { SVGProps } from 'react';
|
||||
import {ProviderIconTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||
|
||||
|
||||
|
||||
|
||||
export const notificationProviders: ProviderIconTypes[] = [
|
||||
{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},
|
||||
{value: "microsoft-teams", label: "Microsoft Teams", icon: MSTeamsIcon, preview: true}
|
||||
]
|
||||
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function MSTeamsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (<svg xmlns="http://www.w3.org/2000/svg" width={256} height={256} viewBox="0 0 256 239" {...props}>
|
||||
<defs>
|
||||
<path id="logosMicrosoftTeams0"
|
||||
d="M136.93 64.476v12.8a32.674 32.674 0 0 1-5.953-.952a38.698 38.698 0 0 1-26.79-22.742h21.848c6.008.022 10.872 4.887 10.895 10.894Z"
|
||||
opacity=".2"/>
|
||||
</defs>
|
||||
<defs>
|
||||
<linearGradient id="logosMicrosoftTeams1" x1="17.372%" x2="82.628%" y1="-6.51%" y2="106.51%">
|
||||
<stop offset="0%" stop-color="#5A62C3"/>
|
||||
<stop offset="50%" stop-color="#4D55BD"/>
|
||||
<stop offset="100%" stop-color="#3940AB"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="#5059C9"
|
||||
d="M178.563 89.302h66.125c6.248 0 11.312 5.065 11.312 11.312v60.231c0 22.96-18.613 41.574-41.573 41.574h-.197c-22.96.003-41.576-18.607-41.579-41.568V95.215a5.912 5.912 0 0 1 5.912-5.913Z"/>
|
||||
<circle cx="223.256" cy="50.605" r="26.791" fill="#5059C9"/>
|
||||
<circle cx="139.907" cy="38.698" r="38.698" fill="#7B83EB"/>
|
||||
<path fill="#7B83EB"
|
||||
d="M191.506 89.302H82.355c-6.173.153-11.056 5.276-10.913 11.449v68.697c-.862 37.044 28.445 67.785 65.488 68.692c37.043-.907 66.35-31.648 65.489-68.692v-68.697c.143-6.173-4.74-11.296-10.913-11.449Z"/>
|
||||
<path
|
||||
d="M142.884 89.302v96.268a10.96 10.96 0 0 1-6.787 10.062c-1.3.55-2.697.833-4.108.833H76.68c-.774-1.965-1.488-3.93-2.084-5.953a72.509 72.509 0 0 1-3.155-21.076v-68.703c-.143-6.163 4.732-11.278 10.895-11.43h60.547Z"
|
||||
opacity=".1"/>
|
||||
<path
|
||||
d="M136.93 89.302v102.222c0 1.411-.283 2.808-.833 4.108a10.96 10.96 0 0 1-10.062 6.787H79.48c-1.012-1.965-1.965-3.93-2.798-5.954a59.049 59.049 0 0 1-2.084-5.953a72.508 72.508 0 0 1-3.155-21.076v-68.703c-.143-6.163 4.732-11.278 10.895-11.43h54.593Z"
|
||||
opacity=".2"/>
|
||||
<path
|
||||
d="M136.93 89.302v90.315c-.045 5.998-4.896 10.85-10.895 10.895H74.597a72.508 72.508 0 0 1-3.155-21.076v-68.703c-.143-6.163 4.732-11.278 10.895-11.43h54.593Z"
|
||||
opacity=".2"/>
|
||||
<path
|
||||
d="M130.977 89.302v90.315c-.046 5.998-4.897 10.85-10.895 10.895H74.597a72.508 72.508 0 0 1-3.155-21.076v-68.703c-.143-6.163 4.732-11.278 10.895-11.43h48.64Z"
|
||||
opacity=".2"/>
|
||||
<path
|
||||
d="M142.884 58.523v18.753c-1.012.06-1.965.12-2.977.12c-1.012 0-1.965-.06-2.977-.12a32.674 32.674 0 0 1-5.953-.952a38.698 38.698 0 0 1-26.791-22.742a33.082 33.082 0 0 1-1.905-5.954h29.708c6.007.023 10.872 4.887 10.895 10.895Z"
|
||||
opacity=".1"/>
|
||||
<use href="#logosMicrosoftTeams0" opacity=".2"/>
|
||||
<use href="#logosMicrosoftTeams0" opacity=".2"/>
|
||||
<path d="M130.977 64.476v11.848a38.698 38.698 0 0 1-26.791-22.743h15.896c6.008.023 10.872 4.888 10.895 10.895Z"
|
||||
opacity=".2"/>
|
||||
<path fill="url(#logosMicrosoftTeams1)"
|
||||
d="M10.913 53.581h109.15c6.028 0 10.914 4.886 10.914 10.913v109.151c0 6.027-4.886 10.913-10.913 10.913H10.913C4.886 184.558 0 179.672 0 173.645V64.495C0 58.466 4.886 53.58 10.913 53.58Z"/>
|
||||
<path fill="#FFF" d="M94.208 95.125h-21.82v59.416H58.487V95.125H36.769V83.599h57.439z"/>
|
||||
</svg>)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import {Server} from "lucide-react";
|
||||
import type {SVGProps} from "react";
|
||||
import {ProviderIconTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||
|
||||
export const storageProviders: ProviderIconTypes[] = [
|
||||
{value: "local", label: "Local", icon: Server},
|
||||
{value: "s3", label: "S3", icon: S3Icon},
|
||||
{value: "google-drive", label: "Google Drive", icon: GoogleDriveIcon},
|
||||
{value: "blob", label: "Azure Blob Storage", icon: BlobIcon, preview: true},
|
||||
{value: "gcs", label: "Google Cloud Storage", icon: GCSIcon, preview: true},
|
||||
]
|
||||
|
||||
export function S3Icon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={256} height={199} {...props} viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="M13.207.006a2.16 2.16 0 0 0-1.62.582a2.15 2.15 0 0 0-.095 3.035l3.408 3.55a3.042 3.042 0 0 1-.663 4.688l-.463.239V7.285a15.42 15.42 0 0 0-8.018 10.487v.017l6.549-3.328v7.621L13.779 24V13.682l.897-.463a4.443 4.443 0 0 0 1.22-7.03l-3.37-3.525a.75.75 0 0 1 .037-1.055a.75.75 0 0 1 1.056.038l.467.486l-.006.006l4.07 4.244a.057.057 0 0 0 .082 0a.06.06 0 0 0 0-.07l-3.14-5.143l-.149.143l.149-.145C14.494.393 13.829.054 13.207.006m-.902 9.865v2.994l-4.152 2.149a14 14 0 0 1 2.767-3.928a14 14 0 0 1 1.385-1.215"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function GoogleDriveIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={256} height={199} {...props} viewBox="0 0 256 229"><path fill="#0066da" d="m19.354 196.034l11.29 19.5c2.346 4.106 5.718 7.332 9.677 9.678q17.009-21.591 23.68-33.137q6.77-11.717 16.641-36.655q-26.604-3.502-40.32-3.502q-13.165 0-40.322 3.502c0 4.545 1.173 9.09 3.519 13.196z"/><path fill="#ea4335" d="M215.681 225.212c3.96-2.346 7.332-5.572 9.677-9.677l4.692-8.064l22.434-38.855a26.57 26.57 0 0 0 3.518-13.196q-27.315-3.502-40.247-3.502q-13.899 0-40.248 3.502q9.754 25.075 16.422 36.655q6.724 11.683 23.752 33.137"/><path fill="#00832d" d="M128.001 73.311q19.68-23.768 27.125-36.655q5.996-10.377 13.196-33.137C164.363 1.173 159.818 0 155.126 0h-54.25C96.184 0 91.64 1.32 87.68 3.519q9.16 26.103 15.544 37.154q7.056 12.213 24.777 32.638"/><path fill="#2684fc" d="M175.36 155.42H80.642l-40.32 69.792c3.958 2.346 8.503 3.519 13.195 3.519h148.968c4.692 0 9.238-1.32 13.196-3.52z"/><path fill="#00ac47" d="M128.001 73.311L87.681 3.52c-3.96 2.346-7.332 5.571-9.678 9.677L3.519 142.224A26.57 26.57 0 0 0 0 155.42h80.642z"/><path fill="#ffba00" d="m215.242 77.71l-37.243-64.514c-2.345-4.106-5.718-7.331-9.677-9.677l-40.32 69.792l47.358 82.109h80.496c0-4.546-1.173-9.09-3.519-13.196z"/></svg>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function BlobIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width={256} height={199} {...props} viewBox="0 0 48 48">
|
||||
<path fill="#2979ff"
|
||||
d="M32.66,7H15.34c-0.715,0-1.375,0.381-1.732,1l-8.66,15c-0.357,0.619-0.357,1.381,0,2l8.66,15c0.357,0.619,1.018,1,1.732,1H32.66c0.715,0,1.375-0.381,1.732-1l8.66-15c0.357-0.619,0.357-1.381,0-2l-8.66-15C34.035,7.381,33.375,7,32.66,7z"></path>
|
||||
<path fill="#fff"
|
||||
d="M32,35H16c-0.552,0-1-0.448-1-1V14c0-0.552,0.448-1,1-1h13l4,4v17C33,34.552,32.552,35,32,35z"></path>
|
||||
<path fill="#b1cfff" d="M29,13v3c0,0.552,0.448,1,1,1h3L29,13z"></path>
|
||||
<path fill="#03a9f4"
|
||||
d="M22.265 23.831H20.93V19.58l-1.312.383v-1.011l2.524-.873h.122V23.831zM28.702 21.48c0 .416-.049.776-.146 1.081-.097.303-.234.556-.411.756-.176.2-.387.349-.632.446-.245.097-.515.146-.81.146-.298 0-.57-.049-.816-.146-.246-.097-.457-.246-.633-.446-.176-.2-.315-.453-.413-.756-.099-.305-.147-.665-.147-1.081v-1.051c0-.413.049-.773.146-1.078.097-.305.234-.558.411-.758.176-.2.387-.349.632-.446C26.126 18.049 26.397 18 26.695 18c.295 0 .565.049.811.146.246.097.458.246.635.446.176.2.313.453.412.758.099.305.149.665.149 1.078V21.48zM27.37 20.26c0-.229-.016-.421-.047-.579-.032-.155-.076-.283-.134-.381-.058-.097-.129-.167-.212-.209-.083-.042-.176-.063-.282-.063-.105 0-.2.021-.283.063-.083.042-.153.112-.209.209-.057.097-.1.225-.13.381-.03.158-.045.35-.045.579v1.383c0 .234.014.431.045.589.03.158.075.286.133.381.058.097.129.166.213.208.084.042.179.063.284.063.103 0 .195-.021.278-.063.083-.042.154-.111.212-.208.058-.095.101-.223.132-.381.03-.158.046-.354.046-.589V20.26zM23.308 29.571c0 .416-.049.776-.146 1.081-.097.303-.234.556-.411.756-.176.2-.387.349-.632.446C21.874 31.951 21.604 32 21.309 32c-.298 0-.57-.049-.816-.146-.246-.097-.457-.246-.633-.446-.176-.2-.315-.453-.413-.756-.099-.305-.147-.665-.147-1.081V28.52c0-.413.049-.773.146-1.078.097-.305.234-.558.411-.758.176-.2.387-.349.632-.446.245-.097.516-.146.814-.146.295 0 .565.049.811.146.246.097.458.246.635.446.176.2.313.453.412.758s.149.665.149 1.078V29.571zM21.977 28.35c0-.229-.016-.421-.047-.579-.032-.155-.076-.283-.134-.381-.058-.097-.129-.167-.212-.209-.083-.042-.176-.063-.282-.063s-.2.021-.283.063c-.083.042-.153.112-.209.209-.057.097-.1.225-.13.381-.03.158-.045.35-.045.579v1.383c0 .234.014.431.045.589.03.158.075.286.133.381.058.097.129.166.213.208s.179.063.284.063c.103 0 .195-.021.278-.063.083-.042.154-.111.212-.208.058-.095.101-.223.132-.381.03-.158.046-.354.046-.589V28.35zM27.659 31.921h-1.335V27.67l-1.312.383v-1.011l2.524-.873h.122V31.921z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function GCSIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
id="standard_product_icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
{...props}
|
||||
>
|
||||
<g id="bounding_box">
|
||||
<rect width="512" height="512" fill="none"/>
|
||||
</g>
|
||||
<g id="art">
|
||||
<path
|
||||
fill="#34a853"
|
||||
d="M442,277.9H70c-8.8,0-16,7.2-16,16v148.1c0,8.8,7.2,16,16,16h107.2c1.5.5,3.1.7,4.8.7s3.3-.3,4.8-.7h255.2c8.8,0,16-7.2,16-16v-148.1c0-8.8-7.2-16-16-16ZM86,309.9h80v116.1h-80v-116.1ZM426,425.9h-228v-116.1h228v116.1Z"
|
||||
/>
|
||||
<path
|
||||
fill="#fbbc04"
|
||||
d="M442,54H70c-8.8,0-16,7.2-16,16v148.8c0,8.8,7.2,16,16,16h372c8.8,0,16-7.2,16-16V70c0-8.8-7.2-16-16-16ZM86,86h80v116.8h-80v-116.8ZM426,202.8h-228v-116.8h228v116.8Z"
|
||||
/>
|
||||
<path
|
||||
fill="#ea4335"
|
||||
d="M442,234.8h-16V86H54v-16c0-8.8,7.2-16,16-16h372c8.8,0,16,7.2,16,16v148.8c0,8.8-7.2,16-16,16Z"
|
||||
/>
|
||||
<path
|
||||
fill="#4285f4"
|
||||
d="M442,457.9h-16v-148.1H54v-16c0-8.8,7.2-16,16-16h372c8.8,0,16,7.2,16,16v148.1c0,8.8-7.2,16-16,16Z"
|
||||
/>
|
||||
<circle fill="#ea4335" cx="349" cy="144.4" r="37"/>
|
||||
<circle fill="#4285f4" cx="349" cy="367.9" r="37"/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
+24
-20
@@ -6,26 +6,30 @@ import {Form, FormControl, FormField, FormItem, useZodForm} from "@/components/u
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {
|
||||
NotificationChannelsOrganizationSchema,
|
||||
NotificationChannelsOrganizationType
|
||||
} from "@/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization.schema";
|
||||
import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
|
||||
import {
|
||||
updateNotificationChannelsOrganizationAction
|
||||
} from "@/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization.action";
|
||||
|
||||
import {toast} from "sonner";
|
||||
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[];
|
||||
defaultValues?: NotificationChannelWith
|
||||
defaultValues?: NotificationChannelWith | StorageChannelWith
|
||||
kind: ChannelKind
|
||||
};
|
||||
|
||||
export const NotifierChannelOrganisationForm = ({
|
||||
organizations,
|
||||
defaultValues,
|
||||
}: NotifierChannelOrganisationFormProps) => {
|
||||
export const ChannelOrganisationForm = ({
|
||||
organizations,
|
||||
defaultValues,
|
||||
kind
|
||||
}: ChannelOrganisationFormProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -33,7 +37,7 @@ export const NotifierChannelOrganisationForm = ({
|
||||
|
||||
|
||||
const form = useZodForm({
|
||||
schema: NotificationChannelsOrganizationSchema,
|
||||
schema: ChannelsOrganizationSchema,
|
||||
// @ts-ignore
|
||||
defaultValues: {
|
||||
organizations: defaultOrganizationIds
|
||||
@@ -49,15 +53,15 @@ export const NotifierChannelOrganisationForm = ({
|
||||
};
|
||||
|
||||
|
||||
const mutationUpdateNotificationChannelOrganizations = useMutation({
|
||||
mutationFn: async (values: NotificationChannelsOrganizationType) => {
|
||||
const mutationUpdateChannelOrganizations = useMutation({
|
||||
mutationFn: async (values: ChannelsOrganizationType) => {
|
||||
|
||||
const payload = {
|
||||
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;
|
||||
|
||||
if (inner?.success) {
|
||||
@@ -76,7 +80,7 @@ export const NotifierChannelOrganisationForm = ({
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutationUpdateNotificationChannelOrganizations.mutateAsync(values);
|
||||
await mutationUpdateChannelOrganizations.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
@@ -100,7 +104,7 @@ export const NotifierChannelOrganisationForm = ({
|
||||
|
||||
<div className="flex justify-end">
|
||||
<div className="flex gap-2 justify-end">
|
||||
<ButtonWithLoading isPending={mutationUpdateNotificationChannelOrganizations.isPending}>
|
||||
<ButtonWithLoading isPending={mutationUpdateChannelOrganizations.isPending}>
|
||||
Save
|
||||
</ButtonWithLoading>
|
||||
</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 {NotificationLogWithRelations} from "@/db/services/notification-log";
|
||||
import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers";
|
||||
import {humanReadableDate} from "@/utils/date-formatting";
|
||||
import {CheckCircle2, XCircle} from "lucide-react";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {NotificationLogModal} from "@/components/wrappers/dashboard/admin/notifications/logs/notification-log-modal";
|
||||
import {getChannelIcon} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||
|
||||
|
||||
export function notificationLogsColumns(): ColumnDef<NotificationLogWithRelations>[] {
|
||||
@@ -33,7 +33,7 @@ export function notificationLogsColumns(): ColumnDef<NotificationLogWithRelation
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="flex h-10 w-10 items-center justify-center rounded-md bg-secondary border border-border">
|
||||
{getNotificationChannelIcon(channel?.provider ?? "")}
|
||||
{getChannelIcon(channel?.provider ?? "")}
|
||||
</div>
|
||||
{channel?.name}
|
||||
</div>
|
||||
@@ -89,7 +89,7 @@ export function notificationLogsColumns(): ColumnDef<NotificationLogWithRelation
|
||||
}
|
||||
|
||||
|
||||
const getStatusIcon = (status: boolean) => {
|
||||
export const getStatusIcon = (status: boolean) => {
|
||||
switch (status) {
|
||||
case true:
|
||||
return <CheckCircle2 className="h-4 w-4"/>
|
||||
@@ -98,10 +98,12 @@ const getStatusIcon = (status: boolean) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
export const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "delivered":
|
||||
return "bg-green-100 dark:bg-green-100/10"
|
||||
case "success":
|
||||
return "bg-green-100 dark:bg-green-100/10"
|
||||
case "failed":
|
||||
return "bg-red-100 dark:bg-red-100/10"
|
||||
case "pending":
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import {Trash2} from "lucide-react";
|
||||
import {toast} from "sonner";
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {deleteOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import {deleteOrganizationAction} from "@/features/organization/organization.action";
|
||||
|
||||
export type ButtonDeleteFleetProps = {
|
||||
text?: string;
|
||||
|
||||
+1
-3
@@ -13,16 +13,14 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {MemberRoleType} from "@/types/common";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
import {updateMemberRoleAction} from "@/components/wrappers/dashboard/settings/update-member.action";
|
||||
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
|
||||
import {
|
||||
updateMemberRoleAdminAction
|
||||
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/role-member.action";
|
||||
import {RoleSchemaMember} from "@/components/wrappers/dashboard/organization/settings/member.schema";
|
||||
|
||||
type OrganizationMemberChangeRoleModalProps = {
|
||||
open: boolean;
|
||||
|
||||
+1
-1
@@ -3,11 +3,11 @@ import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {Member} from "better-auth/plugins";
|
||||
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
|
||||
import {db as dbClient} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {RoleSchemaMember} from "@/components/wrappers/dashboard/organization/settings/member.schema";
|
||||
|
||||
|
||||
export const updateMemberRoleAdminAction = userAction.schema(
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import {Input} from "@/components/ui/input";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {toast} from "sonner";
|
||||
import {OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
import {updateOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import {updateOrganizationAction} from "@/features/organization/organization.action";
|
||||
|
||||
type UpdateOrganizationFormProps = {
|
||||
onSuccessAction?: () => void;
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from "@/components/wrappers/dashboard/admin/settings/email/email-form/email-form.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {sendEmail} from "@/lib/email/email-helper";
|
||||
import {sendEmail} from "@/lib/email";
|
||||
import {render} from "@react-email/components";
|
||||
import EmailSettingsTest from "@/components/emails/email-settings-test";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Download} from "lucide-react";
|
||||
import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action";
|
||||
import {toast} from "sonner";
|
||||
|
||||
export type AdminSettingsTabProps = {};
|
||||
|
||||
export const AdminSettingsSection = (props: AdminSettingsTabProps) => {
|
||||
|
||||
const handleDownloadKey = async () => {
|
||||
|
||||
let url: string = "";
|
||||
const data = await getFileUrlPresignedLocal({dir: "private/keys/", fileName: "server_public.pem"})
|
||||
if (data?.data?.success) {
|
||||
url = data.data.value ?? "";
|
||||
} else {
|
||||
// @ts-ignore
|
||||
const errorMessage = data?.data?.actionError?.message || "Failed to get file!";
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
window.open(url, "_self");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4 h-full py-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Instance settings</CardTitle>
|
||||
<CardDescription>Manage portabase settings</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Download Public Key</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Used for encrypting communications with this instance.
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={handleDownloadKey} variant="outline" size="sm">
|
||||
<Download className="h-4 w-4 mr-2"/>
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useRouter, useSearchParams} from "next/navigation";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {SettingsEmailSection} from "@/components/wrappers/dashboard/admin/settings/email/settings-email-section";
|
||||
import {SettingsStorageSection} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage-section";
|
||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||
|
||||
export type SettingsTabsProps = {
|
||||
settings: Setting
|
||||
storageChannels: StorageChannelWith[]
|
||||
};
|
||||
|
||||
export const SettingsTabs = ({settings, storageChannels}: SettingsTabsProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [tab, setTab] = useState<string>(() => searchParams.get("tab") ?? "email");
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const newTab = searchParams.get("tab") ?? "email";
|
||||
setTab(newTab);
|
||||
}, [searchParams]);
|
||||
|
||||
const handleChangeTab = (value: string) => {
|
||||
router.push(`?tab=${value}`);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="h-full mt-3">
|
||||
<Tabs className="h-full" value={tab} onValueChange={handleChangeTab}>
|
||||
<TabsList className='bg-background rounded-none border-b p-0 min-w-48'>
|
||||
<TabsTrigger
|
||||
value="email"
|
||||
className='bg-background data-[state=active]:border-primary dark:data-[state=active]:border-primary h-full rounded-none border-0 border-b-2 border-transparent data-[state=active]:shadow-none'
|
||||
>
|
||||
Email
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="storage"
|
||||
className='bg-background data-[state=active]:border-primary dark:data-[state=active]:border-primary h-full rounded-none border-0 border-b-2 border-transparent data-[state=active]:shadow-none'
|
||||
>
|
||||
Default storage
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-full" value="email">
|
||||
<SettingsEmailSection settings={settings}/>
|
||||
</TabsContent>
|
||||
<TabsContent className="h-full" value="storage">
|
||||
<SettingsStorageSection storageChannels={storageChannels} settings={settings}/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
|
||||
);
|
||||
};
|
||||
+71
-75
@@ -1,106 +1,102 @@
|
||||
"use client"
|
||||
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
||||
import {Info, ShieldCheck} from "lucide-react";
|
||||
import {Switch} from "@/components/ui/switch";
|
||||
import {Label} from "@/components/ui/label";
|
||||
import {StorageS3Form} from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/storage-s3-form";
|
||||
import {useState} from "react";
|
||||
import {Info} from "lucide-react";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {checkConnexionToS3} from "@/features/upload/public/upload.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {Form, FormField, FormItem, useZodForm} from "@/components/ui/form";
|
||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {
|
||||
DefaultStorageSchema,
|
||||
DefaultStorageType
|
||||
} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage.schema";
|
||||
import {getChannelIcon} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||
import {
|
||||
updateStorageSettingsAction
|
||||
} from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.action";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {S3FormType} from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.schema";
|
||||
} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage.action";
|
||||
import {toast} from "sonner";
|
||||
|
||||
export type SettingsStorageSectionProps = {
|
||||
settings: Setting;
|
||||
storageChannels: StorageChannelWith[];
|
||||
};
|
||||
|
||||
export const SettingsStorageSection = (props: SettingsStorageSectionProps) => {
|
||||
export const SettingsStorageSection = ({settings, storageChannels}: SettingsStorageSectionProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const form = useZodForm({
|
||||
schema: DefaultStorageSchema,
|
||||
defaultValues: {
|
||||
storageChannelId: settings.defaultStorageChannelId ?? undefined
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const result = await checkConnexionToS3();
|
||||
if (result.error) {
|
||||
toast.error("An error occured during the connexion !");
|
||||
mutationFn: async (values: DefaultStorageType) => {
|
||||
const result = await updateStorageSettingsAction({name: "system", data: values})
|
||||
const inner = result?.data;
|
||||
|
||||
if (inner?.success) {
|
||||
toast.success(inner.actionSuccess?.message);
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.success("Connexion succeed!");
|
||||
toast.error(inner?.actionError?.message);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
const [isSwitched, setIsSwitched] = useState<boolean>(props.settings.storage !== "local");
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: () => updateStorageSettingsAction({name: "system", data: {storage: isSwitched ? "s3" : "local"}}),
|
||||
onSuccess: () => {
|
||||
toast.success(`Settings updated successfully.`);
|
||||
router.refresh();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(`An error occurred while updating settings information.`);
|
||||
},
|
||||
});
|
||||
|
||||
const HandleSwitchStorage = async () => {
|
||||
setIsSwitched(!isSwitched);
|
||||
await updateMutation.mutateAsync();
|
||||
};
|
||||
|
||||
const extractS3FormValues = (settings: Setting): S3FormType | undefined => {
|
||||
if (!settings.s3EndPointUrl) return undefined;
|
||||
return {
|
||||
s3EndPointUrl: settings.s3EndPointUrl,
|
||||
s3AccessKeyId: settings.s3AccessKeyId!,
|
||||
s3SecretAccessKey: settings.s3SecretAccessKey!,
|
||||
S3BucketName: settings.S3BucketName!,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<Alert className="mt-3">
|
||||
<Info className="h-4 w-4"/>
|
||||
<AlertTitle>Informations</AlertTitle>
|
||||
<AlertDescription>
|
||||
Actually you can only store you data in one place : s3 compatible or in local. For exemple you
|
||||
cannot choose to store images in one place
|
||||
and backups files in another.
|
||||
</AlertDescription>
|
||||
The default storage channel will be used by default to store your backups if no storage policy is
|
||||
configured at the database level. </AlertDescription>
|
||||
</Alert>
|
||||
<div className="flex flex-col h-full py-4 ">
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="storage-mode">Storage Mode (Local/s3 compatible)</Label>
|
||||
<Switch
|
||||
checked={isSwitched}
|
||||
onCheckedChange={async () => {
|
||||
await HandleSwitchStorage();
|
||||
}}
|
||||
id="storage-mode"
|
||||
<Form
|
||||
form={form}
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="storageChannelId"
|
||||
render={({field}) => (
|
||||
<FormItem className="flex items-center justify-center">
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="w-90 h-full mb-0">
|
||||
<SelectValue placeholder="Select a default storage channel"/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{storageChannels.map((channel) => (
|
||||
<SelectItem key={channel.id} value={channel.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
{getChannelIcon(channel.provider)}
|
||||
<span className="font-medium">{channel.name}</span>
|
||||
<span
|
||||
className="text-[9px] uppercase bg-secondary px-1.5 py-0.5 rounded">
|
||||
{channel.provider}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<ButtonWithLoading type="submit">
|
||||
Confirm
|
||||
</ButtonWithLoading>
|
||||
</div>
|
||||
<div>
|
||||
<ButtonWithLoading
|
||||
size={"default"}
|
||||
disabled={!isSwitched}
|
||||
isPending={mutation.isPending}
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync();
|
||||
}}
|
||||
icon={<ShieldCheck/>}>Test connexion</ButtonWithLoading>
|
||||
</div>
|
||||
</div>
|
||||
{isSwitched && (
|
||||
<div className="mt-5">
|
||||
<StorageS3Form defaultValues={extractS3FormValues(props.settings)}/>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"use server"
|
||||
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {z} from "zod";
|
||||
import {DefaultStorageSchema} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage.schema";
|
||||
|
||||
export const updateStorageSettingsAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
data: DefaultStorageSchema,
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<Setting>> => {
|
||||
const {name, data} = parsedInput;
|
||||
|
||||
try {
|
||||
|
||||
const [updatedSettings] = await db
|
||||
.update(drizzleDb.schemas.setting)
|
||||
.set({
|
||||
defaultStorageChannelId: data.storageChannelId,
|
||||
})
|
||||
.where(eq(drizzleDb.schemas.setting.name, name))
|
||||
.returning();
|
||||
return {
|
||||
success: true,
|
||||
value: updatedSettings,
|
||||
actionSuccess: {
|
||||
message: "Settings successfully updated",
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed update settings.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const DefaultStorageSchema = z.object({
|
||||
storageChannelId: z.string(),
|
||||
});
|
||||
|
||||
export type DefaultStorageType = z.infer<typeof DefaultStorageSchema>;
|
||||
@@ -46,14 +46,14 @@ export function usersListColumns(): ColumnDef<User>[] {
|
||||
<TableCell
|
||||
className="text-right text-muted-foreground">{formatLocalizedDate(row.original.createdAt)}</TableCell>
|
||||
</TableRow>
|
||||
{/*{row.original.lastConnectedAt && (*/}
|
||||
{/* <TableRow>*/}
|
||||
{/* <TableCell>Last connected</TableCell>*/}
|
||||
{/* <TableCell className="text-right text-muted-foreground">*/}
|
||||
{/* {formatLocalizedDate(row.original.lastConnectedAt)} ({timeAgo(row.original.lastConnectedAt, locale)})*/}
|
||||
{/* </TableCell>*/}
|
||||
{/* </TableRow>*/}
|
||||
{/*)}*/}
|
||||
{row.original.lastConnectedAt && (
|
||||
<TableRow>
|
||||
<TableCell>Last connected</TableCell>
|
||||
<TableCell className="text-right text-muted-foreground">
|
||||
{formatLocalizedDate(row.original.lastConnectedAt)} ({timeAgo(row.original.lastConnectedAt)})
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{row.original.lastChangedPasswordAt && (
|
||||
<TableRow>
|
||||
<TableCell>Last password change</TableCell>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user