mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
419b6d4891 | ||
|
|
b4840318d5 | ||
|
|
bf81f437d1 | ||
|
|
81a4d33df1 | ||
|
|
baab364b56 | ||
|
|
585ee041ec | ||
|
|
da3d4350cc | ||
|
|
df9390847f | ||
|
|
2a1acbfde8 | ||
|
|
16ef8507b0 | ||
|
|
df5d4e336c | ||
|
|
ad2015329b | ||
|
|
4cc4bed785 | ||
|
|
6c506e6907 | ||
|
|
977f9344c7 | ||
|
|
2adb2f3b68 | ||
|
|
40a6e9e69f | ||
|
|
e1d1a3fb9a | ||
|
|
3b93257890 | ||
|
|
1104e2a074 |
+1
-1
@@ -34,4 +34,4 @@ S3_USE_SSL=true
|
||||
STORAGE_TYPE=local
|
||||
|
||||
# Retention
|
||||
RETENTION_CRON="* * * * *"
|
||||
RETENTION_CRON="* * * * *"
|
||||
|
||||
@@ -5,6 +5,10 @@ on:
|
||||
tags:
|
||||
- '*-rc*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
docker_publish:
|
||||
uses: ./.github/workflows/docker.yml
|
||||
|
||||
@@ -6,6 +6,10 @@ on:
|
||||
- '*.*.*'
|
||||
- '!*-*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
docker_publish:
|
||||
uses: ./.github/workflows/docker.yml
|
||||
|
||||
+2
-2
@@ -26,5 +26,5 @@ keywords:
|
||||
- web-ui
|
||||
- agent
|
||||
license: Apache-2.0
|
||||
version: 1.1.6
|
||||
date-released: "2026-01-01"
|
||||
version: 1.1.11-rc.1
|
||||
date-released: "2026-01-09"
|
||||
|
||||
@@ -25,8 +25,6 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
orderBy: (fields) => desc(fields.createdAt),
|
||||
});
|
||||
|
||||
console.log(agents);
|
||||
|
||||
|
||||
if (!agents) {
|
||||
notFound();
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ export default async function RoutePage(props: PageParams<{
|
||||
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
||||
});
|
||||
|
||||
const isAlreadyBackup = backups.some((b) => b.status === "waiting");
|
||||
const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing");
|
||||
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
||||
|
||||
const totalBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
||||
|
||||
@@ -78,8 +78,8 @@ export default async function RoutePage(props: PageParams<{
|
||||
organizationSlug={organization.slug}
|
||||
// @ts-ignore
|
||||
cardItem={ProjectDatabaseCard}
|
||||
cardsPerPage={4}
|
||||
numberOfColumns={1}
|
||||
cardsPerPage={6}
|
||||
numberOfColumns={3}
|
||||
extendedProps={proj}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -56,8 +56,8 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
organizationSlug={organization.slug}
|
||||
data={projects}
|
||||
cardItem={ProjectCard}
|
||||
cardsPerPage={4}
|
||||
numberOfColumns={1}
|
||||
cardsPerPage={6}
|
||||
numberOfColumns={3}
|
||||
/>
|
||||
) : isMember ? (
|
||||
<EmptyStatePlaceholder text="No project available"/>
|
||||
|
||||
@@ -24,7 +24,6 @@ export async function POST(
|
||||
const agentId = (await params).agentId
|
||||
const body: BodyResultRestore = await request.json();
|
||||
|
||||
console.log(body)
|
||||
|
||||
if (!isUuidv4(body.generatedId)) {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -6,7 +6,7 @@ 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} from "drizzle-orm";
|
||||
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";
|
||||
@@ -75,8 +75,15 @@ 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 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),
|
||||
inArray(drizzleDb.schemas.backup.status, ["waiting", "ongoing"])
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -85,13 +92,13 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
})
|
||||
|
||||
|
||||
if (backup) {
|
||||
if (activeBackup && activeBackup.status == "waiting") {
|
||||
backupAction = true
|
||||
|
||||
await dbClient
|
||||
.update(drizzleDb.schemas.backup)
|
||||
.set(withUpdatedAt({status: "ongoing"}))
|
||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||
.where(eq(drizzleDb.schemas.backup.id, activeBackup.id));
|
||||
}
|
||||
|
||||
if (restoration) {
|
||||
|
||||
@@ -41,7 +41,6 @@ export async function POST(
|
||||
|
||||
if (!isUuidv4(agentId)) {
|
||||
message = "agentId is not a valid uuid"
|
||||
console.log(message)
|
||||
return NextResponse.json(
|
||||
{error: "agentId is not a valid uuid"},
|
||||
{status: 500}
|
||||
@@ -54,7 +53,6 @@ export async function POST(
|
||||
|
||||
if (!agent) {
|
||||
message = "Agent not found"
|
||||
console.log(message)
|
||||
return NextResponse.json({error: message}, {status: 404})
|
||||
}
|
||||
const databasesResponse = await handleDatabases(body, agent, lastContact)
|
||||
@@ -77,7 +75,6 @@ export async function POST(
|
||||
},
|
||||
databases: databasesResponse
|
||||
}
|
||||
console.log(response)
|
||||
|
||||
return Response.json(response)
|
||||
} catch (error) {
|
||||
|
||||
+1
-13
@@ -19,7 +19,6 @@ export async function GET(request: Request) {
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
console.log('Stream started');
|
||||
|
||||
const handleModification = (data: any) => {
|
||||
console.log('Modification event triggered:', data);
|
||||
controller.enqueue(`event: modification\n`);
|
||||
@@ -45,15 +44,4 @@ export async function GET(request: Request) {
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// export async function POST(request: Request) {
|
||||
// console.log('POST request received');
|
||||
// const data = await request.json();
|
||||
// console.log('Data received:', data);
|
||||
//
|
||||
// // Emit the event to all connected clients
|
||||
// eventEmitter.emit('modification', data);
|
||||
//
|
||||
// return new Response('Event sent', {status: 200});
|
||||
// }
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "portabase",
|
||||
"version": "1.1.8",
|
||||
"version": "1.1.11-rc.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 8887",
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: ./release <version>"
|
||||
echo "Example: ./release v1.0.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$1
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
if [ "$CURRENT_BRANCH" = "main" ]; then
|
||||
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: On 'main' branch, only release tags (X.Y.Z) are allowed."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
||||
echo "Error: On branch '$CURRENT_BRANCH', only RC tags matching X.Y.Z-rc.W are allowed (e.g., 1.0.0-rc.1)."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
CLEAN_VERSION=${VERSION#v}
|
||||
CURRENT_DATE=$(date +%Y-%m-%d)
|
||||
|
||||
echo "Preparing release $VERSION..."
|
||||
|
||||
|
||||
# package.json
|
||||
if [ -f package.json ]; then
|
||||
echo "Updating package.json..."
|
||||
if sed --version >/dev/null 2>&1; then
|
||||
sed -i "s/\"version\": \".*\"/\"version\": \"$CLEAN_VERSION\"/" package.json
|
||||
else
|
||||
sed -i '' "s/\"version\": \".*\"/\"version\": \"$CLEAN_VERSION\"/" package.json
|
||||
fi
|
||||
fi
|
||||
|
||||
# pyproject.toml
|
||||
if [ -f pyproject.toml ]; then
|
||||
echo "Updating pyproject.toml..."
|
||||
if sed --version >/dev/null 2>&1; then
|
||||
sed -i "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" pyproject.toml
|
||||
else
|
||||
sed -i '' "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" pyproject.toml
|
||||
fi
|
||||
fi
|
||||
|
||||
# Cargo.toml
|
||||
if [ -f Cargo.toml ]; then
|
||||
echo "Updating Cargo.toml..."
|
||||
if sed --version >/dev/null 2>&1; then
|
||||
sed -i "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" Cargo.toml
|
||||
else
|
||||
sed -i '' "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" Cargo.toml
|
||||
fi
|
||||
fi
|
||||
|
||||
# CITATION.cff
|
||||
if [ -f CITATION.cff ]; then
|
||||
echo "Updating CITATION.cff..."
|
||||
if sed --version >/dev/null 2>&1; then
|
||||
sed -i "s/^version: .*/version: $CLEAN_VERSION/" CITATION.cff
|
||||
sed -i "s/^date-released: .*/date-released: \"$CURRENT_DATE\"/" CITATION.cff
|
||||
else
|
||||
sed -i '' "s/^version: .*/version: $CLEAN_VERSION/" CITATION.cff
|
||||
sed -i '' "s/^date-released: .*/date-released: \"$CURRENT_DATE\"/" CITATION.cff
|
||||
fi
|
||||
fi
|
||||
|
||||
git add .
|
||||
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
echo "Committing changes..."
|
||||
git commit -m "chore(release): $VERSION"
|
||||
else
|
||||
echo "No changes to commit. Proceeding to tag..."
|
||||
fi
|
||||
|
||||
if git rev-parse "$VERSION" >/dev/null 2>&1; then
|
||||
echo "Tag $VERSION already exists. Aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Creating tag $VERSION..."
|
||||
git tag -a "$VERSION" -m "Release $VERSION"
|
||||
|
||||
echo "Pushing changes and tags to remote..."
|
||||
git push
|
||||
git push origin "$VERSION"
|
||||
|
||||
echo "Successfully released $VERSION!"
|
||||
@@ -63,7 +63,6 @@ export const LoginForm = (props: loginFormProps) => {
|
||||
toast.success("Login success");
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error);
|
||||
toast.error(error.error.message);
|
||||
},
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ export const resetPasswordAction = action
|
||||
}
|
||||
|
||||
const user = await (await auth.$context).internalAdapter.findUserById(verification.value);
|
||||
console.log(user)
|
||||
if (!user) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -39,28 +38,6 @@ export const resetPasswordAction = action
|
||||
};
|
||||
}
|
||||
|
||||
const hashedPassword = await (await auth.$context).password.hash(parsedInput.schema.password);
|
||||
console.log(hashedPassword)
|
||||
console.log("ok")
|
||||
// await (await auth.$context).internalAdapter.updatePassword(user.id, hashedPassword);
|
||||
console.log("ici")
|
||||
// await (await auth.$context).internalAdapter.deleteSessions(user.id);
|
||||
// console.log("ici2")
|
||||
// await (await auth.$context).internalAdapter.deleteVerificationValue(verification.id);
|
||||
// console.log("ici3");
|
||||
//
|
||||
// (await auth.$context).internalAdapter.updateUser(user.id, {
|
||||
// lastChangedPasswordAt: new Date(),
|
||||
// });
|
||||
|
||||
// await auth.api.resetPassword({
|
||||
// headers: await headers(),
|
||||
// body: {
|
||||
// newPassword: parsedInput.schema.password,
|
||||
// token: parsedInput.token,
|
||||
// },
|
||||
// });
|
||||
|
||||
await (
|
||||
await auth.$context
|
||||
).internalAdapter.updateUser(user.id, {
|
||||
|
||||
@@ -44,7 +44,6 @@ export const ResetPasswordForm = (props: ResetPasswordFormProps) => {
|
||||
setTimeout(() => router.push("/"), 1400);
|
||||
},
|
||||
onError: (error: BetterAuthError) => {
|
||||
console.log(error)
|
||||
toast.error("An error occurred while resetting password");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -36,7 +36,6 @@ export const RegisterForm = (props: registerFormProps) => {
|
||||
router.push(`/login`);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error);
|
||||
toast.error(error.error.message);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import {Form, FormControl, FormField, FormItem, FormLabel, useZodForm} from "@/c
|
||||
import {PasswordStrengthInput} from "@/components/ui/password-input-indicator";
|
||||
import {ResetPasswordSchema, ResetPasswordType} from "@/components/wrappers/auth/reset-password/reset-password-schema";
|
||||
import {PasswordInput} from "@/components/ui/password-input";
|
||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||
import {CardContent, CardHeader} from "@/components/ui/card";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {toast} from "sonner";
|
||||
@@ -31,7 +31,6 @@ export const ResetPasswordForm = ({token}: ResetPasswordFormProps) => {
|
||||
token,
|
||||
}, {
|
||||
onSuccess: (response) => {
|
||||
console.log(response);
|
||||
toast.success("Password changed successfully.");
|
||||
setTimeout(() => router.push("/"), 1000);
|
||||
},
|
||||
|
||||
@@ -12,8 +12,6 @@ export const ConnectionCircle = ({date}: ConnectionCircleProps) => {
|
||||
const timestamp = date.getTime();
|
||||
const interval_seconds = (now - timestamp) / 1000;
|
||||
|
||||
console.log({now, timestamp, interval_seconds});
|
||||
|
||||
if (interval_seconds < 55) {
|
||||
style = "bg-green-400 border-green-600";
|
||||
} else if (interval_seconds <= 60) {
|
||||
@@ -25,7 +23,5 @@ export const ConnectionCircle = ({date}: ConnectionCircleProps) => {
|
||||
console.warn("Invalid date passed to ConnectionCircle:", date);
|
||||
}
|
||||
|
||||
console.log(style);
|
||||
|
||||
return <div className={cn("w-5 h-5 rounded-full border-4", style)}/>;
|
||||
};
|
||||
|
||||
-4
@@ -29,10 +29,6 @@ export const NotifierChannelOrganisationForm = ({
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
console.log("defaultValues", defaultValues);
|
||||
console.log("organizations", organizations);
|
||||
|
||||
const defaultOrganizationIds = defaultValues?.organizations?.map(organization => organization.organizationId) ?? []
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {Mail, MessageSquare, Webhook} from "lucide-react";
|
||||
import {Mail} from "lucide-react";
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
export const getNotificationChannelIcon = (type: string) => {
|
||||
const Icon = notificationTypes.find((t) => t.value === type)?.icon
|
||||
@@ -6,8 +7,481 @@ export const getNotificationChannelIcon = (type: string) => {
|
||||
}
|
||||
|
||||
|
||||
const notificationTypes = [
|
||||
export const notificationTypes = [
|
||||
{value: "smtp", label: "Email", icon: Mail},
|
||||
{value: "slack", label: "Slack", icon: MessageSquare},
|
||||
{value: "webhook", label: "Webhook", icon: Webhook},
|
||||
]
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
-3
@@ -34,13 +34,11 @@ export const OrganizationAddMemberForm = ({onSuccessAction, users, organization}
|
||||
|
||||
const mutationAddMemberOrganisation = useMutation({
|
||||
mutationFn: async (data: AddMemberSchemaType) => {
|
||||
console.log(data);
|
||||
const result = await addMemberOrganizationAction({
|
||||
userId: data.userId,
|
||||
organizationId: organization.id,
|
||||
role: "member",
|
||||
});
|
||||
console.log(result);
|
||||
toast.success("Member successfully added!");
|
||||
router.refresh();
|
||||
onSuccessAction?.();
|
||||
@@ -71,7 +69,6 @@ export const OrganizationAddMemberForm = ({onSuccessAction, users, organization}
|
||||
placeholder="Enter a user email"
|
||||
entries={filteredUsers}
|
||||
onSelect={(entySelected: any) => {
|
||||
console.log("Form selection:", entySelected);
|
||||
field.onChange(entySelected.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
-2
@@ -35,13 +35,11 @@ export const OrganizationDeleteMemberModal = ({ member, open, onOpenChangeAction
|
||||
},
|
||||
{
|
||||
onSuccess: async (response) => {
|
||||
console.log(response);
|
||||
toast.success("Member successfully deleted!");
|
||||
onOpenChangeAction(false);
|
||||
router.refresh();
|
||||
},
|
||||
onError: async (error) => {
|
||||
console.log(error);
|
||||
toast.error("An error occurred while deleting member!");
|
||||
onOpenChangeAction(false);
|
||||
},
|
||||
|
||||
-1
@@ -49,7 +49,6 @@ export const OrganizationMemberChangeRoleModal = (props: OrganizationMemberChang
|
||||
router.refresh();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error);
|
||||
toast.error("An error occurred while updating member");
|
||||
onOpenChangeAction(false);
|
||||
},
|
||||
|
||||
-2
@@ -28,11 +28,9 @@ export const StorageS3Form = (props: S3FormProps) => {
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: S3FormType) => {
|
||||
console.log(values);
|
||||
const updateS3Settings = await updateS3SettingsAction({ name: "system", data: values });
|
||||
const data = updateS3Settings?.data?.data;
|
||||
if (updateS3Settings?.serverError || !data) {
|
||||
console.log(updateS3Settings?.serverError);
|
||||
toast.error(updateS3Settings?.serverError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ export const AdminUserEditForm = ({ onSuccess, defaultValues }: AdminUserEditFor
|
||||
...data,
|
||||
id: defaultValues?.id || "",
|
||||
});
|
||||
console.log(result)
|
||||
const inner = result?.data;
|
||||
if (inner?.success) {
|
||||
toast.success("User Successfully updated");
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||
import {useState} from "react";
|
||||
import Link from "next/link";
|
||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||
import {Card} from "@/components/ui/card";
|
||||
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
|
||||
import {Agent, AgentWith} from "@/db/schema/08_agent";
|
||||
import {Activity, Database, ShieldCheck} from "lucide-react";
|
||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||
import {AgentWith} from "@/db/schema/08_agent";
|
||||
import {Activity, ChevronRight, Copy, Check, Fingerprint, Server, Database, ShieldCheck} from "lucide-react";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
|
||||
export type agentCardProps = {
|
||||
data: AgentWith;
|
||||
@@ -13,39 +15,78 @@ export type agentCardProps = {
|
||||
|
||||
export const AgentCard = (props: agentCardProps) => {
|
||||
const {data: agent} = props;
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const handleCopy = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(agent.id);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Link href={`/dashboard/agents/${agent.id}`}
|
||||
className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md rounded-xl"
|
||||
<Link
|
||||
href={`/dashboard/agents/${agent.id}`}
|
||||
className="group block transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl"
|
||||
>
|
||||
<Card className="flex flex-row justify-between">
|
||||
<div className="flex-1 text-left">
|
||||
<CardHeader className="text-2xl font-bold">{agent.name}</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4 w-4"/>
|
||||
<span>{formatDateLastContact(agent.lastContact)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-4 w-4"/>
|
||||
<span>{agent.databases?.length ?? 0} DB</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="h-4 w-4"/>
|
||||
{agent.version ?
|
||||
<span>v{agent.version}</span>
|
||||
:
|
||||
<span>N/A</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<Card className="flex flex-row items-center p-4 gap-5 transition-all border-border/50 bg-card hover:bg-accent/50 hover:border-primary/50 group-hover:shadow-md overflow-hidden">
|
||||
<div className="relative w-12 h-12 p-2.5 bg-background rounded-xl border border-border/50 shadow-sm flex items-center justify-center group-hover:border-primary/30 transition-all duration-300 group-hover:scale-105 shrink-0">
|
||||
<Server className="w-6 h-6 text-foreground/80 group-hover:text-primary transition-colors" />
|
||||
</div>
|
||||
<div className="flex items-center px-4">
|
||||
<ConnectionCircle date={agent.lastContact}/>
|
||||
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-1.5">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="text-lg font-black text-foreground group-hover:text-primary transition-colors truncate tracking-tight leading-none">
|
||||
{agent.name}
|
||||
</h3>
|
||||
{agent.version && (
|
||||
<Badge variant="secondary" className="h-5 px-1.5 text-[10px] font-bold tracking-wider">
|
||||
v{agent.version}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-muted-foreground group-hover:text-foreground transition-colors">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<Fingerprint className="w-3.5 h-3.5 shrink-0" />
|
||||
<span className="font-mono text-xs font-bold truncate opacity-80">
|
||||
{agent.id}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="ml-0.5 p-1 hover:bg-muted rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 z-10"
|
||||
title="Copy ID"
|
||||
>
|
||||
{isCopied ? <Check className="w-3 h-3 text-green-500" /> : <Copy className="w-3 h-3" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:flex items-center gap-1.5 shrink-0">
|
||||
<Database className="w-3.5 h-3.5" />
|
||||
<span className="text-xs font-bold uppercase tracking-tight opacity-80">
|
||||
{agent.databases?.length ?? 0} DBs
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 shrink-0">
|
||||
<div className="hidden sm:flex flex-col items-end gap-1 text-right">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/70">Last Contact</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Activity className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-bold uppercase tracking-tight">
|
||||
{formatDateLastContact(agent.lastContact)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="scale-110">
|
||||
<ConnectionCircle date={agent.lastContact}/>
|
||||
</div>
|
||||
|
||||
<ChevronRight className="w-5 h-5 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {Server} from "lucide-react";
|
||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/agent-card-key/agent-card-key";
|
||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
|
||||
import {AgentDatabaseCard} from "@/components/wrappers/dashboard/agent/agent-database-card";
|
||||
import {AgentWithDatabases} from "@/db/schema/08_agent";
|
||||
import {eventUpdate} from "@/types/events";
|
||||
import {useAutoRefresh} from "@/hooks/use-auto-refresh";
|
||||
@@ -72,7 +72,7 @@ export const AgentContentPage = ({edgeKey, agent}: AgentContentPageProps) => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
<CardsWithPagination cardsPerPage={4} numberOfColumns={2} data={agent.databases}
|
||||
cardItem={DatabaseCard}/>
|
||||
cardItem={AgentDatabaseCard}/>
|
||||
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import {Database} from "@/db/schema/07_database";
|
||||
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
|
||||
|
||||
export type agentDatabaseCardProps = {
|
||||
data: Database;
|
||||
};
|
||||
|
||||
export const AgentDatabaseCard = (props: agentDatabaseCardProps) => {
|
||||
const {data: database} = props;
|
||||
|
||||
return <DatabaseCard data={database}/>;
|
||||
|
||||
//todo: à remettre quand on aura fait l'impersonation des admins
|
||||
/* if (!database.projectId) {
|
||||
return <DatabaseCard data={database}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className="group block transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl"
|
||||
href={`/dashboard/projects/${database.projectId}/database/${database.id}`}>
|
||||
<DatabaseCard data={database}/>
|
||||
</Link>
|
||||
);*/
|
||||
};
|
||||
@@ -47,7 +47,6 @@ export const AgentForm = (props: agentFormProps) => {
|
||||
|
||||
const data = createAgent?.data?.data;
|
||||
if (createAgent?.serverError || !data) {
|
||||
console.log(createAgent?.serverError);
|
||||
toast.error(createAgent?.serverError);
|
||||
return;
|
||||
}
|
||||
|
||||
+2
-2
@@ -7,11 +7,11 @@ export const NotificationChannelFormSchema = z.object({
|
||||
.min(5, "Name must be at least 5 characters long")
|
||||
.max(40, "Name must be at most 40 characters long"),
|
||||
|
||||
provider: z.enum(["slack", "smtp"], {
|
||||
provider: z.enum(["slack", "smtp", "discord", "telegram", "gotify", "ntfy", "webhook"], {
|
||||
required_error: "Provider is required",
|
||||
}),
|
||||
|
||||
config: z.record(z.string()).optional(),
|
||||
config: z.record(z.union([z.string(), z.number(), z.boolean(), z.null(), z.undefined()])).optional(),
|
||||
|
||||
|
||||
enabled: z.boolean().default(true),
|
||||
|
||||
@@ -8,7 +8,6 @@ import {Input} from "@/components/ui/input";
|
||||
import {
|
||||
NotificationChannelFormSchema, NotificationChannelFormType
|
||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.schema";
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||
import {
|
||||
addNotificationChannelAction, updateNotificationChannelAction
|
||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.action";
|
||||
@@ -20,12 +19,31 @@ import {
|
||||
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 {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {
|
||||
NotifierTestChannelButton
|
||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-test-channel-button";
|
||||
import {useEffect} from "react";
|
||||
import {notificationTypes} from "@/components/wrappers/dashboard/admin/notifications/helpers";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {Card} from "@/components/ui/card";
|
||||
import {ArrowLeft} from "lucide-react";
|
||||
|
||||
type NotifierFormProps = {
|
||||
onSuccessAction?: () => void;
|
||||
@@ -34,7 +52,7 @@ type NotifierFormProps = {
|
||||
adminView?: boolean
|
||||
};
|
||||
|
||||
export const NotifierForm = ({onSuccessAction, organization, defaultValues, adminView}: NotifierFormProps) => {
|
||||
export const NotifierForm = ({onSuccessAction, organization, defaultValues}: NotifierFormProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
const isCreate = !Boolean(defaultValues);
|
||||
@@ -75,10 +93,36 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues, admi
|
||||
});
|
||||
|
||||
const provider = form.watch("provider");
|
||||
const selectedProviderDetails = notificationTypes.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) => {
|
||||
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",
|
||||
)}
|
||||
onClick={() => {
|
||||
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"/>
|
||||
</div>
|
||||
<span className="font-medium text-sm">{type.label}</span>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
@@ -86,6 +130,32 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues, admi
|
||||
await mutationAddNotificationChannel.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
<selectedProviderDetails.icon className="h-5 w-5"/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Configuring {selectedProviderDetails?.label}</p>
|
||||
<p className="text-xs text-muted-foreground">{isCreate ? "New Channel" : "Edit Channel"}</p>
|
||||
</div>
|
||||
{isCreate && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
form.setValue("provider", undefined as any);
|
||||
form.setValue("config", undefined);
|
||||
}}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2"/>
|
||||
Change
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -93,7 +163,7 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues, admi
|
||||
<FormItem>
|
||||
<FormLabel>Channel Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="e.g., Primary email, Team Slack" value={field.value ?? ""}/>
|
||||
<Input {...field} placeholder={`My ${selectedProviderDetails?.label} Channel`} value={field.value ?? ""}/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
@@ -104,28 +174,7 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues, admi
|
||||
control={form.control}
|
||||
name="provider"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Provider</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
form.setValue("config", {});
|
||||
field.onChange(value);
|
||||
}}
|
||||
value={field.value || ""}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select provider"/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="smtp">SMTP (Email)</SelectItem>
|
||||
<SelectItem value="slack">Slack</SelectItem>
|
||||
{/*<SelectItem value="curl">Curl</SelectItem>*/}
|
||||
{/*<SelectItem value="webhook">Webhook</SelectItem>*/}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
<input type="hidden" {...field} value={field.value || ""} />
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -137,8 +186,28 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues, admi
|
||||
<NotifierSlackForm form={form}/>
|
||||
)}
|
||||
|
||||
{provider === "discord" && (
|
||||
<NotifierDiscordForm form={form}/>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between">
|
||||
{provider === "telegram" && (
|
||||
<NotifierTelegramForm form={form}/>
|
||||
)}
|
||||
|
||||
{provider === "gotify" && (
|
||||
<NotifierGotifyForm form={form}/>
|
||||
)}
|
||||
|
||||
{provider === "ntfy" && (
|
||||
<NotifierNtfyForm form={form}/>
|
||||
)}
|
||||
|
||||
{provider === "webhook" && (
|
||||
<NotifierWebhookForm form={form}/>
|
||||
)}
|
||||
|
||||
|
||||
<div className="flex justify-between mt-4">
|
||||
<div>
|
||||
{defaultValues && (
|
||||
<NotifierTestChannelButton organizationId={organization?.id}
|
||||
@@ -164,4 +233,4 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues, admi
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
};
|
||||
-9
@@ -20,15 +20,6 @@ export const NotifierTestChannelButton = ({notificationChannel, organizationId}:
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
|
||||
// const payload: EventPayload = {
|
||||
// title: 'Database Down',
|
||||
// message: 'Primary DB instance is unreachable',
|
||||
// level: 'critical',
|
||||
// data: {host: 'db-prod-01', error: 'connection timeout'},
|
||||
// };
|
||||
|
||||
console.log("organizationId ici", organizationId);
|
||||
|
||||
const payload: EventPayload = {
|
||||
title: 'Test Channel',
|
||||
message: `We are testing channel ${notificationChannel.name}`,
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
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";
|
||||
|
||||
|
||||
type NotifierDiscordFormProps = {
|
||||
form: UseFormReturn<any, any, any>
|
||||
}
|
||||
|
||||
export const NotifierDiscordForm = ({form}: NotifierDiscordFormProps) => {
|
||||
return (
|
||||
<>
|
||||
<Separator className="my-1"/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.discordWebhook"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Discord Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="https://discord.com/api/webhooks/..."/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
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";
|
||||
|
||||
|
||||
type NotifierGotifyFormProps = {
|
||||
form: UseFormReturn<any, any, any>
|
||||
}
|
||||
|
||||
export const NotifierGotifyForm = ({form}: NotifierGotifyFormProps) => {
|
||||
return (
|
||||
<>
|
||||
<Separator className="my-1"/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.gotifyServerUrl"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gotify Server URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="https://gotify.example.com"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.gotifyAppToken"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Application Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="A1b2C3d4E5f6G7h"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
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";
|
||||
|
||||
|
||||
type NotifierNtfyFormProps = {
|
||||
form: UseFormReturn<any, any, any>
|
||||
}
|
||||
|
||||
export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
|
||||
return (
|
||||
<>
|
||||
<Separator className="my-1"/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.ntfyTopic"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Topic Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="my-secret-topic"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.ntfyServerUrl"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server URL (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="https://ntfy.sh (default)"/>
|
||||
</FormControl>
|
||||
<p className="text-xs text-muted-foreground">Leave empty to use the official ntfy.sh server.</p>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.ntfyToken"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Access Token (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="tk_..."/>
|
||||
</FormControl>
|
||||
<p className="text-xs text-muted-foreground">Only required for protected topics or self-hosted instances with auth.</p>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
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";
|
||||
|
||||
|
||||
type NotifierTelegramFormProps = {
|
||||
form: UseFormReturn<any, any, any>
|
||||
}
|
||||
|
||||
export const NotifierTelegramForm = ({form}: NotifierTelegramFormProps) => {
|
||||
return (
|
||||
<>
|
||||
<Separator className="my-1"/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.telegramBotToken"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Telegram Bot Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.telegramChatId"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Telegram Chat ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="-100123456789 or 123456789"/>
|
||||
</FormControl>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
You must start the conversation with the bot first (<strong>/start</strong>). <br/>
|
||||
For groups, add the bot to the group. <br/>
|
||||
You can use <a href="https://t.me/userinfobot" target="_blank" rel="noopener noreferrer" className="underline">@userinfobot</a> to find your ID.
|
||||
</p>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
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";
|
||||
|
||||
|
||||
type NotifierWebhookFormProps = {
|
||||
form: UseFormReturn<any, any, any>
|
||||
}
|
||||
|
||||
export const NotifierWebhookForm = ({form}: NotifierWebhookFormProps) => {
|
||||
return (
|
||||
<>
|
||||
<Separator className="my-1"/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.webhookUrl"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="https://example.com/api/webhook"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.webhookSecretHeader"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Header Name (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="X-Webhook-Secret"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.webhookSecret"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Secret Value (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Secret value..."/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
If provided, the secret will be sent in the specified header (defaults to <code>X-Webhook-Secret</code>).
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Form, FormControl, FormField, FormItem, FormMessage, useZodForm} from "@/components/ui/form";
|
||||
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
||||
import {InfoIcon, Plus, Trash2} from "lucide-react";
|
||||
import {useFieldArray} from "react-hook-form";
|
||||
import {
|
||||
@@ -11,7 +11,6 @@ import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||
import {Label} from "@/components/ui/label";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||
import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
@@ -22,7 +21,10 @@ import {
|
||||
} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy.action";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {Switch} from "@/components/ui/switch";
|
||||
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers";
|
||||
import {Card} from "@/components/ui/card";
|
||||
import {cn} from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
type AlertPolicyFormProps = {
|
||||
onSuccess?: () => void;
|
||||
@@ -36,9 +38,8 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
const organizationNotificationChannels = notificationChannels.map(channel => channel.id) ?? [];
|
||||
|
||||
|
||||
|
||||
const formattedAlertPoliciesList = (alertPolicies: AlertPolicy[]) => {
|
||||
return alertPolicies.filter((alertPolicy)=> organizationNotificationChannels.includes(alertPolicy.notificationChannelId)).map((alertPolicy) => ({
|
||||
return alertPolicies.filter((alertPolicy) => organizationNotificationChannels.includes(alertPolicy.notificationChannelId)).map((alertPolicy) => ({
|
||||
notificationChannelId: alertPolicy.notificationChannelId,
|
||||
eventKinds: alertPolicy.eventKinds,
|
||||
enabled: alertPolicy.enabled
|
||||
@@ -48,11 +49,7 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
const form = useZodForm({
|
||||
schema: AlertPoliciesSchema,
|
||||
defaultValues: {
|
||||
alertPolicies: database.alertPolicies && database.alertPolicies.length > 0 ? formattedAlertPoliciesList(database.alertPolicies) : [{
|
||||
notificationChannelId: "",
|
||||
eventKinds: [],
|
||||
enabled: true,
|
||||
}],
|
||||
alertPolicies: database.alertPolicies && database.alertPolicies.length > 0 ? formattedAlertPoliciesList(database.alertPolicies) : [],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -63,7 +60,7 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
|
||||
|
||||
const addAlertPolicy = () => {
|
||||
append({id: "", eventKinds: [], enabled: true});
|
||||
append({notificationChannelId: "", eventKinds: [], enabled: true});
|
||||
}
|
||||
|
||||
const removeAlertPolicy = (index: number) => {
|
||||
@@ -78,12 +75,6 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async ({alertPolicies}: AlertPoliciesType) => {
|
||||
|
||||
|
||||
|
||||
|
||||
console.log(alertPolicies)
|
||||
|
||||
const defaultFormatedAlertPolicies = formattedAlertPoliciesList(database?.alertPolicies ?? []);
|
||||
|
||||
const alertPoliciesToAdd = alertPolicies?.filter(
|
||||
@@ -97,15 +88,10 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
const alertPoliciesToUpdate = alertPolicies?.filter((alertPolicy) => {
|
||||
const existing = defaultFormatedAlertPolicies.find((a) => a.notificationChannelId === alertPolicy.notificationChannelId);
|
||||
return existing &&
|
||||
(existing.eventKinds !== alertPolicy.eventKinds);
|
||||
(existing.eventKinds !== alertPolicy.eventKinds || existing.enabled !== alertPolicy.enabled);
|
||||
}) ?? [];
|
||||
|
||||
|
||||
console.log("alertPoliciesToAdd", alertPoliciesToAdd)
|
||||
console.log("alertPoliciesToRemove", alertPoliciesToRemove)
|
||||
console.log("alertPoliciesToUpdate", alertPoliciesToUpdate)
|
||||
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
alertPoliciesToAdd.length > 0
|
||||
? createAlertPoliciesAction({
|
||||
@@ -129,8 +115,6 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
|
||||
console.log(results);
|
||||
|
||||
const rejected = results.find((r): r is PromiseRejectedResult => r.status === "rejected");
|
||||
if (rejected) {
|
||||
throw new Error(rejected.reason?.message || "Network or server error");
|
||||
@@ -144,7 +128,6 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
);
|
||||
|
||||
|
||||
console.log("failedActions", failedActions);
|
||||
if (failedActions.length > 0) {
|
||||
const firstError = failedActions[0].data.actionError;
|
||||
const message = firstError?.message || "One or more operations failed";
|
||||
@@ -155,7 +138,7 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Alert policies saved successfully");
|
||||
// onSuccess?.();
|
||||
//onSuccess?.();
|
||||
router.refresh();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
@@ -167,160 +150,201 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
className="flex flex-col gap-6"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">Alert Policies</Label>
|
||||
<div>
|
||||
<Label className="text-base font-medium">Configure Alerts</Label>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Choose which channels receive notifications for specific events.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
disabled={
|
||||
fields.length >= notificationChannels.length ||
|
||||
notificationChannels.length === 0
|
||||
}
|
||||
type="button"
|
||||
variant="outline" size="sm" onClick={addAlertPolicy}>
|
||||
<Plus className="w-4 h-4 mr-2"/>
|
||||
Add
|
||||
size="sm"
|
||||
className="h-8"
|
||||
onClick={addAlertPolicy}>
|
||||
<Plus className="w-4 h-4 mr-1.5"/>
|
||||
Add Policy
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 w-full">
|
||||
{organizationNotificationChannels.length === 0 ? (
|
||||
<div className="text-muted-foreground text-sm text-center border border-dashed rounded-lg p-4">
|
||||
No alerts policy in organization
|
||||
<div
|
||||
className="flex flex-col items-center justify-center p-8 border border-dashed rounded-xl bg-muted/20 text-center gap-2">
|
||||
<InfoIcon className="h-8 w-8 text-muted-foreground/50"/>
|
||||
<p className="font-medium text-sm text-foreground">No notification channels</p>
|
||||
<p className="text-xs text-muted-foreground max-w-xs">
|
||||
Please <Link href={`/dashboard/settings`} className="underline underline-offset-4 hover:text-primary transition-colors">configure notification channels</Link> in your organization settings first.
|
||||
</p>
|
||||
</div>
|
||||
) : fields.length === 0 ? (
|
||||
<div className="text-muted-foreground text-sm text-center">
|
||||
No alert policy, create one
|
||||
<div
|
||||
className="flex flex-col items-center justify-center p-8 border border-dashed rounded-xl bg-muted/20 text-center gap-2">
|
||||
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Plus className="h-4 w-4 text-primary"/>
|
||||
</div>
|
||||
<p className="font-medium text-sm text-foreground">No alert policies</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Click "Add Policy" to start receiving notifications.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
fields.map((field, index) => (
|
||||
<div key={field.id}>
|
||||
<div className="flex w-full gap-3">
|
||||
<div className="flex-1">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`alertPolicies.${index}.notificationChannelId`}
|
||||
render={({field}) => {
|
||||
const selectedIds = form
|
||||
.watch("alertPolicies")
|
||||
.map((a: AlertPolicyType) => a.notificationChannelId)
|
||||
.filter(Boolean);
|
||||
<div className="grid gap-4">
|
||||
{fields.map((field, index) => (
|
||||
<Card key={field.id} className="p-4 transition-all hover:border-primary/50 relative group">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-[1fr_auto_auto] gap-3 items-end">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest pl-0.5">
|
||||
Notification Channel
|
||||
</Label>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`alertPolicies.${index}.notificationChannelId`}
|
||||
render={({field}) => {
|
||||
const selectedIds = form
|
||||
.watch("alertPolicies")
|
||||
.map((a: AlertPolicyType) => a.notificationChannelId)
|
||||
.filter(Boolean);
|
||||
|
||||
const availableNotificationChannels = notificationChannels.filter(
|
||||
(channel) =>
|
||||
channel.id.toString() === field.value?.toString() ||
|
||||
!selectedIds.includes(channel.id.toString())
|
||||
);
|
||||
const availableNotificationChannels = notificationChannels.filter(
|
||||
(channel) =>
|
||||
channel.id.toString() === field.value?.toString() ||
|
||||
!selectedIds.includes(channel.id.toString())
|
||||
);
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
<Select onValueChange={field.onChange}
|
||||
value={field.value?.toString() || ""}
|
||||
>
|
||||
const selectedChannel = notificationChannels.find(c => c.id === field.value);
|
||||
|
||||
return (
|
||||
<FormItem className="space-y-0">
|
||||
<Select onValueChange={field.onChange}
|
||||
value={field.value?.toString() || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="h-9 w-full bg-background border-input">
|
||||
<SelectValue placeholder="Select channel">
|
||||
{selectedChannel ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={cn("flex items-center justify-center h-4 w-4 rounded")}>
|
||||
{getNotificationChannelIcon(selectedChannel.provider)}
|
||||
</div>
|
||||
<span className="truncate font-medium text-sm">{selectedChannel.name}</span>
|
||||
<span className="text-[9px] bg-secondary px-1.5 py-0.5 rounded text-muted-foreground ml-auto font-mono uppercase tracking-tighter">{selectedChannel.provider}</span>
|
||||
</div>
|
||||
) : "Select channel"}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{availableNotificationChannels.map((channel) => (
|
||||
<SelectItem key={channel.id.toString()}
|
||||
value={channel.id.toString()}>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="text-muted-foreground scale-90">
|
||||
{getNotificationChannelIcon(channel.provider)}
|
||||
</div>
|
||||
<span className="font-medium">{channel.name}</span>
|
||||
<span
|
||||
className="text-xs text-muted-foreground ml-2 capitalize">({channel.provider})</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage className="mt-1"/>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest pl-0.5">
|
||||
Status
|
||||
</Label>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`alertPolicies.${index}.enabled`}
|
||||
render={({field}) => (
|
||||
<FormItem className="space-y-0">
|
||||
<FormControl>
|
||||
<SelectTrigger className="h-10 w-full ">
|
||||
<SelectValue
|
||||
placeholder="Select notification channel"/>
|
||||
</SelectTrigger>
|
||||
<div className="flex items-center h-9 px-3 rounded-md border border-input bg-background min-w-[90px] justify-between">
|
||||
<Label htmlFor={`switch-${index}`} className="text-xs cursor-pointer font-medium text-foreground mr-2">
|
||||
{field.value ? "Active" : "Off"}
|
||||
</Label>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`switch-${index}`}
|
||||
className="scale-75 origin-right"
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{availableNotificationChannels.map((channel) => (
|
||||
<SelectItem key={channel.id.toString()}
|
||||
value={channel.id.toString()}>
|
||||
{channel.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage className="mb-2"/>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9 text-muted-foreground hover:text-destructive hover:border-destructive/50 hover:bg-destructive/10 transition-colors border-input bg-background"
|
||||
onClick={() => removeAlertPolicy(index)}>
|
||||
<Trash2 className="w-4 h-4"/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`alertPolicies.${index}.eventKinds`}
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormItem className="space-y-1.5">
|
||||
<FormLabel className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Trigger Events</FormLabel>
|
||||
<FormControl>
|
||||
<MultiSelect
|
||||
options={EVENT_KIND_OPTIONS}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value ?? []}
|
||||
placeholder="Select event kinds"
|
||||
placeholder="Select events to trigger notifications..."
|
||||
variant="inverted"
|
||||
animation={0}
|
||||
className="bg-background/50"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 justify-between items-center ">
|
||||
<Button type="button" variant="outline"
|
||||
onClick={() => removeAlertPolicy(index)}>
|
||||
<Trash2 className="w-4 h-4"/>
|
||||
</Button>
|
||||
<div className="flex h-full justify-center items-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div tabIndex={0} className="inline-flex rounded-md">
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`alertPolicies.${index}.enabled`}
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-64 text-pretty">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<InfoIcon className="size-4"/>
|
||||
<p>This is for activating the alert policy</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{index + 1 < fields.length && <Separator className="mt-3"/>}
|
||||
</div>
|
||||
))
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 justify-end">
|
||||
<div className="flex gap-3 justify-end pt-2 border-t mt-2">
|
||||
<ButtonWithLoading variant="outline" type="button" onClick={onCancel}>
|
||||
Cancel
|
||||
</ButtonWithLoading>
|
||||
<ButtonWithLoading isPending={false}>
|
||||
Save
|
||||
<ButtonWithLoading isPending={mutation.isPending}>
|
||||
Save Changes
|
||||
</ButtonWithLoading>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -38,7 +38,6 @@ export function CreateOrganizationModal({
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: OrganizationSchema) => {
|
||||
console.log(values);
|
||||
|
||||
const result = await createOrganizationAction(values);
|
||||
|
||||
@@ -73,7 +72,6 @@ export function CreateOrganizationModal({
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
console.log(values);
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { UploadIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { uploadImageAction } from "@/features/upload/public/upload.action";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { updateImageUserAction } from "@/components/wrappers/dashboard/profile/actions/avatar.action";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { User } from "@/db/schema/02_user";
|
||||
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
|
||||
import {UploadIcon} from "lucide-react";
|
||||
import {toast} from "sonner";
|
||||
import {uploadImageAction} from "@/features/upload/public/upload.action";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {updateImageUserAction} from "@/components/wrappers/dashboard/profile/actions/avatar.action";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
import React, {ChangeEvent} from "react";
|
||||
|
||||
export type AvatarWithUploadProps = {
|
||||
@@ -25,7 +25,6 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
||||
const data = uploadImage?.data?.data;
|
||||
|
||||
if (uploadImage?.serverError || !data) {
|
||||
console.log(uploadImage?.serverError);
|
||||
toast.error(uploadImage?.serverError);
|
||||
return;
|
||||
}
|
||||
@@ -34,7 +33,6 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
||||
const dataUser = updateUser?.data?.data;
|
||||
|
||||
if (updateUser?.serverError || !dataUser) {
|
||||
console.log(updateUser?.serverError);
|
||||
toast.error(updateUser?.serverError);
|
||||
return;
|
||||
}
|
||||
@@ -55,12 +53,11 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="relative ">
|
||||
|
||||
<Avatar className="w-24 h-24 lg:w-32 lg:h-32 border-4 border-muted/20">
|
||||
<AvatarImage src={user.image || undefined}/>
|
||||
<AvatarImage className="object-cover" src={user.image || undefined}/>
|
||||
<AvatarFallback className="text-3xl">{user.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
@@ -75,7 +72,7 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
||||
}}
|
||||
className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-100 hover:bg-gray-500 hover:bg-opacity-50 rounded-full w-24 h-24 lg:w-32 lg:h-32"
|
||||
>
|
||||
<UploadIcon className="w-12 h-12 lg:w-16 lg:h-16 text-primary" />
|
||||
<UploadIcon className="w-12 h-12 lg:w-16 lg:h-16 text-primary"/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -58,7 +58,6 @@ export function ProfileAccount({user}: ProfileAccountProps) {
|
||||
router.refresh();
|
||||
},
|
||||
onError: (error: BetterAuthError) => {
|
||||
console.log(error)
|
||||
if (error.code === "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL") {
|
||||
toast.error("User already exists, use another email address!");
|
||||
emailForm.reset({email: user.email});
|
||||
|
||||
-2
@@ -21,8 +21,6 @@ export const deleteProjectAction = userAction.schema(z.string()).action(async ({
|
||||
|
||||
|
||||
const databasesToRemove = databasesUpdated.map((db) => db.id);
|
||||
console.log(databasesUpdated);
|
||||
console.log(databasesToRemove);
|
||||
|
||||
await db.delete(drizzleDb.schemas.retentionPolicy)
|
||||
.where(inArray(drizzleDb.schemas.retentionPolicy.databaseId, databasesToRemove)).execute();
|
||||
|
||||
@@ -125,7 +125,6 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
console.log("Deleting rows:", rows)
|
||||
await mutationDeleteBackups.mutateAsync(rows)
|
||||
}}
|
||||
className="text-red-600 focus:text-red-700"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import Link from "next/link";
|
||||
import {ProjectWith} from "@/db/schema/06_project";
|
||||
import {ChevronRight, Folder} from "lucide-react";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
|
||||
export type projectCardProps = {
|
||||
data: ProjectWith;
|
||||
@@ -10,19 +12,39 @@ export type projectCardProps = {
|
||||
};
|
||||
|
||||
export const ProjectCard = (props: projectCardProps) => {
|
||||
const { data: project, organizationSlug } = props;
|
||||
const { data: project } = props;
|
||||
const dbCount = project.databases.length;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/dashboard/projects/${project.id}`}
|
||||
className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md rounded-xl"
|
||||
className="group block transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl"
|
||||
>
|
||||
<Card className="flex flex-row justify-between">
|
||||
<div className="flex-1 text-left">
|
||||
<CardHeader className="text-2xl font-bold">
|
||||
<Card className="relative h-full flex flex-col p-4 transition-all border-border/50 bg-card hover:bg-accent/50 hover:border-primary/50 group-hover:shadow-lg overflow-hidden gap-0">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary group-hover:bg-primary group-hover:text-primary-foreground transition-all duration-300 shadow-inner">
|
||||
<Folder className="w-8 h-8" />
|
||||
</div>
|
||||
<Badge className="text-[10px] font-medium px-2 py-1 rounded-lg bg-secondary/50 text-foreground">{dbCount} {dbCount === 1 ? "Database" : "Databases"}
|
||||
</Badge>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<h3 className="text-lg font-black text-foreground group-hover:text-primary transition-colors line-clamp-1 tracking-tight">
|
||||
{project.name}
|
||||
</CardHeader>
|
||||
<CardContent>{project.databases.length} databases</CardContent>
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed">
|
||||
Manage your databases and backup policies for this project.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between pt-3 border-t border-border/50">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-primary opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-x-[-10px] group-hover:translate-x-0">View Project</span>
|
||||
<div className="flex items-center gap-1 text-muted-foreground group-hover:text-primary transition-colors">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest group-hover:hidden">Details</span>
|
||||
<ChevronRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||
import {useState} from "react";
|
||||
import {Card} from "@/components/ui/card";
|
||||
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
|
||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||
import {Database} from "@/db/schema/07_database";
|
||||
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
|
||||
import {ChevronRight, Activity, Fingerprint, Copy, Check} from "lucide-react";
|
||||
|
||||
export type projectDatabaseCardProps = {
|
||||
data: Database;
|
||||
@@ -15,10 +16,10 @@ export type projectDatabaseCardProps = {
|
||||
};
|
||||
|
||||
export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
|
||||
const {organizationSlug, data: database, extendedProps: extendedProps} = props;
|
||||
const {data: database, extendedProps: extendedProps} = props;
|
||||
|
||||
return (
|
||||
<Link className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md rounded-xl"
|
||||
<Link className="group block transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl"
|
||||
href={`/dashboard/projects/${extendedProps.id}/database/${database.id}`}>
|
||||
<DatabaseCard data={database}/>
|
||||
</Link>
|
||||
@@ -31,22 +32,73 @@ export type databaseCardProps = {
|
||||
|
||||
export const DatabaseCard = (props: databaseCardProps) => {
|
||||
const {data: database} = props;
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const handleCopy = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(database.agentDatabaseId);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="flex flex-row justify-between space-x-2 ">
|
||||
<div className="flex items-center justify-start w-full space-x-5 ">
|
||||
<Image src={`/images/${database.dbms}.png`} alt="Database type Icon" width={60} height={60}
|
||||
className="object-cover ml-4"/>
|
||||
<div className="justify-between">
|
||||
<div className="font-medium">Name: {database.name}</div>
|
||||
<div className="text-sm text-muted-foreground">Generated Id: {database.agentDatabaseId}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Last contact: {formatDateLastContact(database.lastContact)}
|
||||
<Card className="relative h-full flex flex-col p-4 transition-all border-border/50 bg-card hover:bg-accent/50 hover:border-primary/50 group-hover:shadow-lg overflow-hidden gap-0">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="relative w-12 h-12 p-2 bg-background rounded-xl border border-border/50 shadow-sm flex items-center justify-center group-hover:border-primary/30 transition-all duration-300 group-hover:scale-105">
|
||||
<Image
|
||||
src={`/images/${database.dbms}.png`}
|
||||
alt={`${database.dbms} icon`}
|
||||
width={32}
|
||||
height={32}
|
||||
className="object-contain w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-3">
|
||||
<div className="scale-100 origin-right">
|
||||
<ConnectionCircle date={database.lastContact}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center px-5 justify-center">
|
||||
<ConnectionCircle date={database.lastContact}/>
|
||||
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<h3 className="text-lg font-black text-foreground group-hover:text-primary transition-colors truncate tracking-tight">
|
||||
{database.name}
|
||||
</h3>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 text-muted-foreground group-hover:text-foreground transition-colors">
|
||||
<div className="p-1 bg-muted/50 rounded-lg">
|
||||
<Fingerprint className="w-3.5 h-3.5" />
|
||||
</div>
|
||||
<span className="font-mono text-[10px] font-bold truncate">
|
||||
{database.agentDatabaseId}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="ml-1 p-0.5 hover:bg-muted rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 z-10"
|
||||
title="Copy ID"
|
||||
>
|
||||
{isCopied ? <Check className="w-2.5 h-2.5 text-green-500" /> : <Copy className="w-2.5 h-2.5" />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground group-hover:text-foreground transition-colors">
|
||||
<div className="p-1 bg-muted/50 rounded-lg">
|
||||
<Activity className="w-3.5 h-3.5" />
|
||||
</div>
|
||||
<span className="text-[10px] font-bold uppercase tracking-tight">
|
||||
{formatDateLastContact(database.lastContact)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between pt-3 border-t border-border/50">
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-primary opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-x-[-10px] group-hover:translate-x-0">Open</span>
|
||||
<div className="flex items-center gap-1 text-muted-foreground group-hover:text-primary transition-colors">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest group-hover:hidden">Details</span>
|
||||
<ChevronRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -62,7 +62,6 @@ export const createProjectAction = userAction
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TYPE "public"."provider_kind" ADD VALUE 'discord';--> statement-breakpoint
|
||||
ALTER TYPE "public"."provider_kind" ADD VALUE 'telegram';
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TYPE "public"."provider_kind" ADD VALUE 'gotify';--> statement-breakpoint
|
||||
ALTER TYPE "public"."provider_kind" ADD VALUE 'ntfy';
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TYPE "public"."provider_kind" ADD VALUE 'webhook';
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -120,6 +120,27 @@
|
||||
"when": 1767363779637,
|
||||
"tag": "0016_broken_morgan_stark",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 17,
|
||||
"version": "7",
|
||||
"when": 1767778503470,
|
||||
"tag": "0017_wild_purple_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 18,
|
||||
"version": "7",
|
||||
"when": 1767780819624,
|
||||
"tag": "0018_smiling_mole_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 19,
|
||||
"version": "7",
|
||||
"when": 1767782077775,
|
||||
"tag": "0019_overjoyed_butterfly",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {z} from "zod";
|
||||
import {OrganizationInvitation} from "@/db/schema/05_invitation";
|
||||
|
||||
|
||||
export const providerKindEnum = pgEnum('provider_kind', ['slack', 'smtp']);
|
||||
export const providerKindEnum = pgEnum('provider_kind', ['slack', 'smtp', 'discord', 'telegram', 'gotify', 'ntfy', 'webhook']);
|
||||
|
||||
export const notificationChannel = pgTable('notification_channel', {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
|
||||
@@ -66,10 +66,8 @@ export function backupColumns(
|
||||
header: "Reference",
|
||||
cell: ({row}) => {
|
||||
const fileName = row.original.file
|
||||
console.log(row.original)
|
||||
const reference = row.original.id
|
||||
const isImported = isImportedFilename(`${fileName}`)
|
||||
console.log(isImported)
|
||||
return isImported ? `${reference} (imported)` : `${reference}`
|
||||
},
|
||||
},
|
||||
@@ -77,7 +75,6 @@ export function backupColumns(
|
||||
accessorKey: "fileSize",
|
||||
header: "Size",
|
||||
cell: ({row}) => {
|
||||
console.log(row.original.fileSize)
|
||||
return formatBytes(row.getValue("fileSize"))
|
||||
},
|
||||
},
|
||||
@@ -163,7 +160,6 @@ export function backupColumns(
|
||||
} else if (settings.storage == "s3") {
|
||||
data = await getFileUrlPreSignedS3Action(`backups/${database.project?.slug}/${fileName}`);
|
||||
}
|
||||
console.log(data)
|
||||
if (data?.data?.success) {
|
||||
url = data.data.value ?? "";
|
||||
} else {
|
||||
|
||||
@@ -7,7 +7,6 @@ import {db} from "@/db";
|
||||
import {notificationLog} from "@/db/schema/11_notification-log";
|
||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||
import {Json} from "drizzle-zod";
|
||||
import {AlertPolicy} from "@/db/schema/10_alert-policy";
|
||||
|
||||
export async function dispatchNotification(
|
||||
payload: EventPayload,
|
||||
@@ -85,7 +84,7 @@ export async function dispatchNotification(
|
||||
success: false,
|
||||
channelId: channelId || "",
|
||||
provider: null,
|
||||
error: "Channel not active¬",
|
||||
error: "Channel not active",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,67 +1,64 @@
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
import {EventKind, EventPayload} from "@/features/notifications/types";
|
||||
import {dispatchNotification} from "@/features/notifications/dispatch";
|
||||
|
||||
import { DatabaseWith } from "@/db/schema/07_database";
|
||||
import { EventKind, EventPayload } from "@/features/notifications/types";
|
||||
import { dispatchNotification } from "@/features/notifications/dispatch";
|
||||
|
||||
export async function sendNotificationsBackupRestore(database: DatabaseWith, event: EventKind) {
|
||||
|
||||
if (database.alertPolicies && database.alertPolicies.length > 0) {
|
||||
for (const alertPolicy of database.alertPolicies) {
|
||||
if (alertPolicy.enabled) {
|
||||
|
||||
const eventKinds = alertPolicy.eventKinds
|
||||
|
||||
if (eventKinds.includes(event)) {
|
||||
|
||||
const date = new Date()
|
||||
|
||||
let level: "info" | "critical" = "info";
|
||||
let message = "";
|
||||
let error: string | null = null;
|
||||
|
||||
switch (event) {
|
||||
case "error_backup":
|
||||
case "error_restore":
|
||||
level = "critical";
|
||||
message = `An error occurred during ${event.includes("backup") ? "backup" : "restore"} on ${date.toISOString()}.`;
|
||||
error = "Check database connection or agent";
|
||||
break;
|
||||
case "success_backup":
|
||||
case "success_restore":
|
||||
level = "info";
|
||||
message = `${event.includes("backup") ? "Backup" : "Restore"} completed successfully at ${date.toISOString()}.`;
|
||||
break;
|
||||
case "weekly_report":
|
||||
level = "info";
|
||||
message = `Weekly report generated at ${date.toISOString()}.`;
|
||||
break;
|
||||
}
|
||||
|
||||
const titleMap: Record<EventKind, string> = {
|
||||
error_backup: `Backup Notification`,
|
||||
error_restore: `Restore Notification`,
|
||||
success_backup: `Backup Notification`,
|
||||
success_restore: `Restore Notification`,
|
||||
weekly_report: `Weekly Report Notification`,
|
||||
};
|
||||
|
||||
|
||||
const payload: EventPayload = {
|
||||
title: titleMap[event],
|
||||
message,
|
||||
level: level,
|
||||
event: event,
|
||||
data: {
|
||||
host: database.name,
|
||||
id: database.id,
|
||||
agentDatabaseId: database.agentDatabaseId,
|
||||
error,
|
||||
},
|
||||
};
|
||||
return await dispatchNotification(payload, alertPolicy.id, undefined, undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!database.alertPolicies || database.alertPolicies.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const activePolicies = database.alertPolicies.filter(policy =>
|
||||
policy.enabled && policy.eventKinds.includes(event)
|
||||
);
|
||||
|
||||
const promises = activePolicies.map(alertPolicy => {
|
||||
const date = new Date();
|
||||
let level: "info" | "critical" = "info";
|
||||
let message = "";
|
||||
let error: string | null = null;
|
||||
|
||||
switch (event) {
|
||||
case "error_backup":
|
||||
case "error_restore":
|
||||
level = "critical";
|
||||
message = `An error occurred during ${event.includes("backup") ? "backup" : "restore"} on ${date.toISOString()}.`;
|
||||
error = "Check database connection or agent";
|
||||
break;
|
||||
case "success_backup":
|
||||
case "success_restore":
|
||||
level = "info";
|
||||
message = `${event.includes("backup") ? "Backup" : "Restore"} completed successfully at ${date.toISOString()}.`;
|
||||
break;
|
||||
case "weekly_report":
|
||||
level = "info";
|
||||
message = `Weekly report generated at ${date.toISOString()}.`;
|
||||
break;
|
||||
}
|
||||
|
||||
const titleMap: Record<EventKind, string> = {
|
||||
error_backup: `Backup Notification`,
|
||||
error_restore: `Restore Notification`,
|
||||
success_backup: `Backup Notification`,
|
||||
success_restore: `Restore Notification`,
|
||||
weekly_report: `Weekly Report Notification`,
|
||||
};
|
||||
|
||||
const payload: EventPayload = {
|
||||
title: titleMap[event],
|
||||
message,
|
||||
level: level,
|
||||
event: event,
|
||||
data: {
|
||||
host: database.name,
|
||||
id: database.id,
|
||||
agentDatabaseId: database.agentDatabaseId,
|
||||
error,
|
||||
},
|
||||
};
|
||||
|
||||
return dispatchNotification(payload, alertPolicy.id, undefined, undefined);
|
||||
});
|
||||
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type {EventPayload, DispatchResult} from '../types';
|
||||
|
||||
export async function sendDiscord(
|
||||
config: { discordWebhook: string },
|
||||
payload: EventPayload
|
||||
): Promise<DispatchResult> {
|
||||
const {discordWebhook: webhookUrl} = config;
|
||||
|
||||
const embeds = [
|
||||
{
|
||||
title: `[${payload.level.toUpperCase()}] ${payload.title}`,
|
||||
description: payload.message,
|
||||
color: payload.level === 'critical' ? 15158332 : payload.level === 'warning' ? 16776960 : 3447003,
|
||||
fields: payload.data ? [
|
||||
{
|
||||
name: 'Data',
|
||||
value: `
|
||||
${JSON.stringify(payload.data, null, 2).substring(0, 1000)}
|
||||
`,
|
||||
inline: false
|
||||
}
|
||||
] : [],
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
];
|
||||
|
||||
const body = {
|
||||
embeds,
|
||||
};
|
||||
|
||||
const res = await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`Discord error: ${res.status} ${err}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: 'discord',
|
||||
message: 'Sent to Discord',
|
||||
response: res.statusText,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type {EventPayload, DispatchResult} from '../types';
|
||||
|
||||
export async function sendGotify(
|
||||
config: { gotifyServerUrl: string; gotifyAppToken: string },
|
||||
payload: EventPayload
|
||||
): Promise<DispatchResult> {
|
||||
const {gotifyServerUrl, gotifyAppToken} = config;
|
||||
|
||||
const baseUrl = gotifyServerUrl.replace(/\/$/, "");
|
||||
|
||||
const body = {
|
||||
title: `[${payload.level.toUpperCase()}] ${payload.title}`,
|
||||
message: `${payload.message}\n\n${payload.data ? `Data:\n${JSON.stringify(payload.data, null, 2)}` : ''}`,
|
||||
priority: payload.level === 'critical' ? 8 : payload.level === 'warning' ? 5 : 2,
|
||||
};
|
||||
|
||||
const res = await fetch(`${baseUrl}/message?token=${gotifyAppToken}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`Gotify error: ${res.status} ${err}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: 'gotify',
|
||||
message: 'Sent to Gotify',
|
||||
response: await res.json(),
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,11 @@
|
||||
import type {ProviderKind, EventPayload, DispatchResult} from '../types';
|
||||
import {sendSlack} from './slack';
|
||||
import {sendSmtp} from './smtp';
|
||||
import {sendDiscord} from "@/features/notifications/providers/discord";
|
||||
import {sendTelegram} from "@/features/notifications/providers/telegram";
|
||||
import {sendGotify} from "@/features/notifications/providers/gotify";
|
||||
import {sendNtfy} from "@/features/notifications/providers/ntfy";
|
||||
import {sendWebhook} from "@/features/notifications/providers/webhook";
|
||||
|
||||
const handlers: Record<
|
||||
ProviderKind,
|
||||
@@ -9,6 +14,11 @@ const handlers: Record<
|
||||
> = {
|
||||
slack: sendSlack,
|
||||
smtp: sendSmtp,
|
||||
discord: sendDiscord,
|
||||
telegram: sendTelegram,
|
||||
gotify: sendGotify,
|
||||
ntfy: sendNtfy,
|
||||
webhook: sendWebhook
|
||||
};
|
||||
|
||||
export async function dispatchViaProvider(
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type {EventPayload, DispatchResult} from '../types';
|
||||
|
||||
export async function sendNtfy(
|
||||
config: { ntfyServerUrl?: string; ntfyTopic: string; ntfyToken?: string },
|
||||
payload: EventPayload
|
||||
): Promise<DispatchResult> {
|
||||
const {ntfyServerUrl, ntfyTopic, ntfyToken} = config;
|
||||
|
||||
const baseUrl = (ntfyServerUrl || "https://ntfy.sh").replace(/\/$/, "");
|
||||
|
||||
const body = {
|
||||
topic: ntfyTopic,
|
||||
title: payload.title,
|
||||
message: payload.message + (payload.data ? `\n\nData:\n${JSON.stringify(payload.data, null, 2)}` : ''),
|
||||
priority: 1,
|
||||
tags: [payload.level === 'critical' ? 'rotating_light' : payload.level === 'warning' ? 'warning' : 'information_source'],
|
||||
};
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (ntfyToken) {
|
||||
headers['Authorization'] = `Bearer ${ntfyToken}`;
|
||||
}
|
||||
|
||||
const res = await fetch(`${baseUrl}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: headers,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`ntfy error: ${res.status} ${err}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: 'ntfy',
|
||||
message: 'Sent to ntfy',
|
||||
response: await res.json(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type {EventPayload, DispatchResult} from '../types';
|
||||
|
||||
export async function sendTelegram(
|
||||
config: { telegramBotToken: string; telegramChatId: string },
|
||||
payload: EventPayload
|
||||
): Promise<DispatchResult> {
|
||||
const {telegramBotToken, telegramChatId} = config;
|
||||
|
||||
// Helper to escape HTML characters
|
||||
const escapeHtml = (unsafe: string) => {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
const title = escapeHtml(payload.title);
|
||||
const message = escapeHtml(payload.message);
|
||||
const level = escapeHtml(payload.level.toUpperCase());
|
||||
const dataString = payload.data ? escapeHtml(JSON.stringify(payload.data, null, 2).substring(0, 1000)) : '';
|
||||
|
||||
const text = `<b>${title}</b>\n\n${message}\n\nLevel: <code>${level}</code>${payload.data ? `\n\nData:\n<pre>${dataString}</pre>` : ''}`;
|
||||
|
||||
const body = {
|
||||
chat_id: telegramChatId,
|
||||
text,
|
||||
parse_mode: 'HTML',
|
||||
};
|
||||
|
||||
const res = await fetch(`https://api.telegram.org/bot${telegramBotToken}/sendMessage`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`Telegram error: ${res.status} ${err}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: 'telegram',
|
||||
message: 'Sent to Telegram',
|
||||
response: await res.text(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type {EventPayload, DispatchResult} from '../types';
|
||||
|
||||
export async function sendWebhook(
|
||||
config: { webhookUrl: string; webhookSecret?: string; webhookSecretHeader?: string },
|
||||
payload: EventPayload
|
||||
): Promise<DispatchResult> {
|
||||
const {webhookUrl, webhookSecret, webhookSecretHeader} = config;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Portabase-Notifier/1.0'
|
||||
};
|
||||
|
||||
if (webhookSecret) {
|
||||
headers[webhookSecretHeader || 'X-Webhook-Secret'] = webhookSecret;
|
||||
}
|
||||
|
||||
const res = await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
headers: headers,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`Webhook error: ${res.status} ${err}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: 'webhook',
|
||||
message: 'Sent to Webhook',
|
||||
response: await res.text(),
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export type ProviderKind = 'slack' | 'smtp';
|
||||
export type ProviderKind = 'slack' | 'smtp' | 'discord' | 'telegram' | 'gotify' | 'ntfy' | 'webhook';
|
||||
|
||||
export interface DispatchResult {
|
||||
success: boolean;
|
||||
|
||||
@@ -15,7 +15,7 @@ import {Setting} from "@/db/schema/01_setting";
|
||||
import * as drizzleDb from "@/db";
|
||||
|
||||
|
||||
const privateS3ImageDir = "images/";
|
||||
const imageDir = "images/";
|
||||
|
||||
|
||||
export const uploadImageAction = userAction
|
||||
@@ -39,10 +39,10 @@ export const uploadImageAction = userAction
|
||||
let result: void | UploadedObjectInfo;
|
||||
|
||||
if (settings.storage === "local") {
|
||||
fileName = `${uuid}.${fileFormat}`;
|
||||
fileName = `${imageDir}${uuid}.${fileFormat}`;
|
||||
result = await uploadLocal(fileName, buffer);
|
||||
} else if (settings.storage === "s3") {
|
||||
fileName = `${privateS3ImageDir}${uuid}.${fileFormat}`;
|
||||
fileName = `${imageDir}${uuid}.${fileFormat}`;
|
||||
result = await uploadS3Compatible(env.S3_BUCKET_NAME ?? "", fileName, buffer);
|
||||
} else {
|
||||
throw new Error(`Unsupported storage type: ${settings.storage}`);
|
||||
@@ -54,12 +54,11 @@ export const uploadImageAction = userAction
|
||||
|
||||
|
||||
async function uploadLocal(fileName: string, buffer: any) {
|
||||
const localDir = "private/uploads/images/";
|
||||
const localDir = "private/uploads/";
|
||||
try {
|
||||
await mkdir(path.join(process.cwd(), localDir), {recursive: true});
|
||||
return await writeFile(path.join(process.cwd(), localDir + fileName), buffer);
|
||||
} catch (error) {
|
||||
console.log("Error occured ", error);
|
||||
throw new Error("An error occured while importing image");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,15 +45,12 @@ export function useAutoRefresh(options: UseAutoRefreshOptions) {
|
||||
|
||||
try {
|
||||
payload = JSON.parse(event.data);
|
||||
} catch {
|
||||
// keep raw payload
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const shouldRefresh =
|
||||
options.sse?.shouldRefresh?.(payload) ?? true;
|
||||
|
||||
if (shouldRefresh) {
|
||||
console.log("Received event source", eventSource);
|
||||
router.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
+4
-15
@@ -46,11 +46,7 @@ export const auth = betterAuth({
|
||||
{}
|
||||
),
|
||||
});
|
||||
|
||||
},
|
||||
onPasswordReset: async ({user}, request) => {
|
||||
console.log(`Password for user ${user.email} has been reset.`);
|
||||
},
|
||||
}
|
||||
},
|
||||
emailVerification: {
|
||||
async sendVerificationEmail({user, token, url}) {
|
||||
@@ -176,13 +172,12 @@ export const auth = betterAuth({
|
||||
const role = userCount === 0 ? "owner" : "admin";
|
||||
|
||||
|
||||
const defaultOrgSlug = "default"; // change this if your default org has a different slug
|
||||
const defaultOrgSlug = "default";
|
||||
const defaultOrg = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.slug, defaultOrgSlug),
|
||||
});
|
||||
|
||||
if (defaultOrg) {
|
||||
console.log(user)
|
||||
await db.insert(drizzleDb.schemas.member).values({
|
||||
userId: user.id,
|
||||
organizationId: defaultOrg.id,
|
||||
@@ -295,7 +290,6 @@ export const auth = betterAuth({
|
||||
};
|
||||
}
|
||||
|
||||
console.log("sessionId", session.id);
|
||||
|
||||
const [aa] = await db
|
||||
.update(drizzleUser.session)
|
||||
@@ -303,7 +297,6 @@ export const auth = betterAuth({
|
||||
.where(eq(drizzleUser.session.id, session.id))
|
||||
.returning();
|
||||
|
||||
console.log("aaaaa", aa);
|
||||
|
||||
return {
|
||||
...session,
|
||||
@@ -516,9 +509,7 @@ export const checkSlugOrganization = async (slug: string) => {
|
||||
});
|
||||
|
||||
return status;
|
||||
} catch (e) {
|
||||
console.log("err", e);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getActiveMember = async () => {
|
||||
@@ -542,7 +533,5 @@ export const setActiveOrganization = async (slug: string) => {
|
||||
organizationSlug: slug,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("error", e);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
@@ -18,7 +18,6 @@ export const retentionCleanTask = async () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log(databases);
|
||||
|
||||
for (const db of databases) {
|
||||
if (!db.retentionPolicy) continue; // no policy = skip
|
||||
@@ -34,7 +33,6 @@ export async function enforceRetention(
|
||||
databaseId: string,
|
||||
policy: typeof retentionPolicy.$inferSelect
|
||||
) {
|
||||
console.log(`EnforceRetention: ${policy}`);
|
||||
|
||||
switch (policy.type) {
|
||||
case "count":
|
||||
|
||||
@@ -8,7 +8,6 @@ export async function generateEdgeKey(serverUrl: string, agentId: string): Promi
|
||||
agentId,
|
||||
publicKey
|
||||
};
|
||||
console.log(edgeKeyData);
|
||||
const edgeKeyJson = JSON.stringify(edgeKeyData);
|
||||
const edgeKeyBuffer = Buffer.from(edgeKeyJson, 'utf-8');
|
||||
return edgeKeyBuffer.toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
|
||||
|
||||
@@ -171,22 +171,18 @@ export async function createPresignedUrlToUpload({
|
||||
return await s3Client.presignedPutObject(bucketName, fileName, expiry);
|
||||
}
|
||||
|
||||
// Function to create a bucket and make it public
|
||||
export async function createPublicBucket({bucketName}: { bucketName: string }) {
|
||||
const s3Client = await getS3Client();
|
||||
|
||||
try {
|
||||
// Check if the bucket already exists
|
||||
const exists = await s3Client.bucketExists(bucketName);
|
||||
if (!exists) {
|
||||
// Create the bucket
|
||||
await s3Client.makeBucket(bucketName); // Change region if needed
|
||||
await s3Client.makeBucket(bucketName);
|
||||
console.log(`Bucket ${bucketName} created successfully.`);
|
||||
} else {
|
||||
console.log(`Bucket ${bucketName} already exists.`);
|
||||
}
|
||||
|
||||
// Define a bucket policy for public access
|
||||
const policy = {
|
||||
Version: "2012-10-17",
|
||||
Statement: [
|
||||
@@ -199,7 +195,6 @@ export async function createPublicBucket({bucketName}: { bucketName: string }) {
|
||||
],
|
||||
};
|
||||
|
||||
// Set the policy to the bucket
|
||||
await s3Client.setBucketPolicy(bucketName, JSON.stringify(policy));
|
||||
console.log(`Bucket ${bucketName} is now public.`);
|
||||
} catch (error) {
|
||||
@@ -242,7 +237,6 @@ export async function createPresignedUrlToDownload({
|
||||
bucketName,
|
||||
fileName,
|
||||
errorMessage: err?.message,
|
||||
// stack: err?.stack,
|
||||
});
|
||||
throw {error: err.message ?? "Unknown error"};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user