mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Working on statistics
This commit is contained in:
@@ -1,18 +1,21 @@
|
|||||||
import { PageParams } from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
import { AdminTabs } from "@/components/wrappers/dashboard/admin/admin-tabs";
|
import {AdminTabs} from "@/components/wrappers/dashboard/admin/admin-tabs";
|
||||||
import { db } from "@/db";
|
import {db} from "@/db";
|
||||||
import {isNull} from "drizzle-orm";
|
import {isNull} from "drizzle-orm";
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
const users = await db.query.user.findMany({
|
const users = await db.query.user.findMany({
|
||||||
where: (fields) => isNull(fields.deletedAt)
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
|
with: {
|
||||||
|
accounts: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const settings = await db.query.setting.findFirst({
|
const settings = await db.query.setting.findFirst({
|
||||||
where: (fields, { eq }) => eq(fields.name, "system"),
|
where: (fields, {eq}) => eq(fields.name, "system"),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -21,7 +24,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
<PageTitle>Administration Panel</PageTitle>
|
<PageTitle>Administration Panel</PageTitle>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<AdminTabs settings={settings!} users={users} />
|
<AdminTabs settings={settings!} users={users}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
@@ -19,6 +19,7 @@ export default async function RoutePage(props: PageParams<{ databaseId: string }
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
@@ -27,6 +28,7 @@ export default async function RoutePage(props: PageParams<{ databaseId: string }
|
|||||||
<PageContent>
|
<PageContent>
|
||||||
<DatabaseForm
|
<DatabaseForm
|
||||||
databaseId={databaseId}
|
databaseId={databaseId}
|
||||||
|
// @ts-ignore
|
||||||
defaultValues={{ ...dbItem, dbms: dbItem.dbms ?? "inactive", description: dbItem.description ?? undefined }}
|
defaultValues={{ ...dbItem, dbms: dbItem.dbms ?? "inactive", description: dbItem.description ?? undefined }}
|
||||||
/>
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ export default async function RoutePage(props: PageParams<{ projectId: string }>
|
|||||||
},
|
},
|
||||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
// .filter((db): db is DatabaseWith => db.project !== null);
|
// .filter((db) => db.project !== null) as DatabaseWith[];
|
||||||
|
|
||||||
console.log("ici34",availableDatabases);
|
console.log("ici34",availableDatabases);
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ export default async function RoutePage(props: PageParams<{ projectId: string }>
|
|||||||
<ProjectForm
|
<ProjectForm
|
||||||
organization={org}
|
organization={org}
|
||||||
databases={availableDatabases as DatabaseWith[]}
|
databases={availableDatabases as DatabaseWith[]}
|
||||||
defaultValues={{name: proj.name, slug: proj.slug, databases: proj.databases.map((db) => db.id)}}
|
defaultValues={{...proj, databases: proj.databases.map((db) => db.id)}}
|
||||||
projectId={proj.id}
|
projectId={proj.id}
|
||||||
/>
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
},
|
},
|
||||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
||||||
})
|
})
|
||||||
).filter((db) => db.project !== null) as DatabaseWith[];
|
).filter((db) => db.project == null) as DatabaseWith[];
|
||||||
|
|
||||||
const org = await db.query.organization.findFirst({
|
const org = await db.query.organization.findFirst({
|
||||||
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
|
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
|
||||||
@@ -35,6 +35,8 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
|
|
||||||
if (!org) notFound();
|
if (!org) notFound();
|
||||||
|
|
||||||
|
console.log("ici",availableDatabases)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
|
|||||||
@@ -45,6 +45,56 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
console.log("evolution",backupsEvolution);
|
||||||
|
|
||||||
|
// const tomorrow = new Date();
|
||||||
|
// tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
//
|
||||||
|
// const backupsEvolution = [
|
||||||
|
// {
|
||||||
|
// id: '22e84aa4-228c-45b3-82ec-846a639cd509',
|
||||||
|
// createdAt: new Date()
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
||||||
|
// createdAt: new Date()
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: '6a6106fe-7f45-48eb-a56f-0a1e734126a1',
|
||||||
|
// createdAt: new Date()
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'a529d790-502e-4609-ad37-9b1c00c73477',
|
||||||
|
// createdAt: new Date()
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'a8c105a8-3e29-423e-b7dd-d3218092cde1',
|
||||||
|
// createdAt: new Date()
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'd33aaf4f-8525-4490-addb-12e3c8650d6d',
|
||||||
|
// createdAt: new Date()
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'e88a588a-2353-4470-9976-8c3eb2ffc88d',
|
||||||
|
// createdAt: new Date()
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'ee11441e-4b41-4c1b-9d91-929565b4204a',
|
||||||
|
// createdAt: new Date()
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// // Entries with tomorrow's date
|
||||||
|
// {
|
||||||
|
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
|
||||||
|
// createdAt: new Date(tomorrow)
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'c0a8323d-9241-4896-9e64-01e905c24e51',
|
||||||
|
// createdAt: new Date(tomorrow)
|
||||||
|
// }
|
||||||
|
// ];
|
||||||
|
|
||||||
|
|
||||||
const backupsRate = await db
|
const backupsRate = await db
|
||||||
.select({
|
.select({
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {Badge} from "@/components/ui/badge";
|
|||||||
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/button-delete-account/button-delete-account";
|
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";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
@@ -18,8 +19,8 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
// const sessions = await getSessions();
|
const sessions = await getSessions();
|
||||||
// const accounts = await getAccounts();
|
const accounts = await getAccounts();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
@@ -43,18 +44,19 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
{/* <ButtonDeleteAccount text="Delete my account"/>*/}
|
{/* <ButtonDeleteAccount text="Delete my account"/>*/}
|
||||||
{/*</PageActions>*/}
|
{/*</PageActions>*/}
|
||||||
</div>
|
</div>
|
||||||
<PageContent>
|
<PageContent >
|
||||||
<UserForm
|
<UserForm
|
||||||
userId={user.id}
|
userId={user.id}
|
||||||
|
sessions={sessions} accounts={accounts}
|
||||||
defaultValues={{
|
defaultValues={{
|
||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.role ?? undefined,
|
role: user.role ?? undefined,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="mt-4 sm:hidden">
|
{/*<div className="mt-4 sm:hidden ">*/}
|
||||||
<ButtonDeleteAccount text="Delete my account"/>
|
{/* <ButtonDeleteAccount text="Delete my account"/>*/}
|
||||||
</div>
|
{/*</div>*/}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ echo " / /_/ / __ \/ ___/ __/ __ / __ \/ __ / ___/ _ \ "
|
|||||||
echo " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ "
|
echo " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ "
|
||||||
echo " /_/ \____/_/ \__/\__,_/_.___/\__,_/____/\___/ "
|
echo " /_/ \____/_/ \__/\__,_/_.___/\__,_/____/\___/ "
|
||||||
echo " "
|
echo " "
|
||||||
echo " Community Edition v1.0.0 "
|
echo " Community Edition v1.1.1 "
|
||||||
echo " "
|
echo " "
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 8887",
|
"dev": "next dev --turbopack -p 8887",
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import {Icon} from "@iconify/react";
|
||||||
|
import {CircleHelp, KeyRound} from "lucide-react";
|
||||||
|
|
||||||
|
|
||||||
|
export const providerSwitch = (provider: string) => {
|
||||||
|
switch (provider) {
|
||||||
|
case "google":
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<Icon icon={"logos:google"} height="24" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "credential":
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row gap-x-2 items-center p-4">
|
||||||
|
<KeyRound height="24" />
|
||||||
|
<span>Email and Password</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row gap-x-2 items-center p-4">
|
||||||
|
<CircleHelp height="24" />
|
||||||
|
<span>No credentials</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||||
import { SettingsEmailTab } from "@/components/wrappers/dashboard/admin/admin-email-tab/settings-email-tab";
|
import {SettingsEmailTab} from "@/components/wrappers/dashboard/admin/admin-email-tab/settings-email-tab";
|
||||||
import { SettingsStorageTab } from "@/components/wrappers/dashboard/admin/admin-storage-tab/settings-storage-tab";
|
import {SettingsStorageTab} from "@/components/wrappers/dashboard/admin/admin-storage-tab/settings-storage-tab";
|
||||||
import { AdminUsersTable } from "@/components/wrappers/dashboard/admin/admin-user-table";
|
import {User, UserWithAccounts} from "@/db/schema/01_user";
|
||||||
import { User } from "@/db/schema/01_user";
|
import {Setting} from "@/db/schema/00_setting";
|
||||||
import { Setting } from "@/db/schema/00_setting";
|
import {useEffect, useState} from "react";
|
||||||
import { useEffect, useState } from "react";
|
import {useRouter, useSearchParams} from "next/navigation";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import {AdminUsersTable} from "@/components/wrappers/dashboard/admin/admin-user-tab/admin-user-table";
|
||||||
|
|
||||||
export type AdminTabsProps = {
|
export type AdminTabsProps = {
|
||||||
users: User[];
|
users: UserWithAccounts[];
|
||||||
settings: Setting;
|
settings: Setting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdminTabs = ({ users, settings }: AdminTabsProps) => {
|
export const AdminTabs = ({users, settings}: AdminTabsProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
@@ -44,13 +44,13 @@ export const AdminTabs = ({ users, settings }: AdminTabsProps) => {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="users">
|
<TabsContent value="users">
|
||||||
<AdminUsersTable users={users} />
|
<AdminUsersTable users={users}/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="email">
|
<TabsContent value="email">
|
||||||
<SettingsEmailTab settings={settings} />
|
<SettingsEmailTab settings={settings}/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="storage">
|
<TabsContent value="storage">
|
||||||
<SettingsStorageTab settings={settings} />
|
<SettingsStorageTab settings={settings}/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||||
|
import {useMutation} from "@tanstack/react-query";
|
||||||
|
import {ColumnDef} from "@tanstack/react-table";
|
||||||
|
import {Unlink} from "lucide-react";
|
||||||
|
import {toast} from "sonner";
|
||||||
|
import {useRouter} from "next/navigation";
|
||||||
|
import {unlinkUserProviderAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
|
||||||
|
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
||||||
|
|
||||||
|
export const accountsColumns: ColumnDef<{
|
||||||
|
id: string;
|
||||||
|
provider: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
accountId: string;
|
||||||
|
scopes: string[];
|
||||||
|
}>[] = [
|
||||||
|
{
|
||||||
|
id: "provider",
|
||||||
|
header: "Provider",
|
||||||
|
cell: ({row}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{providerSwitch(row.original.provider)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Action",
|
||||||
|
id: "actions",
|
||||||
|
cell: ({row, table}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (row.original.provider === "credential") {
|
||||||
|
toast.error(`This provider cannot be unlinked.`);
|
||||||
|
router.refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (table.getRowModel().rows.length <= 1) {
|
||||||
|
toast.error(`You only have one provider linked to your account. Please add more one to unlink this`);
|
||||||
|
router.refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = await unlinkUserProviderAction({
|
||||||
|
provider: row.original.provider,
|
||||||
|
account: row.original.accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status?.serverError || !status) {
|
||||||
|
toast.error(status?.serverError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success(`Provider unlinked successfully.`);
|
||||||
|
router.refresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ButtonWithLoading
|
||||||
|
variant="outline"
|
||||||
|
text=""
|
||||||
|
disabled={row.original.provider === "credential" || table.getRowModel().rows.length <= 1}
|
||||||
|
icon={<Unlink color="red" size={15}/>}
|
||||||
|
onClick={async () => {
|
||||||
|
await mutation.mutateAsync();
|
||||||
|
}}
|
||||||
|
size="icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import {User, UserWithAccounts} from "@/db/schema/01_user";
|
||||||
|
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
|
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||||
|
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/admin-user-tab/columns-users";
|
||||||
|
|
||||||
|
export type AdminUsersTableProps = {
|
||||||
|
users: UserWithAccounts[];
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AdminUsersTable = (props: AdminUsersTableProps) => {
|
||||||
|
const {users} = props;
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-4 h-full py-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Active users</CardTitle>
|
||||||
|
<CardDescription>Manage your users</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<DataTable columns={usersColumnsAdmin} data={users}/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+32
-19
@@ -9,22 +9,20 @@ import {useState} from "react";
|
|||||||
import {Trash2} from "lucide-react";
|
import {Trash2} from "lucide-react";
|
||||||
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action";
|
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action";
|
||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||||
import {User} from "@/db/schema/01_user";
|
import {UserWithAccounts} from "@/db/schema/01_user";
|
||||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||||
import {formatFrenchDate} from "@/utils/date-formatting";
|
import {formatFrenchDate} from "@/utils/date-formatting";
|
||||||
|
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
||||||
|
|
||||||
export const usersColumnsAdmin: ColumnDef<User>[] = [
|
export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "role",
|
accessorKey: "role",
|
||||||
header: "Role",
|
header: "Role",
|
||||||
cell: ({row}) => {
|
cell: ({row}) => {
|
||||||
const [role, setRole] = useState<string>(row.getValue("role"));
|
const [role, setRole] = useState<string>(row.getValue("role"));
|
||||||
|
|
||||||
|
|
||||||
const {data: session, isPending, error} = authClient.useSession();
|
const {data: session, isPending, error} = authClient.useSession();
|
||||||
|
const isSuperAdmin = session?.user.role == "superadmin";
|
||||||
|
|
||||||
|
|
||||||
const isCurrentUser = session?.user.email === row.original.email;
|
const isCurrentUser = session?.user.email === row.original.email;
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
@@ -51,11 +49,10 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
className={isCurrentUser ? "cursor-not-allowed opacity-50" : "cursor-pointer"}
|
className={isCurrentUser || !isSuperAdmin ? "cursor-not-allowed opacity-50" : "cursor-pointer"}
|
||||||
onClick={isCurrentUser ? undefined : () => handleUpdateRole()}
|
onClick={isCurrentUser || !isSuperAdmin ? undefined : () => handleUpdateRole()}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{role}
|
{role}
|
||||||
@@ -71,6 +68,21 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
|
|||||||
accessorKey: "email",
|
accessorKey: "email",
|
||||||
header: "Email",
|
header: "Email",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "accounts",
|
||||||
|
header: "Provider ID",
|
||||||
|
cell: ({row}) => {
|
||||||
|
return(
|
||||||
|
<div>
|
||||||
|
{row.original.accounts.map((item) => (
|
||||||
|
<div key={item.id}>
|
||||||
|
{providerSwitch(item.providerId)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "updatedAt",
|
accessorKey: "updatedAt",
|
||||||
header: "Updated At",
|
header: "Updated At",
|
||||||
@@ -84,6 +96,7 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
|
|||||||
cell: ({row}) => {
|
cell: ({row}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const {data: session, isPending} = useSession();
|
const {data: session, isPending} = useSession();
|
||||||
|
const isSuperAdmin = session?.user.role == "superadmin";
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: () => deleteUserAction(row.original.id),
|
mutationFn: () => deleteUserAction(row.original.id),
|
||||||
@@ -95,16 +108,16 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ButtonWithLoading
|
<ButtonWithLoading
|
||||||
disabled={!session || session?.user.email === row.original.email}
|
disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
text=""
|
text=""
|
||||||
icon={<Trash2 color="red" size={15}/>}
|
icon={<Trash2 color="red" size={15}/>}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutation.mutateAsync();
|
await mutation.mutateAsync();
|
||||||
}}
|
}}
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { Session } from "better-auth";
|
||||||
|
import { Unlink } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import detectOSWithUA from "@/utils/os-parser";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import { authClient } from "@/lib/auth/auth-client";
|
||||||
|
import { timeAgo } from "@/utils/date-formatting";
|
||||||
|
import {deleteUserSessionAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
|
||||||
|
|
||||||
|
export const sessionsColumns: ColumnDef<Session>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "expiresAt",
|
||||||
|
header: "Expires At",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return timeAgo(row.original.expiresAt);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "ipAddress",
|
||||||
|
header: "IP Address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "device",
|
||||||
|
header: "Device",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const os = detectOSWithUA(row.original.userAgent!);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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.showText && <span>{os.name}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "userAgent",
|
||||||
|
header: "User Agent",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Action",
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { data: session } = authClient.useSession();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (session?.session.id === row.original.id) {
|
||||||
|
toast.error(`Unable to unlink active session.`);
|
||||||
|
router.refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = await deleteUserSessionAction(row.original.token);
|
||||||
|
|
||||||
|
if (status?.serverError || !status) {
|
||||||
|
toast.error(status?.serverError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success(`Session deleted successfully.`);
|
||||||
|
router.refresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ButtonWithLoading
|
||||||
|
variant="outline"
|
||||||
|
disabled={session?.session.id === row.original.id}
|
||||||
|
text=""
|
||||||
|
icon={<Unlink color="red" size={15} />}
|
||||||
|
onClick={async () => {
|
||||||
|
await mutation.mutateAsync();
|
||||||
|
}}
|
||||||
|
size="icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { usersColumnsAdmin } from "@/components/wrappers/dashboard/admin/columns-users";
|
|
||||||
import { User } from "@/db/schema/01_user";
|
|
||||||
import { DataTable } from "../../common/table/data-table";
|
|
||||||
|
|
||||||
export type AdminUsersTableProps = {
|
|
||||||
users: User[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AdminUsersTable = (props: AdminUsersTableProps) => {
|
|
||||||
const { users } = props;
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col h-full py-4">
|
|
||||||
<div className="flex gap-4 h-fit justify-between">
|
|
||||||
<h1>List of Portabase's users</h1>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5 h-full">
|
|
||||||
<DataTable columns={usersColumnsAdmin} data={users} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -20,9 +20,11 @@ export type CronButtonProps = {
|
|||||||
export const CronButton = (props: CronButtonProps) => {
|
export const CronButton = (props: CronButtonProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isSwitched, setIsSwitched] = useState(props.database.backupPolicy !== null);
|
const [isSwitched, setIsSwitched] = useState(props.database.backupPolicy !== null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const updateDatabaseBackupPolicy = useMutation({
|
const updateDatabaseBackupPolicy = useMutation({
|
||||||
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({ databaseId: props.database.id, backupPolicy: value }),
|
mutationFn: (value: string) =>
|
||||||
|
updateDatabaseBackupPolicyAction({ databaseId: props.database.id, backupPolicy: value }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(`Method updated successfully.`);
|
toast.success(`Method updated successfully.`);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
@@ -34,15 +36,15 @@ export const CronButton = (props: CronButtonProps) => {
|
|||||||
|
|
||||||
const handleTypeChange = async (state: boolean) => {
|
const handleTypeChange = async (state: boolean) => {
|
||||||
setIsSwitched(state);
|
setIsSwitched(state);
|
||||||
if (state == false) {
|
if (!state) {
|
||||||
await updateDatabaseBackupPolicy.mutateAsync("");
|
await updateDatabaseBackupPolicy.mutateAsync("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" {...props}>
|
<Button variant="outline" {...props} onClick={() => setOpen(true)}>
|
||||||
<Clock9 />
|
<Clock9 />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@@ -64,7 +66,14 @@ export const CronButton = (props: CronButtonProps) => {
|
|||||||
id="type-mode"
|
id="type-mode"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isSwitched ? <CronInput database={props.database} /> : null}
|
{isSwitched ? (
|
||||||
|
<CronInput
|
||||||
|
database={props.database}
|
||||||
|
onSuccess={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
import { AdvancedCronSelect } from "./advanced-cron-select";
|
import {AdvancedCronSelect} from "./advanced-cron-select";
|
||||||
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/cron-button/cron.action";
|
import {updateDatabaseBackupPolicyAction} from "@/components/wrappers/dashboard/database/cron-button/cron.action";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import {useState} from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import { toast } from "sonner";
|
import {toast} from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import {Separator} from "@/components/ui/separator";
|
||||||
import {Database} from "@/db/schema/06_database";
|
import {Database} from "@/db/schema/06_database";
|
||||||
|
|
||||||
export type CronInputProps = {
|
export type CronInputProps = {
|
||||||
database: Database;
|
database: Database;
|
||||||
|
onSuccess?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CronInput = ({ database }: CronInputProps) => {
|
export const CronInput = ({database, onSuccess}: CronInputProps) => {
|
||||||
const [cron, setCron] = useState<string>(database.backupPolicy ?? "* * * * *");
|
const [cron, setCron] = useState<string>(database.backupPolicy ?? "* * * * *");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const updateBackupPolicy = useMutation({
|
const updateBackupPolicy = useMutation({
|
||||||
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({ databaseId: database.id, backupPolicy: value }),
|
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({databaseId: database.id, backupPolicy: value}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(`Cron updated successfully.`);
|
toast.success(`Cron updated successfully.`);
|
||||||
|
onSuccess?.()
|
||||||
router.refresh();
|
router.refresh();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
@@ -29,7 +31,7 @@ export const CronInput = ({ database }: CronInputProps) => {
|
|||||||
|
|
||||||
const handleChangeCron = (type: "minute" | "hour" | "day-of-month" | "month" | "day-of-week", value: string) => {
|
const handleChangeCron = (type: "minute" | "hour" | "day-of-month" | "month" | "day-of-week", value: string) => {
|
||||||
const cronParts = cron.split(" ");
|
const cronParts = cron.split(" ");
|
||||||
const indexMap = { minute: 0, hour: 1, "day-of-month": 2, month: 3, "day-of-week": 4 };
|
const indexMap = {minute: 0, hour: 1, "day-of-month": 2, month: 3, "day-of-week": 4};
|
||||||
cronParts[indexMap[type]] = value;
|
cronParts[indexMap[type]] = value;
|
||||||
setCron(cronParts.join(" "));
|
setCron(cronParts.join(" "));
|
||||||
};
|
};
|
||||||
@@ -44,7 +46,7 @@ export const CronInput = ({ database }: CronInputProps) => {
|
|||||||
<AdvancedCronSelect
|
<AdvancedCronSelect
|
||||||
id="minute"
|
id="minute"
|
||||||
label="Minute"
|
label="Minute"
|
||||||
options={Array.from({ length: 60 }, (_, i) => String(i).padStart(2, "0"))}
|
options={Array.from({length: 60}, (_, i) => String(i).padStart(2, "0"))}
|
||||||
type="minute"
|
type="minute"
|
||||||
value={cron.split(" ")[0]}
|
value={cron.split(" ")[0]}
|
||||||
defaultValue={cron.split(" ")[0]}
|
defaultValue={cron.split(" ")[0]}
|
||||||
@@ -53,7 +55,7 @@ export const CronInput = ({ database }: CronInputProps) => {
|
|||||||
<AdvancedCronSelect
|
<AdvancedCronSelect
|
||||||
id="hour"
|
id="hour"
|
||||||
label="Hour"
|
label="Hour"
|
||||||
options={Array.from({ length: 24 }, (_, i) => String(i).padStart(2, "0"))}
|
options={Array.from({length: 24}, (_, i) => String(i).padStart(2, "0"))}
|
||||||
type="hour"
|
type="hour"
|
||||||
value={cron.split(" ")[1]}
|
value={cron.split(" ")[1]}
|
||||||
defaultValue={cron.split(" ")[1]}
|
defaultValue={cron.split(" ")[1]}
|
||||||
@@ -62,7 +64,7 @@ export const CronInput = ({ database }: CronInputProps) => {
|
|||||||
<AdvancedCronSelect
|
<AdvancedCronSelect
|
||||||
id="day-of-month"
|
id="day-of-month"
|
||||||
label="Day of Month"
|
label="Day of Month"
|
||||||
options={Array.from({ length: 31 }, (_, i) => String(i + 1).padStart(2, "0"))}
|
options={Array.from({length: 31}, (_, i) => String(i + 1).padStart(2, "0"))}
|
||||||
type="day-of-month"
|
type="day-of-month"
|
||||||
value={cron.split(" ")[2]}
|
value={cron.split(" ")[2]}
|
||||||
defaultValue={cron.split(" ")[2]}
|
defaultValue={cron.split(" ")[2]}
|
||||||
@@ -86,13 +88,14 @@ export const CronInput = ({ database }: CronInputProps) => {
|
|||||||
defaultValue={cron.split(" ")[4]}
|
defaultValue={cron.split(" ")[4]}
|
||||||
onValueChange={(value) => handleChangeCron("day-of-week", value)}
|
onValueChange={(value) => handleChangeCron("day-of-week", value)}
|
||||||
/>
|
/>
|
||||||
<Separator />
|
<Separator/>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="font-semibold">Cron Expression</div>
|
<div className="font-semibold">Cron Expression</div>
|
||||||
<div className="font-mono text-muted-foreground">{cron}</div>
|
<div className="font-mono text-muted-foreground">{cron}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">This cron expression determines when the job will run.</div>
|
<div className="text-sm text-muted-foreground">This cron expression determines when the job will run.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { UserSchema } from "@/components/wrappers/dashboard/profile/user-form/us
|
|||||||
import { db } from "@/db";
|
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";
|
||||||
|
|
||||||
export const updateUserAction = userAction
|
export const updateUserAction = userAction
|
||||||
.schema(
|
.schema(
|
||||||
@@ -19,3 +20,23 @@ export const updateUserAction = userAction
|
|||||||
data: updatedUser,
|
data: updatedUser,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const deleteUserSessionAction = userAction.schema(z.string()).action(async ({ parsedInput }) => {
|
||||||
|
const status = await revokeSession(parsedInput);
|
||||||
|
return status;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const unlinkUserProviderAction = userAction
|
||||||
|
.schema(
|
||||||
|
z.object({
|
||||||
|
provider: z.string(),
|
||||||
|
account: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.action(async ({ parsedInput }) => {
|
||||||
|
const status = await unlinkAccount(parsedInput.provider, parsedInput.account);
|
||||||
|
|
||||||
|
return status;
|
||||||
|
});
|
||||||
|
|||||||
@@ -11,13 +11,26 @@ import { TooltipProvider } from "@/components/ui/tooltip";
|
|||||||
import { UserSchema, UserType } from "@/components/wrappers/dashboard/profile/user-form/user-form.schema";
|
import { UserSchema, UserType } from "@/components/wrappers/dashboard/profile/user-form/user-form.schema";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { updateUserAction } from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
|
import { updateUserAction } from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
|
||||||
|
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||||
|
import {sessionsColumns} from "@/components/wrappers/dashboard/admin/admin-user-tab/sessions/table-columns";
|
||||||
|
import {accountsColumns} from "@/components/wrappers/dashboard/admin/admin-user-tab/accounts/table-columns";
|
||||||
|
import {Session} from "better-auth";
|
||||||
|
|
||||||
export type userFormProps = {
|
export type UserFormProps = {
|
||||||
defaultValues?: UserType;
|
defaultValues?: UserType;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
sessions: Session[];
|
||||||
|
accounts: {
|
||||||
|
id: string;
|
||||||
|
provider: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
accountId: string;
|
||||||
|
scopes: string[];
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserForm = (props: userFormProps) => {
|
export const UserForm = (props: UserFormProps) => {
|
||||||
const isCreate = !Boolean(props.defaultValues);
|
const isCreate = !Boolean(props.defaultValues);
|
||||||
|
|
||||||
const form = useZodForm({
|
const form = useZodForm({
|
||||||
@@ -49,6 +62,7 @@ export const UserForm = (props: userFormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="flex flex-col gap-y-4 h-full py-4">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -93,6 +107,25 @@ export const UserForm = (props: userFormProps) => {
|
|||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Active sessions</CardTitle>
|
||||||
|
<CardDescription>Manage your active sessions</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<DataTable columns={sessionsColumns} data={props.sessions} enableSelect={false}/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Auth providers</CardTitle>
|
||||||
|
<CardDescription>Manage your active auth providers</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<DataTable columns={accountsColumns} data={props.accounts} enableSelect={false}/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-1
@@ -4,13 +4,19 @@ import {userAction} from "@/safe-actions";
|
|||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {v4 as uuidv4} from "uuid";
|
import {v4 as uuidv4} from "uuid";
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
import {eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
|
||||||
export const deleteProjectAction = userAction.schema(z.string()).action(async ({parsedInput}): Promise<ServerActionResult<typeof drizzleDb.schemas.project.$inferSelect>> => {
|
export const deleteProjectAction = userAction.schema(z.string()).action(async ({parsedInput}): Promise<ServerActionResult<typeof drizzleDb.schemas.project.$inferSelect>> => {
|
||||||
try {
|
try {
|
||||||
const uuid = uuidv4();
|
const uuid = uuidv4();
|
||||||
|
await db
|
||||||
|
.update(drizzleDb.schemas.database)
|
||||||
|
.set({
|
||||||
|
projectId: null,
|
||||||
|
})
|
||||||
|
.where(eq(drizzleDb.schemas.database.projectId, parsedInput));
|
||||||
|
|
||||||
const updatedProjects = await db
|
const updatedProjects = await db
|
||||||
.update(drizzleDb.schemas.project)
|
.update(drizzleDb.schemas.project)
|
||||||
@@ -27,6 +33,7 @@ export const deleteProjectAction = userAction.schema(z.string()).action(async ({
|
|||||||
throw new Error("Project not found or update failed");
|
throw new Error("Project not found or update failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
value: updatedProject,
|
value: updatedProject,
|
||||||
@@ -36,6 +43,7 @@ export const deleteProjectAction = userAction.schema(z.string()).action(async ({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
actionError: {
|
actionError: {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const ProjectForm = (props: projectFormProps) => {
|
|||||||
const formatDatabasesList = (databases: DatabaseWith[]) => {
|
const formatDatabasesList = (databases: DatabaseWith[]) => {
|
||||||
return databases.map((database) => ({
|
return databases.map((database) => ({
|
||||||
value: database.id,
|
value: database.id,
|
||||||
label: `${database.name} (${database.id}) | ${database.agent.name}`,
|
label: `${database.name} (${database.agentDatabaseId}) | ${database.agent.name}`,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,15 +89,15 @@ export const ProjectForm = (props: projectFormProps) => {
|
|||||||
await mutation.mutateAsync(values);
|
await mutation.mutateAsync(values);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
defaultValue=""
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Project 1" {...field} />
|
<Input placeholder="Project 1" {...field} value={field.value ?? ""} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -15,25 +15,38 @@ export type evolutionLineChartProps = {
|
|||||||
export function EvolutionLineChart(props: evolutionLineChartProps) {
|
export function EvolutionLineChart(props: evolutionLineChartProps) {
|
||||||
const { data } = props;
|
const { data } = props;
|
||||||
|
|
||||||
// Process data to calculate cumulative count
|
// const cumulativeData = data.reduce(
|
||||||
const cumulativeData = data.reduce(
|
// (acc, backup) => {
|
||||||
(acc, backup) => {
|
// const date = backup.createdAt.toISOString().split("T")[0]; // Format as YYYY-MM-DD
|
||||||
const date = backup.createdAt.toISOString().split("T")[0]; // Format as YYYY-MM-DD
|
//
|
||||||
|
// // Increment count for the current date or initialize it
|
||||||
|
// if (acc.length && acc[acc.length - 1].date === date) {
|
||||||
|
// acc[acc.length - 1].count += 1;
|
||||||
|
// } else {
|
||||||
|
// const lastCount = acc.length ? acc[acc.length - 1].count : 0;
|
||||||
|
// acc.push({ date, count: lastCount + 1 });
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return acc;
|
||||||
|
// },
|
||||||
|
// [] as { date: string; count: number }[]
|
||||||
|
// );
|
||||||
|
const dailyData = data
|
||||||
|
.reduce((acc, backup) => {
|
||||||
|
const date = backup.createdAt.toISOString().split("T")[0]; // Format: YYYY-MM-DD
|
||||||
|
|
||||||
// Increment count for the current date or initialize it
|
// Find if the date already exists in the accumulator
|
||||||
if (acc.length && acc[acc.length - 1].date === date) {
|
const existing = acc.find(item => item.date === date);
|
||||||
acc[acc.length - 1].count += 1;
|
|
||||||
|
if (existing) {
|
||||||
|
existing.count += 1;
|
||||||
} else {
|
} else {
|
||||||
const lastCount = acc.length ? acc[acc.length - 1].count : 0;
|
acc.push({ date, count: 1 });
|
||||||
acc.push({ date, count: lastCount + 1 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
}, [] as { date: string; count: number }[]);
|
||||||
[] as { date: string; count: number }[]
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(cumulativeData);
|
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
date: {
|
date: {
|
||||||
@@ -50,7 +63,7 @@ export function EvolutionLineChart(props: evolutionLineChartProps) {
|
|||||||
<ChartContainer config={chartConfig}>
|
<ChartContainer config={chartConfig}>
|
||||||
<LineChart
|
<LineChart
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={cumulativeData}
|
data={dailyData}
|
||||||
margin={{
|
margin={{
|
||||||
left: 12,
|
left: 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
@@ -68,7 +81,9 @@ export function EvolutionLineChart(props: evolutionLineChartProps) {
|
|||||||
/>
|
/>
|
||||||
<YAxis />
|
<YAxis />
|
||||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||||
<Line dataKey="count" type="linear" stroke="var(--color-desktop)" strokeWidth={2} dot={false} />
|
<Line dataKey="count" type="linear" stroke="#60a5fa" strokeWidth={2} dot={false} />
|
||||||
|
|
||||||
|
{/*<Line dataKey="count" type="linear" stroke="var(--color-desktop)" strokeWidth={2} dot={false} />*/}
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|||||||
import { createSelectSchema } from "drizzle-zod";
|
import { createSelectSchema } from "drizzle-zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { project } from "./05_project";
|
import { project } from "./05_project";
|
||||||
import {member} from "@/db/schema/03_member";
|
import {member, OrganizationMember} from "@/db/schema/03_member";
|
||||||
import {invitation} from "@/db/schema/04_invitation";
|
import {invitation} from "@/db/schema/04_invitation";
|
||||||
import {organization} from "@/db/schema/02_organization";
|
import {organization} from "@/db/schema/02_organization";
|
||||||
|
import {Account} from "better-auth";
|
||||||
|
|
||||||
export const user = pgTable("user", {
|
export const user = pgTable("user", {
|
||||||
id: uuid("id").defaultRandom().primaryKey(),
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
@@ -94,3 +95,11 @@ export const projectRelations = relations(project, ({ one }) => ({
|
|||||||
|
|
||||||
export const userSchema = createSelectSchema(user);
|
export const userSchema = createSelectSchema(user);
|
||||||
export type User = z.infer<typeof userSchema>;
|
export type User = z.infer<typeof userSchema>;
|
||||||
|
|
||||||
|
type FixedAccount = Omit<Account, 'updatedAt'> & {
|
||||||
|
updatedAt: Date | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserWithAccounts = User & {
|
||||||
|
accounts: FixedAccount[];
|
||||||
|
};
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import {createAuthClient} from "better-auth/react";
|
import {createAuthClient} from "better-auth/react";
|
||||||
|
|
||||||
import {adminClient, organizationClient} from "better-auth/client/plugins";
|
import {adminClient, inferAdditionalFields, organizationClient} from "better-auth/client/plugins";
|
||||||
import {env} from "@/env.mjs";
|
import {env} from "@/env.mjs";
|
||||||
import {ac, user, admin as adminRole, pending, superadmin, orgAdmin, orgMember, orgOwner} from "./permissions";
|
import {ac, user, admin as adminRole, pending, superadmin, orgAdmin, orgMember, orgOwner} from "./permissions";
|
||||||
|
import {auth} from "@/lib/auth/auth";
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
baseURL: env.NEXT_PUBLIC_PROJECT_URL,
|
baseURL: env.NEXT_PUBLIC_PROJECT_URL,
|
||||||
@@ -24,7 +25,9 @@ export const authClient = createAuthClient({
|
|||||||
superadmin,
|
superadmin,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
inferAdditionalFields<typeof auth>(),
|
||||||
],
|
],
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {signIn, signOut, signUp, useSession, listAccounts, admin} = authClient;
|
export const {signIn, signOut, signUp, useSession, listAccounts, admin} = authClient;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {format} from "date-fns";
|
import {format, formatDistanceToNow} from "date-fns";
|
||||||
|
|
||||||
export function humanReadableDate(rawDate: string | number | Date) {
|
export function humanReadableDate(rawDate: string | number | Date) {
|
||||||
return formatFrenchDate(rawDate);
|
return formatFrenchDate(rawDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function timeAgo(rawDate: string | number | Date) {
|
export function timeAgo(rawDate: string | number | Date) {
|
||||||
const date = new Date(rawDate)
|
const date = new Date(rawDate);
|
||||||
return "Not implemented"
|
return formatDistanceToNow(date, { addSuffix: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDateLastContact(lastContact: string | number | Date | null) {
|
export function formatDateLastContact(lastContact: string | number | Date | null) {
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
export default function detectOSWithUA(userAgent: string) {
|
||||||
|
const osList = [
|
||||||
|
{
|
||||||
|
name: "Windows",
|
||||||
|
keywords: ["Win", "NT", "Windows"],
|
||||||
|
icon: {
|
||||||
|
name: "microsoft-windows-icon",
|
||||||
|
size: {
|
||||||
|
width: 24,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showText: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ubuntu",
|
||||||
|
keywords: ["Ubuntu"],
|
||||||
|
icon: {
|
||||||
|
name: "ubuntu",
|
||||||
|
size: {
|
||||||
|
width: 24,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showText: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "iOS",
|
||||||
|
keywords: ["iOS"],
|
||||||
|
icon: {
|
||||||
|
name: "ios",
|
||||||
|
size: {
|
||||||
|
width: 24,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showText: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "iPadOS",
|
||||||
|
keywords: ["iPadOS", "iPad"],
|
||||||
|
icon: {
|
||||||
|
name: "ios",
|
||||||
|
size: {
|
||||||
|
width: 24,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showText: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MacOS",
|
||||||
|
keywords: ["MacOS", "Macintosh", "Mac OS", "Mac OS X"],
|
||||||
|
icon: {
|
||||||
|
name: "macos",
|
||||||
|
size: {
|
||||||
|
width: 0,
|
||||||
|
height: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showText: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Android",
|
||||||
|
keywords: ["Android"],
|
||||||
|
icon: {
|
||||||
|
name: "android-icon",
|
||||||
|
size: {
|
||||||
|
width: 24,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showText: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Linux",
|
||||||
|
keywords: ["X11", "Linux"],
|
||||||
|
icon: {
|
||||||
|
name: "linux-tux",
|
||||||
|
size: {
|
||||||
|
width: 24,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showText: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Playstation 4",
|
||||||
|
keywords: ["PlayStation 4"],
|
||||||
|
showText: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Playstation 5",
|
||||||
|
keywords: ["PlayStation 5"],
|
||||||
|
showText: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Xbox Series X",
|
||||||
|
keywords: ["Xbox Series X"],
|
||||||
|
showText: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Xbox One S",
|
||||||
|
keywords: ["XBOX_ONE_ED"],
|
||||||
|
showText: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Xbox One",
|
||||||
|
keywords: ["Xbox One"],
|
||||||
|
showText: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nintendo Switch",
|
||||||
|
keywords: ["Nintendo Switch"],
|
||||||
|
showText: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AppleTV",
|
||||||
|
keywords: ["AppleTV"],
|
||||||
|
icon: {
|
||||||
|
name: "apple",
|
||||||
|
size: {
|
||||||
|
width: 24,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showText: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let os of osList) {
|
||||||
|
if (os.keywords.some((keyword) => userAgent.includes(keyword))) {
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "Unknown OS",
|
||||||
|
icon: {
|
||||||
|
name: "unknown",
|
||||||
|
size: {
|
||||||
|
width: 24,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showText: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user