mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Working on implementation of alert policies. Some refactoring in user profile and patching a bug in status endpoint api.
This commit is contained in:
Binary file not shown.
@@ -49,10 +49,12 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
|||||||
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
|
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</div>
|
</div>
|
||||||
<PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription>
|
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between gap-8 mb-6">
|
|
||||||
|
|
||||||
|
{agent.description && (
|
||||||
|
<PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription>
|
||||||
|
)}
|
||||||
|
<PageContent className="flex flex-col w-full h-full justify-between gap-6">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:justify-between gap-6 ">
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
<Card className="w-full sm:w-auto flex-1">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">Databases</CardTitle>
|
<CardTitle className="text-sm font-medium">Databases</CardTitle>
|
||||||
@@ -76,7 +78,7 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<Card className="w-full sm:w-auto flex-1 mb-4">
|
<Card className="w-full sm:w-auto flex-1 ">
|
||||||
<CardHeader className="font-bold text-xl">
|
<CardHeader className="font-bold text-xl">
|
||||||
Edge Key
|
Edge Key
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -86,7 +88,8 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
|||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<CardsWithPagination cardsPerPage={2} data={agent.databases} cardItem={DatabaseCard}/>
|
<CardsWithPagination cardsPerPage={4} numberOfColumns={2} data={agent.databases}
|
||||||
|
cardItem={DatabaseCard}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
|||||||
|
|
||||||
const notificationChannels = await getOrganizationChannels(organization.id)
|
const notificationChannels = await getOrganizationChannels(organization.id)
|
||||||
|
|
||||||
console.log("notificationChannels", notificationChannels);
|
|
||||||
|
|
||||||
|
|
||||||
const isMember = activeMember?.role === "member";
|
const isMember = activeMember?.role === "member";
|
||||||
const isOwner = activeMember?.role === "owner";
|
const isOwner = activeMember?.role === "owner";
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {Page, PageContent, PageTitle} from "@/features/layout/page";
|
|||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {UserForm} from "@/components/wrappers/dashboard/profile/user-form/user-form";
|
import {UserForm} from "@/components/wrappers/dashboard/profile/user-form/user-form";
|
||||||
import {Badge} from "@/components/ui/badge";
|
import {Badge} from "@/components/ui/badge";
|
||||||
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/button-delete-account/button-delete-account";
|
|
||||||
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/avatar/avatar-with-upload";
|
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/avatar/avatar-with-upload";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
import {getAccounts, getSessions} from "@/lib/auth/auth";
|
import {getAccounts, getSessions} from "@/lib/auth/auth";
|
||||||
@@ -26,6 +25,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
const sessions = await getSessions();
|
const sessions = await getSessions();
|
||||||
const accounts = await getAccounts();
|
const accounts = await getAccounts();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="justify-between gap-2 sm:flex">
|
<div className="justify-between gap-2 sm:flex">
|
||||||
@@ -48,7 +48,8 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
<PageContent>
|
<PageContent>
|
||||||
<UserForm
|
<UserForm
|
||||||
userId={user.id}
|
userId={user.id}
|
||||||
sessions={sessions} accounts={accounts}
|
sessions={sessions}
|
||||||
|
accounts={accounts}
|
||||||
defaultValues={{
|
defaultValues={{
|
||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
|||||||
@@ -64,10 +64,12 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
|
||||||
const [databaseUpdated] = await dbClient
|
const [databaseUpdated] = await dbClient
|
||||||
.update(drizzleDb.schemas.database)
|
.update(drizzleDb.schemas.database)
|
||||||
.set({lastContact: lastContact})
|
.set({
|
||||||
|
agentId: agent.id,
|
||||||
|
lastContact: lastContact
|
||||||
|
})
|
||||||
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
|
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -52,7 +52,7 @@
|
|||||||
"@zenstackhq/runtime": "2.14.2",
|
"@zenstackhq/runtime": "2.14.2",
|
||||||
"argon2": "^0.43.0",
|
"argon2": "^0.43.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"better-auth": "1.3.1",
|
"better-auth": "1.4.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.553.0",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
"motion": "^12.23.24",
|
"motion": "^12.23.24",
|
||||||
"next": "16.0.0",
|
"next": "16.0.5",
|
||||||
"next-safe-action": "^7.10.8",
|
"next-safe-action": "^7.10.8",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
@@ -105,6 +105,7 @@
|
|||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.5",
|
||||||
"@zenstackhq/openapi": "^2.14.2",
|
"@zenstackhq/openapi": "^2.14.2",
|
||||||
"@zenstackhq/tanstack-query": "^2.14.2",
|
"@zenstackhq/tanstack-query": "^2.14.2",
|
||||||
|
"baseline-browser-mapping": "^2.8.32",
|
||||||
"drizzle-kit": "^0.31.1",
|
"drizzle-kit": "^0.31.1",
|
||||||
"eslint": "^9.39.0",
|
"eslint": "^9.39.0",
|
||||||
"eslint-config-next": "^16.0.1",
|
"eslint-config-next": "^16.0.1",
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const LoginSchema = z.object({
|
export const LoginSchema = z.object({
|
||||||
email: z.string(),
|
email: z.string().email({message: "Email is invalid"}),
|
||||||
password: z.string(),
|
password: z.string().nonempty({message: "Password could not be empty"}),
|
||||||
});
|
|
||||||
|
})
|
||||||
|
|
||||||
export type LoginType = z.infer<typeof LoginSchema>;
|
export type LoginType = z.infer<typeof LoginSchema>;
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import {cn} from "@/lib/utils";
|
||||||
|
|
||||||
export type ConnectionCircleProps = {
|
export type ConnectionCircleProps = {
|
||||||
date?: Date | null;
|
date?: Date | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ConnectionCircle = ({ date }: ConnectionCircleProps) => {
|
export const ConnectionCircle = ({date}: ConnectionCircleProps) => {
|
||||||
let style = "bg-gray-300 border-gray-400";
|
let style = "bg-gray-300 border-gray-400";
|
||||||
|
|
||||||
if (date instanceof Date && !isNaN(date.getTime())) {
|
if (date instanceof Date && !isNaN(date.getTime())) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const timestamp = date.getTime();
|
const timestamp = date.getTime();
|
||||||
const interval = now - timestamp;
|
const interval_seconds = (now - timestamp) / 1000;
|
||||||
|
|
||||||
console.log({ now, timestamp, interval });
|
console.log({now, timestamp, interval_seconds});
|
||||||
|
|
||||||
if (interval < 10000) {
|
if (interval_seconds < 55) {
|
||||||
style = "bg-green-400 border-green-600";
|
style = "bg-green-400 border-green-600";
|
||||||
} else if (interval <= 20000) {
|
} else if (interval_seconds <= 60) {
|
||||||
style = "bg-orange-400 border-orange-600";
|
style = "bg-orange-400 border-orange-600";
|
||||||
} else {
|
} else {
|
||||||
style = "bg-red-400 border-red-600";
|
style = "bg-red-400 border-red-600";
|
||||||
@@ -27,5 +27,5 @@ export const ConnectionCircle = ({ date }: ConnectionCircleProps) => {
|
|||||||
|
|
||||||
console.log(style);
|
console.log(style);
|
||||||
|
|
||||||
return <div className={cn("w-5 h-5 rounded-full border-4", style)} />;
|
return <div className={cn("w-5 h-5 rounded-full border-4", style)}/>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,28 +1,36 @@
|
|||||||
import {Icon} from "@iconify/react";
|
import {Icon} from "@iconify/react";
|
||||||
import {CircleHelp, KeyRound} from "lucide-react";
|
import {CircleHelp, KeyRound} from "lucide-react";
|
||||||
|
import {cn} from "@/lib/utils";
|
||||||
|
|
||||||
|
|
||||||
export const providerSwitch = (provider: string) => {
|
export const providerSwitch = (provider: string, small?: boolean) => {
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
case "google":
|
case "google":
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className={cn(small ? "p-0" : "p-4")}>
|
||||||
<Icon icon={"logos:google"} height="24" />
|
{small ?
|
||||||
|
<Icon icon={"flat-color-icons:google"} height="24"/>
|
||||||
|
:
|
||||||
|
<Icon icon={"logos:google"} height="24"/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case "credential":
|
case "credential":
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-x-2 items-center p-4">
|
<div className={cn("flex flex-row gap-x-2 items-center", small ? "p-0" : "p-4")}>
|
||||||
<KeyRound height="24" />
|
<KeyRound height="24"/>
|
||||||
<span>Email and Password</span>
|
{!small && (<span>Email and Password</span>)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-x-2 items-center p-4">
|
<div className={cn("flex flex-row gap-x-2 items-center", small ? "p-0" : "p-4")}>
|
||||||
<CircleHelp height="24" />
|
<CircleHelp height="24"/>
|
||||||
<span>No credentials</span>
|
{!small && (<span>No credentials</span>)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+7
-12
@@ -6,22 +6,17 @@ import {toast} from "sonner";
|
|||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {unlinkUserProviderAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
|
import {unlinkUserProviderAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
|
||||||
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
||||||
|
import {Account} from "better-auth";
|
||||||
|
|
||||||
export const accountsColumns: ColumnDef<{
|
|
||||||
id: string;
|
export const accountsColumns: ColumnDef<Account>[] = [
|
||||||
provider: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
accountId: string;
|
|
||||||
scopes: string[];
|
|
||||||
}>[] = [
|
|
||||||
{
|
{
|
||||||
id: "provider",
|
id: "provider",
|
||||||
header: "Provider",
|
header: "Provider",
|
||||||
cell: ({row}) => {
|
cell: ({row}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{providerSwitch(row.original.provider)}
|
{providerSwitch(row.original.providerId)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)
|
)
|
||||||
@@ -36,7 +31,7 @@ export const accountsColumns: ColumnDef<{
|
|||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
if (row.original.provider === "credential") {
|
if (row.original.providerId === "credential") {
|
||||||
toast.error(`This provider cannot be unlinked.`);
|
toast.error(`This provider cannot be unlinked.`);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
return;
|
return;
|
||||||
@@ -49,7 +44,7 @@ export const accountsColumns: ColumnDef<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const status = await unlinkUserProviderAction({
|
const status = await unlinkUserProviderAction({
|
||||||
provider: row.original.provider,
|
provider: row.original.providerId,
|
||||||
account: row.original.accountId,
|
account: row.original.accountId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -66,7 +61,7 @@ export const accountsColumns: ColumnDef<{
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ButtonWithLoading
|
<ButtonWithLoading
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={row.original.provider === "credential" || table.getRowModel().rows.length <= 1}
|
disabled={row.original.providerId === "credential" || table.getRowModel().rows.length <= 1}
|
||||||
icon={<Unlink color="red" size={15}/>}
|
icon={<Unlink color="red" size={15}/>}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutation.mutateAsync();
|
await mutation.mutateAsync();
|
||||||
|
|||||||
@@ -68,13 +68,13 @@ export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "accounts",
|
accessorKey: "accounts",
|
||||||
header: "Provider ID",
|
header: "Provider(s)",
|
||||||
cell: ({row}) => {
|
cell: ({row}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-row items-center gap-x-2">
|
||||||
{row.original.accounts.map((item) => (
|
{row.original.accounts.map((item) => (
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
{providerSwitch(item.providerId)}
|
{providerSwitch(item.providerId, true)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -93,9 +93,11 @@ export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
|||||||
id: "actions",
|
id: "actions",
|
||||||
cell: ({row}) => {
|
cell: ({row}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const {data: session, isPending} = useSession();
|
const {data: session, isPending, error} = useSession();
|
||||||
const isSuperAdmin = session?.user.role == "superadmin";
|
const isSuperAdmin = session?.user.role == "superadmin";
|
||||||
|
|
||||||
|
if (isPending || error) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonDeleteUser
|
<ButtonDeleteUser
|
||||||
disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
|
disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
|
||||||
|
|||||||
+23
-16
@@ -1,33 +1,34 @@
|
|||||||
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import {ColumnDef} from "@tanstack/react-table";
|
||||||
import { Session } from "better-auth";
|
import {Session} from "better-auth";
|
||||||
import { Unlink } from "lucide-react";
|
import {Unlink} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import {toast} from "sonner";
|
||||||
import { useRouter } from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import detectOSWithUA from "@/utils/os-parser";
|
import detectOSWithUA from "@/utils/os-parser";
|
||||||
import { Icon } from "@iconify/react";
|
import {Icon} from "@iconify/react";
|
||||||
import { authClient } from "@/lib/auth/auth-client";
|
import {authClient} from "@/lib/auth/auth-client";
|
||||||
import { timeAgo } from "@/utils/date-formatting";
|
import {timeAgo} from "@/utils/date-formatting";
|
||||||
import {deleteUserSessionAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
|
import {deleteUserSessionAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
|
||||||
|
|
||||||
export const sessionsColumns: ColumnDef<Session>[] = [
|
export const sessionsColumns: ColumnDef<Session>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "expiresAt",
|
accessorKey: "expiresAt",
|
||||||
header: "Expires At",
|
header: "Expires At",
|
||||||
cell: ({ row }) => {
|
cell: ({row}) => {
|
||||||
return timeAgo(row.original.expiresAt);
|
return timeAgo(row.original.expiresAt);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "device",
|
id: "device",
|
||||||
header: "Device",
|
header: "Device",
|
||||||
cell: ({ row }) => {
|
cell: ({row}) => {
|
||||||
const os = detectOSWithUA(row.original.userAgent!);
|
const os = detectOSWithUA(row.original.userAgent!);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-x-2 items-center pt-4 pb-4">
|
<div className="flex flex-row gap-x-2 items-center pt-4 pb-4">
|
||||||
{os.icon && <Icon icon={`logos:${os.icon.name}`} height={os.icon.size.height} width={os.icon.size.width} />}
|
{os.icon &&
|
||||||
|
<Icon icon={`logos:${os.icon.name}`} height={os.icon.size.height} width={os.icon.size.width}/>}
|
||||||
{os.showText && <span>{os.name}</span>}
|
{os.showText && <span>{os.name}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -40,10 +41,11 @@ export const sessionsColumns: ColumnDef<Session>[] = [
|
|||||||
{
|
{
|
||||||
header: "Action",
|
header: "Action",
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: ({ row }) => {
|
cell: ({row}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { data: session } = authClient.useSession();
|
const {data: session, isPending, error} = authClient.useSession();
|
||||||
|
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
@@ -64,12 +66,17 @@ export const sessionsColumns: ColumnDef<Session>[] = [
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (isPending || error) return null;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ButtonWithLoading
|
<ButtonWithLoading
|
||||||
|
isPending={mutation.isPending}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={session?.session.id === row.original.id}
|
disabled={session?.session.id === row.original.id}
|
||||||
icon={<Unlink color="red" size={15} />}
|
icon={<Unlink color="red" size={15}/>}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutation.mutateAsync();
|
await mutation.mutateAsync();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const NotifierAddEditModal = ({
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle> {isCreate ? "Add" : "Edit"} Notification Channel</DialogTitle>
|
<DialogTitle> {isCreate ? "Add" : "Edit"} Notification Channel</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ export const DeleteNotifierButton = ({notificationChannelId, organizationId}: De
|
|||||||
return (
|
return (
|
||||||
<ButtonWithConfirm
|
<ButtonWithConfirm
|
||||||
title="Delete Notifier"
|
title="Delete Notifier"
|
||||||
description="Are you sure you want to remove this notifier? This action cannot be undone."
|
description="Are you sure you want to remove this notifier ? This action cannot be undone and will delete all alert policies related to this channel !"
|
||||||
button={{
|
button={{
|
||||||
main: {
|
main: {
|
||||||
size: "icon",
|
size: "icon",
|
||||||
|
|||||||
+1
-1
@@ -153,7 +153,7 @@ export const updateNotificationChannelAction = userAction.schema(
|
|||||||
config: channel.config as JSON
|
config: channel.config as JSON
|
||||||
},
|
},
|
||||||
actionSuccess: {
|
actionSuccess: {
|
||||||
message: "Notification channel has been successfully updated.",
|
message: `Notification channel "${channel.name}" has been successfully updated.`,
|
||||||
messageParams: {notificationChannelId: channel.id},
|
messageParams: {notificationChannelId: channel.id},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
|||||||
import {
|
import {
|
||||||
NotifierTestChannelButton
|
NotifierTestChannelButton
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-test-channel-button";
|
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-test-channel-button";
|
||||||
|
import {useEffect} from "react";
|
||||||
|
|
||||||
type NotifierFormProps = {
|
type NotifierFormProps = {
|
||||||
onSuccessAction?: () => void;
|
onSuccessAction?: () => void;
|
||||||
@@ -35,16 +36,17 @@ type NotifierFormProps = {
|
|||||||
export const NotifierForm = ({onSuccessAction, organization, defaultValues}: NotifierFormProps) => {
|
export const NotifierForm = ({onSuccessAction, organization, defaultValues}: NotifierFormProps) => {
|
||||||
|
|
||||||
const isCreate = !Boolean(defaultValues);
|
const isCreate = !Boolean(defaultValues);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const form = useZodForm({
|
const form = useZodForm({
|
||||||
schema: NotificationChannelFormSchema,
|
schema: NotificationChannelFormSchema,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
defaultValues: {...defaultValues},
|
defaultValues: {...defaultValues},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset(defaultValues ? {...defaultValues} : {});
|
||||||
|
}, [defaultValues]);
|
||||||
|
|
||||||
const mutationCreateOrganisation = useMutation({
|
const mutationCreateOrganisation = useMutation({
|
||||||
mutationFn: async (values: NotificationChannelFormType) => {
|
mutationFn: async (values: NotificationChannelFormType) => {
|
||||||
|
|
||||||
@@ -60,11 +62,11 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
|||||||
|
|
||||||
if (inner?.success) {
|
if (inner?.success) {
|
||||||
toast.success(inner.actionSuccess?.message);
|
toast.success(inner.actionSuccess?.message);
|
||||||
onSuccessAction?.();
|
isCreate && onSuccessAction?.();
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} else {
|
} else {
|
||||||
toast.error(inner?.actionError?.message);
|
toast.error(inner?.actionError?.message);
|
||||||
onSuccessAction?.();
|
isCreate && onSuccessAction?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -112,8 +114,8 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="smtp">SMTP (Email)</SelectItem>
|
<SelectItem value="smtp">SMTP (Email)</SelectItem>
|
||||||
<SelectItem value="slack">Slack</SelectItem>
|
<SelectItem value="slack">Slack</SelectItem>
|
||||||
<SelectItem value="curl">Curl</SelectItem>
|
{/*<SelectItem value="curl">Curl</SelectItem>*/}
|
||||||
<SelectItem value="webhook">Webhook</SelectItem>
|
{/*<SelectItem value="webhook">Webhook</SelectItem>*/}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -137,7 +139,7 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
|||||||
<NotifierTestChannelButton notificationChannel={defaultValues}/>
|
<NotifierTestChannelButton notificationChannel={defaultValues}/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 ">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -149,7 +151,7 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<ButtonWithLoading isPending={mutationCreateOrganisation.isPending}>
|
<ButtonWithLoading isPending={mutationCreateOrganisation.isPending}>
|
||||||
Add Channel
|
{isCreate ? "Add" : "Save"} Channel
|
||||||
</ButtonWithLoading>
|
</ButtonWithLoading>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+2
@@ -2,6 +2,7 @@
|
|||||||
import {UseFormReturn} from "react-hook-form";
|
import {UseFormReturn} from "react-hook-form";
|
||||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
||||||
import {Input} from "@/components/ui/input";
|
import {Input} from "@/components/ui/input";
|
||||||
|
import {Separator} from "@/components/ui/separator";
|
||||||
|
|
||||||
|
|
||||||
type NotifierSmtpFormProps = {
|
type NotifierSmtpFormProps = {
|
||||||
@@ -11,6 +12,7 @@ type NotifierSmtpFormProps = {
|
|||||||
export const NotifierSlackForm = ({form}: NotifierSmtpFormProps) => {
|
export const NotifierSlackForm = ({form}: NotifierSmtpFormProps) => {
|
||||||
return(
|
return(
|
||||||
<>
|
<>
|
||||||
|
<Separator className="my-1"/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.slackWebhook"
|
name="config.slackWebhook"
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ type NotifierSmtpFormProps = {
|
|||||||
export const NotifierSmtpForm = ({form}: NotifierSmtpFormProps) => {
|
export const NotifierSmtpForm = ({form}: NotifierSmtpFormProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-4"/>
|
<Separator className="my-1"/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.host"
|
name="config.host"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import {Megaphone} from "lucide-react";
|
import {Filter, Megaphone} from "lucide-react";
|
||||||
|
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {
|
import {
|
||||||
@@ -12,12 +12,13 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {AlertPolicyForm} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy-form";
|
import {AlertPolicyForm} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy-form";
|
||||||
import {Database} from "@/db/schema/07_database";
|
import {DatabaseWith} from "@/db/schema/07_database";
|
||||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||||
import {Separator} from "@/components/ui/separator";
|
import {Separator} from "@/components/ui/separator";
|
||||||
|
import {Badge} from "@/components/ui/badge";
|
||||||
|
|
||||||
type AlertPolicyModalProps = {
|
type AlertPolicyModalProps = {
|
||||||
database: Database;
|
database: DatabaseWith;
|
||||||
notificationChannels: NotificationChannel[];
|
notificationChannels: NotificationChannel[];
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
}
|
}
|
||||||
@@ -28,8 +29,15 @@ export const AlertPolicyModal = ({database, notificationChannels, organizationId
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline">
|
<Button variant="outline" onClick={() => setOpen(true)} className="relative">
|
||||||
<Megaphone/>
|
<Megaphone/>
|
||||||
|
{database.alertPolicies && database.alertPolicies.length > 0 && (
|
||||||
|
<Badge
|
||||||
|
className="absolute -top-1.5 -right-1.5 h-4 w-4 rounded-full p-0 text-[10px] flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{database.alertPolicies.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|||||||
+1
-1
@@ -61,7 +61,6 @@ export const OrganizationNotifiersTab = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-6 h-full py-4">
|
<div className="flex flex-col gap-y-6 h-full py-4">
|
||||||
|
|
||||||
<div className="h-full flex flex-col gap-y-6">
|
<div className="h-full flex flex-col gap-y-6">
|
||||||
<div className={cn("hidden flex-row justify-between items-start", hasNotifiers && "flex")}>
|
<div className={cn("hidden flex-row justify-between items-start", hasNotifiers && "flex")}>
|
||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
@@ -74,6 +73,7 @@ export const OrganizationNotifiersTab = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<NotifierAddEditModal
|
<NotifierAddEditModal
|
||||||
|
|
||||||
organization={organization}
|
organization={organization}
|
||||||
open={isAddModalOpen}
|
open={isAddModalOpen}
|
||||||
onOpenChangeAction={setIsAddModalOpen}
|
onOpenChangeAction={setIsAddModalOpen}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { db } from "@/db";
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {revokeSession, unlinkAccount} from "@/lib/auth/auth";
|
import {revokeSession, unlinkAccount} from "@/lib/auth/auth";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
|
||||||
export const updateUserAction = userAction
|
export const updateUserAction = userAction
|
||||||
.schema(
|
.schema(
|
||||||
@@ -15,7 +16,7 @@ export const updateUserAction = userAction
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.action(async ({ parsedInput }) => {
|
.action(async ({ parsedInput }) => {
|
||||||
const [updatedUser] = await db.update(drizzleDb.schemas.user).set(parsedInput.data).where(eq(drizzleDb.schemas.user.id, parsedInput.id)).returning();
|
const [updatedUser] = await db.update(drizzleDb.schemas.user).set(withUpdatedAt(parsedInput.data)).where(eq(drizzleDb.schemas.user.id, parsedInput.id)).returning();
|
||||||
return {
|
return {
|
||||||
data: updatedUser,
|
data: updatedUser,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,20 +14,13 @@ import { updateUserAction } from "@/components/wrappers/dashboard/profile/user-f
|
|||||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||||
import {sessionsColumns} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/sessions/table-columns";
|
import {sessionsColumns} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/sessions/table-columns";
|
||||||
import {accountsColumns} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/accounts/table-columns";
|
import {accountsColumns} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/accounts/table-columns";
|
||||||
import {Session} from "better-auth";
|
import {Account, Session} from "better-auth";
|
||||||
|
|
||||||
export type UserFormProps = {
|
export type UserFormProps = {
|
||||||
defaultValues?: UserType;
|
defaultValues?: UserType;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
sessions: Session[];
|
sessions: Session[];
|
||||||
accounts: {
|
accounts?: Account[];
|
||||||
id: string;
|
|
||||||
provider: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
accountId: string;
|
|
||||||
scopes: string[];
|
|
||||||
}[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserForm = (props: UserFormProps) => {
|
export const UserForm = (props: UserFormProps) => {
|
||||||
@@ -120,7 +113,7 @@ export const UserForm = (props: UserFormProps) => {
|
|||||||
<CardDescription>Manage your active auth providers</CardDescription>
|
<CardDescription>Manage your active auth providers</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<DataTable columns={accountsColumns} data={props.accounts} enableSelect={false}/>
|
<DataTable columns={accountsColumns} data={props.accounts ?? []} enableSelect={false}/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
|||||||
<ButtonWithLoading
|
<ButtonWithLoading
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
||||||
}}
|
}}
|
||||||
disabled={rows.length === 0 || mutationDeleteBackups.isPending}
|
disabled={rows.length === 0 || mutationDeleteBackups.isPending}
|
||||||
icon={<MoreHorizontal/>}
|
icon={<MoreHorizontal/>}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||||
import { ConnectionCircle } from "@/components/wrappers/common/connection-circle";
|
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
|
||||||
import { formatDateLastContact } from "@/utils/date-formatting";
|
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||||
import {Database} from "@/db/schema/07_database";
|
import {Database} from "@/db/schema/07_database";
|
||||||
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
|
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
|
||||||
|
|
||||||
@@ -15,11 +15,12 @@ export type projectDatabaseCardProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
|
export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
|
||||||
const { organizationSlug, data: database, extendedProps: extendedProps } = props;
|
const {organizationSlug, data: database, extendedProps: extendedProps} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md rounded-xl" href={`/dashboard/projects/${extendedProps.id}/database/${database.id}`}>
|
<Link className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md rounded-xl"
|
||||||
<DatabaseCard data={database} />
|
href={`/dashboard/projects/${extendedProps.id}/database/${database.id}`}>
|
||||||
|
<DatabaseCard data={database}/>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -29,12 +30,13 @@ export type databaseCardProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DatabaseCard = (props: databaseCardProps) => {
|
export const DatabaseCard = (props: databaseCardProps) => {
|
||||||
const { data: database } = props;
|
const {data: database} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="flex flex-row justify-between">
|
<Card className="flex flex-row justify-between space-x-2 ">
|
||||||
<div className="flex items-center space-x-4 px-4">
|
<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" />
|
<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="justify-between">
|
||||||
<div className="font-medium">Name: {database.name}</div>
|
<div className="font-medium">Name: {database.name}</div>
|
||||||
<div className="text-sm text-muted-foreground">Generated Id: {database.agentDatabaseId}</div>
|
<div className="text-sm text-muted-foreground">Generated Id: {database.agentDatabaseId}</div>
|
||||||
@@ -43,9 +45,8 @@ export const DatabaseCard = (props: databaseCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center px-5 justify-center">
|
||||||
<div className="flex items-center px-4">
|
<ConnectionCircle date={database.lastContact}/>
|
||||||
<ConnectionCircle date={database.lastContact} />
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "alert_policy" DROP CONSTRAINT "alert_policy_notification_channel_id_notification_channel_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "alert_policy" ADD CONSTRAINT "alert_policy_notification_channel_id_notification_channel_id_fk" FOREIGN KEY ("notification_channel_id") REFERENCES "public"."notification_channel"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -64,6 +64,13 @@
|
|||||||
"when": 1763914207236,
|
"when": 1763914207236,
|
||||||
"tag": "0008_aberrant_scorpion",
|
"tag": "0008_aberrant_scorpion",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1764364481615,
|
||||||
|
"tag": "0009_lucky_edwin_jarvis",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ export const alertPolicy = pgTable('alert_policy', {
|
|||||||
id: uuid('id').defaultRandom().primaryKey(),
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
notificationChannelId: uuid('notification_channel_id')
|
notificationChannelId: uuid('notification_channel_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => notificationChannel.id, {onDelete: 'restrict'}),
|
.references(() => notificationChannel.id, {onDelete: 'cascade'}),
|
||||||
eventKinds: eventKindEnum("event_kind").array().notNull(),
|
eventKinds: eventKindEnum("event_kind").array().notNull(),
|
||||||
enabled: boolean('enabled').default(true).notNull(),
|
enabled: boolean('enabled').default(true).notNull(),
|
||||||
databaseId: uuid('database_id')
|
databaseId: uuid('database_id')
|
||||||
|
|||||||
Reference in New Issue
Block a user