Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f74f378c0b | ||
|
|
385836d5fe | ||
|
|
461e3ff7c9 | ||
|
|
54a233b207 | ||
|
|
2d2453acf7 | ||
|
|
654ff6b9a5 | ||
|
|
34ff11f377 | ||
|
|
b95e145a8b | ||
|
|
bce6c9d55f | ||
|
|
c3e2978919 | ||
|
|
2d559e456d | ||
|
|
45e3b31fda | ||
|
|
c8da2579e8 | ||
|
|
c7e30e1659 | ||
|
|
7be23876aa | ||
|
|
98f1b329ae | ||
|
|
19ea5543ea | ||
|
|
010396ba4d | ||
|
|
f055744432 | ||
|
|
1920b29a55 | ||
|
|
a0c8d1fb68 | ||
|
|
59dc3b71b8 | ||
|
|
598eb44952 | ||
|
|
ac49047177 | ||
|
|
4448c9e166 | ||
|
|
d70778b556 | ||
|
|
66edb77113 | ||
|
|
0f7b3eae11 | ||
|
|
2f9e33f4db | ||
|
|
dd7a65262c | ||
|
|
4c0d2b1f7f | ||
|
|
60728885b6 | ||
|
|
7ac99a6618 | ||
|
|
4ed91c1297 | ||
|
|
44be8009fd | ||
|
|
96630a0ef7 | ||
|
|
8c9d694b40 | ||
|
|
1f09c1507a | ||
|
|
141241d555 | ||
|
|
c605e8173b | ||
|
|
37eb8f319f | ||
|
|
0492d244b3 | ||
|
|
baeec9f7a0 | ||
|
|
449c9c2cfe | ||
|
|
d13e4b7d40 | ||
|
|
13241811d0 | ||
|
|
4b52dc4a1e | ||
|
|
63cb6cb7a3 | ||
|
|
0511ad289c | ||
|
|
057a51499e | ||
|
|
1f01d9c372 | ||
|
|
0684a4887a | ||
|
|
96be14e1ba | ||
|
|
eec3c2737b | ||
|
|
a6b960ffb2 | ||
|
|
d3f465b5d4 | ||
|
|
2139dfd55d | ||
|
|
9f53181467 | ||
|
|
c96e13c72f | ||
|
|
58d9179152 | ||
|
|
17469696ee | ||
|
|
85047e7509 | ||
|
|
f9dc15ba73 | ||
|
|
7be542f676 | ||
|
|
847a567c52 | ||
|
|
1ffbd6efc8 | ||
|
|
f8f599e4dd | ||
|
|
6562718635 |
@@ -1,4 +1,25 @@
|
|||||||
|
# Ignore Docker-related configs
|
||||||
|
.dockerignore
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
README.md
|
docker-compose*.yml
|
||||||
|
|
||||||
|
# Development/Editor config
|
||||||
.idea
|
.idea
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Node-related
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
pnpm-debug.log
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
.next
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Docs and markdowns
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
name: Security Checks
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sca-deps: # Dependency & container scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: aquasecurity/trivy-action@0.20.0
|
||||||
|
with:
|
||||||
|
scan-type: 'fs'
|
||||||
|
format: 'table'
|
||||||
|
severity: 'CRITICAL,HIGH'
|
||||||
|
ignore-unfixed: true
|
||||||
|
|
||||||
|
|
||||||
|
secrets-gitleaks: # Secrets exposure
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Fetch full history for gitleaks
|
||||||
|
- uses: gitleaks/gitleaks-action@v2
|
||||||
|
with:
|
||||||
|
config-path: .gitleaks.toml # Optional, if you have a custom config
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
title = "Custom gitleaks config"
|
||||||
|
|
||||||
|
[allowlist]
|
||||||
|
# Global allowlist patterns (won’t be flagged)
|
||||||
|
regexes = [
|
||||||
|
"s3SecretAccessKey: env.S3_SECRET_KEY ?? null",
|
||||||
|
]
|
||||||
|
paths = [
|
||||||
|
"src/utils/init.ts"
|
||||||
|
]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
|
rev: v8.24.2
|
||||||
|
hooks:
|
||||||
|
- id: gitleaks
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
migrate:
|
CLUSTER_SCRIPT=docker/entrypoints/app-dev-entrypoint.sh
|
||||||
docker compose run --rm app
|
|
||||||
|
up:
|
||||||
|
@bash $(CLUSTER_SCRIPT)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<br />
|
<br />
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://portabase.io">
|
<a href="https://portabase.io">
|
||||||
<img src="images/logo.png" alt="Logo" width="80" height="80">
|
<img src="/public/images/logo.png" alt="Logo" width="80" height="80">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<h3 align="center">Portabase</h3>
|
<h3 align="center">Portabase</h3>
|
||||||
@@ -12,14 +12,21 @@
|
|||||||
<a href="https://portabase.io"><strong>Explore the Docs »</strong></a>
|
<a href="https://portabase.io"><strong>Explore the Docs »</strong></a>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<a href="https://portabase.io">View Demo</a>
|
<a href="https://www.youtube.com/watch?v=D9uFrGxLc4s">View Demo</a>
|
||||||
·
|
·
|
||||||
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
|
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
|
||||||
·
|
·
|
||||||
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 📚 Table of Contents
|
## 📚 Table of Contents
|
||||||
|
|
||||||
- [About The Project](#about-the-project)
|
- [About The Project](#about-the-project)
|
||||||
@@ -185,14 +192,6 @@ major.minor.patch-rc.release-tag
|
|||||||
# Example: 1.0.0-rc.1-dev
|
# Example: 1.0.0-rc.1-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Zenstack Support
|
|
||||||
|
|
||||||
To regenerate schema:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
zenstack generate
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|||||||
@@ -1,19 +1,54 @@
|
|||||||
import React from "react";
|
"use client"
|
||||||
|
import React, {useEffect, useState} from "react";
|
||||||
import {LayoutAdmin} from "@/components/layout";
|
import {LayoutAdmin} from "@/components/layout";
|
||||||
|
import Image from "next/image";
|
||||||
|
import {env} from "@/env.mjs";
|
||||||
|
import {useTheme} from "next-themes";
|
||||||
|
import {useSession} from "@/lib/auth/auth-client";
|
||||||
|
import {useRouter} from "next/navigation";
|
||||||
|
|
||||||
|
|
||||||
export default async function Layout({children}: { children: React.ReactNode }) {
|
export default function Layout({children}: { children: React.ReactNode }) {
|
||||||
|
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
if (session && session.user && !session.user.banned && session.user.role !== "pending") {
|
||||||
|
router.replace("/dashboard/home");
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) return null;
|
||||||
|
|
||||||
|
const imageTheme = resolvedTheme === "dark" ? "/images/logo-white.png" : "/images/logo-black.png";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutAdmin>
|
<div className="flex min-h-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8 ">
|
||||||
<div
|
<div className="mx-auto w-full max-w-md">
|
||||||
className="w-full h-full grid "
|
<div className="sm:mx-auto sm:w-full sm:max-w-md flex items-center justify-center space-x-2">
|
||||||
>
|
<img
|
||||||
<div className="flex items-center justify-center py-12">
|
className="p-12 text-black dark:text-white"
|
||||||
{children}
|
src={imageTheme}
|
||||||
|
// loading="eager"
|
||||||
|
alt="Logo"
|
||||||
|
// width={1024}
|
||||||
|
// height={1024}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="text-sm text-muted-foreground -ml-12 -mb-12">
|
||||||
|
v{env.NEXT_PUBLIC_PROJECT_VERSION}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</LayoutAdmin>
|
<footer className="py-4 text-center text-xs justify-items-end text-muted-foreground">
|
||||||
|
Powered by <span className="font-medium">Soluce Technologies</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,31 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import {useEffect, useRef} from "react";
|
import {useEffect, useRef, useState} from "react";
|
||||||
import {useSearchParams} from "next/navigation";
|
import {useSearchParams} from "next/navigation";
|
||||||
|
|
||||||
import {LoginForm} from "@/components/wrappers/auth/login/loginForm/LoginForm";
|
import {LoginForm} from "@/components/wrappers/auth/login/login-form/login-form";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
|
|
||||||
export default function SignInPage(props: {
|
export default function SignInPage(props: {
|
||||||
searchParams: Promise<{ callbackUrl: string | undefined }>
|
searchParams: Promise<{ callbackUrl: string | undefined }>
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const [urlParams, setUrlParams] = useState<URLSearchParams>();
|
||||||
const error = searchParams.get("error");
|
|
||||||
const isFirstRender = useRef(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFirstRender.current && error) {
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
toast.error("Error occurred.");
|
setUrlParams(urlParams);
|
||||||
isFirstRender.current = false;
|
const error = urlParams.get("error");
|
||||||
|
console.log(urlParams.get("redirect"));
|
||||||
|
if (error?.includes("pending")) {
|
||||||
|
toast.error("Your account is not active.");
|
||||||
|
urlParams.delete("error");
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname + "?" + urlParams.toString());
|
||||||
}
|
}
|
||||||
}, [error]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid w-[350px] gap-6">
|
<div className="mx-auto grid w-full gap-6">
|
||||||
<LoginForm/>
|
<LoginForm/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {RegisterForm} from "@/components/wrappers/auth/register/register-form/re
|
|||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid w-[350px] gap-6">
|
<div className="mx-auto grid w-full gap-6">
|
||||||
<RegisterForm/>
|
<RegisterForm/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
import { PageParams } from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import * as drizzleDb from "@/db";
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {AdminTabs} from "@/components/wrappers/dashboard/admin/admin-tabs";
|
||||||
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
import {db} from "@/db";
|
||||||
import { AdminTabs } from "@/components/wrappers/dashboard/admin/admin-tabs";
|
import {isNull} from "drizzle-orm";
|
||||||
import { currentUser } from "@/lib/auth/current-user";
|
|
||||||
import { db } from "@/db";
|
|
||||||
import {eq, isNotNull, isNull} from "drizzle-orm";
|
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
const user = await currentUser();
|
|
||||||
|
|
||||||
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 users = await db.query.user.findMany();
|
|
||||||
console.log("users",users);
|
|
||||||
|
|
||||||
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 (
|
||||||
@@ -26,8 +23,8 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>Administration Panel</PageTitle>
|
<PageTitle>Administration Panel</PageTitle>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="mt-10">
|
<PageContent>
|
||||||
<AdminTabs settings={settings!} users={users} />
|
<AdminTabs settings={settings!} users={users}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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 { AgentForm } from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
|
import { AgentForm } from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {GearIcon} from "@radix-ui/react-icons";
|
import {GearIcon} from "@radix-ui/react-icons";
|
||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||||
import {buttonVariants} from "@/components/ui/button";
|
import {buttonVariants} from "@/components/ui/button";
|
||||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
|
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
|
||||||
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/AgentCardKey/AgentCardKey";
|
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/agent-card-key/agent-card-key";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
|
import {
|
||||||
|
ButtonDeleteProject
|
||||||
|
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
|
||||||
|
import {ButtonDeleteAgent} from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ agentId: string }>) {
|
export default async function RoutePage(props: PageParams<{ agentId: string }>) {
|
||||||
@@ -24,8 +28,8 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
|||||||
databases: true
|
databases: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
//
|
|
||||||
console.log(agent)
|
|
||||||
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
notFound()
|
notFound()
|
||||||
@@ -33,6 +37,12 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
|||||||
//
|
//
|
||||||
// const databaseId = 'db-123';
|
// const databaseId = 'db-123';
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// const totalBackupsResult = await db
|
// const totalBackupsResult = await db
|
||||||
// .select({ count: drizzleDb.schemas.backup.id })
|
// .select({ count: drizzleDb.schemas.backup.id })
|
||||||
// .from(drizzleDb.schemas.backup)
|
// .from(drizzleDb.schemas.backup)
|
||||||
@@ -67,22 +77,19 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
|||||||
href={`/dashboard/agents/${agent.id}/edit`}>
|
href={`/dashboard/agents/${agent.id}/edit`}>
|
||||||
<GearIcon className="w-7 h-7"/>
|
<GearIcon className="w-7 h-7"/>
|
||||||
</Link>
|
</Link>
|
||||||
{/*<AgentModalKey agent={agent}>*/}
|
|
||||||
{/* <Button variant="outline">*/}
|
|
||||||
{/* <KeyRound/>*/}
|
|
||||||
{/* </Button>*/}
|
|
||||||
{/*</AgentModalKey>*/}
|
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
|
<PageActions className="justify-between">
|
||||||
|
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"} />
|
||||||
|
</PageActions>
|
||||||
</div>
|
</div>
|
||||||
<PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription>
|
<PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription>
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
<PageContent className="flex flex-col w-full h-full">
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between gap-8 mb-6">
|
<div className="flex flex-col sm:flex-row sm:justify-between gap-8 mb-6">
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
<Card className="w-full sm:w-auto flex-1">
|
||||||
<CardHeader className="font-bold text-xl">
|
<CardHeader className="font-bold text-xl">
|
||||||
Backups
|
Databases
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent></CardContent>
|
<CardContent>{agent.databases.length}</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
<Card className="w-full sm:w-auto flex-1">
|
||||||
<CardHeader className="font-bold text-xl">
|
<CardHeader className="font-bold text-xl">
|
||||||
@@ -102,7 +109,7 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
<Card className="w-full sm:w-auto flex-1 mb-4">
|
||||||
<CardHeader className="font-bold text-xl">
|
<CardHeader className="font-bold text-xl">
|
||||||
Edge Key
|
Edge Key
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -110,7 +117,7 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
|||||||
<AgentCardKey agent={agent}/>
|
<AgentCardKey agent={agent}/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<CardsWithPagination data={agent.databases} cardItem={DatabaseCard}/>
|
<CardsWithPagination cardsPerPage={2} data={agent.databases} cardItem={DatabaseCard}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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 { AgentForm } from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
|
import { AgentForm } from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
import { PageParams } from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import { AgentCard } from "@/components/wrappers/dashboard/agent/AgentCard/AgentCard";
|
import { AgentCard } from "@/components/wrappers/dashboard/agent/agent-card/agent-card";
|
||||||
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
|
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Page, PageActions, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
import { Page, PageActions, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
|
||||||
|
import {and, eq, not} from "drizzle-orm";
|
||||||
|
import {Plus} from "lucide-react";
|
||||||
|
import {cn} from "@/lib/utils";
|
||||||
|
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
const agents = await db.query.agent.findMany();
|
|
||||||
|
const agents = await db.query.agent.findMany({
|
||||||
|
where: not(eq(drizzleDb.schemas.agent.isArchived, true))
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
if (!agents) {
|
if (!agents) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -27,16 +37,14 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
</PageActions>
|
</PageActions>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="mt-10">
|
<PageContent>
|
||||||
{agents.length > 0 ? (
|
{agents.length > 0 ? (
|
||||||
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1} />
|
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1} />
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<EmptyStatePlaceholder
|
||||||
href={"/dashboard/agents/new"}
|
url={"/dashboard/agents/new"}
|
||||||
className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md"
|
text={"Create new Agent"}
|
||||||
>
|
/>
|
||||||
Create new Agent
|
|
||||||
</Link>
|
|
||||||
)}
|
)}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {RestoreForm} from "@/components/wrappers/dashboard/database/RestoreForm";
|
import {RestoreForm} from "@/components/wrappers/dashboard/database/restore-form";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
|
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { authClient } from "@/lib/auth/auth-client";
|
|
||||||
import { PageParams } from "@/types/next";
|
|
||||||
|
|
||||||
export default function RoutePage(props: PageParams<{}>) {
|
|
||||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4">
|
|
||||||
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
|
||||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
|
||||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
|
||||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
|
||||||
</div>
|
|
||||||
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
|
|
||||||
{JSON.stringify(activeOrganization)}
|
|
||||||
{activeOrganization?.name}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
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 { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { DatabaseForm } from "@/components/wrappers/dashboard/database/DatabaseForm/DatabaseForm";
|
import { DatabaseForm } from "@/components/wrappers/dashboard/database/database-form/database-form";
|
||||||
|
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
@@ -27,6 +27,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>
|
||||||
|
|||||||
@@ -1,25 +1,47 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound, redirect} from "next/navigation";
|
||||||
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
||||||
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
|
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
|
||||||
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/Database/DatabaseTabs";
|
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/database/database-tabs";
|
||||||
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/Database/DatabaseKpi";
|
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi";
|
||||||
import {EditButton} from "@/components/wrappers/dashboard/database/EditButton/EditButton";
|
import {EditButton} from "@/components/wrappers/dashboard/database/edit-button/edit-button";
|
||||||
import {CronButton} from "@/components/wrappers/dashboard/database/CronButton/CronButton";
|
import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button";
|
||||||
|
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {eq, and} from "drizzle-orm";
|
import {eq, and, inArray} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
import {getOrganizationProjectDatabases} from "@/lib/services";
|
||||||
|
import {getOrganization} from "@/lib/auth/auth";
|
||||||
|
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
|
||||||
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
|
export default async function RoutePage(props: PageParams<{
|
||||||
const {databaseId} = await props.params;
|
projectId: string;
|
||||||
|
databaseId: string
|
||||||
|
}>) {
|
||||||
|
const {projectId, databaseId} = await props.params;
|
||||||
|
|
||||||
|
const organization = await getOrganization({});
|
||||||
|
|
||||||
|
if (!organization) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const databasesProject = await getOrganizationProjectDatabases({
|
||||||
|
organizationSlug: organization.slug,
|
||||||
|
projectId: projectId
|
||||||
|
})
|
||||||
|
|
||||||
const dbItem = await db.query.database.findFirst({
|
const dbItem = await db.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.id, databaseId),
|
where: and(inArray(drizzleDb.schemas.backup.id, databasesProject.ids ?? []), eq(drizzleDb.schemas.database.id, databaseId), eq(drizzleDb.schemas.database.projectId, projectId)),
|
||||||
|
with: {
|
||||||
|
project: true,
|
||||||
|
retentionPolicy: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!dbItem) {
|
if (!dbItem) {
|
||||||
notFound();
|
redirect("/dashboard/projects");
|
||||||
}
|
}
|
||||||
|
|
||||||
const backups = await db.query.backup.findMany({
|
const backups = await db.query.backup.findMany({
|
||||||
@@ -38,37 +60,56 @@ export default async function RoutePage(props: PageParams<{ databaseId: string }
|
|||||||
const isAlreadyBackup = backups.some((b) => b.status === "waiting");
|
const isAlreadyBackup = backups.some((b) => b.status === "waiting");
|
||||||
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
||||||
|
|
||||||
const [totalBackups, successfulBackups] = await Promise.all([
|
const totalBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
||||||
db
|
.from(drizzleDb.schemas.backup)
|
||||||
.select({count: drizzleDb.schemas.backup.id})
|
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
|
||||||
.from(drizzleDb.schemas.backup)
|
.then(rows => rows.length);
|
||||||
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
|
|
||||||
.then((rows) => rows.length),
|
const availableBackups = backups.filter(b => !b.deletedAt).length;
|
||||||
db
|
|
||||||
.select({count: drizzleDb.schemas.backup.id})
|
const successfulBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
||||||
.from(drizzleDb.schemas.backup)
|
.from(drizzleDb.schemas.backup)
|
||||||
.where(and(eq(drizzleDb.schemas.backup.databaseId, dbItem.id), eq(drizzleDb.schemas.backup.status, "success")))
|
.where(and(
|
||||||
.then((rows) => rows.length),
|
eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
|
||||||
]);
|
eq(drizzleDb.schemas.backup.status, "success")
|
||||||
|
))
|
||||||
|
.then(rows => rows.length);
|
||||||
|
|
||||||
|
|
||||||
|
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||||
|
if (!settings) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="justify-between gap-2 sm:flex">
|
<div className="justify-between gap-2 sm:flex">
|
||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex items-center justify-between w-full">
|
||||||
{dbItem.name}
|
{capitalizeFirstLetter(dbItem.name)}
|
||||||
<EditButton/>
|
<div className="flex items-center gap-2 justify-between w-full">
|
||||||
<CronButton database={dbItem}/>
|
<div className="flex items-center gap-2">
|
||||||
|
<EditButton/>
|
||||||
|
<RetentionPolicySheet database={dbItem}/>
|
||||||
|
<CronButton database={dbItem}/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<PageActions className="justify-between">
|
|
||||||
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
|
|
||||||
</PageActions>
|
|
||||||
</div>
|
</div>
|
||||||
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
|
|
||||||
|
{dbItem.description && (
|
||||||
|
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
|
||||||
|
)}
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
<PageContent className="flex flex-col w-full h-full">
|
||||||
<DatabaseKpi successRate={successRate} database={dbItem} totalBackups={totalBackups} />
|
<DatabaseKpi successRate={successRate} database={dbItem} availableBackups={availableBackups}
|
||||||
<DatabaseTabs database={dbItem} isAlreadyRestore={isAlreadyRestore} backups={backups} restorations={restorations} />
|
totalBackups={totalBackups}/>
|
||||||
|
<DatabaseTabs settings={settings} database={dbItem} isAlreadyRestore={isAlreadyRestore}
|
||||||
|
backups={backups}
|
||||||
|
restorations={restorations}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
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 {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
|
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {DatabaseWith} from "@/db/schema/06_database";
|
import {DatabaseWith} from "@/db/schema/07_database";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
|
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
|
||||||
const {projectId} = await props.params;
|
const {projectId} = await props.params;
|
||||||
@@ -42,10 +42,9 @@ 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);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,7 +56,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>
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
import { PageParams } from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import { Page, PageActions, PageContent, PageDescription, PageTitle } from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import {buttonVariants} from "@/components/ui/button";
|
||||||
import { GearIcon } from "@radix-ui/react-icons";
|
import {GearIcon} from "@radix-ui/react-icons";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ButtonDeleteProject } from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/ButtonDeleteProject";
|
import {
|
||||||
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
|
ButtonDeleteProject
|
||||||
import { ProjectDatabaseCard } from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
|
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
|
||||||
import { notFound } from "next/navigation";
|
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||||
|
import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
|
||||||
|
import {notFound, redirect} from "next/navigation";
|
||||||
|
|
||||||
import { db } from "@/db";
|
import {db} from "@/db";
|
||||||
import { eq } from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getOrganization} from "@/lib/auth/auth";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
export default async function RoutePage(props: PageParams<{
|
||||||
// slug: string;
|
|
||||||
projectId: string
|
projectId: string
|
||||||
}>) {
|
}>) {
|
||||||
const {
|
const {
|
||||||
// slug: organizationSlug,
|
projectId
|
||||||
projectId } = await props.params;
|
} = await props.params;
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
@@ -33,25 +34,32 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
if (!org) notFound();
|
if (!org) notFound();
|
||||||
|
|
||||||
const proj = await db.query.project.findFirst({
|
const proj = await db.query.project.findFirst({
|
||||||
where: (proj, { and, eq, not }) => and(eq(proj.id, projectId), eq(proj.organizationId, org.id), not(eq(proj.isArchived, true))),
|
where: (proj, {
|
||||||
|
and,
|
||||||
|
eq,
|
||||||
|
not
|
||||||
|
}) => and(eq(proj.id, projectId), eq(proj.organizationId, org.id), not(eq(proj.isArchived, true))),
|
||||||
with: {
|
with: {
|
||||||
databases: true,
|
databases: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!proj) notFound();
|
if (!proj) {
|
||||||
|
redirect("/dashboard/projects");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="justify-between gap-2 sm:flex">
|
<div className="justify-between gap-2 sm:flex">
|
||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex items-center">
|
||||||
{proj.name}
|
{capitalizeFirstLetter(proj.name)}
|
||||||
<Link className={buttonVariants({ variant: "outline" })} href={`/dashboard/projects/${proj.id}/edit`}>
|
<Link className={buttonVariants({variant: "outline"})} href={`/dashboard/projects/${proj.id}/edit`}>
|
||||||
<GearIcon className="w-7 h-7" />
|
<GearIcon className="w-7 h-7"/>
|
||||||
</Link>
|
</Link>
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<PageActions className="justify-between">
|
<PageActions className="justify-between">
|
||||||
<ButtonDeleteProject projectId={projectId} text={"Delete Project"} />
|
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,12 +70,18 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
<CardsWithPagination
|
<CardsWithPagination
|
||||||
data={proj.databases}
|
data={proj.databases}
|
||||||
organizationSlug={organization.slug}
|
organizationSlug={organization.slug}
|
||||||
|
// @ts-ignore
|
||||||
cardItem={ProjectDatabaseCard}
|
cardItem={ProjectDatabaseCard}
|
||||||
cardsPerPage={4}
|
cardsPerPage={4}
|
||||||
numberOfColumns={1}
|
numberOfColumns={1}
|
||||||
extendedProps={proj}
|
extendedProps={proj}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-muted-foreground py-20">
|
||||||
|
<p className="text-lg font-medium">No databases found</p>
|
||||||
|
<p className="text-sm mt-2">You haven’t added any databases to this project yet.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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 {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
|
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getOrganization} from "@/lib/auth/auth";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {DatabaseWith} from "@/db/schema/06_database";
|
import {DatabaseWith} from "@/db/schema/07_database";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ }>) {
|
export default async function RoutePage(props: PageParams<{ }>) {
|
||||||
|
|
||||||
@@ -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,7 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
|
|
||||||
if (!org) notFound();
|
if (!org) notFound();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import {PageParams} from "@/types/next";
|
|||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
import {ProjectCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectCard";
|
import {ProjectCard} from "@/components/wrappers/dashboard/projects/project-card/project-card";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getOrganization} from "@/lib/auth/auth";
|
||||||
|
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ }>) {
|
export default async function RoutePage(props: PageParams<{ }>) {
|
||||||
|
|
||||||
@@ -41,17 +42,15 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<PageContent className="mt-10">
|
<PageContent>
|
||||||
{projects.length > 0 ? (
|
{projects.length > 0 ? (
|
||||||
<CardsWithPagination organizationSlug={organization.slug} data={projects} cardItem={ProjectCard}
|
<CardsWithPagination organizationSlug={organization.slug} data={projects} cardItem={ProjectCard}
|
||||||
cardsPerPage={4} numberOfColumns={1}/>
|
cardsPerPage={4} numberOfColumns={1}/>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<EmptyStatePlaceholder
|
||||||
href={`/dashboard/projects/new`}
|
url={"/dashboard/projects/new"}
|
||||||
className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md"
|
text={"Create new Project"}
|
||||||
>
|
/>
|
||||||
Create new Project
|
|
||||||
</Link>
|
|
||||||
)}
|
)}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
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 {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/OrganizationForm/OrganizationForm";
|
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/organization-form/organization-form";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getOrganization} from "@/lib/auth/auth";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {isNull} from "drizzle-orm";
|
||||||
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
export default async function RoutePage(props: PageParams<{
|
||||||
|
|
||||||
}>) {
|
}>) {
|
||||||
|
const user = await currentUser();
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
|
|
||||||
if (!organization) {
|
const users = await db.query.user.findMany({
|
||||||
|
where: (fields) => isNull(fields.deletedAt)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (!user || !users || !organization || organization.slug == "default") {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +31,7 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
</PageTitle>
|
</PageTitle>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<OrganizationForm members={organization.members} defaultValues={organization}/>
|
<OrganizationForm currentUser={user} users={users} defaultValues={organization}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,42 +1,52 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageActions, PageContent, PageDescription, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {
|
import {
|
||||||
DeleteOrganizationButton
|
DeleteOrganizationButton
|
||||||
} from "@/components/wrappers/dashboard/organization/DeleteOrganization/DeleteOrganizationButton";
|
} from "@/components/wrappers/dashboard/organization/delete-organization/delete-organization-button";
|
||||||
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/EditButtonSettings/EditButtonSettings";
|
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/edit-button-settings/edit-button-settings";
|
||||||
|
import {
|
||||||
|
SettingsOrganizationMembersTable
|
||||||
|
} from "@/components/wrappers/dashboard/settings/settings-organization-members-table";
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
|
const activeMember = await getActiveMember()
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
|
if (isMember) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex items-center">
|
||||||
Settings
|
Organization settings
|
||||||
{organization.slug != "default" ?
|
{!isMember && organization.slug !== "default" && (
|
||||||
<EditButtonSettings/>
|
<EditButtonSettings/>
|
||||||
: null}
|
)}
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<PageActions>
|
<PageActions>
|
||||||
{organization.slug != "default" ?
|
{!isMember && organization.slug !== "default" && (
|
||||||
<DeleteOrganizationButton organizationSlug={organization.slug}/>
|
<DeleteOrganizationButton organizationSlug={organization.slug}/>
|
||||||
: null}
|
)}
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageDescription>
|
{/*<PageDescription>*/}
|
||||||
Manage your organization settings.
|
{/* Manage your organization settings.*/}
|
||||||
</PageDescription>
|
{/*</PageDescription>*/}
|
||||||
<PageContent>
|
<PageContent>
|
||||||
{/* TODO add the list of organisation members (add, remove, edit) */}
|
<SettingsOrganizationMembersTable organization={organization}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import { EvolutionLineChart } from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
|
import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
|
||||||
import { PercentageLineChart } from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
|
import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
|
||||||
import { notFound } from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import { db } from "@/db";
|
import {db} from "@/db";
|
||||||
import { asc, count, eq, inArray } from "drizzle-orm";
|
import {and, asc, count, eq, inArray} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getOrganization} from "@/lib/auth/auth";
|
||||||
|
import {DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ }>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
@@ -26,7 +27,13 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
where: eq(drizzleDb.schemas.project.organizationId, org.id),
|
where: eq(drizzleDb.schemas.project.organizationId, org.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectsCount = projects.length;
|
const projectIds = projects.map(project => project.id);
|
||||||
|
|
||||||
|
const databasesOfAllProjects = await db.query.database.findMany({
|
||||||
|
where: inArray(drizzleDb.schemas.database.projectId, projectIds),
|
||||||
|
})
|
||||||
|
const databaseIds = databasesOfAllProjects.map((database) => database.id);
|
||||||
|
|
||||||
|
|
||||||
const backupsEvolution = await db.query.backup.findMany({
|
const backupsEvolution = await db.query.backup.findMany({
|
||||||
columns: {
|
columns: {
|
||||||
@@ -34,9 +41,83 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
createdAt: true,
|
createdAt: true,
|
||||||
},
|
},
|
||||||
orderBy: [asc(drizzleDb.schemas.backup.id)],
|
orderBy: [asc(drizzleDb.schemas.backup.id)],
|
||||||
|
where: inArray(drizzleDb.schemas.backup.databaseId, databaseIds),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// const tomorrow = new Date();
|
||||||
|
// tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
//
|
||||||
|
// const before = new Date();
|
||||||
|
// before.setDate(before.getDate() - 1);
|
||||||
|
//
|
||||||
|
// const backupsEvolution = [
|
||||||
|
// {
|
||||||
|
// id: '22e84aa4-228c-45b3-82ec-846a639cd509',
|
||||||
|
// createdAt: new Date()
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
||||||
|
// createdAt: new Date()
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
||||||
|
// createdAt: new Date()
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
||||||
|
// createdAt: new Date()
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
||||||
|
// createdAt: new Date(before)
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: '6a6106fe-7f45-48eb-a56f-0a1e734126a1',
|
||||||
|
// createdAt: new Date(before)
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// 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: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
|
||||||
|
// createdAt: new Date(tomorrow)
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
|
||||||
|
// createdAt: new Date(tomorrow)
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// 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({
|
||||||
createdAt: drizzleDb.schemas.backup.createdAt,
|
createdAt: drizzleDb.schemas.backup.createdAt,
|
||||||
@@ -44,13 +125,30 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
_count: count(),
|
_count: count(),
|
||||||
})
|
})
|
||||||
.from(drizzleDb.schemas.backup)
|
.from(drizzleDb.schemas.backup)
|
||||||
.where(inArray(drizzleDb.schemas.backup.status, ["success", "failed"]))
|
.where(and(inArray(drizzleDb.schemas.backup.status, ["success", "failed"]), inArray(drizzleDb.schemas.backup.databaseId, databaseIds)))
|
||||||
.groupBy(drizzleDb.schemas.backup.createdAt, drizzleDb.schemas.backup.status)
|
.groupBy(drizzleDb.schemas.backup.createdAt, drizzleDb.schemas.backup.status)
|
||||||
.orderBy(drizzleDb.schemas.backup.createdAt);
|
.orderBy(drizzleDb.schemas.backup.createdAt);
|
||||||
|
|
||||||
console.log(backupsRate);
|
|
||||||
|
|
||||||
|
|
||||||
|
const restorationsCountResult = await db
|
||||||
|
.select({
|
||||||
|
count: count(),
|
||||||
|
})
|
||||||
|
.from(drizzleDb.schemas.restoration)
|
||||||
|
.where(inArray(drizzleDb.schemas.restoration.databaseId, databaseIds));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const restorationsCount = restorationsCountResult[0]?.count ?? 0;
|
||||||
|
const projectsCount = projects.length;
|
||||||
|
const backupsEvolutionCount = backupsEvolution.length;
|
||||||
|
|
||||||
|
|
||||||
|
const sortedBackupsEvolution = backupsEvolution.sort(
|
||||||
|
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
@@ -58,23 +156,26 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="flex flex-col gap-y-4">
|
<PageContent className="flex flex-col gap-y-4">
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
<Card className="w-full">
|
<Card className="w-full flex-1">
|
||||||
<CardHeader>
|
<CardHeader className="flex items-center gap-2">
|
||||||
|
<Folder className="w-5 h-5 text-muted-foreground" />
|
||||||
<CardTitle>Projects</CardTitle>
|
<CardTitle>Projects</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>{projectsCount}</CardContent>
|
<CardContent className="text-3xl font-bold">{projectsCount}</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="w-full">
|
<Card className="w-full flex-1">
|
||||||
<CardHeader>
|
<CardHeader className="flex items-center gap-2">
|
||||||
<CardTitle>KPI 2</CardTitle>
|
<DatabaseBackup className="w-5 h-5 text-muted-foreground" />
|
||||||
|
<CardTitle>Backups</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent></CardContent>
|
<CardContent className="text-3xl font-bold">{backupsEvolutionCount}</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="w-full">
|
<Card className="w-full flex-1">
|
||||||
<CardHeader>
|
<CardHeader className="flex items-center gap-2">
|
||||||
<CardTitle>KPI 3</CardTitle>
|
<RefreshCcw className="w-5 h-5 text-muted-foreground" />
|
||||||
|
<CardTitle>Restorations</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent></CardContent>
|
<CardContent className="text-3xl font-bold">{restorationsCount}</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
@@ -83,7 +184,7 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
<CardTitle>Evolution of the number of backups</CardTitle>
|
<CardTitle>Evolution of the number of backups</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<EvolutionLineChart data={backupsEvolution} />
|
<EvolutionLineChart data={sortedBackupsEvolution}/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
@@ -91,7 +192,7 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
<CardTitle>Success rate of backups</CardTitle>
|
<CardTitle>Success rate of backups</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<PercentageLineChart data={backupsRate} />
|
<PercentageLineChart data={backupsRate}/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
|
import {Building2, DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
|
||||||
|
import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
|
||||||
|
import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
|
||||||
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {asc, eq, inArray} from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {auth, listOrganizations} from "@/lib/auth/auth";
|
||||||
|
import {authClient} from "@/lib/auth/auth-client";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const user = await currentUser();
|
||||||
|
const organizations = await listOrganizations()
|
||||||
|
|
||||||
|
if (!user || !organizations) notFound();
|
||||||
|
|
||||||
|
const organizationIds = organizations.map(project => project.id);
|
||||||
|
|
||||||
|
|
||||||
|
const projects = await db.query.project.findMany({
|
||||||
|
where: inArray(drizzleDb.schemas.project.organizationId, organizationIds),
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectIds = projects.map(project => project.id);
|
||||||
|
|
||||||
|
|
||||||
|
const databasesOfAllProjects = await db.query.database.findMany({
|
||||||
|
where: inArray(drizzleDb.schemas.database.projectId, projectIds),
|
||||||
|
})
|
||||||
|
const databaseIds = databasesOfAllProjects.map((database) => database.id);
|
||||||
|
|
||||||
|
|
||||||
|
const backupsEvolution = await db.query.backup.findMany({
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
orderBy: [asc(drizzleDb.schemas.backup.id)],
|
||||||
|
where: inArray(drizzleDb.schemas.backup.databaseId, databaseIds),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<Page>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle>Dashboard</PageTitle>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent className="flex flex-col gap-y-4">
|
||||||
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
|
<Card className="w-full flex-1">
|
||||||
|
<CardHeader className="flex items-center gap-2">
|
||||||
|
<Building2 className="w-5 h-5 text-muted-foreground"/>
|
||||||
|
<CardTitle>Organizations</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-3xl font-bold">{organizations.length}</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="w-full flex-1">
|
||||||
|
<CardHeader className="flex items-center gap-2">
|
||||||
|
<Folder className="w-5 h-5 text-muted-foreground"/>
|
||||||
|
<CardTitle>Projects</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-3xl font-bold">{projects.length}</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="w-full flex-1">
|
||||||
|
<CardHeader className="flex items-center gap-2">
|
||||||
|
<DatabaseBackup className="w-5 h-5 text-muted-foreground"/>
|
||||||
|
<CardTitle>Backups</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-3xl font-bold">{backupsEvolution.length}</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col gap-4">
|
||||||
|
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||||
|
<div className="aspect-video rounded-xl bg-muted/50"/>
|
||||||
|
<div className="aspect-video rounded-xl bg-muted/50"/>
|
||||||
|
<div className="aspect-video rounded-xl bg-muted/50"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import {redirect} from "next/navigation";
|
import {redirect} from "next/navigation";
|
||||||
|
|
||||||
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar";
|
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar";
|
||||||
import {AppSidebar} from "@/components/wrappers/dashboard/sideBar/app-sidebar";
|
import {AppSidebar} from "@/components/wrappers/dashboard/common/sidebar/app-sidebar";
|
||||||
import {Header} from "@/features/layout/Header";
|
import {Header} from "@/features/layout/Header";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LoadingSpinner } from "@/components/wrappers/common/loading/LoadingSpinner";
|
import { LoadingSpinner } from "@/components/wrappers/common/loading/loading-spinner";
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,28 +1,25 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
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/UserForm/UserForm";
|
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/ButtonDeleteAccount/ButtonDeleteAccount";
|
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/button-delete-account/button-delete-account";
|
||||||
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/Avatar/AvatarWithUpload";
|
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";
|
||||||
//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();
|
||||||
console.log("my user",user);
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (user.role !== "user" && user.role !== "admin" && user.role !== "superadmin") {
|
if (user.role !== "user" && user.role !== "admin" && user.role !== "superadmin") {
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
// const sessions = await getSessions();
|
const sessions = await getSessions();
|
||||||
// const accounts = await getAccounts();
|
const accounts = await getAccounts();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
@@ -42,22 +39,23 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
{user.name}
|
{user.name}
|
||||||
<Badge className="ml-3 hidden lg:block">{user.role}</Badge>
|
<Badge className="ml-3 hidden lg:block">{user.role}</Badge>
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<PageActions className="mt-2 hidden sm:block">
|
{/*<PageActions className="mt-2 hidden sm:block">*/}
|
||||||
<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 @@ export default async function Index() {
|
|||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
if (user) {
|
if (user) {
|
||||||
const currentOrganizationSlug = await getCurrentOrganizationSlug();
|
const currentOrganizationSlug = await getCurrentOrganizationSlug();
|
||||||
redirect(`/dashboard/${currentOrganizationSlug}/projects`);
|
redirect(`/dashboard/home`);
|
||||||
}
|
}
|
||||||
redirect("/login");
|
redirect("/login");
|
||||||
//Do not delete
|
//Do not delete
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import {uploadLocalPrivate} from "@/features/upload/private/upload.action";
|
import {uploadLocalPrivate, uploadS3Private} from "@/features/upload/private/upload.action";
|
||||||
import {v4 as uuidv4} from "uuid";
|
import {v4 as uuidv4} from "uuid";
|
||||||
import {eventEmitter} from "../../../events/route";
|
import {eventEmitter} from "../../../events/route";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {Backup} from "@/db/schema/06_database";
|
import {Backup} from "@/db/schema/07_database";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
import {env} from "@/env.mjs";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -48,6 +50,9 @@ export async function POST(
|
|||||||
|
|
||||||
const database = await db.query.database.findFirst({
|
const database = await db.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
||||||
|
with: {
|
||||||
|
project: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!database) {
|
if (!database) {
|
||||||
@@ -108,7 +113,19 @@ export async function POST(
|
|||||||
const fileName = `${uuid}.dump`;
|
const fileName = `${uuid}.dump`;
|
||||||
const buffer = Buffer.from(await file.arrayBuffer());
|
const buffer = Buffer.from(await file.arrayBuffer());
|
||||||
|
|
||||||
const {success, message, filePath} = await uploadLocalPrivate(fileName, buffer);
|
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||||
|
if (!settings) {
|
||||||
|
throw new Error("System settings not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let success: boolean, message: string, filePath: string;
|
||||||
|
|
||||||
|
const result =
|
||||||
|
settings.storage === "local"
|
||||||
|
? await uploadLocalPrivate(fileName, buffer)
|
||||||
|
: await uploadS3Private(`${database.project?.slug}/${fileName}`, buffer, env.S3_BUCKET_NAME!);
|
||||||
|
|
||||||
|
({success, message, filePath} = result);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -119,10 +136,10 @@ export async function POST(
|
|||||||
|
|
||||||
await db
|
await db
|
||||||
.update(drizzleDb.schemas.backup)
|
.update(drizzleDb.schemas.backup)
|
||||||
.set({
|
.set(withUpdatedAt({
|
||||||
file: fileName,
|
file: fileName,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
})
|
}))
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
eventEmitter.emit('modification', {update: true});
|
||||||
@@ -137,7 +154,7 @@ export async function POST(
|
|||||||
|
|
||||||
await db
|
await db
|
||||||
.update(drizzleDb.schemas.backup)
|
.update(drizzleDb.schemas.backup)
|
||||||
.set({status: 'failed'})
|
.set(withUpdatedAt({status: 'failed'}))
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
eventEmitter.emit('modification', {update: true});
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export async function POST(
|
|||||||
const body: BodyResultRestore = await request.json();
|
const body: BodyResultRestore = await request.json();
|
||||||
|
|
||||||
console.log(body)
|
console.log(body)
|
||||||
console.log(agentId)
|
|
||||||
|
|
||||||
|
|
||||||
if (!isUuidv4(body.generatedId)) {
|
if (!isUuidv4(body.generatedId)) {
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {Body} from "./route";
|
import {Body} from "./route";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action";
|
import {getFileUrlPresignedLocal, getFileUrlPreSignedS3Action} from "@/features/upload/private/upload.action";
|
||||||
import {Agent} from "@/db/schema/07_agent";
|
import {Agent} from "@/db/schema/08_agent";
|
||||||
import {Database} from "@/db/schema/06_database";
|
import {Database} from "@/db/schema/07_database";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db as dbClient} from "@/db";
|
import {db as dbClient} from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
||||||
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
|
import {SafeActionResult} from "next-safe-action";
|
||||||
|
import {ZodString} from "zod";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
|
||||||
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) {
|
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) {
|
||||||
const databasesResponse = [];
|
const databasesResponse = [];
|
||||||
@@ -35,17 +39,15 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
|
|
||||||
let backupAction: boolean = false
|
let backupAction: boolean = false
|
||||||
let restoreAction: boolean = false
|
let restoreAction: boolean = false
|
||||||
let UrlBackup: string = ""
|
let urlBackup: string = ""
|
||||||
|
|
||||||
if (!existingDatabase) {
|
if (!existingDatabase) {
|
||||||
if (!isUuidv4(db.generatedId)) {
|
if (!isUuidv4(db.generatedId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "generatedId is not a valid uuid" },
|
{error: "generatedId is not a valid uuid"},
|
||||||
{ status: 500 }
|
{status: 500}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.log(db)
|
|
||||||
console.log(dbmsEnumSchema.parse(db.dbms))
|
|
||||||
const [databaseCreated] = await dbClient
|
const [databaseCreated] = await dbClient
|
||||||
.insert(drizzleDb.schemas.database)
|
.insert(drizzleDb.schemas.database)
|
||||||
.values({
|
.values({
|
||||||
@@ -58,62 +60,108 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (databaseCreated) {
|
if (databaseCreated) {
|
||||||
databasesResponse.push(formatDatabase(databaseCreated, backupAction,restoreAction, UrlBackup));
|
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const [databaseUpdated] = await dbClient
|
const [databaseUpdated] = await dbClient
|
||||||
.update(drizzleDb.schemas.database)
|
.update(drizzleDb.schemas.database)
|
||||||
.set({ lastContact: lastContact })
|
.set({lastContact: lastContact})
|
||||||
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
|
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const backup = await dbClient.query.backup.findFirst({
|
const backup = await dbClient.query.backup.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status,"waiting"))
|
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting"))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const restoration = await dbClient.query.restoration.findFirst({
|
const restoration = await dbClient.query.restoration.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status,"waiting"))
|
where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status, "waiting"))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
if(backup){
|
if (backup) {
|
||||||
backupAction = true
|
backupAction = true
|
||||||
|
|
||||||
await dbClient
|
await dbClient
|
||||||
.update(drizzleDb.schemas.backup)
|
.update(drizzleDb.schemas.backup)
|
||||||
.set({ status: "ongoing" })
|
.set(withUpdatedAt({status: "ongoing"}))
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(restoration){
|
if (restoration) {
|
||||||
restoreAction = true
|
restoreAction = true
|
||||||
|
|
||||||
|
|
||||||
const backupToRestore = await dbClient.query.backup.findFirst({
|
const backupToRestore = await dbClient.query.backup.findFirst({
|
||||||
where: eq(drizzleDb.schemas.backup.id, restoration.backupId),
|
where: eq(drizzleDb.schemas.backup.id, restoration.backupId),
|
||||||
|
with: {
|
||||||
|
database: {
|
||||||
|
with: {
|
||||||
|
project: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [settings] = await dbClient.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||||
|
if (!settings) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Unable to find settings"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const fileName = backupToRestore?.file
|
const fileName = backupToRestore?.file
|
||||||
UrlBackup = await getFileUrlPresignedLocal(fileName ?? "")
|
|
||||||
|
|
||||||
|
let data: SafeActionResult<string, ZodString, readonly [], {
|
||||||
|
_errors?: string[] | undefined;
|
||||||
|
}, readonly [], ServerActionResult<string>, object> | undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (settings.storage == "local") {
|
||||||
|
data = await getFileUrlPresignedLocal(fileName!)
|
||||||
|
} else if (settings.storage == "s3") {
|
||||||
|
|
||||||
|
data = await getFileUrlPreSignedS3Action(`backups/${backupToRestore?.database.project?.slug}/${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (data?.data?.success) {
|
||||||
|
urlBackup = data.data.value ?? "";
|
||||||
|
} else {
|
||||||
|
await dbClient
|
||||||
|
.update(drizzleDb.schemas.restoration)
|
||||||
|
.set({status: "failed"})
|
||||||
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const errorMessage = data?.data?.actionError?.message || "Failed to get presigned URL";
|
||||||
|
console.error("Restoration failed: ", errorMessage);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Restoration crashed unexpectedly:", err);
|
||||||
|
await dbClient
|
||||||
|
.update(drizzleDb.schemas.restoration)
|
||||||
|
.set({status: "failed"})
|
||||||
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
await dbClient
|
await dbClient
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set({ status: "ongoing" })
|
.set({status: "ongoing"})
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup));
|
||||||
|
|
||||||
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, UrlBackup));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return databasesResponse;
|
return databasesResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export type Body = {
|
|||||||
databases: databaseAgent[]
|
databases: databaseAgent[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to test the get file url presigned local
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const url = await getFileUrlPresignedLocal("d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump")
|
const url = await getFileUrlPresignedLocal("d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump")
|
||||||
return Response.json({
|
return Response.json({
|
||||||
|
|||||||
@@ -2,9 +2,17 @@ import path from "path";
|
|||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
export async function GET({ params }: { params: Promise<{ fileName: string }> }) {
|
export async function GET(
|
||||||
|
request: Request,
|
||||||
|
{params}: { params: Promise<{ fileName: string }> }
|
||||||
|
) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileName = (await params).fileName;
|
const fileName = (await params).fileName;
|
||||||
|
|
||||||
|
|
||||||
|
console.log("fileName", fileName);
|
||||||
|
|
||||||
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
|
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
|
||||||
|
|
||||||
// Check if the file exists
|
// Check if the file exists
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import type {Metadata} from "next";
|
import type {Metadata} from "next";
|
||||||
import {Inter} from "next/font/google";
|
import {Inter} from "next/font/google";
|
||||||
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import {Providers} from "./providers";
|
import {Providers} from "./providers";
|
||||||
import {cn} from "@/lib/utils";
|
import {cn} from "@/lib/utils";
|
||||||
|
import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer";
|
||||||
|
|
||||||
const inter = Inter({subsets: ["latin"]});
|
const inter = Inter({subsets: ["latin"]});
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ export default function RootLayout({
|
|||||||
<meta name="apple-mobile-web-app-title" content="Portabase"/>
|
<meta name="apple-mobile-web-app-title" content="Portabase"/>
|
||||||
</head>
|
</head>
|
||||||
<body className={cn(inter.className, "h-full")}>
|
<body className={cn(inter.className, "h-full")}>
|
||||||
|
<ConsoleSilencer/>
|
||||||
<Providers>{children}</Providers>
|
<Providers>{children}</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { redirect } from "next/navigation";
|
|
||||||
|
|
||||||
export default async function NotFound() {
|
export default async function NotFound() {
|
||||||
// redirect("/dashboard/profile");
|
|
||||||
return(
|
return(
|
||||||
<>
|
<div className="flex items-center min-h-screen px-4 py-12 sm:px-6 md:px-8 lg:px-12 xl:px-16">
|
||||||
Error
|
<div className="w-full space-y-6 text-center">
|
||||||
</>
|
<div className="space-y-3">
|
||||||
|
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">Not found</h1>
|
||||||
|
<p className="leading-7 [&:not(:first-child)]:mt-6">The content you are trying to view is not available.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: docker/dockerfile/Dockerfile
|
dockerfile: docker/dockerfile/Dockerfile
|
||||||
target: prod
|
target: prod
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- '8887:80'
|
- '8887:80'
|
||||||
env_file:
|
env_file:
|
||||||
|
|||||||
@@ -1,31 +1,24 @@
|
|||||||
name: portabase-dev
|
name: portabase-dev
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
# app:
|
||||||
build:
|
# build:
|
||||||
context: .
|
# context: .
|
||||||
dockerfile: docker/dockerfile/Dockerfile
|
# dockerfile: docker/dockerfile/Dockerfile
|
||||||
target: dev
|
# target: dev
|
||||||
ports:
|
# ports:
|
||||||
- "8887:80"
|
# - "8887:8887"
|
||||||
environment:
|
# environment:
|
||||||
- TIME_ZONE="Europe/Paris"
|
# - TIME_ZONE="Europe/Paris"
|
||||||
- NODE_ENV=development
|
# - NODE_ENV=development
|
||||||
depends_on:
|
# depends_on:
|
||||||
db:
|
# db:
|
||||||
condition: service_healthy
|
# condition: service_healthy
|
||||||
volumes:
|
# volumes:
|
||||||
- .:/app
|
# - .:/app
|
||||||
- /app/node_modules
|
# - /app/node_modules
|
||||||
container_name: portabase-app
|
# container_name: portabase-app
|
||||||
|
#
|
||||||
develop:
|
|
||||||
watch:
|
|
||||||
- action: sync
|
|
||||||
path: .
|
|
||||||
target: /app
|
|
||||||
- action: rebuild
|
|
||||||
path: package.json
|
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
@@ -43,35 +36,38 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
db-pgadmin:
|
# db-pgadmin:
|
||||||
image: dpage/pgadmin4
|
# image: dpage/pgadmin4
|
||||||
environment:
|
# environment:
|
||||||
PGADMIN_DEFAULT_EMAIL: "devuser@devuser.devuser"
|
# PGADMIN_DEFAULT_EMAIL: "devuser@devuser.devuser"
|
||||||
PGADMIN_DEFAULT_PASSWORD: "changeme"
|
# PGADMIN_DEFAULT_PASSWORD: "changeme"
|
||||||
PGADMIN_CONFIG_SERVER_MODE: "False"
|
# PGADMIN_CONFIG_SERVER_MODE: "False"
|
||||||
POSTGRES_USER: "devuser"
|
# POSTGRES_USER: "devuser"
|
||||||
POSTGRES_PASSWORD: "changeme"
|
# POSTGRES_PASSWORD: "changeme"
|
||||||
volumes:
|
# volumes:
|
||||||
- pgadmin-data:/var/lib/pgadmin
|
# - pgadmin-data:/var/lib/pgadmin
|
||||||
ports:
|
# ports:
|
||||||
- "8080:80"
|
# - "8080:80"
|
||||||
restart: unless-stopped
|
# restart: unless-stopped
|
||||||
depends_on:
|
# depends_on:
|
||||||
- db
|
# - db
|
||||||
|
|
||||||
s3:
|
s3:
|
||||||
|
container_name: s3-portabase-dev
|
||||||
image: docker.io/bitnami/minio:latest
|
image: docker.io/bitnami/minio:latest
|
||||||
ports:
|
ports:
|
||||||
- "9000:9000"
|
- "9000:9000"
|
||||||
- "9001:9001"
|
- "9001:9001"
|
||||||
volumes:
|
volumes:
|
||||||
- minio_data:/data
|
- minio_data:/data
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
|
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
|
||||||
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
|
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
|
||||||
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
|
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
|
||||||
|
- MINIO_BROWSER=on
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
minio_data:
|
minio_data:
|
||||||
pgadmin-data:
|
# pgadmin-data:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:23-alpine AS base
|
FROM node:22-alpine AS base
|
||||||
|
|
||||||
ENV YARN_VERSION=4.9.1
|
ENV YARN_VERSION=4.9.1
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
#npx drizzle-kit generate
|
||||||
|
#npx drizzle-kit migrate
|
||||||
|
#
|
||||||
|
#npm run dev
|
||||||
|
#
|
||||||
|
#exec "$@"
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
npm install -g npm@11.0.0
|
echo "▶ Running Drizzle codegen..."
|
||||||
npm -v
|
|
||||||
|
|
||||||
npx drizzle-kit generate
|
npx drizzle-kit generate
|
||||||
|
|
||||||
|
echo "▶ Applying migrations..."
|
||||||
npx drizzle-kit migrate
|
npx drizzle-kit migrate
|
||||||
|
|
||||||
npm run dev
|
echo "▶ Starting Next.js dev server..."
|
||||||
|
exec npm run dev
|
||||||
exec "$@"
|
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
echo " ____ __ __ "
|
#echo " ____ __ __ "
|
||||||
echo " / __ \____ _____/ /_____ _/ /_ ____ _________ "
|
#echo " / __ \____ _____/ /_____ _/ /_ ____ _________ "
|
||||||
echo " / /_/ / __ \/ ___/ __/ __ / __ \/ __ / ___/ _ \ "
|
#echo " / /_/ / __ \/ ___/ __/ __ / __ \/ __ / ___/ _ \ "
|
||||||
echo " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ "
|
#echo " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ "
|
||||||
echo " /_/ \____/_/ \__/\__,_/_.___/\__,_/____/\___/ "
|
#echo " /_/ \____/_/ \__/\__,_/_.___/\__,_/____/\___/ "
|
||||||
echo " "
|
#echo " "
|
||||||
echo " Community Edition v1.0.0 "
|
#echo " Community Edition v1.1.1 "
|
||||||
echo " "
|
#echo " "
|
||||||
|
|
||||||
|
|
||||||
node server.js
|
node server.js
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export async function register() {
|
export async function register() {
|
||||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
const init = await import("@/utils/init");
|
const init = await import("@/utils/init");
|
||||||
init.init();
|
await init.init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { loggingMiddleware } from "@/middleware/loggingMiddleware";
|
|||||||
import { errorHandler } from "@/middleware/errorHandler";
|
import { errorHandler } from "@/middleware/errorHandler";
|
||||||
import { auth } from "@/lib/auth/auth";
|
import { auth } from "@/lib/auth/auth";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
import { signOut } from "@/lib/auth/auth-client";
|
||||||
|
|
||||||
export async function middleware(request: NextRequest) {
|
export async function middleware(request: NextRequest) {
|
||||||
const url = request.nextUrl.clone();
|
const url = request.nextUrl.clone();
|
||||||
|
const redirectUrl = encodeURIComponent(request.nextUrl.pathname)
|
||||||
|
|
||||||
if (url.pathname.startsWith("/dashboard")) {
|
if (url.pathname.startsWith("/dashboard")) {
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
@@ -13,7 +15,21 @@ export async function middleware(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return NextResponse.redirect(new URL("/login", request.url));
|
return NextResponse.redirect(new URL(`/login?redirect=${redirectUrl}`, request.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.user.banned) {
|
||||||
|
signOut();
|
||||||
|
return NextResponse.redirect(new URL("/login?error=banned", request.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.user.role === "pending") {
|
||||||
|
signOut();
|
||||||
|
return NextResponse.redirect(new URL(`/login?error=pending?redirect=${redirectUrl}`, request.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === "/dashboard") {
|
||||||
|
return NextResponse.redirect(new URL(`/dashboard/home`, request.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
// next-auth.d.ts
|
|
||||||
import NextAuth from "next-auth";
|
|
||||||
|
|
||||||
declare module "next-auth" {
|
|
||||||
interface Session {
|
|
||||||
user: {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
authMethod: string; // Add authMethod to the session user type
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
authMethod: string; // Add authMethod to the user type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
/// <reference path="./.next/types/routes.d.ts" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { NextConfig } from "next";
|
import type {NextConfig} from "next";
|
||||||
import {PORTABASE_DEFAULT_SETTINGS} from "./portabase.config";
|
import {PORTABASE_DEFAULT_SETTINGS} from "./portabase.config";
|
||||||
|
|
||||||
|
|
||||||
function buildCSPHeader(): string {
|
function buildCSPHeader(): string {
|
||||||
const { CSP } = PORTABASE_DEFAULT_SETTINGS.SECURITY;
|
const {CSP} = PORTABASE_DEFAULT_SETTINGS.SECURITY;
|
||||||
|
|
||||||
const directives = [
|
const directives = [
|
||||||
`default-src ${CSP.DEFAULT_SRC.join(" ")}`,
|
`default-src ${CSP.DEFAULT_SRC.join(" ")}`,
|
||||||
@@ -12,6 +12,7 @@ function buildCSPHeader(): string {
|
|||||||
`img-src ${CSP.IMG_SRC.join(" ")}`,
|
`img-src ${CSP.IMG_SRC.join(" ")}`,
|
||||||
`font-src ${CSP.FONT_SRC.join(" ")}`,
|
`font-src ${CSP.FONT_SRC.join(" ")}`,
|
||||||
`object-src ${CSP.OBJECT_SRC.join(" ")}`,
|
`object-src ${CSP.OBJECT_SRC.join(" ")}`,
|
||||||
|
`connect-src ${CSP.CONNECT_SRC.join(" ")}`,
|
||||||
`base-uri ${CSP.BASE_URI.join(" ")}`,
|
`base-uri ${CSP.BASE_URI.join(" ")}`,
|
||||||
`form-action ${CSP.FORM_ACTION.join(" ")}`,
|
`form-action ${CSP.FORM_ACTION.join(" ")}`,
|
||||||
`frame-ancestors ${CSP.FRAME_ANCESTORS.join(" ")}`,
|
`frame-ancestors ${CSP.FRAME_ANCESTORS.join(" ")}`,
|
||||||
@@ -35,7 +36,6 @@ function buildPermissionsPolicy(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
typescript: {
|
typescript: {
|
||||||
@@ -44,12 +44,6 @@ const nextConfig: NextConfig = {
|
|||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
compiler: {
|
|
||||||
removeConsole: process.env.NODE_ENV === "production",
|
|
||||||
},
|
|
||||||
experimental: {
|
|
||||||
nodeMiddleware: true,
|
|
||||||
},
|
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.1.0",
|
"version": "1.1.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 80",
|
"dev": "next dev --turbopack -p 8887",
|
||||||
"build": "next build --experimental-build-mode compile",
|
"build": "next build --experimental-build-mode compile",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
@@ -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.2.8",
|
"better-auth": "1.3.1",
|
||||||
"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",
|
||||||
@@ -65,9 +65,10 @@
|
|||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.510.0",
|
"lucide-react": "^0.510.0",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
"next": "^15.4.2-canary.5",
|
"next": "15.5.2",
|
||||||
"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",
|
||||||
"nodemailer": "^7.0.3",
|
"nodemailer": "^7.0.3",
|
||||||
"npm-check-updates": "^18.0.1",
|
"npm-check-updates": "^18.0.1",
|
||||||
"pg": "^8.16.0",
|
"pg": "^8.16.0",
|
||||||
|
|||||||
@@ -3,10 +3,47 @@ export const PORTABASE_DEFAULT_SETTINGS = {
|
|||||||
SECURITY: {
|
SECURITY: {
|
||||||
CSP: {
|
CSP: {
|
||||||
DEFAULT_SRC: ["'self'"],
|
DEFAULT_SRC: ["'self'"],
|
||||||
SCRIPT_SRC: ["'self'", "'unsafe-eval'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://www.googletagmanager.com"],
|
SCRIPT_SRC: [
|
||||||
STYLE_SRC: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
"'self'",
|
||||||
IMG_SRC: ["'self'", "blob:", "data:", "https:"],
|
"'unsafe-eval'",
|
||||||
FONT_SRC: ["'self'"],
|
"'unsafe-inline'",
|
||||||
|
"https://cdn.jsdelivr.net",
|
||||||
|
"https://www.googletagmanager.com",
|
||||||
|
"https://code.iconify.design",
|
||||||
|
"https://code.iconify.com",
|
||||||
|
"https://cdn.iconify.design",
|
||||||
|
"https://api.iconify.design",
|
||||||
|
|
||||||
|
],
|
||||||
|
STYLE_SRC: [
|
||||||
|
"'self'",
|
||||||
|
"'unsafe-inline'",
|
||||||
|
"https://fonts.googleapis.com",
|
||||||
|
"https://code.iconify.design",
|
||||||
|
"https://cdn.iconify.design",
|
||||||
|
"https://code.iconify.com",
|
||||||
|
],
|
||||||
|
IMG_SRC: [
|
||||||
|
"'self'",
|
||||||
|
"blob:",
|
||||||
|
"data:",
|
||||||
|
"https:",
|
||||||
|
"https://code.iconify.design",
|
||||||
|
"https://cdn.iconify.design",
|
||||||
|
"https://code.iconify.com",
|
||||||
|
"https://api.iconify.design",
|
||||||
|
"http://localhost:9000",
|
||||||
|
],
|
||||||
|
FONT_SRC: [
|
||||||
|
"'self'",
|
||||||
|
"https://fonts.gstatic.com",
|
||||||
|
"https://cdn.iconify.design",
|
||||||
|
],
|
||||||
|
CONNECT_SRC: [
|
||||||
|
"'self'",
|
||||||
|
"https://api.iconify.design",
|
||||||
|
"https://code.iconify.design",
|
||||||
|
],
|
||||||
OBJECT_SRC: ["'none'"],
|
OBJECT_SRC: ["'none'"],
|
||||||
BASE_URI: ["'self'"],
|
BASE_URI: ["'self'"],
|
||||||
FORM_ACTION: ["'self'"],
|
FORM_ACTION: ["'self'"],
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB |
@@ -35,6 +35,8 @@ const buttonVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export type ButtonVariantsProps = VariantProps<typeof buttonVariants>;
|
||||||
|
|
||||||
function Button({
|
function Button({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
|
|||||||
@@ -11,119 +11,121 @@ function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SheetTrigger({
|
function SheetTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetClose({
|
function SheetClose({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetPortal({
|
function SheetPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetOverlay({
|
function SheetOverlay({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||||
return (
|
return (
|
||||||
<SheetPrimitive.Overlay
|
<SheetPrimitive.Overlay
|
||||||
data-slot="sheet-overlay"
|
data-slot="sheet-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetContent({
|
function SheetContent({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
side = "right",
|
side = "right",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||||
side?: "top" | "right" | "bottom" | "left"
|
side?: "top" | "right" | "bottom" | "left"
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SheetPortal>
|
<SheetPortal>
|
||||||
<SheetOverlay />
|
<SheetOverlay />
|
||||||
<SheetPrimitive.Content
|
<SheetPrimitive.Content
|
||||||
data-slot="sheet-content"
|
data-slot="sheet-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||||
side === "right" &&
|
side === "right" &&
|
||||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l",
|
||||||
side === "left" &&
|
// "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
side === "left" &&
|
||||||
side === "top" &&
|
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r",
|
||||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
// "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||||
side === "bottom" &&
|
side === "top" &&
|
||||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||||
className
|
side === "bottom" &&
|
||||||
)}
|
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||||
{...props}
|
className
|
||||||
>
|
)}
|
||||||
{children}
|
{...props}
|
||||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
>
|
||||||
<XIcon className="size-4" />
|
{children}
|
||||||
<span className="sr-only">Close</span>
|
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||||
</SheetPrimitive.Close>
|
<XIcon className="size-4" />
|
||||||
</SheetPrimitive.Content>
|
<span className="sr-only">Close</span>
|
||||||
</SheetPortal>
|
</SheetPrimitive.Close>
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="sheet-header"
|
data-slot="sheet-header"
|
||||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="sheet-footer"
|
data-slot="sheet-footer"
|
||||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetTitle({
|
function SheetTitle({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||||
return (
|
return (
|
||||||
<SheetPrimitive.Title
|
<SheetPrimitive.Title
|
||||||
data-slot="sheet-title"
|
data-slot="sheet-title"
|
||||||
className={cn("text-foreground font-semibold", className)}
|
className={cn("text-foreground font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetDescription({
|
function SheetDescription({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||||
return (
|
return (
|
||||||
<SheetPrimitive.Description
|
<SheetPrimitive.Description
|
||||||
data-slot="sheet-description"
|
data-slot="sheet-description"
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import { toast } from "sonner";
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { PasswordInput } from "@/components/wrappers/auth/PaswordInput/password-input";
|
import { PasswordInput } from "@/components/wrappers/auth/password-input/password-input";
|
||||||
import { LoginSchema, LoginType } from "@/components/wrappers/auth/login/loginForm/login-form.schema";
|
import { LoginSchema, LoginType } from "@/components/wrappers/auth/login/login-form/login-form.schema";
|
||||||
import { SocialAuthButton, SocialProviderType } from "@/components/wrappers/auth/login/buttonAuth/SocialAuthButton";
|
import { SocialAuthButton, SocialProviderType } from "@/components/wrappers/auth/login/button-auth/social-auth-button";
|
||||||
import { signIn } from "@/lib/auth/auth-client";
|
import { signIn } from "@/lib/auth/auth-client";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
@@ -12,7 +12,7 @@ import { Form } from "@/components/ui/form";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { TooltipProvider, TooltipTrigger, Tooltip, TooltipContent } from "@/components/ui/tooltip";
|
import { TooltipProvider, TooltipTrigger, Tooltip, TooltipContent } from "@/components/ui/tooltip";
|
||||||
import { RegisterSchema, RegisterType } from "@/components/wrappers/auth/register/register-form/register-form.schema";
|
import { RegisterSchema, RegisterType } from "@/components/wrappers/auth/register/register-form/register-form.schema";
|
||||||
import { PasswordInput } from "@/components/wrappers/auth/PaswordInput/password-input";
|
import { PasswordInput } from "@/components/wrappers/auth/password-input/password-input";
|
||||||
import {signUp} from "@/lib/auth/auth-client";
|
import {signUp} from "@/lib/auth/auth-client";
|
||||||
|
|
||||||
export type registerFormProps = {
|
export type registerFormProps = {
|
||||||
|
|||||||
@@ -1,45 +1,161 @@
|
|||||||
"use client";
|
// "use client";
|
||||||
|
//
|
||||||
import { Button } from "@/components/ui/button";
|
// import { Button } from "@/components/ui/button";
|
||||||
import { useState } from "react";
|
// import { useState } from "react";
|
||||||
import { Loader2 } from "lucide-react";
|
// import { Loader2 } from "lucide-react";
|
||||||
|
//
|
||||||
export type VariantButton = {
|
// export type VariantButton = {
|
||||||
secondary: string;
|
// secondary: string;
|
||||||
default: string;
|
// default: string;
|
||||||
outline: string;
|
// outline: string;
|
||||||
ghost: string;
|
// ghost: string;
|
||||||
link: string;
|
// link: string;
|
||||||
destructive: string;
|
// destructive: string;
|
||||||
};
|
// };
|
||||||
|
//
|
||||||
|
// export type ButtonWithConfirmProps = {
|
||||||
|
// icon?: any;
|
||||||
|
// text: string;
|
||||||
|
// variant?: keyof VariantButton;
|
||||||
|
// className?: string;
|
||||||
|
// onClick?: () => void;
|
||||||
|
// isPending?: boolean;
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
||||||
|
// const [isConfirming, setIsConfirming] = useState(false);
|
||||||
|
//
|
||||||
|
// return (
|
||||||
|
// <Button
|
||||||
|
// onClick={() => {
|
||||||
|
// if (isConfirming && props.onClick) {
|
||||||
|
// props.onClick();
|
||||||
|
// } else {
|
||||||
|
// setIsConfirming(true);
|
||||||
|
// }
|
||||||
|
// }}
|
||||||
|
// variant={props.variant ? props.variant : "default"}
|
||||||
|
// className={props.className}
|
||||||
|
// >
|
||||||
|
// {props.isPending && <Loader2 className="animate-spin mr-4" size={16} />}
|
||||||
|
// {isConfirming ? "Are you sure ?" : `${props.text}`}
|
||||||
|
// <>{props.icon ? props.icon : null}</>
|
||||||
|
// </Button>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
"use client"
|
||||||
|
import {Button, ButtonVariantsProps} from "@/components/ui/button";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {Loader2} from "lucide-react";
|
||||||
|
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
|
||||||
|
import {cn} from "@/lib/utils";
|
||||||
|
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
export type ButtonWithConfirmProps = {
|
export type ButtonWithConfirmProps = {
|
||||||
icon?: any;
|
title: string;
|
||||||
text: string;
|
description: string;
|
||||||
variant?: keyof VariantButton;
|
button: {
|
||||||
className?: string;
|
main: {
|
||||||
onClick?: () => void;
|
className?: string;
|
||||||
|
text?: string;
|
||||||
|
icon?: any;
|
||||||
|
variant?: ButtonVariantsProps["variant"];
|
||||||
|
size?: ButtonVariantsProps["size"];
|
||||||
|
disabled?: boolean;
|
||||||
|
tooltipText?: string;
|
||||||
|
};
|
||||||
|
confirm: {
|
||||||
|
className?: string;
|
||||||
|
text: string;
|
||||||
|
icon?: any;
|
||||||
|
variant?: ButtonVariantsProps["variant"];
|
||||||
|
size?: ButtonVariantsProps["size"];
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
cancel: {
|
||||||
|
className?: string;
|
||||||
|
text: string;
|
||||||
|
icon?: any;
|
||||||
|
variant?: ButtonVariantsProps["variant"];
|
||||||
|
size?: ButtonVariantsProps["size"];
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
isPending?: boolean;
|
isPending?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
||||||
const [isConfirming, setIsConfirming] = useState(false);
|
const [isConfirming, setIsConfirming] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Popover open={isConfirming} onOpenChange={!props.button.main.disabled ? setIsConfirming : undefined}>
|
||||||
onClick={() => {
|
<Tooltip>
|
||||||
if (isConfirming && props.onClick) {
|
<TooltipTrigger asChild>
|
||||||
props.onClick();
|
<PopoverTrigger asChild>
|
||||||
} else {
|
<span
|
||||||
setIsConfirming(true);
|
className={cn(
|
||||||
}
|
"inline-flex",
|
||||||
}}
|
props.button.main.disabled ? "cursor-not-allowed" : ""
|
||||||
variant={props.variant ? props.variant : "default"}
|
)}
|
||||||
className={props.className}
|
role="button">
|
||||||
>
|
<Button
|
||||||
{props.isPending && <Loader2 className="animate-spin mr-4" size={16} />}
|
disabled={!!props.button.main.disabled}
|
||||||
{isConfirming ? "Are you sure ?" : `${props.text}`}
|
variant={props.button.main.variant ?? "default"}
|
||||||
<>{props.icon ? props.icon : null}</>
|
size={props.button.main.size ?? "default"}
|
||||||
</Button>
|
className={props.button.main.className}
|
||||||
|
onClick={() => {
|
||||||
|
if (!props.button.main.disabled) setIsConfirming(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.button.main.icon}
|
||||||
|
{props.button.main.text && <span>{props.button.main.text}</span>}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{props.button.main.tooltipText && props.button.main.disabled && (
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{props.button.main.tooltipText}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
<PopoverContent className="w-80">
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium leading-none">{props.title}</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">{props.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (isConfirming && props.button.confirm.onClick) {
|
||||||
|
props.button.confirm.onClick();
|
||||||
|
setIsConfirming(false);
|
||||||
|
} else {
|
||||||
|
setIsConfirming(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
variant={props.button.confirm.variant ? props.button.confirm.variant : "default"}
|
||||||
|
size={props.button.main.size ?? "default"}
|
||||||
|
className={cn(props.button.main.className, "w-full")}
|
||||||
|
>
|
||||||
|
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
|
||||||
|
{props.button.confirm.icon ? props.button.confirm.icon : null}
|
||||||
|
<span>{props.button.confirm.text}</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={props.button.cancel.variant ? props.button.cancel.variant : "default"}
|
||||||
|
onClick={props.button.cancel.onClick ? () => props.button.cancel.onClick!() : () => setIsConfirming(false)}
|
||||||
|
className={cn(props.button.main.className, "w-full")}
|
||||||
|
size={props.button.main.size ?? "default"}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function CardsWithPagination<T>(props: CardsWithPaginationProps<T>) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<PaginationNavigation
|
<PaginationNavigation
|
||||||
className="justify-end"
|
className="justify-end mt-4"
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
goToPage={goToPage}
|
goToPage={goToPage}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
|
|
||||||
import {Check, ChevronDown} from "lucide-react";
|
import {Check, ChevronDown} from "lucide-react";
|
||||||
|
|
||||||
import {cn} from "@/lib/utils";
|
import {cn} from "@/lib/utils";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList} from "@/components/ui/command";
|
import {Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList} from "@/components/ui/command";
|
||||||
@@ -110,7 +108,6 @@ export type comboBoxFormItemProps = comboBoxProps & {
|
|||||||
onChange: any;
|
onChange: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Combobox to use when working with zodForm. */
|
|
||||||
export function ComboBoxFormItem(props: comboBoxFormItemProps) {
|
export function ComboBoxFormItem(props: comboBoxFormItemProps) {
|
||||||
const {values: choices, searchField = false, value, name, onChange} = props;
|
const {values: choices, searchField = false, value, name, onChange} = props;
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ export type ConnectionCircleProps = {
|
|||||||
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) {
|
if (date instanceof Date && !isNaN(date.getTime())) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const timestamp = new Date(date).getTime();
|
const timestamp = date.getTime();
|
||||||
const interval = now - timestamp;
|
const interval = now - timestamp;
|
||||||
|
|
||||||
|
console.log({ now, timestamp, interval });
|
||||||
|
|
||||||
if (interval < 10000) {
|
if (interval < 10000) {
|
||||||
style = "bg-green-400 border-green-600";
|
style = "bg-green-400 border-green-600";
|
||||||
} else if (interval <= 20000) {
|
} else if (interval <= 20000) {
|
||||||
@@ -19,7 +21,11 @@ export const ConnectionCircle = ({ date }: ConnectionCircleProps) => {
|
|||||||
} else {
|
} else {
|
||||||
style = "bg-red-400 border-red-600";
|
style = "bg-red-400 border-red-600";
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Invalid date passed to ConnectionCircle:", date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export function ConsoleSilencer() {
|
||||||
|
useEffect(() => {
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
for (const method of ["log", "debug", "info", "warn", "error"] as const) {
|
||||||
|
console[method] = () => {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import {cn} from "@/lib/utils";
|
||||||
|
import {Plus} from "lucide-react";
|
||||||
|
|
||||||
|
type EmptyStatePlaceholderProps = {
|
||||||
|
url: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const EmptyStatePlaceholder = ({url, text}: EmptyStatePlaceholderProps) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={url}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center justify-center w-full rounded-2xl border border-dashed border-muted p-6 lg:p-10",
|
||||||
|
"hover:bg-muted/50 transition-colors text-muted-foreground hover:text-primary text-center space-y-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Plus className="w-5 h-5 lg:w-6 lg:h-6"/>
|
||||||
|
<span className="text-sm lg:text-base font-medium">{text}</span>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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,5 +1,4 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
flexRender,
|
flexRender,
|
||||||
@@ -12,12 +11,14 @@ import {
|
|||||||
getFilteredRowModel,
|
getFilteredRowModel,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table";
|
||||||
import { Input } from "@/components/ui/input";
|
import {Input} from "@/components/ui/input";
|
||||||
|
|
||||||
import { useState } from "react";
|
import {ReactNode, useEffect, useState} from "react";
|
||||||
import { TablePagination } from "./table-pagination";
|
import {TablePagination} from "./table-pagination";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import {Checkbox} from "@/components/ui/checkbox";
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
import {useRouter} from "next/navigation";
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
@@ -34,37 +35,54 @@ interface DataTableProps<TData, TValue> {
|
|||||||
title?: string;
|
title?: string;
|
||||||
key: string;
|
key: string;
|
||||||
};
|
};
|
||||||
emptyText?: string;
|
emptyButton?: {
|
||||||
|
text: string;
|
||||||
|
variant: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
highlightRow?: (row: TData) => boolean;
|
||||||
|
selectedActions?: (rows: TData[]) => ReactNode;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable<TData, TValue>({
|
export function DataTable<TData, TValue>({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
enableFilter = false,
|
enableFilter = false,
|
||||||
enablePagination = true,
|
enablePagination = true,
|
||||||
enableSelect = true,
|
enableSelect = true,
|
||||||
paginationOptions = { pageSize: [10, 20, 30, 40, 50, 100], pageVisible: 3 },
|
paginationOptions = {pageSize: [10, 20, 30, 40, 50, 100], pageVisible: 3},
|
||||||
filterOptions = { key: "id", title: "Filter by ID" },
|
filterOptions = {key: "id", title: "Filter by ID"},
|
||||||
emptyText = "No data.",
|
emptyButton,
|
||||||
}: DataTableProps<TData, TValue>) {
|
highlightRow,
|
||||||
|
selectedActions,
|
||||||
|
|
||||||
|
|
||||||
|
}: DataTableProps<TData, TValue>) {
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
const [rowSelection, setRowSelection] = useState({});
|
const [rowSelection, setRowSelection] = useState({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRowSelection({});
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
if (enableSelect && data.length > 0) {
|
if (enableSelect && data.length > 0) {
|
||||||
const selectColumnExists = columns.some((column) => column.id === "select");
|
const selectColumnExists = columns.some((column) => column.id === "select");
|
||||||
|
|
||||||
if (!selectColumnExists) {
|
if (!selectColumnExists) {
|
||||||
columns.unshift({
|
columns.unshift({
|
||||||
id: "select",
|
id: "select",
|
||||||
header: ({ table }) => (
|
header: ({table}) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
|
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
|
||||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||||
aria-label="Select all"
|
aria-label="Select all"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => <Checkbox checked={row.getIsSelected()} onCheckedChange={(value) => row.toggleSelected(!!value)} aria-label="Select row" />,
|
cell: ({row}) => <Checkbox checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
aria-label="Select row"/>,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
});
|
});
|
||||||
@@ -87,68 +105,106 @@ export function DataTable<TData, TValue>({
|
|||||||
rowSelection,
|
rowSelection,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div
|
||||||
{enableFilter && (
|
className="flex flex-col h-full"
|
||||||
|
>
|
||||||
|
{enableFilter || selectedActions && (
|
||||||
<div className="flex items-center py-4">
|
<div className="flex items-center py-4">
|
||||||
<Input
|
{enableFilter && (
|
||||||
placeholder={`${filterOptions.title ?? `Filter by ${filterOptions.key}`}`}
|
<Input
|
||||||
value={(table.getColumn(filterOptions.key)?.getFilterValue() as string) ?? ""}
|
placeholder={`${filterOptions.title ?? `Filter by ${filterOptions.key}`}`}
|
||||||
onChange={(event) => table.getColumn(filterOptions.key)?.setFilterValue(event.target.value)}
|
value={(table.getColumn(filterOptions.key)?.getFilterValue() as string) ?? ""}
|
||||||
className="max-w-sm"
|
onChange={(event) => table.getColumn(filterOptions.key)?.setFilterValue(event.target.value)}
|
||||||
/>
|
className="max-w-sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/*{selectedActions && table.getSelectedRowModel().rows.length > 0 && (*/}
|
||||||
|
{selectedActions && (
|
||||||
|
selectedActions(table.getSelectedRowModel().rows.map(row => row.original))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="rounded-md border w-full">
|
<div className="flex flex-col justify-between h-full">
|
||||||
<Table className="w-full">
|
<div className="rounded-md border w-full">
|
||||||
<TableHeader>
|
<Table className="w-full">
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
<TableHeader>
|
||||||
<TableRow key={headerGroup.id}>
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
{headerGroup.headers.map((header) => {
|
<TableRow key={headerGroup.id}>
|
||||||
return (
|
{headerGroup.headers.map((header) => {
|
||||||
<TableHead key={header.id}>
|
return (
|
||||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
<TableHead key={header.id}>
|
||||||
</TableHead>
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
);
|
|
||||||
})}
|
</TableHead>
|
||||||
</TableRow>
|
);
|
||||||
))}
|
})}
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{table.getRowModel().rows?.length ? (
|
|
||||||
table.getRowModel().rows.map((row) => (
|
|
||||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
|
||||||
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))}
|
||||||
) : (
|
</TableHeader>
|
||||||
<TableRow>
|
<TableBody>
|
||||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
{table.getRowModel().rows?.length ? (
|
||||||
{emptyText}
|
table.getRowModel().rows.map((row) => (
|
||||||
</TableCell>
|
<TableRow key={row.id}
|
||||||
</TableRow>
|
className={highlightRow && highlightRow(row.original) ? "bg-gray-100 pointer-events-none" : ""}
|
||||||
)}
|
data-state={row.getIsSelected() && "selected"}
|
||||||
</TableBody>
|
>
|
||||||
</Table>
|
{row.getVisibleCells().map((cell) => (
|
||||||
</div>
|
<TableCell
|
||||||
<div className="flex items-center justify-end space-x-2 py-4 mt-6">
|
key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
|
||||||
{enableSelect && (
|
))}
|
||||||
<div className="flex-1 text-sm text-muted-foreground">
|
</TableRow>
|
||||||
{table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s) selected.
|
))
|
||||||
</div>
|
) : (
|
||||||
)}
|
<TableRow>
|
||||||
{enablePagination && (
|
<TableCell colSpan={columns.length} className="p-0">
|
||||||
<TablePagination
|
{emptyButton ? (
|
||||||
table={table}
|
<Button
|
||||||
maxVisiblePages={paginationOptions?.pageVisible}
|
variant={emptyButton.variant}
|
||||||
pageSizeOptions={paginationOptions.pageSize}
|
className="btn btn-primary cursor-pointer w-full rounded-tr-none rounded-tl-none py-12 text-lg font-bold"
|
||||||
className={paginationOptions.className}
|
onClick={() => {
|
||||||
/>
|
router.push(emptyButton.path)
|
||||||
)}
|
}}
|
||||||
|
>
|
||||||
|
<span>{emptyButton.text}</span>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center py-12 text-lg font-bold">
|
||||||
|
<span className="text-lg font-bold">No data available</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end space-x-2 py-4 mt-6">
|
||||||
|
{enableSelect && table.getFilteredRowModel().rows.length >= 1 && (
|
||||||
|
<div className="flex-1 text-sm text-muted-foreground">
|
||||||
|
{table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s)
|
||||||
|
selected.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{enablePagination && table.getFilteredRowModel().rows.length >= 1 && (
|
||||||
|
<TablePagination
|
||||||
|
table={table}
|
||||||
|
maxVisiblePages={paginationOptions?.pageVisible}
|
||||||
|
pageSizeOptions={(() => {
|
||||||
|
const rowCount = table.getFilteredRowModel().rows.length;
|
||||||
|
const allSizes = paginationOptions.pageSize.sort((a, b) => a - b);
|
||||||
|
const validSizes = allSizes.filter((size) => size <= rowCount);
|
||||||
|
const nextSize = allSizes.find((size) => size > rowCount);
|
||||||
|
if (nextSize) validSizes.push(nextSize);
|
||||||
|
return validSizes;
|
||||||
|
})()}
|
||||||
|
className={paginationOptions.className}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
|
||||||
|
import {Toggle} from "@/components/ui/toggle";
|
||||||
|
import {CheckIcon, Filter, RefreshCcw} from "lucide-react";
|
||||||
|
import {Badge} from "@/components/ui/badge";
|
||||||
|
|
||||||
|
export type FilterItem = {
|
||||||
|
label: string,
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FiltersDropdownProps = {
|
||||||
|
items: FilterItem[];
|
||||||
|
selectedItems: FilterItem[];
|
||||||
|
onSelect: (item: FilterItem) => void;
|
||||||
|
clearFilters: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FiltersDropdown = ({items, selectedItems, onSelect, clearFilters}: FiltersDropdownProps) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<Toggle variant="outline" size="sm" className="cursor-pointer w-fit">
|
||||||
|
<Filter className="h-4 w-4"/>
|
||||||
|
</Toggle>
|
||||||
|
{selectedItems.length > 0 && (
|
||||||
|
<Badge className="absolute -top-2 -right-2 h-4 w-4 rounded-full p-0 flex items-center justify-center text-[10px]">
|
||||||
|
{selectedItems.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-[150px]">
|
||||||
|
{items.map((item, index) => {
|
||||||
|
const isSelected = selectedItems.some(f => f.value === item.value);
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSelect(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
{isSelected && <CheckIcon className="h-4 w-4 text-blue-500"/>}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<DropdownMenuItem
|
||||||
|
disabled={selectedItems.length === 0}
|
||||||
|
className="flex gap-2 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
clearFilters();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCcw className='h-4 w-4'/>
|
||||||
|
Clear filters
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import { userAction } from "@/safe-actions";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.schema";
|
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
import {userAction} from "@/lib/safe-actions/actions";
|
||||||
|
|
||||||
export const updateEmailSettingsAction = userAction
|
export const updateEmailSettingsAction = userAction
|
||||||
.schema(
|
.schema(
|
||||||
@@ -1,18 +1,31 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import {Card, CardContent} from "@/components/ui/card";
|
||||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
import {
|
||||||
import { Input } from "@/components/ui/input";
|
FormControl,
|
||||||
import { Form } from "@/components/ui/form";
|
FormDescription,
|
||||||
import { Button } from "@/components/ui/button";
|
FormField,
|
||||||
import { useMutation } from "@tanstack/react-query";
|
FormItem,
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
useZodForm
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import {Input} from "@/components/ui/input";
|
||||||
|
import {Form} from "@/components/ui/form";
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
import {useMutation} from "@tanstack/react-query";
|
||||||
|
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
import { EmailFormSchema, EmailFormType } from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.schema";
|
import {
|
||||||
import { PasswordInput } from "@/components/wrappers/auth/PaswordInput/password-input";
|
EmailFormSchema,
|
||||||
import { updateEmailSettingsAction } from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.action";
|
EmailFormType
|
||||||
import { toast } from "sonner";
|
} from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema";
|
||||||
import { useRouter } from "next/navigation";
|
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
|
||||||
|
import {
|
||||||
|
updateEmailSettingsAction
|
||||||
|
} from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.action";
|
||||||
|
import {toast} from "sonner";
|
||||||
|
import {useRouter} from "next/navigation";
|
||||||
|
|
||||||
export type EmailFormProps = {
|
export type EmailFormProps = {
|
||||||
defaultValues?: EmailFormType;
|
defaultValues?: EmailFormType;
|
||||||
@@ -27,7 +40,7 @@ export const EmailForm = (props: EmailFormProps) => {
|
|||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async (values: EmailFormType) => {
|
mutationFn: async (values: EmailFormType) => {
|
||||||
const updateEmailSettings = await updateEmailSettingsAction({ name: "system", data: values });
|
const updateEmailSettings = await updateEmailSettingsAction({name: "system", data: values});
|
||||||
const data = updateEmailSettings?.data?.data;
|
const data = updateEmailSettings?.data?.data;
|
||||||
if (updateEmailSettings?.serverError || !data) {
|
if (updateEmailSettings?.serverError || !data) {
|
||||||
toast.error(updateEmailSettings?.serverError);
|
toast.error(updateEmailSettings?.serverError);
|
||||||
@@ -53,14 +66,14 @@ export const EmailForm = (props: EmailFormProps) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="smtpFrom"
|
name="smtpFrom"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field }) => (
|
render={({field}) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>From Email *</FormLabel>
|
<FormLabel>From Email *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={"exemple@portabase.com"} {...field} />
|
<Input placeholder={"exemple@portabase.com"} {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>{"The email from where the email will be send"}</FormDescription>
|
<FormDescription>{"The email from where the email will be send"}</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -68,14 +81,14 @@ export const EmailForm = (props: EmailFormProps) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="smtpHost"
|
name="smtpHost"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field }) => (
|
render={({field}) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Server Host *</FormLabel>
|
<FormLabel>Server Host *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={"ssl0.ovh.net"} {...field} />
|
<Input placeholder={"ssl0.ovh.net"} {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>{"Your email server host"}</FormDescription>
|
<FormDescription>{"Your email server host"}</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -83,14 +96,14 @@ export const EmailForm = (props: EmailFormProps) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="smtpPort"
|
name="smtpPort"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field }) => (
|
render={({field}) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Server Port *</FormLabel>
|
<FormLabel>Server Port *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={"465"} {...field} />
|
<Input placeholder={"465"} {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>{"Your email server port (send)"}</FormDescription>
|
<FormDescription>{"Your email server port (send)"}</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -99,7 +112,7 @@ export const EmailForm = (props: EmailFormProps) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="smtpPassword"
|
name="smtpPassword"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field }) => (
|
render={({field}) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -107,7 +120,7 @@ export const EmailForm = (props: EmailFormProps) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>{"Your email server password"}</FormDescription>
|
<FormDescription>{"Your email server password"}</FormDescription>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -115,14 +128,14 @@ export const EmailForm = (props: EmailFormProps) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="smtpUser"
|
name="smtpUser"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field }) => (
|
render={({field}) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>User Email *</FormLabel>
|
<FormLabel>User Email *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={"exemple@portabase.com"} {...field} />
|
<Input placeholder={"exemple@portabase.com"} {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>{"The email server user"}</FormDescription>
|
<FormDescription>{"The email server user"}</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { EmailForm } from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/EmailForm";
|
import { EmailForm } from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form";
|
||||||
import { Send } from "lucide-react";
|
import { Send } from "lucide-react";
|
||||||
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";
|
||||||
@@ -6,7 +6,8 @@ import { sendEmail } from "@/utils/email-helper";
|
|||||||
import TestEmailSettings from "../../../../../../emails/TestEmailSettings";
|
import TestEmailSettings from "../../../../../../emails/TestEmailSettings";
|
||||||
import { render } from "@react-email/render";
|
import { render } from "@react-email/render";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {Setting} from "@/db/schema/00_setting";
|
import {Setting} from "@/db/schema/01_setting";
|
||||||
|
import {EmailFormType} from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema";
|
||||||
|
|
||||||
export type SettingsEmailTabProps = {
|
export type SettingsEmailTabProps = {
|
||||||
settings: Setting;
|
settings: Setting;
|
||||||
@@ -53,7 +54,7 @@ export const SettingsEmailTab = (props: SettingsEmailTabProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings : null} />
|
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings as EmailFormType : undefined } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
||||||
import { Info, ShieldCheck } from "lucide-react";
|
import {Info, ShieldCheck} from "lucide-react";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import {Switch} from "@/components/ui/switch";
|
||||||
import { Label } from "@/components/ui/label";
|
import {Label} from "@/components/ui/label";
|
||||||
import { StorageS3Form } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/StorageS3Form";
|
import {StorageS3Form} from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/storage-s3-form";
|
||||||
import { useState } from "react";
|
import {useState} from "react";
|
||||||
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 { checkConnexionToS3 } from "@/features/upload/public/upload.action";
|
import {checkConnexionToS3} from "@/features/upload/public/upload.action";
|
||||||
import { toast } from "sonner";
|
import {toast} from "sonner";
|
||||||
import { useRouter } from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import { updateStorageSettingsAction } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.action";
|
import {
|
||||||
import {Setting} from "@/db/schema/00_setting";
|
updateStorageSettingsAction
|
||||||
|
} from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.action";
|
||||||
|
import {Setting} from "@/db/schema/01_setting";
|
||||||
|
import {S3FormType} from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.schema";
|
||||||
|
|
||||||
export type SettingsStorageTabProps = {
|
export type SettingsStorageTabProps = {
|
||||||
settings: Setting;
|
settings: Setting;
|
||||||
@@ -33,7 +36,7 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
|||||||
const [isSwitched, setIsSwitched] = useState<boolean>(props.settings.storage !== "local");
|
const [isSwitched, setIsSwitched] = useState<boolean>(props.settings.storage !== "local");
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: () => updateStorageSettingsAction({ name: "system", data: { storage: isSwitched ? "s3" : "local" } }),
|
mutationFn: () => updateStorageSettingsAction({name: "system", data: {storage: isSwitched ? "s3" : "local"}}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(`Settings updated successfully.`);
|
toast.success(`Settings updated successfully.`);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
@@ -48,14 +51,25 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
|||||||
await updateMutation.mutateAsync();
|
await updateMutation.mutateAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extractS3FormValues = (settings: Setting): S3FormType | undefined => {
|
||||||
|
if (!settings.s3EndPointUrl) return undefined;
|
||||||
|
return {
|
||||||
|
s3EndPointUrl: settings.s3EndPointUrl,
|
||||||
|
s3AccessKeyId: settings.s3AccessKeyId!,
|
||||||
|
s3SecretAccessKey: settings.s3SecretAccessKey!,
|
||||||
|
S3BucketName: settings.S3BucketName!,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full py-4">
|
<div className="flex flex-col h-full py-4">
|
||||||
<h1>Settings for Portabase storage</h1>
|
<h1>Settings for Portabase storage</h1>
|
||||||
<Alert className="mt-3">
|
<Alert className="mt-3">
|
||||||
<Info className="h-4 w-4" />
|
<Info className="h-4 w-4"/>
|
||||||
<AlertTitle>Informations</AlertTitle>
|
<AlertTitle>Informations</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Actually you can only store you data in one place : s3 compatible or in local. For exemple you cannot choose to store images in one place
|
Actually you can only store you data in one place : s3 compatible or in local. For exemple you
|
||||||
|
cannot choose to store images in one place
|
||||||
and .dump files in another.
|
and .dump files in another.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
@@ -79,14 +93,14 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutation.mutateAsync();
|
await mutation.mutateAsync();
|
||||||
}}
|
}}
|
||||||
icon={<ShieldCheck />}
|
icon={<ShieldCheck/>}
|
||||||
text="Test connexion"
|
text="Test connexion"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isSwitched && (
|
{isSwitched && (
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<StorageS3Form defaultValues={props.settings.s3EndPointUrl ? props.settings : null} />
|
<StorageS3Form defaultValues={extractS3FormValues(props.settings)}/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { userAction } from "@/safe-actions";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { S3FormSchema, StorageSwitchSchema } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.schema";
|
import { S3FormSchema, StorageSwitchSchema } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
import {userAction} from "@/lib/safe-actions/actions";
|
||||||
|
|
||||||
export const updateS3SettingsAction = userAction
|
export const updateS3SettingsAction = userAction
|
||||||
.schema(
|
.schema(
|
||||||
@@ -8,10 +8,11 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
import { S3FormSchema, S3FormType } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.schema";
|
import { S3FormSchema, S3FormType } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.schema";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.action";
|
import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.action";
|
||||||
|
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
|
||||||
|
|
||||||
export type S3FormProps = {
|
export type S3FormProps = {
|
||||||
defaultValues?: S3FormType;
|
defaultValues?: S3FormType;
|
||||||
@@ -86,7 +87,7 @@ export const StorageS3Form = (props: S3FormProps) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Secret Key *</FormLabel>
|
<FormLabel>Secret Key *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={"The secret key token"} {...field} />
|
<PasswordInput placeholder={"The secret key token"} {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>{"Add your secret key"}</FormDescription>
|
<FormDescription>{"Add your secret key"}</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -1,42 +1,56 @@
|
|||||||
"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/AdminEmailTab/SettingsEmailTab";
|
import {SettingsEmailTab} from "@/components/wrappers/dashboard/admin/admin-email-tab/settings-email-tab";
|
||||||
import { SettingsStorageTab } from "@/components/wrappers/dashboard/admin/AdminStorageTab/SettingsStorageTab";
|
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/02_user";
|
||||||
import { User } from "@/db/schema/01_user";
|
import {Setting} from "@/db/schema/01_setting";
|
||||||
import {Setting} from "@/db/schema/00_setting";
|
import {useEffect, useState} from "react";
|
||||||
|
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 = (props: AdminTabsProps) => {
|
export const AdminTabs = ({users, settings}: AdminTabsProps) => {
|
||||||
const { users, settings } = props;
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const [tab, setTab] = useState<string>(() => searchParams.get("tab") ?? "users");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newTab = searchParams.get("tab") ?? "users";
|
||||||
|
setTab(newTab);
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
const handleChangeTab = (value: string) => {
|
||||||
|
router.push(`?tab=${value}`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue="users">
|
<Tabs className="h-full" value={tab} onValueChange={handleChangeTab}>
|
||||||
<TabsList className="w-full">
|
<TabsList className="w-full">
|
||||||
<TabsTrigger className="w-full " value="users">
|
<TabsTrigger className="w-full" value="users">
|
||||||
Users
|
Users
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger className="w-full " value="email">
|
<TabsTrigger className="w-full" value="email">
|
||||||
Email
|
Email
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger className="w-full " value="storage">
|
<TabsTrigger className="w-full" value="storage">
|
||||||
Storage
|
Storage
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</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/02_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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,25 +1,30 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import {ColumnDef} from "@tanstack/react-table";
|
import {ColumnDef} from "@tanstack/react-table";
|
||||||
import {Badge} from "@/components/ui/badge";
|
import {Badge} from "@/components/ui/badge";
|
||||||
import {updateUserAction} from "@/components/wrappers/dashboard/profile/UserForm/user-form.action";
|
import {updateUserAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
|
||||||
import {useMutation} from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {Trash2} from "lucide-react";
|
import {Trash2} from "lucide-react";
|
||||||
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/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/02_user";
|
||||||
import {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 isSuperAdmin = session?.user.role == "superadmin";
|
||||||
|
const isCurrentUser = session?.user.email === row.original.email;
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: () => updateUserAction({id: row.original.id, data: {role: role}}),
|
mutationFn: () => updateUserAction({id: row.original.id, data: {role: role}}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -36,8 +41,20 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
|
|||||||
await updateMutation.mutateAsync();
|
await updateMutation.mutateAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (isPending) return null;
|
||||||
|
|
||||||
|
if (error || !session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge className="cursor-pointer" onClick={() => handleUpdateRole()} variant="outline">
|
<Badge
|
||||||
|
className={isCurrentUser || !isSuperAdmin ? "cursor-not-allowed opacity-50" : "cursor-pointer"}
|
||||||
|
onClick={isCurrentUser || !isSuperAdmin ? undefined : () => handleUpdateRole()}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
{role}
|
{role}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
@@ -51,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",
|
||||||
@@ -64,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),
|
||||||
@@ -73,23 +106,18 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{!session || session?.user.email === row.original.email ? null :
|
<ButtonWithLoading
|
||||||
<ButtonWithLoading
|
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="icon"
|
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">
|
|
||||||
<DataTable columns={usersColumnsAdmin} data={users} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
"use server";
|
|
||||||
import { ActionError, userAction } from "@/safe-actions";
|
|
||||||
import { AgentSchema } from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.schema";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { eq, and, ne, count } from "drizzle-orm";
|
|
||||||
import { db } from "@/db";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
|
|
||||||
const verifySlugUniqueness = async (slug: string, agentId?: string) => {
|
|
||||||
const conditions = agentId ? and(eq(drizzleDb.schemas.agent.slug, slug), ne(drizzleDb.schemas.agent.id, agentId)) : eq(drizzleDb.schemas.agent.slug, slug);
|
|
||||||
|
|
||||||
const [countResult] = await db.select({ count: count() }).from(drizzleDb.schemas.agent).where(conditions);
|
|
||||||
|
|
||||||
if (countResult.count > 0) {
|
|
||||||
throw new ActionError("Slug already exists");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createAgentAction = userAction.schema(AgentSchema).action(async ({ parsedInput }) => {
|
|
||||||
await verifySlugUniqueness(parsedInput.slug);
|
|
||||||
|
|
||||||
const [createdAgent] = await db.insert(drizzleDb.schemas.agent).values(parsedInput).returning();
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: createdAgent,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateAgentAction = userAction
|
|
||||||
.schema(
|
|
||||||
z.object({
|
|
||||||
id: z.string(),
|
|
||||||
data: AgentSchema,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.action(async ({ parsedInput }) => {
|
|
||||||
await verifySlugUniqueness(parsedInput.data.slug, parsedInput.id);
|
|
||||||
|
|
||||||
const [updatedAgent] = await db.update(drizzleDb.schemas.agent).set(parsedInput.data).where(eq(drizzleDb.schemas.agent.id, parsedInput.id)).returning();
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: updatedAgent,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import {generateEdgeKey} from "@/utils/edge_key";
|
import {generateEdgeKey} from "@/utils/edge_key";
|
||||||
import {getServerUrl} from "@/utils/get-server-url";
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
import {PasswordInput} from "@/components/wrappers/auth/PaswordInput/password-input";
|
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {CopyButton} from "@/components/wrappers/common/button/copy-button";
|
import {CopyButton} from "@/components/wrappers/common/button/copy-button";
|
||||||
import {Agent} from "@/db/schema/07_agent";
|
import {Agent} from "@/db/schema/08_agent";
|
||||||
|
|
||||||
export type AgentCardKeyProps = {
|
export type AgentCardKeyProps = {
|
||||||
agent: Agent;
|
agent: Agent;
|
||||||
@@ -4,7 +4,7 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { formatDateLastContact } from "@/utils/date-formatting";
|
import { formatDateLastContact } from "@/utils/date-formatting";
|
||||||
import { ConnectionCircle } from "@/components/wrappers/common/connection-circle";
|
import { ConnectionCircle } from "@/components/wrappers/common/connection-circle";
|
||||||
import {Agent} from "@/db/schema/07_agent";
|
import {Agent} from "@/db/schema/08_agent";
|
||||||
|
|
||||||
export type agentCardProps = {
|
export type agentCardProps = {
|
||||||
data: Agent;
|
data: Agent;
|
||||||
@@ -14,13 +14,13 @@ export const AgentCard = (props: agentCardProps) => {
|
|||||||
const { data: agent } = props;
|
const { data: agent } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/dashboard/agents/${agent.id}`}>
|
<Link href={`/dashboard/agents/${agent.id}`} className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md">
|
||||||
<Card className="flex flex-row justify-between">
|
<Card className="flex flex-row justify-between">
|
||||||
<div className="">
|
<div className="flex-1 text-left">
|
||||||
<CardHeader>{agent.name}</CardHeader>
|
<CardHeader className="text-2xl font-bold">{agent.name}</CardHeader>
|
||||||
<CardContent>Last contact : {formatDateLastContact(agent.lastContact)}</CardContent>
|
<CardContent>Last contact: {formatDateLastContact(agent.lastContact)}</CardContent>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 mr-3">
|
<div className="flex items-center px-4">
|
||||||
<ConnectionCircle date={agent.lastContact} />
|
<ConnectionCircle date={agent.lastContact} />
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"use server";
|
||||||
|
import {ActionError, userAction} from "@/lib/safe-actions/actions";
|
||||||
|
import {AgentSchema} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.schema";
|
||||||
|
import {z} from "zod";
|
||||||
|
import {eq, and, ne, count} from "drizzle-orm";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {slugify} from "@/utils/slugify";
|
||||||
|
|
||||||
|
const verifySlugUniqueness = async (slug: string, agentId?: string) => {
|
||||||
|
const conditions = agentId ? and(eq(drizzleDb.schemas.agent.slug, slug), ne(drizzleDb.schemas.agent.id, agentId)) : eq(drizzleDb.schemas.agent.slug, slug);
|
||||||
|
|
||||||
|
const [countResult] = await db.select({count: count()}).from(drizzleDb.schemas.agent).where(conditions);
|
||||||
|
|
||||||
|
if (countResult.count > 0) {
|
||||||
|
throw new ActionError("Slug already exists");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createAgentAction = userAction.schema(AgentSchema).action(async ({parsedInput}) => {
|
||||||
|
const slug = slugify(parsedInput.name);
|
||||||
|
await verifySlugUniqueness(slug);
|
||||||
|
const [createdAgent] = await db.insert(drizzleDb.schemas.agent).values({...parsedInput, slug: slug}).returning();
|
||||||
|
return {
|
||||||
|
data: createdAgent,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateAgentAction = userAction
|
||||||
|
.schema(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
data: AgentSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.action(async ({parsedInput}) => {
|
||||||
|
const slug = slugify(parsedInput.data.name);
|
||||||
|
await verifySlugUniqueness(slug, parsedInput.id);
|
||||||
|
|
||||||
|
const [updatedAgent] = await db.update(drizzleDb.schemas.agent).set({
|
||||||
|
...parsedInput.data,
|
||||||
|
slug: slug
|
||||||
|
}).where(eq(drizzleDb.schemas.agent.id, parsedInput.id)).returning();
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: updatedAgent,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -2,11 +2,6 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export const AgentSchema = z.object({
|
export const AgentSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
slug: z
|
|
||||||
.string()
|
|
||||||
.regex(/^[a-zA-Z0-9_-]*$/)
|
|
||||||
.min(5)
|
|
||||||
.max(25),
|
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1,16 +1,24 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import {Card, CardContent} from "@/components/ui/card";
|
||||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
import {
|
||||||
import { Input } from "@/components/ui/input";
|
FormControl,
|
||||||
import { Form } from "@/components/ui/form";
|
FormDescription,
|
||||||
import { Button } from "@/components/ui/button";
|
FormField,
|
||||||
import { useRouter } from "next/navigation";
|
FormItem,
|
||||||
import { useMutation } from "@tanstack/react-query";
|
FormLabel,
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
FormMessage,
|
||||||
import { AgentSchema, AgentType } from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.schema";
|
useZodForm
|
||||||
import { toast } from "sonner";
|
} from "@/components/ui/form";
|
||||||
import { createAgentAction, updateAgentAction } from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.action";
|
import {Input} from "@/components/ui/input";
|
||||||
|
import {Form} from "@/components/ui/form";
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
import {useRouter} from "next/navigation";
|
||||||
|
import {useMutation} from "@tanstack/react-query";
|
||||||
|
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||||
|
import {AgentSchema, AgentType} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.schema";
|
||||||
|
import {toast} from "sonner";
|
||||||
|
import {createAgentAction, updateAgentAction} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.action";
|
||||||
|
|
||||||
export type agentFormProps = {
|
export type agentFormProps = {
|
||||||
defaultValues?: AgentType;
|
defaultValues?: AgentType;
|
||||||
@@ -29,14 +37,13 @@ export const AgentForm = (props: agentFormProps) => {
|
|||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async (values: AgentType) => {
|
mutationFn: async (values: AgentType) => {
|
||||||
console.log("values", values);
|
|
||||||
|
|
||||||
const createAgent = isCreate
|
const createAgent = isCreate
|
||||||
? await createAgentAction(values)
|
? await createAgentAction(values)
|
||||||
: await updateAgentAction({
|
: await updateAgentAction({
|
||||||
id: props.agentId ?? "-",
|
id: props.agentId ?? "-",
|
||||||
data: values,
|
data: values,
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = createAgent?.data?.data;
|
const data = createAgent?.data?.data;
|
||||||
if (createAgent?.serverError || !data) {
|
if (createAgent?.serverError || !data) {
|
||||||
@@ -44,7 +51,7 @@ export const AgentForm = (props: agentFormProps) => {
|
|||||||
toast.error(createAgent?.serverError);
|
toast.error(createAgent?.serverError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toast.success(`Success`);
|
toast.success(`Success ${isCreate ? "creating" : "updating"} agent`);
|
||||||
router.push(`/dashboard/agents/${data.id}`);
|
router.push(`/dashboard/agents/${data.id}`);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
},
|
},
|
||||||
@@ -65,37 +72,14 @@ export const AgentForm = (props: agentFormProps) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field }) => (
|
render={({field}) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Agent 1" {...field} />
|
<Input placeholder="Agent 1" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Your agent project name</FormDescription>
|
<FormDescription>Your agent project name</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage/>
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
defaultValue=""
|
|
||||||
name="slug"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Slug</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
//value={field.value ?? ""}
|
|
||||||
placeholder="agent-1"
|
|
||||||
{...field}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value.replaceAll(" ", "-").toLowerCase();
|
|
||||||
field.onChange(value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>The slug is used in the url of the agent</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -103,14 +87,15 @@ export const AgentForm = (props: agentFormProps) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
name="description"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({field}) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>Description</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="This agent is for the client exemple.com" {...field} value={field.value ?? ""} />
|
<Input placeholder="This agent is for the client exemple.com" {...field}
|
||||||
|
value={field.value ?? ""}/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Enter your project agent description</FormDescription>
|
<FormDescription>Enter your project agent description</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -7,8 +7,8 @@ import { generateEdgeKey } from "@/utils/edge_key";
|
|||||||
import { PropsWithChildren } from "react";
|
import { PropsWithChildren } from "react";
|
||||||
import { CopyButton } from "@/components/wrappers/common/button/copy-button";
|
import { CopyButton } from "@/components/wrappers/common/button/copy-button";
|
||||||
import { getServerUrl } from "@/utils/get-server-url";
|
import { getServerUrl } from "@/utils/get-server-url";
|
||||||
import { CodeSnippet } from "@/components/wrappers/code-snippet/CodeSnippet";
|
import { CodeSnippet } from "@/components/wrappers/code-snippet/code-snippet";
|
||||||
import {Agent} from "@/db/schema/07_agent";
|
import {Agent} from "@/db/schema/08_agent";
|
||||||
|
|
||||||
export type agentRegistrationDialogProps = PropsWithChildren<{
|
export type agentRegistrationDialogProps = PropsWithChildren<{
|
||||||
agent: Agent;
|
agent: Agent;
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {Trash2} from "lucide-react";
|
||||||
|
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||||
|
import {useMutation} from "@tanstack/react-query";
|
||||||
|
import {useRouter} from "next/navigation";
|
||||||
|
import {toast} from "sonner";
|
||||||
|
import {deleteAgentAction} from "@/components/wrappers/dashboard/agent/button-delete-agent/delete-agent.action";
|
||||||
|
|
||||||
|
export type ButtonDeleteAgentProps = {
|
||||||
|
text?: string;
|
||||||
|
agentId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ButtonDeleteAgent = (props: ButtonDeleteAgentProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: () => deleteAgentAction(props.agentId),
|
||||||
|
onSuccess: async (result: any) => {
|
||||||
|
if (result.data?.success) {
|
||||||
|
toast.success(result.data.actionSuccess.message);
|
||||||
|
router.push("/dashboard/agents");
|
||||||
|
} else {
|
||||||
|
toast.error(result.data.actionError.message || "Unknown error occurred.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonWithConfirm
|
||||||
|
title={props.text ? props.text : ""}
|
||||||
|
description="Are you sure you want to remove this agent? This action cannot be undone."
|
||||||
|
button={{
|
||||||
|
main: {
|
||||||
|
text: props.text ? props.text : "",
|
||||||
|
variant: "outline",
|
||||||
|
icon: <Trash2 color="red"/>,
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
className: "w-full",
|
||||||
|
text: "Delete",
|
||||||
|
icon: <Trash2/>,
|
||||||
|
variant: "destructive",
|
||||||
|
onClick: () => {
|
||||||
|
mutation.mutate();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
className: "w-full",
|
||||||
|
text: "Cancel",
|
||||||
|
icon: <Trash2/>,
|
||||||
|
variant: "outline",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
isPending={mutation.isPending}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import {z} from "zod";
|
||||||
|
import {v4 as uuidv4} from "uuid";
|
||||||
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {Agent} from "@/db/schema/08_agent";
|
||||||
|
import {userAction} from "@/lib/safe-actions/actions";
|
||||||
|
|
||||||
|
export const deleteAgentAction = userAction.schema(z.string()).action(async ({parsedInput}): Promise<ServerActionResult<Agent>> => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// const deletedAgent: Agent[] = await db.delete(drizzleDb.schemas.agent).where(eq(drizzleDb.schemas.agent.id, parsedInput)).returning();
|
||||||
|
|
||||||
|
const uuid = uuidv4();
|
||||||
|
|
||||||
|
const updatedAgent = await db
|
||||||
|
.update(drizzleDb.schemas.agent)
|
||||||
|
.set({
|
||||||
|
isArchived: true,
|
||||||
|
slug: uuid,
|
||||||
|
})
|
||||||
|
.where(eq(drizzleDb.schemas.agent.id, parsedInput))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
|
if (!updatedAgent[0]) {
|
||||||
|
throw new Error("Agent not found or update failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: updatedAgent[0],
|
||||||
|
actionSuccess: {
|
||||||
|
message: "Agent has been successfully deleted.",
|
||||||
|
messageParams: {projectId: parsedInput},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
actionError: {
|
||||||
|
message: "Failed to delete agent.",
|
||||||
|
status: 500,
|
||||||
|
cause: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
messageParams: {projectId: parsedInput},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||