migration

This commit is contained in:
Théo LAGACHE
2025-05-16 15:54:17 +02:00
parent 01019fe1a3
commit 6f2523e524
285 changed files with 15492 additions and 46090 deletions
+33
View File
@@ -0,0 +1,33 @@
# Environnement
NODE_ENV=production
# Base de données
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
# Projet
NEXT_PUBLIC_PROJECT_NAME="Portabase"
NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager"
NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io
PROJECT_SECRET=
# SMTP (email)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
# Google
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
# S3
S3_ENDPOINT=http://app.s3.portabase.io
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET_NAME=portabase
S3_PORT=9000
S3_USE_SSL=true
# Storage Type (s3, local)
STORAGE_TYPE=local
+3
View File
@@ -0,0 +1,3 @@
{
"WillLuke.nextjs.hasPrompted": true
}
+1 -1
View File
@@ -1,2 +1,2 @@
migrate: migrate:
docker compose run --rm app sh -c "npx prisma migrate dev" docker compose run --rm app
+157 -116
View File
@@ -1,21 +1,15 @@
<!-- PROJECT LOGO --> <br />
<div align="center">
<a href="https://portabase.io">
<img src="images/logo.png" alt="Logo" width="80" height="80">
</a>
[//]: # (<br />) <h3 align="center">Portabase</h3>
[//]: # (<div align="center">)
[//]: # ( <a href="https://github.com/othneildrew/Best-README-Template">)
[//]: # ( <img src="images/logo.png" alt="Logo" width="80" height="80">)
[//]: # ( </a>)
<h3 align="center">Portabase</h3>
<p align="center"> <p align="center">
Backup / Restore your databases instances Easily backup and restore your database instances with Portabase
<br /> <br />
<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://portabase.io">View Demo</a>
@@ -24,159 +18,206 @@
· ·
<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>
## 📚 Table of Contents
- [About The Project](#about-the-project)
- [Built With](#built-with)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Usage](#usage)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [License](#license)
- [Contact](#contact)
- [Acknowledgments](#acknowledgments)
<!-- TABLE OF CONTENTS --> ---
<details>
<summary>Table of Contents</summary>
<ol>
<li>
<a href="#about-the-project">About The Project</a>
<ul>
<li><a href="#built-with">Built With</a></li>
</ul>
</li>
<li>
<a href="#getting-started">Getting Started</a>
<ul>
<li><a href="#prerequisites">Prerequisites</a></li>
<li><a href="#installation">Installation</a></li>
</ul>
</li>
<li><a href="#usage">Usage</a></li>
<li><a href="#roadmap">Roadmap</a></li>
<li><a href="#contributing">Contributing</a></li>
<li><a href="#license">License</a></li>
<li><a href="#contact">Contact</a></li>
<li><a href="#acknowledgments">Acknowledgments</a></li>
</ol>
</details>
<!-- ABOUT THE PROJECT -->
## About The Project ## About The Project
Portabase is the server dashboard service working with portabase agents. **Portabase** is a server dashboard tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with Portabase agents for managing operations securely and efficiently.
<a href="https://github.com/Soluce-Technologies/portabase">see Portabase</a>
<p align="right">(<a href="#readme-top">back to top</a>)</p> GitHub Repository: [Portabase](https://github.com/Soluce-Technologies/portabase)
### 🔧 Built With
- [![NextJS][NextJS]][NextJS-url] (v15 with App Router)
- [![Drizzle][Drizzle]][Drizzle-url]
- [![ShadcnUI][ShadcnUI]][ShadcnUI-url]
- BetterAuth
- React Email
### Built With ---
* [![NextJS][NextJS]][NextJS-url] ## 🚀 Getting Started
* [![Prisma][Prisma]][Prisma-url]
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- GETTING STARTED -->
## Getting Started
### Prerequisites ### Prerequisites
Install docker on your machine Ensure Docker is installed on your machine before getting started.
### Installation ### Installation
Installation steps in order to use it locally and work on some features improvements To run Portabase locally:
1. Clone the repo 1. Clone the repository:
```sh ```bash
git clone https://github.com/Soluce-Technologies/portabase git clone https://github.com/Soluce-Technologies/portabase
``` cd portabase
3. Start the project ```
```sh 2. Start the development environment:
docker compose up ```bash
``` docker compose up
```
<p align="right">(<a href="#readme-top">back to top</a>)</p> ---
## 🛠️ Usage
<!-- ROADMAP --> Portabase provides a dashboard to manage database instances and backups. It supports:
## Roadmap
- [ ] Add Changelog - PostgreSQL (current)
- [ ] Improve documentation and repositories - MongoDB (coming soon)
- [ ] Add tests procedure - MySQL (coming soon)
- [ ] Add Release File
- [ ] Security improvements You can access the dashboard at `http://localhost:3000` after starting the project.
- [ ] Improve UX/UI
- [ ] Improve Workspace system ---
- [ ] Migrate to Drizzle ORM
- [ ] Multi locale support ## 🗺️ Roadmap
- [ ] Multi db support
- [ ] Add changelog
- [ ] Enhance documentation
- [ ] Implement testing procedures
- [ ] Include release file
- [ ] Improve security
- [ ] Enhance UX/UI
- [ ] Improve workspace management system
- [x] Migrate to Drizzle ORM
- [ ] Add multi-language support
- [ ] Extend multi-database support:
- [x] PostgreSQL - [x] PostgreSQL
- [ ] MongoDB - [ ] MongoDB
- [ ] MySQL - [ ] MySQL
See the [open issues](https://github.com/Soluce-Technologies/portabase/issues) for a full list of proposed features (and known issues). Check out [open issues](https://github.com/Soluce-Technologies/portabase/issues) for more.
<p align="right">(<a href="#readme-top">back to top</a>)</p> ---
## 🤝 Contributing
Contributions are welcome and appreciated! Here's how to get started:
<!-- CONTRIBUTING --> 1. Fork the repository
## Contributing 2. Create a new branch:
```bash
git checkout -b feature/YourFeature
```
3. Commit your changes:
```bash
git commit -m "Add YourFeature"
```
4. Push to the branch:
```bash
git push origin feature/YourFeature
```
5. Open a pull request
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Give the project a ⭐ if you like it!
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". ### Top Contributors
Don't forget to give the project a star! Thanks again!
1. Fork the Project [![Contributors](https://contrib.rocks/image?repo=Soluce-Technologies/portabase)](https://github.com/Soluce-Technologies/portabase/graphs/contributors)
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the Branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
### Top contributors: ---
<a href="https://github.com/Soluce-Technologies/portabase/graphs/contributors"> ## Developer Notes
<img src="https://contrib.rocks/image?repo=Soluce-Technologies/portabase" alt="contrib.rocks image" />
</a>
<p align="right">(<a href="#readme-top">back to top</a>)</p> ### Environment Variables
```yml
# Environment
NODE_ENV=production
## Memo # Database
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
If you're using Semantic Versioning (SemVer) to version your Docker images, you can use the following format: # Project Info
NEXT_PUBLIC_PROJECT_NAME="Portabase"
NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager"
NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io
PROJECT_SECRET=
```major.minor.patch-rc.release (e.g. 1.0.0-rc.1)``` # SMTP (Email)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
```major.minor.patch-rc.release-tag (e.g. 1.0.0-rc.1-dev)``` # Google OAuth
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
For zenstask # S3/MinIO Configuration
S3_ENDPOINT=http://app.s3.portabase.io
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET_NAME=portabase
S3_PORT=9000
S3_USE_SSL=true
```zenstack generate``` # Storage Backend: 'local' or 's3'
STORAGE_TYPE=local
```
<!-- LICENSE --> ### Semantic Versioning
## License
Distributed under the Apache License. See `LICENSE.txt` for more information. Use the following format for Docker image versioning:
<p align="right">(<a href="#readme-top">back to top</a>)</p> ```bash
major.minor.patch-rc.release
# Example: 1.0.0-rc.1
major.minor.patch-rc.release-tag
# Example: 1.0.0-rc.1-dev
```
<!-- CONTACT --> ### Zenstack Support
## Contact
Killian Larcher - killian.larcher@soluce-technologies.com To regenerate schema:
Charles Gauthereau - charles.gauthereau@soluce-technologies.com
Project Link: [https://github.com/Soluce-Technologies/portabase](https://github.com/Soluce-Technologies/agent-portabase) ```bash
zenstack generate
```
<p align="right">(<a href="#readme-top">back to top</a>)</p> ---
<!-- MARKDOWN LINKS & IMAGES --> ## 📄 License
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
Distributed under the Apache License. See `LICENSE.txt` for more details.
---
## 📬 Contact
- Killian Larcher - killian.larcher@soluce-technologies.com
- Charles Gauthereau - charles.gauthereau@soluce-technologies.com
- Project Link: [Portabase GitHub](https://github.com/Soluce-Technologies/portabase)
---
## 🙏 Acknowledgments
Thanks to all contributors and the open-source community!
---
[NextJS]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white [NextJS]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
[Prisma]: https://img.shields.io/badge/Prisma-3982CE?style=for-the-badge&logo=Prisma&logoColor=white [Drizzle]: https://img.shields.io/badge/Drizzle-111?style=for-the-badge&logo=Drizzle&logoColor=c5f74f
[Prisma-url]: https://www.prisma.io/ [ShadcnUI]: https://img.shields.io/badge/shadcn/ui-000000?style=for-the-badge&logo=shadcn/ui&logoColor=white
[NextJS-url]: https://nextjs.org/ [NextJS-url]: https://nextjs.org/
[Drizzle-url]: https://orm.drizzle.team/
[ShadcnUI-url]: https://ui.shadcn.com/
+16 -29
View File
@@ -1,42 +1,29 @@
import {PageParams} from "@/types/next"; import { PageParams } from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {requiredCurrentUser} from "@/auth/current-user";
import {prisma} from "@/prisma";
import {AdminTabs} from "@/components/wrappers/dashboard/admin/admin-tabs";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { AdminTabs } from "@/components/wrappers/dashboard/admin/admin-tabs";
import { currentUser } from "@/lib/auth/current-user";
import { db } from "@/db";
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({
where: (fields, { isNull, not }) => not(isNull(fields.deletedAt)),
});
const user = await requiredCurrentUser() const settings = await db.query.setting.findFirst({
where: (fields, { eq }) => eq(fields.name, "system"),
const users = await prisma.user.findMany({ });
where: {
// id: {
// not: user.id
// },
deleted: {not: true},
}
})
const settings = await prisma.settings.findUnique({
where: {
name: "system"
}
})
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle> <PageTitle>Administration Panel</PageTitle>
Administration Panel
</PageTitle>
</PageHeader> </PageHeader>
<PageContent className="mt-10"> <PageContent className="mt-10">
<AdminTabs settings={settings} currentUser={user} users={users}/> <AdminTabs settings={settings!} currentUser={user} users={users} />
</PageContent> </PageContent>
</Page> </Page>
) );
} }
@@ -1,19 +1,18 @@
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/AgentForm/AgentForm";
import {prisma} from "@/prisma"; import { notFound } from "next/navigation";
import {notFound} from "next/navigation"; import { db } from "@/db";
export default async function RoutePage(
props: PageParams<{
agentId: string;
}>
) {
const { agentId } = await props.params;
export default async function RoutePage(props: PageParams<{ const agent = await db.query.agent.findFirst({
agentId: string; where: (fields, { eq }) => eq(fields.id, agentId),
}>) {
const {agentId} = await props.params
const agent = await prisma.agent.findUnique({
where: {
id: agentId,
}
}); });
if (!agent) { if (!agent) {
@@ -23,13 +22,11 @@ export default async function RoutePage(props: PageParams<{
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle> <PageTitle>Edit {agent.name}</PageTitle>
Edit {agent.name}
</PageTitle>
</PageHeader> </PageHeader>
<PageContent> <PageContent>
<AgentForm defaultValues={agent} agentId={agent.id}/> <AgentForm defaultValues={agent} agentId={agent.id} />
</PageContent> </PageContent>
</Page> </Page>
) );
} }
@@ -1,28 +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 {AgentForm} from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm"; import { AgentForm } from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
import {currentUser} from "@/auth/current-user";
import {notFound} from "next/navigation";
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
const user = await currentUser();
if(user.role != "admin"){
notFound()
}
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle> <PageTitle>Create new agent</PageTitle>
Create new agent
</PageTitle>
</PageHeader> </PageHeader>
<PageContent> <PageContent>
<AgentForm/> <AgentForm />
</PageContent> </PageContent>
</Page> </Page>
) );
} }
@@ -1,29 +1,24 @@
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/AgentCard/AgentCard";
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 {prisma} from "@/prisma"; import { notFound } from "next/navigation";
import {notFound} from "next/navigation"; import { db } from "@/db";
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 prisma.agent.findMany() if (!agents) {
notFound();
if(!agents){
notFound()
} }
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle> <PageTitle>Agents</PageTitle>
Agents
</PageTitle>
{agents.length > 0 && ( {agents.length > 0 && (
<PageActions> <PageActions>
<Link href={"/dashboard/agents/new"}> <Link href={"/dashboard/agents/new"}>
@@ -33,21 +28,17 @@ export default async function RoutePage(props: PageParams<{}>) {
)} )}
</PageHeader> </PageHeader>
<PageContent className="mt-10"> <PageContent className="mt-10">
{agents.length > 0 ? {agents.length > 0 ? (
<CardsWithPagination <CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1} />
data={agents} ) : (
cardItem={AgentCard}
cardsPerPage={4}
numberOfColumns={1}
/>
:
<Link <Link
href={"/dashboard/agents/new"} href={"/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"> className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md"
>
Create new Agent Create new Agent
</Link> </Link>
} )}
</PageContent> </PageContent>
</Page> </Page>
) );
} }
+9 -13
View File
@@ -1,17 +1,13 @@
import React from "react"; import React from "react";
import {currentUser, requiredCurrentUser} from "@/auth/current-user"; import { notFound } from "next/navigation";
import {notFound} from "next/navigation"; import { currentUser } from "@/lib/auth/current-user";
export default async function Layout({children}: { children: React.ReactNode }) { export default async function Layout({ children }: { children: React.ReactNode }) {
const user = await currentUser();
const user = await requiredCurrentUser() if (user!.role !== "superadmin" && user!.role !== "admin") {
notFound();
if(user.role != "admin"){
notFound()
} }
return (
<> return <>{children}</>;
{children} }
</>
)
}
@@ -1,55 +1,41 @@
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 {requiredCurrentUser} from "@/auth/current-user"; import { notFound } from "next/navigation";
import {prisma} from "@/prisma"; import { RestoreForm } from "@/components/wrappers/dashboard/database/RestoreForm";
import {notFound} from "next/navigation"; import { currentUser } from "@/lib/auth/current-user";
import {RestoreForm} from "@/components/wrappers/dashboard/database/RestoreForm";
import { db } from "@/db";
import { eq } from "drizzle-orm";
import { database, backup } from "@/db/schema";
export default async function RoutePage(props: PageParams<{ export default async function RoutePage(
databaseId: string; props: PageParams<{
}>) { databaseId: string;
}>
) {
const { databaseId } = await props.params;
const {databaseId} = await props.params const user = await currentUser();
if (!user) notFound();
const [dbToRestore] = await db.select().from(database).where(eq(database.id, databaseId));
const user = await requiredCurrentUser() if (!dbToRestore) {
const database = await prisma.database.findUnique({
where: {
id: databaseId,
}
});
const databases = await prisma.database.findMany({
where: {
dbms: database.dbms,
}
})
const backups = await prisma.backup.findMany({
where: {
status: "success",
}
});
if (!database) {
notFound(); notFound();
} }
const dbsOfSameType = await db.select().from(database).where(eq(database.dbms!, dbToRestore.dbms!));
const successfulBackups = await db.select().from(backup).where(eq(backup.status, "success"));
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle> <PageTitle>Restore {dbToRestore.name}</PageTitle>
Restore {database.name}
</PageTitle>
</PageHeader> </PageHeader>
<PageContent> <PageContent>
<RestoreForm databaseToRestore={database} databases={databases} backups={backups}/> <RestoreForm databaseToRestore={dbToRestore} databases={dbsOfSameType} backups={successfulBackups} />
</PageContent> </PageContent>
</Page> </Page>
) );
} }
+18 -15
View File
@@ -1,17 +1,20 @@
import {PageParams} from "@/types/next"; "use client";
import {redirect} from "next/navigation"; import { authClient } from "@/lib/auth/auth-client";
import { PageParams } from "@/types/next";
export default async function RoutePage(props: PageParams<{}>) { export default function RoutePage(props: PageParams<{}>) {
const { data: activeOrganization } = authClient.useActiveOrganization();
return (
// return ( <div className="flex flex-1 flex-col gap-4 p-4">
// <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="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 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> <div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
// <div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"/> {JSON.stringify(activeOrganization)}
// </div> {activeOrganization?.name}
// ) </div>
} );
}
@@ -1,39 +1,34 @@
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 {requiredCurrentUser} from "@/auth/current-user"; import { notFound } from "next/navigation";
import {prisma} from "@/prisma"; import { DatabaseForm } from "@/components/wrappers/dashboard/database/DatabaseForm/DatabaseForm";
import {notFound} from "next/navigation";
import {DatabaseForm} from "@/components/wrappers/dashboard/database/DatabaseForm/DatabaseForm";
import { db } from "@/db";
import { database } from "@/db/schema";
import { eq } from "drizzle-orm";
export default async function RoutePage(props: PageParams<{ export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
databaseId: string; const { databaseId } = await props.params;
}>) {
const {databaseId} = await props.params const dbItem = await db.query.database.findFirst({
where: eq(database.id, databaseId),
const user = await requiredCurrentUser()
const database = await prisma.database.findUnique({
where: {
id: databaseId,
}
}); });
if (!database) { if (!dbItem) {
notFound(); notFound();
} }
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle> <PageTitle>Edit {dbItem.name}</PageTitle>
Edit {database.name}
</PageTitle>
</PageHeader> </PageHeader>
<PageContent> <PageContent>
<DatabaseForm databaseId={databaseId} defaultValues={database}/> <DatabaseForm
databaseId={databaseId}
defaultValues={{ ...dbItem, dbms: dbItem.dbms ?? "inactive", description: dbItem.description ?? undefined }}
/>
</PageContent> </PageContent>
</Page> </Page>
) );
} }
@@ -1,91 +1,75 @@
import {PageParams} from "@/types/next"; import { PageParams } from "@/types/next";
import {prisma} from "@/prisma"; import { notFound } from "next/navigation";
import {notFound} 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/DatabaseTabs"; import { DatabaseKpi } from "@/components/wrappers/dashboard/projects/Database/DatabaseKpi";
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/Database/DatabaseKpi"; import { EditButton } from "@/components/wrappers/dashboard/database/EditButton/EditButton";
import {EditButton} from "@/components/wrappers/dashboard/database/EditButton/EditButton"; import { CronButton } from "@/components/wrappers/dashboard/database/CronButton/CronButton";
import {CronButton} from "@/components/wrappers/dashboard/database/CronButton/CronButton";
import { db } from "@/db";
import { eq, and } from "drizzle-orm";
import { backup as drizzleBackup, database as drizzleDatabase, restoration as drizzleRestoration } from "@/db/schema";
export default async function RoutePage(props: PageParams<{ databaseId: string }>) { export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
const { databaseId } = await props.params;
const {databaseId} = await props.params const dbItem = await db.query.database.findFirst({
const database = await prisma.database.findUnique({ where: eq(drizzleDatabase.id, databaseId),
where: { });
id: databaseId,
}, if (!dbItem) {
}) notFound();
if (!database) {
notFound()
} }
const backups = await db.query.backup.findMany({
const backups = await prisma.backup.findMany({ where: eq(drizzleBackup.databaseId, dbItem.id),
where: { with: {
databaseId: database.id restorations: true,
},
include:{
restorations: {}
},
orderBy: {
createdAt: "desc"
}
})
const restorations = await prisma.restoration.findMany({
where: {
databaseId: databaseId,
},
orderBy: {
createdAt: "desc"
}
})
const isAlreadyBackup = !!backups.find(backup => backup.status === "waiting");
const isAlreadyRestore = !!restorations.find(restoration => restoration.status === "waiting");
const totalBackups = await prisma.backup.count({
where: {
databaseId: databaseId,
}, },
orderBy: (b, { desc }) => [desc(b.createdAt)],
}); });
const successfulBackups = await prisma.backup.count({
where: { const restorations = await db.query.restoration.findMany({
databaseId: databaseId, where: eq(drizzleRestoration.databaseId, dbItem.id),
status: 'success', orderBy: (r, { desc }) => [desc(r.createdAt)],
},
}); });
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null
const isAlreadyBackup = backups.some((b) => b.status === "waiting");
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
const [totalBackups, successfulBackups] = await Promise.all([
db
.select({ count: drizzleBackup.id })
.from(drizzleBackup)
.where(eq(drizzleBackup.databaseId, dbItem.id))
.then((rows) => rows.length),
db
.select({ count: drizzleBackup.id })
.from(drizzleBackup)
.where(and(eq(drizzleBackup.databaseId, dbItem.id), eq(drizzleBackup.status, "success")))
.then((rows) => rows.length),
]);
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">
{database.name} {dbItem.name}
<EditButton/> <EditButton />
<CronButton database={database}/> <CronButton database={dbItem} />
</PageTitle> </PageTitle>
<PageActions className="justify-between"> <PageActions className="justify-between">
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/> <BackupButton disable={isAlreadyBackup} databaseId={databaseId} />
</PageActions> </PageActions>
</div> </div>
<PageDescription className="mt-5 sm:mt-0">{database.description}</PageDescription> <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 <DatabaseKpi successRate={successRate} database={dbItem} totalBackups={totalBackups} />
successRate={successRate} <DatabaseTabs database={dbItem} isAlreadyRestore={isAlreadyRestore} backups={backups} restorations={restorations} />
database={database}
totalBackups={totalBackups}
/>
<DatabaseTabs
database={database}
isAlreadyRestore={isAlreadyRestore}
backups={backups}
restorations={restorations}
/>
</PageContent> </PageContent>
</Page> </Page>
) );
} }
@@ -1,61 +1,61 @@
import {notFound} from "next/navigation"; import { notFound } from "next/navigation";
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { ProjectForm } from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
import {PageParams} from "@/types/next"; import { db } from "@/db";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import { eq } from "drizzle-orm";
import {prisma} from "@/prisma"; import { project as drizzleProject, organization as drizzleOrganization, DatabaseWith } from "@/db/schema";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
const { projectId } = await props.params;
export default async function RoutePage(props: PageParams<{ const proj = await db.query.project.findFirst({
projectId: string; where: eq(drizzleProject.id, projectId),
}>) { with: {
databases: true,
const {projectId} = await props.params
const project = await prisma.project.findUnique({
where: {
id: projectId,
}, },
include: {
databases: {}
}
}); });
if (!project) { if (!proj) {
notFound(); notFound();
} }
const organization = await prisma.organization.findFirst({
where: {
slug: 'default',
}
})
const availableDatabases = await prisma.database.findMany({
where: {
OR: [
{projectId: null},
{projectId: project.id},
],
},
include: {
agent: {}
},
orderBy: {
createdAt: 'desc',
},
})
//
const org = await db.query.organization.findFirst({
where: eq(drizzleOrganization.slug, "default"),
});
if (!org) {
notFound();
}
const availableDatabases = (
await db.query.database.findMany({
where: (db, { or, eq, isNull }) => or(isNull(db.projectId), eq(db.projectId, proj.id)),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, { desc }) => [desc(db.createdAt)],
})
).filter((db): db is DatabaseWith => db.project !== null);
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle> <PageTitle>Edit {proj.name}</PageTitle>
Edit {project.name}
</PageTitle>
</PageHeader> </PageHeader>
<PageContent> <PageContent>
<ProjectForm organization={organization} databases={availableDatabases} defaultValues={project} <ProjectForm
projectId={project.id}/> organization={org}
databases={availableDatabases}
defaultValues={{ name: proj.name, slug: proj.slug, databases: proj.databases.map((db) => db.id) }}
projectId={proj.id}
/>
</PageContent> </PageContent>
</Page> </Page>
) );
} }
@@ -1,77 +1,70 @@
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 {prisma} from "@/prisma"; 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 { ButtonDeleteProject } from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/ButtonDeleteProject";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard"; import { ProjectDatabaseCard } from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
import {notFound} from "next/navigation"; import { notFound } from "next/navigation";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie"; import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
import { db } from "@/db";
import { eq } from "drizzle-orm";
import { organization as drizzleOrganization } from "@/db/schema";
export default async function RoutePage(props: PageParams<{ slug:string, projectId: string }>) { export default async function RoutePage(props: PageParams<{ slug: string; projectId: string }>) {
const { slug: organizationSlug, projectId } = await props.params;
const {slug: organizationSlug, projectId} = await props.params const currentOrganizationSlug = await getCurrentOrganizationSlug();
if (currentOrganizationSlug !== organizationSlug) {
const currentOrganizationSlug = await getCurrentOrganizationSlug() notFound();
if(currentOrganizationSlug != organizationSlug) {
notFound()
} }
const org = await db.query.organization.findFirst({
where: eq(drizzleOrganization.slug, currentOrganizationSlug),
});
if (!org) notFound();
const project = await prisma.project.findUnique({ const proj = await db.query.project.findFirst({
where: { where: (proj, { and, eq, not }) => and(eq(proj.id, projectId), eq(proj.organizationId, org.id), not(eq(proj.isArchived, true))),
id: projectId, with: {
isArchived: {not: true} databases: true,
}, },
include:{ });
databases: {}
}
})
if(!project){ if (!proj) notFound();
notFound()
}
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">
{project.name} {proj.name}
<Link className={buttonVariants({variant: "outline"})} <Link className={buttonVariants({ variant: "outline" })} href={`/dashboard/${currentOrganizationSlug}/projects/${proj.id}/edit`}>
href={`/dashboard/${currentOrganizationSlug}/projects/${project.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 <ButtonDeleteProject projectId={projectId} text={"Delete Project"} />
projectId={projectId}
text={"Delete Project"}
/>
</PageActions> </PageActions>
</div> </div>
<PageDescription>
The list of associated databases <PageDescription>The list of associated databases</PageDescription>
</PageDescription>
<PageContent className="flex flex-col w-full h-full"> <PageContent className="flex flex-col w-full h-full">
{project.databases.length > 0 ? {proj.databases.length > 0 ? (
<CardsWithPagination <CardsWithPagination
data={project.databases} data={proj.databases}
organizationSlug={organizationSlug} organizationSlug={organizationSlug}
cardItem={ProjectDatabaseCard} cardItem={ProjectDatabaseCard}
cardsPerPage={4} cardsPerPage={4}
numberOfColumns={1} numberOfColumns={1}
extendedProps={project} extendedProps={proj}
/> />
: ) : null}
null
}
</PageContent> </PageContent>
</Page> </Page>
) );
} }
@@ -1,48 +1,48 @@
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 {prisma} from "@/prisma"; import { ProjectForm } from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm"; import { notFound } from "next/navigation";
import {notFound} from "next/navigation"; import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import { db } from "@/db";
import { DatabaseWith, organization as drizzleOrganization } from "@/db/schema";
import { eq } from "drizzle-orm";
export default async function RoutePage(props: PageParams<{slug: string}>) { export default async function RoutePage(props: PageParams<{ slug: string }>) {
const {slug: organizationSlug} = await props.params const { slug: organizationSlug } = await props.params;
const currentOrganizationSlug = await getCurrentOrganizationSlug() const currentOrganizationSlug = await getCurrentOrganizationSlug();
if(currentOrganizationSlug != organizationSlug) { if (currentOrganizationSlug !== organizationSlug) {
notFound() notFound();
} }
const availableDatabases = (
await db.query.database.findMany({
where: (db, { isNull }) => isNull(db.projectId),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, { desc }) => [desc(db.createdAt)],
})
).filter((db) => db.project !== null) as DatabaseWith[];
const availableDatabases = await prisma.database.findMany({ const org = await db.query.organization.findFirst({
where: { where: eq(drizzleOrganization.slug, currentOrganizationSlug),
projectId: null });
},
include: {
agent: {}
},
orderBy: {
createdAt: 'desc',
},
})
const organization = await prisma.organization.findFirst({ if (!org) notFound();
where: {
slug: currentOrganizationSlug,
}
})
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle> <PageTitle>Create new project</PageTitle>
Create new project
</PageTitle>
</PageHeader> </PageHeader>
<PageContent> <PageContent>
<ProjectForm databases={availableDatabases} organization={organization}/> <ProjectForm databases={availableDatabases} organization={org} />
</PageContent> </PageContent>
</Page> </Page>
) );
} }
@@ -1,43 +1,34 @@
import Link from 'next/link' import Link from "next/link";
import {PageParams} from "@/types/next"; import { PageParams } from "@/types/next";
import {prisma} from "@/prisma"; 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/ProjectCard/ProjectCard"; import { db } from "@/db";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie"; import { notFound } from "next/navigation";
import {notFound} from "next/navigation"; import { getOrganization } from "@/lib/auth/auth";
export default async function RoutePage(props: PageParams<{ slug: string }>) {
const { slug: organizationSlug } = await props.params;
export default async function RoutePage(props: PageParams<{slug: string}>) { const organization = await getOrganization(organizationSlug);
const {slug: organizationSlug} = await props.params
if (!organization || organization?.slug !== organizationSlug) {
notFound();
}
const projects = await prisma.project.findMany({ const projects = await db.query.project.findMany({
where: { where: (project, { eq, and, not }) => and(eq(project.organizationId, organization.id), not(eq(project.isArchived, true))),
isArchived: {not: true}, with: {
databases: true,
organization: {
slug: organizationSlug,
// users: {
// some: {
// userId: user.id
// },
// },
}
}, },
include: { });
databases: {}
}
})
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle> <PageTitle>Projects</PageTitle>
Projects
</PageTitle>
{projects.length > 0 && ( {projects.length > 0 && (
<PageActions> <PageActions>
<Link href={`/dashboard/${organizationSlug}/projects/new`}> <Link href={`/dashboard/${organizationSlug}/projects/new`}>
@@ -45,28 +36,20 @@ export default async function RoutePage(props: PageParams<{slug: string}>) {
</Link> </Link>
</PageActions> </PageActions>
)} )}
</PageHeader> </PageHeader>
<PageContent className="mt-10"> <PageContent className="mt-10">
{projects.length > 0 ? (
{projects.length > 0 ? <CardsWithPagination organizationSlug={organizationSlug} data={projects} cardItem={ProjectCard} cardsPerPage={4} numberOfColumns={1} />
<CardsWithPagination ) : (
organizationSlug={organizationSlug}
data={projects}
cardItem={ProjectCard}
cardsPerPage={4}
numberOfColumns={1}
/>
:
<Link <Link
href={`/dashboard/${organizationSlug}/projects/new`} href={`/dashboard/${organizationSlug}/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"> className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md"
>
Create new Project Create new Project
</Link> </Link>
)}
}
</PageContent> </PageContent>
</Page> </Page>
) );
} }
@@ -1,48 +1,57 @@
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 {prisma} from "@/prisma"; 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 { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
import { notFound } from "next/navigation";
import { db } from "@/db";
import { asc, count, eq, inArray } from "drizzle-orm";
import { organization as drizzleOrganization, project as drizzleProject, backup as drizzleBackup } from "@/db/schema";
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{ slug: string }>) {
const { slug: organizationSlug } = await props.params;
const projectsCount = await prisma.project.count({ const currentOrganizationSlug = await getCurrentOrganizationSlug();
where: { if (currentOrganizationSlug !== organizationSlug) {
organization: { notFound();
slug: "default" }
}
}, const org = await db.query.organization.findFirst({
where: eq(drizzleOrganization.slug, currentOrganizationSlug),
}); });
const backupsEvolution = await prisma.backup.findMany({ if (!org) notFound();
select: {
const projects = await db.query.project.findMany({
where: eq(drizzleProject.organizationId, org.id),
});
const projectsCount = projects.length;
const backupsEvolution = await db.query.backup.findMany({
columns: {
id: true,
createdAt: true, createdAt: true,
}, },
orderBy: { orderBy: [asc(drizzleBackup.id)],
createdAt: "asc",
},
});
const backupsRate = await prisma.backup.groupBy({
by: ['createdAt', 'status'],
where: {
status: {
in: ['success', 'failed'],
},
},
_count: {
id: true,
},
}); });
const backupsRate = await db
.select({
createdAt: drizzleBackup.createdAt,
status: drizzleBackup.status,
_count: count(),
})
.from(drizzleBackup)
.where(inArray(drizzleBackup.status, ["success", "failed"]))
.groupBy(drizzleBackup.createdAt, drizzleBackup.status)
.orderBy(drizzleBackup.createdAt);
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle> <PageTitle>Statistics</PageTitle>
Statistics
</PageTitle>
</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">
@@ -50,23 +59,19 @@ export default async function RoutePage(props: PageParams<{}>) {
<CardHeader> <CardHeader>
<CardTitle>Projects</CardTitle> <CardTitle>Projects</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>{projectsCount}</CardContent>
{projectsCount}
</CardContent>
</Card> </Card>
<Card className="w-full"> <Card className="w-full">
<CardHeader> <CardHeader>
<CardTitle>KPI 2</CardTitle> <CardTitle>KPI 2</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent></CardContent>
</CardContent>
</Card> </Card>
<Card className="w-full"> <Card className="w-full">
<CardHeader> <CardHeader>
<CardTitle>KPI 3</CardTitle> <CardTitle>KPI 3</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent></CardContent>
</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">
@@ -75,7 +80,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={backupsEvolution} />
</CardContent> </CardContent>
</Card> </Card>
<Card className="w-full"> <Card className="w-full">
@@ -83,11 +88,11 @@ 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>
</PageContent> </PageContent>
</Page> </Page>
) );
} }
+13 -19
View File
@@ -1,37 +1,31 @@
import React from "react"; 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/sideBar/app-sidebar";
import {currentUser, requiredCurrentUser} from "@/auth/current-user"; import { Header } from "@/features/layout/Header";
import {Header} from "@/features/layout/Header"; import { currentUser } from "@/lib/auth/current-user";
import {prisma} from "@/prisma";
export default async function Layout({ children }: { children: React.ReactNode }) {
const user = await currentUser();
export default async function Layout({children}: { children: React.ReactNode }) { if (!user) redirect("/login");
const user = await requiredCurrentUser()
if (!user) redirect('/login')
return ( return (
// <HydrationZustand> // <HydrationZustand>
// <GlobalStoreProvider> // <GlobalStoreProvider>
<> <>
<SidebarProvider> <SidebarProvider>
<div className="flex flex-col lg:flex-row w-full"> <div className="flex flex-col lg:flex-row w-full">
<AppSidebar/> <AppSidebar />
<SidebarInset> <SidebarInset>
<Header/> <Header />
<main className="h-full"> <main className="h-full">{children}</main>
{children}
</main>
</SidebarInset> </SidebarInset>
</div> </div>
</SidebarProvider> </SidebarProvider>
</> </>
// </GlobalStoreProvider> // </GlobalStoreProvider>
// </HydrationZustand> // </HydrationZustand>
) );
} }
+3 -3
View File
@@ -1,9 +1,9 @@
import {LoadingSpinner} from "@/components/wrappers/common/loading/LoadingSpinner"; import { LoadingSpinner } from "@/components/wrappers/common/loading/LoadingSpinner";
export default function Loading() { export default function Loading() {
return ( return (
<div className="w-full h-full flex flex-col items-center justify-center"> <div className="w-full h-full flex flex-col items-center justify-center">
<LoadingSpinner size={50} /> <LoadingSpinner size={50} />
</div> </div>
) );
} }
+49 -31
View File
@@ -1,44 +1,62 @@
import {PageParams} from "@/types/next"; import { PageParams } from "@/types/next";
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 {requiredCurrentUser} from "@/auth/current-user"; import { UserForm } from "@/components/wrappers/dashboard/profile/UserForm/UserForm";
import {UserForm} from "@/components/wrappers/dashboard/profile/UserForm/UserForm"; import { Badge } from "@/components/ui/badge";
import {prisma} from "@/prisma"; import { ButtonDeleteAccount } from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/ButtonDeleteAccount";
import {Badge} from "@/components/ui/badge"; import { AvatarWithUpload } from "@/components/wrappers/dashboard/profile/Avatar/AvatarWithUpload";
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/ButtonDeleteAccount"; import { currentUser } from "@/lib/auth/current-user";
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/Avatar/AvatarWithUpload"; //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 requiredCurrentUser()
if (!user) { if (!user) {
notFound() return notFound();
} }
const userInfo = await prisma.user.findUnique({
where: { if (user.role !== "user" && user.role !== "admin" && user.role !== "superadmin") {
email: user.email return notFound();
} }
})
// const sessions = await getSessions();
// const accounts = await getAccounts();
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">
<AvatarWithUpload user={userInfo}/> <AvatarWithUpload
{user.name} user={{
<Badge className="ml-3 hidden lg:block">{userInfo.authMethod}</Badge> ...user,
<Badge className="ml-3 hidden lg:block">{userInfo.role}</Badge> image: user.image ?? null,
</PageTitle> role: user.role ?? null,
<PageActions className="mt-2 hidden sm:block"> banned: user.banned ?? null,
<ButtonDeleteAccount text="Delete my account"/> banReason: user.banReason ?? null,
</PageActions> banExpires: user.banExpires ?? null,
</div> deletedAt: user.deletedAt ? new Date(user.deletedAt) : null,
}}
/>
{user.name}
<Badge className="ml-3 hidden lg:block">{user.role}</Badge>
</PageTitle>
<PageActions className="mt-2 hidden sm:block">
<ButtonDeleteAccount text="Delete my account" />
</PageActions>
</div>
<PageContent> <PageContent>
<UserForm userId={userInfo.id} defaultValues={userInfo}/> <UserForm
userId={user.id}
defaultValues={{
name: user.name,
email: user.email,
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>
) );
} }
+5 -5
View File
@@ -1,7 +1,7 @@
export default function Home() { export default function Home() {
return ( return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]"> <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<h1 className="text-3xl font-bold text-4xl leading-tight">Home landing page</h1> <h1 className="font-bold text-4xl leading-tight">Home landing page</h1>
</div> </div>
); );
} }
+7 -8
View File
@@ -1,15 +1,14 @@
import {redirect} from "next/navigation"; import { redirect } from "next/navigation";
import {currentUser} from "@/auth/current-user"; import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie"; import { currentUser } from "@/lib/auth/current-user";
export default async function Index() { 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/${currentOrganizationSlug}/projects`);
} }
redirect("/login") redirect("/login");
//Do not delete //Do not delete
// return ( // return (
// <Home/> // <Home/>
+4
View File
@@ -0,0 +1,4 @@
import { auth } from "@/lib/auth/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth.handler);
-4
View File
@@ -1,4 +0,0 @@
import {handlers} from "@/auth/auth";
export const {GET, POST} = handlers;
+5 -9
View File
@@ -1,21 +1,17 @@
import path from "path"; 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(
request: Request,
{params}: { params: Promise<{ fileName: string }> }
) {
export async function GET({ params }: { params: Promise<{ fileName: string }> }) {
try { try {
const fileName = (await params).fileName const fileName = (await params).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
try { try {
await fs.access(filePath); // Ensures the file exists await fs.access(filePath); // Ensures the file exists
} catch { } catch {
return NextResponse.json({error: "File not found"}, {status: 404}); return NextResponse.json({ error: "File not found" }, { status: 404 });
} }
// Read the file // Read the file
@@ -29,6 +25,6 @@ export async function GET(
}); });
} catch (error) { } catch (error) {
console.error("Error reading file:", error); console.error("Error reading file:", error);
return NextResponse.json({error: "Internal Server Error"}, {status: 500}); return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
} }
} }
+7 -16
View File
@@ -1,20 +1,14 @@
import {NextResponse} from "next/server"; import { NextResponse } from "next/server";
export type BodyInit = { export type BodyInit = {
initialize: boolean initialize: boolean;
} };
export async function POST(request: Request) {
export async function POST(
request: Request,
) {
try { try {
const body: BodyInit = await request.json(); const body: BodyInit = await request.json();
console.log(body) console.log(body);
return NextResponse.json( return NextResponse.json(
{ {
@@ -24,9 +18,6 @@ export async function POST(
); );
} catch (error) { } catch (error) {
console.error("Error in POST initialization:", error); console.error("Error in POST initialization:", error);
return NextResponse.json( return NextResponse.json({ error: "Internal server error" }, { status: 500 });
{ error: "Internal server error" },
{ status: 500 }
);
} }
} }
+2 -2
View File
@@ -24,7 +24,7 @@
// <div className="flex h-screen w-full flex-col items-center justify-center"> // <div className="flex h-screen w-full flex-col items-center justify-center">
// <a // <a
// href="#" // href="#"
// className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700" // className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow-sm hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
// > // >
// <h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white"> // <h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white">
// Something went wrong // Something went wrong
@@ -63,7 +63,7 @@ function AuthErrorPageContent() {
<div className="flex h-screen w-full flex-col items-center justify-center"> <div className="flex h-screen w-full flex-col items-center justify-center">
<a <a
href="#" href="#"
className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700" className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow-sm hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
> >
<h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white"> <h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white">
Something went wrong Something went wrong
+127 -90
View File
@@ -1,105 +1,142 @@
@tailwind base; @import "tailwindcss";
@tailwind components; @import "tw-animate-css";
@tailwind utilities;
/*body {*/ @custom-variant dark (&:is(.dark *));
/* font-family: Arial, Helvetica, sans-serif;*/
/*}*/
@layer utilities { @theme inline {
/*.text-balance {*/ --color-background: var(--background);
/* text-wrap: balance;*/ --color-foreground: var(--foreground);
/*}*/ --font-sans: var(--font-geist-sans);
.popover-content-width-full { --font-mono: var(--font-geist-mono);
width: var(--radix-popover-trigger-width); --color-sidebar-ring: var(--sidebar-ring);
max-height: var(--radix-popover-content-available-height); --color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
} }
} }
:root {
--radius: 0.625rem;
@layer base { --background: oklch(1 0 0);
:root { --foreground: oklch(0.145 0 0);
--background: 0 0% 100%; --card: oklch(1 0 0);
--foreground: 240 10% 3.9%; --card-foreground: oklch(0.145 0 0);
--card: 0 0% 100%; --popover: oklch(1 0 0);
--card-foreground: 240 10% 3.9%; --popover-foreground: oklch(0.145 0 0);
--popover: 0 0% 100%; --primary: oklch(0.205 0 0);
--popover-foreground: 240 10% 3.9%; --primary-foreground: oklch(0.985 0 0);
--primary: 240 5.9% 10%; --secondary: oklch(0.97 0 0);
--primary-foreground: 0 0% 98%; --secondary-foreground: oklch(0.205 0 0);
--secondary: 240 4.8% 95.9%; --muted: oklch(0.97 0 0);
--secondary-foreground: 240 5.9% 10%; --muted-foreground: oklch(0.556 0 0);
--muted: 240 4.8% 95.9%; --accent: oklch(0.97 0 0);
--muted-foreground: 240 3.8% 46.1%; --accent-foreground: oklch(0.205 0 0);
--accent: 240 4.8% 95.9%; --destructive: oklch(0.577 0.245 27.325);
--accent-foreground: 240 5.9% 10%; --border: oklch(0.922 0 0);
--destructive: 0 84.2% 60.2%; --input: oklch(0.922 0 0);
--destructive-foreground: 0 0% 98%; --ring: oklch(0.708 0 0);
--border: 240 5.9% 90%; --chart-1: oklch(0.646 0.222 41.116);
--input: 240 5.9% 90%; --chart-2: oklch(0.6 0.118 184.704);
--ring: 240 5.9% 10%; --chart-3: oklch(0.398 0.07 227.392);
--radius: 0.5rem; --chart-4: oklch(0.828 0.189 84.429);
--chart-1: 12 76% 61%; --chart-5: oklch(0.769 0.188 70.08);
--chart-2: 173 58% 39%; --sidebar: oklch(0.985 0 0);
--chart-3: 197 37% 24%; --sidebar-foreground: oklch(0.145 0 0);
--chart-4: 43 74% 66%; --sidebar-primary: oklch(0.205 0 0);
--chart-5: 27 87% 67%; --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-background: 0 0% 98%; --sidebar-accent: oklch(0.97 0 0);
--sidebar-foreground: 240 5.3% 26.1%; --sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-primary: 240 5.9% 10%; --sidebar-border: oklch(0.922 0 0);
--sidebar-primary-foreground: 0 0% 98%; --sidebar-ring: oklch(0.708 0 0);
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
} }
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border outline-ring/50;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }
+10 -15
View File
@@ -1,13 +1,12 @@
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";
const inter = Inter({ subsets: ["latin"] });
const inter = Inter({subsets: ["latin"]});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Portabase", title: "Portabase",
@@ -15,19 +14,15 @@ export const metadata: Metadata = {
}; };
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body <body className={cn(inter.className, "h-full")}>
className={cn(inter.className, "h-full")} <Providers>{children}</Providers>
> </body>
<Providers>
{children}
</Providers>
</body>
</html> </html>
); );
} }
+3 -4
View File
@@ -1,6 +1,5 @@
import {redirect, useRouter} from "next/navigation"; import { redirect } from "next/navigation";
export default async function NotFound() { export default async function NotFound() {
console.log("not found"); redirect("/dashboard/profile");
redirect("/") }
}
+14 -23
View File
@@ -1,32 +1,23 @@
"use client" "use client";
import {PropsWithChildren, Suspense} from "react"; import { PropsWithChildren, Suspense } from "react";
import {ThemeProvider} from "@/features/theme/theme-provider"; import { ThemeProvider } from "@/features/theme/theme-provider";
import {Toaster} from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import {QueryClient, QueryClientProvider,} from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {SessionProvider} from "next-auth/react";
export type ProviderProps = PropsWithChildren<{}>; export type ProviderProps = PropsWithChildren<{}>;
const queryClient = new QueryClient() const queryClient = new QueryClient();
export const Providers = (props: ProviderProps) => { export const Providers = (props: ProviderProps) => {
return ( return (
<Suspense fallback={null}> <Suspense fallback={null}>
<SessionProvider> <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ThemeProvider <QueryClientProvider client={queryClient}>
attribute="class" <Toaster />
defaultTheme="system" {props.children}
enableSystem </QueryClientProvider>
// disableTransitionOnChange </ThemeProvider>
>
<QueryClientProvider client={queryClient}>
<Toaster/>
{props.children}
</QueryClientProvider>
</ThemeProvider>
</SessionProvider>
</Suspense> </Suspense>
);
) };
}
+68 -59
View File
@@ -1,68 +1,77 @@
name: portabase-dev name: portabase-dev
services: services:
app:
build:
context: .
dockerfile: docker/dockerfile/Dockerfile
target: dev
ports:
- "8887:80"
environment:
- TIME_ZONE="Europe/Paris"
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
volumes:
- .:/app
- /app/node_modules
container_name: portabase-app
app: develop:
build: watch:
context: . - action: sync
dockerfile: docker/dockerfile/Dockerfile path: .
target: dev target: /app
ports: - action: rebuild
- '8887:80' path: package.json
environment:
- TIME_ZONE="Europe/Paris"
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
volumes:
- .:/app
- /app/node_modules
container_name: portabase-app
develop:
watch:
- action: sync
path: .
target: /app
ignore:
- node_modules/
- action: rebuild
path: package.json
db:
image: postgres:16-alpine
ports:
- "5433:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=devdb
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U devuser -d devdb" ]
interval: 10s
timeout: 5s
retries: 5
s3:
image: docker.io/bitnami/minio:latest
ports:
- '9000:9000'
- '9001:9001'
volumes:
- minio_data:/data
environment:
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
db:
image: postgres:16-alpine
ports:
- "5433:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=devdb
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
interval: 10s
timeout: 5s
retries: 5
db-pgadmin:
image: dpage/pgadmin4
environment:
PGADMIN_DEFAULT_EMAIL: "devuser@devuser.devuser"
PGADMIN_DEFAULT_PASSWORD: "changeme"
PGADMIN_CONFIG_SERVER_MODE: "False"
POSTGRES_USER: "devuser"
POSTGRES_PASSWORD: "changeme"
volumes:
- pgadmin-data:/var/lib/pgadmin
ports:
- "8080:80"
restart: unless-stopped
depends_on:
- db
s3:
image: docker.io/bitnami/minio:latest
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio_data:/data
environment:
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
volumes: volumes:
postgres-data: postgres-data:
minio_data: minio_data:
pgadmin-data:
+1 -4
View File
@@ -3,11 +3,8 @@
npm install -g npm@11.0.0 npm install -g npm@11.0.0
npm -v npm -v
current_date=$(date +"%Y-%m-%d")
npx zenstack generate npx zenstack generate
npx prisma migrate dev --name $current_date npx drizzle-kit migrate
npm run dev npm run dev
exec "$@" exec "$@"
+1 -1
View File
@@ -10,7 +10,7 @@ echo " Community Edition v1.0.0 "
echo " " echo " "
npx prisma migrate deploy npx drizzle-kit migrate
node server.js node server.js
exec "$@" exec "$@"
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from "drizzle-kit";
import dotenv from "dotenv";
dotenv.config({
path: ".env",
});
export default defineConfig({
out: "./src/db/migrations",
schema: ["./src/db/schema", "./src/db/schema/types.ts"],
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
+4 -11
View File
@@ -1,13 +1,6 @@
import { import { Button as ReactEmailButton } from "@react-email/components";
Button as ReactEmailButton import { ComponentPropsWithoutRef } from "react";
} from "@react-email/components";
import {ComponentPropsWithoutRef} from "react";
export const Button = (props: ComponentPropsWithoutRef<typeof ReactEmailButton>) => { export const Button = (props: ComponentPropsWithoutRef<typeof ReactEmailButton>) => {
return ( return <ReactEmailButton className="block w-52 rounded bg-blue-600 py-3.5 text-center text-sm font-normal text-white no-underline " {...props} />;
<ReactEmailButton };
className="block w-52 rounded bg-blue-600 py-3.5 text-center text-sm font-normal text-white no-underline "
{...props}
/>
)
}
+5 -27
View File
@@ -1,18 +1,7 @@
import { import { Body, Container, Head, Html, Img, Preview, Section, Tailwind } from "@react-email/components";
Body,
Container,
Head,
Html,
Img,
Preview,
Section,
Tailwind,
} from "@react-email/components";
import * as React from "react"; import * as React from "react";
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
import {getServerUrl} from "@/utils/get-server-url"; import { getServerUrl } from "@/utils/get-server-url";
const baseUrl = getServerUrl(); const baseUrl = getServerUrl();
@@ -22,19 +11,10 @@ export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?:
<Html> <Html>
<Head /> <Head />
{preview ? <Preview>{preview}</Preview> : <Preview>Please check your mails</Preview>} {preview ? <Preview>{preview}</Preview> : <Preview>Please check your mails</Preview>}
<Body className="bg-gray-100 py-4" <Body className="bg-gray-100 py-4" style={{ fontFamily: "Arial, sans-serif" }}>
style={{fontFamily: "Arial, sans-serif"}}>
<Container className="bg-white border border-gray-200 p-12"> <Container className="bg-white border border-gray-200 p-12">
<Img <Img src={`${baseUrl}/logo-title-black.png`} width="200" height="auto" alt="Logo" />
src={`${baseUrl}/logo-title-black.png`} <Section>{children}</Section>
width="200"
height="auto"
alt="Logo"
/>
<Section>
{children}
</Section>
</Container> </Container>
</Body> </Body>
</Html> </Html>
@@ -42,6 +22,4 @@ export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?:
); );
}; };
export default EmailLayout; export default EmailLayout;
+3 -6
View File
@@ -5,12 +5,9 @@ import * as React from "react";
export default function HelloEmail() { export default function HelloEmail() {
return ( return (
<Html> <Html>
<Button <Button href="https://example.com" style={{ background: "#000", color: "#fff" }}>
href="https://example.com" Hello,
style={{ background: "#000", color: "#fff" }}
>
Hello Charles
</Button> </Button>
</Html> </Html>
); );
} }
+6 -32
View File
@@ -1,42 +1,16 @@
import { import { Text } from "@react-email/components";
Text,
} from "@react-email/components";
import * as React from "react"; import * as React from "react";
import EmailLayout from "./EmailLayout"; import EmailLayout from "./EmailLayout";
import {Button} from "@react-email/button"; import { Button } from "@react-email/button";
export const TestEmailSettings = () => { export const TestEmailSettings = () => {
return ( return (
<EmailLayout <EmailLayout preview="Email Setup">
preview="Email Setup" <Text className="text-base font-light leading-8 text-green-800 ">Hi, your email settings are setup !</Text>
> <Text className="text-base font-light leading-8 text-green-800 ">Best regard,</Text>{" "}
<Text className="text-base font-light leading-8 text-green-800 ">Portabase</Text>
<Text
className="text-base font-light leading-8 text-green-800 "
>
Hi, your email settings are setup !
</Text>
<Text
className="text-base font-light leading-8 text-green-800 "
>
Best regard,
</Text> <Text
className="text-base font-light leading-8 text-green-800 "
>
Portabase
</Text>
</EmailLayout> </EmailLayout>
); );
}; };
export default TestEmailSettings; export default TestEmailSettings;
+4 -11
View File
@@ -1,13 +1,6 @@
import { import { Text as ReactEmailText } from "@react-email/components";
Text as ReactEmailText import { ComponentPropsWithoutRef } from "react";
} from "@react-email/components";
import {ComponentPropsWithoutRef} from "react";
export const Text = (props: ComponentPropsWithoutRef<typeof ReactEmailText>) => { export const Text = (props: ComponentPropsWithoutRef<typeof ReactEmailText>) => {
return ( return <ReactEmailText className="text-base font-light leading-8 text-green-800 " {...props} />;
<ReactEmailText };
className="text-base font-light leading-8 text-green-800 "
{...props}
/>
)
}
+5 -5
View File
@@ -1,6 +1,6 @@
import {init} from "@/utils/init"; export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const init = await import("@/utils/init");
export function register() { init.init();
init(); }
} }
+35 -22
View File
@@ -1,43 +1,54 @@
import {NextRequest, NextResponse} from 'next/server' import { NextRequest, NextResponse } from "next/server";
import {loggingMiddleware} from "@/middleware/loggingMiddleware"; import { loggingMiddleware } from "@/middleware/loggingMiddleware";
import {errorHandler} from "@/middleware/errorHandler"; import { errorHandler } from "@/middleware/errorHandler";
import { auth } from "@/lib/auth/auth";
import { headers } from "next/headers";
export function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
const url = request.nextUrl.clone(); const url = request.nextUrl.clone();
// Exclude `/api/auth` and its subpaths if (url.pathname.startsWith("/dashboard")) {
if (url.pathname.startsWith('/api/auth')) { const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next(); return NextResponse.next();
} }
if (url.pathname.startsWith('/api')) { // Exclude `/api/auth` and its subpaths
if (url.pathname.startsWith("/api/auth")) {
return NextResponse.next();
}
if (url.pathname.startsWith("/api")) {
const routeExists = checkRouteExists(url.pathname); const routeExists = checkRouteExists(url.pathname);
// If the route does not exist, return a 404 JSON response // If the route does not exist, return a 404 JSON response
if (!routeExists) { if (!routeExists) {
return new NextResponse( return new NextResponse(JSON.stringify({ message: "This API route does not exist.", status: 404 }), {
JSON.stringify({ message: "This API route does not exist.", status: 404 }), status: 404,
{ status: 404, headers: { 'Content-Type': 'application/json' } } headers: { "Content-Type": "application/json" },
); });
} }
} }
try{ try {
loggingMiddleware(request); loggingMiddleware(request);
}catch(err){ } catch (err) {
errorHandler(err) errorHandler(err);
} }
} }
// Function to check if the route exists (supports dynamic routes) // Function to check if the route exists (supports dynamic routes)
function checkRouteExists(pathname) { function checkRouteExists(pathname: string) {
// Define static and dynamic routes with patterns // Define static and dynamic routes with patterns
const routePatterns = [ const routePatterns = [
//do not delete //do not delete
// /^\/api\/auth\/\d+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123) // /^\/api\/auth\/\d+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123)
// /^\/api\/auth\/\w+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123) // /^\/api\/auth\/\w+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123)
// /^\/api\/agent\/healthcheck\/\w+$/, // Dynamic route with an alphanumeric parameter (e.g., /api/user/username) // /^\/api\/agent\/healthcheck\/\w+$/, // Dynamic route with an alphanumeric parameter (e.g., /api/user/username)
/^\/api\/agent\/[^/]+\/status\/?$/, // Dynamic route for /api/agent/[id]/status /^\/api\/agent\/[^/]+\/status\/?$/, // Dynamic route for /api/agent/[id]/status
/^\/api\/agent\/[^/]+\/backup\/?$/, /^\/api\/agent\/[^/]+\/backup\/?$/,
/^\/api\/agent\/[^/]+\/restore\/?$/, /^\/api\/agent\/[^/]+\/restore\/?$/,
/^\/api\/files\/[^/]+\/?$/, /^\/api\/files\/[^/]+\/?$/,
@@ -45,13 +56,15 @@ function checkRouteExists(pathname) {
/^\/api\/events\/?$/, /^\/api\/events\/?$/,
/^\/api\/init\/?$/, /^\/api\/init\/?$/,
]; ];
return routePatterns.some(pattern => pattern.test(pathname)); return routePatterns.some((pattern) => pattern.test(pathname));
} }
export const config = { export const config = {
runtime: "nodejs",
matcher: [ matcher: [
// '/api/agent/:path*', // '/api/agent/:path*',
'/api/:path*', "/api/:path*",
"/dashboard/:path*",
"/dashboard",
], ],
}; };
+6 -7
View File
@@ -1,6 +1,6 @@
import type { NextConfig } from "next";
/** @type {import('next').NextConfig} */ const nextConfig: NextConfig = {
const nextConfig = {
output: "standalone", output: "standalone",
typescript: { typescript: {
ignoreBuildErrors: true, ignoreBuildErrors: true,
@@ -9,12 +9,11 @@ const nextConfig = {
ignoreDuringBuilds: true, ignoreDuringBuilds: true,
}, },
compiler: { compiler: {
removeConsole: process.env.NODE_ENV === 'production', removeConsole: process.env.NODE_ENV === "production",
},
experimental: {
nodeMiddleware: true,
}, },
}; };
export default nextConfig; export default nextConfig;
+112 -104
View File
@@ -1,106 +1,114 @@
{ {
"name": "portabase", "name": "portabase",
"version": "0.1.0", "version": "1.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 80", "dev": "next dev -p 80",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"email": "email dev" "email": "email dev",
}, "db:generate": "drizzle-kit generate",
"dependencies": { "db:migrate": "drizzle-kit migrate",
"@auth/prisma-adapter": "^2.7.4", "db:drop": "drizzle-kit drop",
"@hookform/resolvers": "^3.9.1", "auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
"@prisma/client": "^6.3.0", },
"@radix-ui/react-accordion": "^1.2.2", "dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.5", "@hookform/resolvers": "^5.0.1",
"@radix-ui/react-aspect-ratio": "^1.1.1", "@radix-ui/react-accordion": "^1.2.10",
"@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.1.13",
"@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-aspect-ratio": "^1.1.6",
"@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-avatar": "^1.1.9",
"@radix-ui/react-context-menu": "^2.2.5", "@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-dialog": "^1.1.5", "@radix-ui/react-collapsible": "^1.1.10",
"@radix-ui/react-dropdown-menu": "^2.1.5", "@radix-ui/react-context-menu": "^2.2.14",
"@radix-ui/react-hover-card": "^1.1.5", "@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-label": "^2.1.1", "@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-menubar": "^1.1.5", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-navigation-menu": "^1.2.4", "@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-popover": "^1.1.5", "@radix-ui/react-menubar": "^1.1.14",
"@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.12",
"@radix-ui/react-radio-group": "^1.2.2", "@radix-ui/react-popover": "^1.1.13",
"@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-progress": "^1.1.6",
"@radix-ui/react-select": "^2.1.5", "@radix-ui/react-radio-group": "^1.3.6",
"@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-scroll-area": "^1.2.8",
"@radix-ui/react-slider": "^1.2.2", "@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-slider": "^1.3.4",
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-toast": "^1.2.5", "@radix-ui/react-switch": "^1.2.4",
"@radix-ui/react-toggle": "^1.1.1", "@radix-ui/react-tabs": "^1.1.11",
"@radix-ui/react-toggle-group": "^1.1.1", "@radix-ui/react-toast": "^1.2.13",
"@radix-ui/react-tooltip": "^1.1.7", "@radix-ui/react-toggle": "^1.1.8",
"@react-email/components": "^0.0.32", "@radix-ui/react-toggle-group": "^1.1.9",
"@t3-oss/env-nextjs": "^0.12.0", "@radix-ui/react-tooltip": "^1.2.6",
"@tanstack/react-query": "^5.66.0", "@react-email/components": "^0.0.41",
"@tanstack/react-table": "^8.20.6", "@t3-oss/env-nextjs": "^0.13.4",
"@types/ws": "^8.5.13", "@tanstack/react-query": "^5.76.1",
"@zenstackhq/runtime": "2.10.2", "@tanstack/react-table": "^8.21.3",
"argon2": "^0.41.1", "@types/nodemailer": "^6.4.17",
"bcrypt": "^5.1.1", "@types/ws": "^8.18.1",
"class-variance-authority": "^0.7.1", "@zenstackhq/runtime": "2.14.2",
"clsx": "^2.1.1", "argon2": "^0.43.0",
"cmdk": "^1.0.4", "bcrypt": "^6.0.0",
"date-fns": "^4.1.0", "better-auth": "^1.2.8",
"dockerode": "^4.0.2", "class-variance-authority": "^0.7.1",
"embla-carousel-react": "^8.3.1", "clsx": "^2.1.1",
"input-otp": "^1.4.2", "cmdk": "^1.1.1",
"lucide-react": "^0.474.0", "date-fns": "^4.1.0",
"minio": "^8.0.2", "dockerode": "^4.0.6",
"next": "15.1.7", "dotenv": "^16.5.0",
"next-auth": "^5.0.0-beta.25", "drizzle-orm": "^0.43.1",
"next-safe-action": "^7.10.2", "drizzle-zod": "^0.7.1",
"next-themes": "^0.4.4", "embla-carousel-react": "^8.6.0",
"nodemailer": "^6.10.0", "input-otp": "^1.4.2",
"npm-check-updates": "^17.1.13", "lucide-react": "^0.510.0",
"react": "19.0.0", "minio": "^8.0.5",
"react-day-picker": "^9.5.1", "next": "^15.4.0-canary.36",
"react-dom": "19.0.0", "next-safe-action": "^7.10.8",
"react-email": "^3.0.6", "next-themes": "^0.4.6",
"react-hook-form": "^7.54.2", "nodemailer": "^7.0.3",
"react-resizable-panels": "^2.1.7", "npm-check-updates": "^18.0.1",
"react-twc": "^1.4.2", "pg": "^8.16.0",
"recharts": "^2.15.1", "react": "19.1.0",
"socket.io": "^4.8.1", "react-day-picker": "9.7.0",
"socket.io-client": "^4.8.1", "react-dom": "19.1.0",
"sonner": "^1.7.3", "react-dropzone": "^14.3.8",
"tailwind-merge": "^3.0.1", "react-email": "^4.0.13",
"tailwindcss-animate": "^1.0.7", "react-hook-form": "^7.56.3",
"uuid": "^11.0.5", "react-resizable-panels": "^3.0.2",
"vaul": "^1.1.2", "react-twc": "^1.4.2",
"ws": "^8.18.0", "recharts": "^2.15.3",
"zod": "^3.24.1" "socket.io": "^4.8.1",
}, "socket.io-client": "^4.8.1",
"devDependencies": { "sonner": "^2.0.3",
"@types/eslint-plugin-tailwindcss": "^3", "tailwind-merge": "^3.3.0",
"@types/node": "^22", "uuid": "^11.1.0",
"@types/pg": "^8", "vaul": "^1.1.2",
"@types/react": "^19", "ws": "^8.18.2",
"@types/react-dom": "^19", "zod": "^3.24.4"
"@zenstackhq/openapi": "^2.10.2", },
"@zenstackhq/tanstack-query": "^2.10.2", "devDependencies": {
"eslint": "^8", "@iconify/react": "^6.0.0",
"eslint-config-next": "15.1.6", "@tailwindcss/postcss": "^4.1.7",
"eslint-plugin-tailwindcss": "^3.17.5", "@types/eslint-plugin-tailwindcss": "^3.17.0",
"postcss": "^8", "@types/node": "^22.15.18",
"prisma": "^6.3.0", "@types/pg": "^8.15.2",
"tailwindcss": "^3.4.1", "@types/react": "^19.1.4",
"typescript": "^5", "@types/react-dom": "^19.1.5",
"zenstack": "2.10.2" "@zenstackhq/openapi": "^2.14.2",
}, "@zenstackhq/tanstack-query": "^2.14.2",
"overrides": { "drizzle-kit": "^0.31.1",
"@types/react": "npm:types-react@19.0.0-rc.1", "eslint": "^9.26.0",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" "eslint-config-next": "15.3.2",
} "eslint-plugin-tailwindcss": "^3.18.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.7",
"tsx": "^4.19.4",
"tw-animate-css": "^1.2.9",
"typescript": "^5.8.3",
"zenstack": "2.14.2"
},
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6"
} }
+1 -1
View File
@@ -1,7 +1,7 @@
/** @type {import('postcss-load-config').Config} */ /** @type {import('postcss-load-config').Config} */
const config = { const config = {
plugins: { plugins: {
tailwindcss: {}, '@tailwindcss/postcss': {},
}, },
}; };
@@ -1,66 +0,0 @@
-- CreateTable
CREATE TABLE "accounts" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"provider_account_id" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sessions" (
"id" TEXT NOT NULL,
"session_token" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "verificationtokens" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT,
"email_verified" TIMESTAMP(3),
"image" TEXT,
"role" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3),
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "accounts_provider_provider_account_id_key" ON "accounts"("provider", "provider_account_id");
-- CreateIndex
CREATE UNIQUE INDEX "sessions_session_token_key" ON "sessions"("session_token");
-- CreateIndex
CREATE UNIQUE INDEX "verificationtokens_identifier_token_key" ON "verificationtokens"("identifier", "token");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- AddForeignKey
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "password" TEXT;
@@ -1,46 +0,0 @@
-- CreateEnum
CREATE TYPE "Status" AS ENUM ('waiting', 'ongoing', 'failed', 'success');
-- CreateTable
CREATE TABLE "Agent" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"last_contact" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Agent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Database" (
"id" TEXT NOT NULL,
"agent_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"backup_policy" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Database_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Backup" (
"id" TEXT NOT NULL,
"database_id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"file" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Backup_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Restore" (
"id" TEXT NOT NULL,
"backup_id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Restore_pkey" PRIMARY KEY ("id")
);
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "authMethod" TEXT;
@@ -1,15 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[slug]` on the table `Agent` will be added. If there are existing duplicate values, this will fail.
- Added the required column `slug` to the `Agent` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Agent" ADD COLUMN "slug" TEXT NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "Agent_slug_key" ON "Agent"("slug");
-- AddForeignKey
ALTER TABLE "Database" ADD CONSTRAINT "Database_agent_id_fkey" FOREIGN KEY ("agent_id") REFERENCES "Agent"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,33 +0,0 @@
/*
Warnings:
- You are about to drop the column `authMethod` on the `users` table. All the data in the column will be lost.
- You are about to drop the `Restore` table. If the table is not empty, all the data it contains will be lost.
*/
-- AlterTable
ALTER TABLE "users" DROP COLUMN "authMethod",
ADD COLUMN "auth_method" TEXT;
-- DropTable
DROP TABLE "Restore";
-- CreateTable
CREATE TABLE "Restauration" (
"id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"backup_id" TEXT NOT NULL,
"database_id" TEXT,
CONSTRAINT "Restauration_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Backup" ADD CONSTRAINT "Backup_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "Database"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Restauration" ADD CONSTRAINT "Restauration_backup_id_fkey" FOREIGN KEY ("backup_id") REFERENCES "Backup"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Restauration" ADD CONSTRAINT "Restauration_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "Database"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,13 +0,0 @@
-- CreateEnum
CREATE TYPE "TypeStorage" AS ENUM ('local', 's3');
-- CreateTable
CREATE TABLE "Settings" (
"id" TEXT NOT NULL,
"storage" "TypeStorage" NOT NULL DEFAULT 'local',
"s3AccessKeyId" TEXT,
"s3SecretAccessKey" TEXT,
"S3BucketName" TEXT,
CONSTRAINT "Settings_pkey" PRIMARY KEY ("id")
);
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "s3EndPointUrl" TEXT;
@@ -1,6 +0,0 @@
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "smtpFrom" TEXT,
ADD COLUMN "smtpHost" TEXT,
ADD COLUMN "smtpPassword" TEXT,
ADD COLUMN "smtpPort" TEXT,
ADD COLUMN "smtpUser" TEXT;
@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `name` to the `Settings` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "name" TEXT NOT NULL;
@@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[name]` on the table `Settings` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "Settings_name_key" ON "Settings"("name");
@@ -1,118 +0,0 @@
/*
Warnings:
- You are about to drop the `Agent` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Backup` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Database` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Restauration` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Settings` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Backup" DROP CONSTRAINT "Backup_database_id_fkey";
-- DropForeignKey
ALTER TABLE "Database" DROP CONSTRAINT "Database_agent_id_fkey";
-- DropForeignKey
ALTER TABLE "Restauration" DROP CONSTRAINT "Restauration_backup_id_fkey";
-- DropForeignKey
ALTER TABLE "Restauration" DROP CONSTRAINT "Restauration_database_id_fkey";
-- DropTable
DROP TABLE "Agent";
-- DropTable
DROP TABLE "Backup";
-- DropTable
DROP TABLE "Database";
-- DropTable
DROP TABLE "Restauration";
-- DropTable
DROP TABLE "Settings";
-- CreateTable
CREATE TABLE "agents" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"last_contact" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "agents_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "databases" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"backup_policy" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"agent_id" TEXT NOT NULL,
CONSTRAINT "databases_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "backups" (
"id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"file" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"database_id" TEXT NOT NULL,
CONSTRAINT "backups_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "restaurations" (
"id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"backup_id" TEXT NOT NULL,
"database_id" TEXT,
CONSTRAINT "restaurations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "settings" (
"id" TEXT NOT NULL,
"storage" "TypeStorage" NOT NULL DEFAULT 'local',
"name" TEXT NOT NULL,
"s3EndPointUrl" TEXT,
"s3AccessKeyId" TEXT,
"s3SecretAccessKey" TEXT,
"S3BucketName" TEXT,
"smtpPassword" TEXT,
"smtpFrom" TEXT,
"smtpHost" TEXT,
"smtpPort" TEXT,
"smtpUser" TEXT,
CONSTRAINT "settings_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "agents_slug_key" ON "agents"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "settings_name_key" ON "settings"("name");
-- AddForeignKey
ALTER TABLE "databases" ADD CONSTRAINT "databases_agent_id_fkey" FOREIGN KEY ("agent_id") REFERENCES "agents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "backups" ADD CONSTRAINT "backups_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "databases"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "restaurations" ADD CONSTRAINT "restaurations_backup_id_fkey" FOREIGN KEY ("backup_id") REFERENCES "backups"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "restaurations" ADD CONSTRAINT "restaurations_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "databases"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,11 +0,0 @@
/*
Warnings:
- Added the required column `dbms` to the `databases` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "Dbms" AS ENUM ('postgresql', 'mysql', 'mongodb');
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "dbms" "Dbms" NOT NULL;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "deleted" BOOLEAN;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "users" ALTER COLUMN "deleted" SET DEFAULT false;
@@ -1,12 +0,0 @@
/*
Warnings:
- Added the required column `role` to the `users` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('pending', 'user', 'admin');
-- AlterTable
ALTER TABLE "users" DROP COLUMN "role",
ADD COLUMN "role" "Role" NOT NULL;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "last_contact" TIMESTAMP(3);
@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `generatedId` to the `databases` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "generatedId" TEXT NOT NULL;
@@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[generatedId]` on the table `databases` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "databases_generatedId_key" ON "databases"("generatedId");
@@ -1,9 +0,0 @@
/*
Warnings:
- Added the required column `backupToRestore` to the `databases` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "backupToRestore" TEXT NOT NULL,
ADD COLUMN "isWaitingForBackup" BOOLEAN NOT NULL DEFAULT false;
@@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "databases" ALTER COLUMN "backupToRestore" DROP NOT NULL,
ALTER COLUMN "isWaitingForBackup" DROP NOT NULL;
@@ -1,35 +0,0 @@
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "project_id" TEXT;
-- CreateTable
CREATE TABLE "organizations" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"name" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "organizations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "projects" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"name" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"organization_id" TEXT NOT NULL,
CONSTRAINT "projects_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "organizations_slug_key" ON "organizations"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "projects_slug_key" ON "projects"("slug");
-- AddForeignKey
ALTER TABLE "projects" ADD CONSTRAINT "projects_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "databases" ADD CONSTRAINT "databases_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -1,17 +0,0 @@
-- CreateTable
CREATE TABLE "_OrganizationToUser" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "_OrganizationToUser_AB_unique" ON "_OrganizationToUser"("A", "B");
-- CreateIndex
CREATE INDEX "_OrganizationToUser_B_index" ON "_OrganizationToUser"("B");
-- AddForeignKey
ALTER TABLE "_OrganizationToUser" ADD CONSTRAINT "_OrganizationToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_OrganizationToUser" ADD CONSTRAINT "_OrganizationToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,32 +0,0 @@
/*
Warnings:
- You are about to drop the `_OrganizationToUser` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "_OrganizationToUser" DROP CONSTRAINT "_OrganizationToUser_A_fkey";
-- DropForeignKey
ALTER TABLE "_OrganizationToUser" DROP CONSTRAINT "_OrganizationToUser_B_fkey";
-- DropTable
DROP TABLE "_OrganizationToUser";
-- CreateTable
CREATE TABLE "UserOrganization" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
CONSTRAINT "UserOrganization_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "UserOrganization_userId_organizationId_key" ON "UserOrganization"("userId", "organizationId");
-- AddForeignKey
ALTER TABLE "UserOrganization" ADD CONSTRAINT "UserOrganization_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserOrganization" ADD CONSTRAINT "UserOrganization_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -1,32 +0,0 @@
/*
Warnings:
- You are about to drop the `UserOrganization` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "UserOrganization" DROP CONSTRAINT "UserOrganization_organizationId_fkey";
-- DropForeignKey
ALTER TABLE "UserOrganization" DROP CONSTRAINT "UserOrganization_userId_fkey";
-- DropTable
DROP TABLE "UserOrganization";
-- CreateTable
CREATE TABLE "users_organisations" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
CONSTRAINT "users_organisations_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_organisations_userId_organizationId_key" ON "users_organisations"("userId", "organizationId");
-- AddForeignKey
ALTER TABLE "users_organisations" ADD CONSTRAINT "users_organisations_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "users_organisations" ADD CONSTRAINT "users_organisations_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -1,31 +0,0 @@
/*
Warnings:
- You are about to drop the `restaurations` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "restaurations" DROP CONSTRAINT "restaurations_backup_id_fkey";
-- DropForeignKey
ALTER TABLE "restaurations" DROP CONSTRAINT "restaurations_database_id_fkey";
-- DropTable
DROP TABLE "restaurations";
-- CreateTable
CREATE TABLE "restorations" (
"id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"backup_id" TEXT NOT NULL,
"database_id" TEXT,
CONSTRAINT "restorations_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "restorations" ADD CONSTRAINT "restorations_backup_id_fkey" FOREIGN KEY ("backup_id") REFERENCES "backups"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "restorations" ADD CONSTRAINT "restorations_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "databases"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,45 +0,0 @@
/*
Warnings:
- The required column `id` was added to the `verificationtokens` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
*/
-- AlterTable
ALTER TABLE "accounts" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "agents" ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "backups" ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "organizations" ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "projects" ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "restorations" ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "sessions" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "settings" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "users_organisations" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "verificationtokens" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "id" TEXT NOT NULL,
ADD COLUMN "updated_at" TIMESTAMP(3),
ADD CONSTRAINT "verificationtokens_pkey" PRIMARY KEY ("id");
@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `isArchived` to the `projects` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "projects" ADD COLUMN "isArchived" BOOLEAN NOT NULL;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "projects" ALTER COLUMN "isArchived" SET DEFAULT false;
@@ -1,9 +0,0 @@
/*
Warnings:
- You are about to drop the column `isArchived` on the `projects` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "projects" DROP COLUMN "isArchived",
ADD COLUMN "is_archived" BOOLEAN NOT NULL DEFAULT false;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "organizations" ADD COLUMN "deleted" BOOLEAN DEFAULT false;
@@ -1,11 +0,0 @@
/*
Warnings:
- Added the required column `role` to the `users_organisations` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "OrganizationRole" AS ENUM ('member', 'admin');
-- AlterTable
ALTER TABLE "users_organisations" ADD COLUMN "role" "OrganizationRole" NOT NULL;
-3
View File
@@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
-228
View File
@@ -1,228 +0,0 @@
//////////////////////////////////////////////////////////////////////////////////////////////
// DO NOT MODIFY THIS FILE //
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
//////////////////////////////////////////////////////////////////////////////////////////////
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl-openssl-3.0.x", "debian-openssl-3.0.x"]
}
enum Role {
pending
user
admin
}
enum OrganizationRole {
member
admin
}
enum Dbms {
postgresql
mysql
mongodb
}
enum Status {
waiting
ongoing
failed
success
}
enum TypeStorage {
local
s3
}
model Account {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
userId String @map("user_id")
type String
provider String
providerAccountId String @map("provider_account_id")
refresh_token String? @db.Text()
access_token String? @db.Text()
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text()
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map("accounts")
}
model Session {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
sessionToken String @unique() @map("session_token")
userId String @map("user_id")
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
model VerificationToken {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
identifier String
token String
expires DateTime
@@unique([identifier, token])
@@map("verificationtokens")
}
model User {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
name String?
email String? @unique()
emailVerified DateTime? @map("email_verified")
image String?
role Role
password String?
authMethod String? @map("auth_method")
deleted Boolean? @default(false)
accounts Account[]
sessions Session[]
organizations UserOrganization[]
@@map("users")
}
model Organization {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
slug String @unique()
name String
projects Project[]
users UserOrganization[]
deleted Boolean? @default(false)
@@map("organizations")
}
model UserOrganization {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
userId String
organizationId String
user User @relation(fields: [userId], references: [id])
organization Organization @relation(fields: [organizationId], references: [id])
role OrganizationRole
@@unique([userId, organizationId])
@@map("users_organisations")
}
model Project {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
slug String @unique()
name String
isArchived Boolean @default(false) @map("is_archived")
organizationId String @map("organization_id")
organization Organization @relation(fields: [organizationId], references: [id])
databases Database[]
@@map("projects")
}
model Agent {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
slug String @unique()
name String
description String?
lastContact DateTime? @map("last_contact")
databases Database[]
@@map("agents")
}
model Database {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
name String
dbms Dbms
generatedId String @unique()
description String?
backupPolicy String? @map("backup_policy")
isWaitingForBackup Boolean? @default(false)
backupToRestore String?
agentId String @map("agent_id")
agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
lastContact DateTime? @map("last_contact")
backups Backup[]
restorations Restoration[]
projectId String? @map("project_id")
project Project? @relation(fields: [projectId], references: [id])
@@map("databases")
}
model Backup {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
status Status @default(waiting)
file String?
databaseId String @map("database_id")
database Database @relation(fields: [databaseId], references: [id], onDelete: Cascade)
restorations Restoration[]
@@map("backups")
}
model Restoration {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
status Status @default(waiting)
backupId String @map("backup_id")
backup Backup @relation(fields: [backupId], references: [id], onDelete: Cascade)
databaseId String? @map("database_id")
database Database? @relation(fields: [databaseId], references: [id], onDelete: Cascade)
@@map("restorations")
}
model Settings {
id String @id() @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt() @map("updated_at")
storage TypeStorage @default(local)
name String @unique()
s3EndPointUrl String?
s3AccessKeyId String?
s3SecretAccessKey String?
S3BucketName String?
smtpPassword String?
smtpFrom String?
smtpHost String?
smtpPort String?
smtpUser String?
@@map("settings")
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

-240
View File
@@ -1,240 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
// https://zenstack.dev/docs/guides/check-permission
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl-openssl-3.0.x", "debian-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
plugin openapi {
provider = "@zenstackhq/openapi"
output = "./src/openapi.yaml"
}
plugin tanstack_query {
provider = "@zenstackhq/tanstack-query"
output = "./src/lib/hooks"
target = "react"
}
plugin enhancer {
provider = '@core/enhancer'
generatePermissionChecker = true
}
abstract model Base {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt @map("updated_at")
}
model Account extends Base {
userId String @map("user_id")
type String
provider String
providerAccountId String @map("provider_account_id")
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map("accounts")
}
model Session extends Base {
sessionToken String @unique @map("session_token")
userId String @map("user_id")
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
model VerificationToken extends Base {
identifier String
token String
expires DateTime
@@unique([identifier, token])
@@map("verificationtokens")
}
model User extends Base {
name String?
email String? @unique @email
emailVerified DateTime? @map("email_verified")
image String?
role Role
password String? @omit @password
authMethod String? @map("auth_method")
deleted Boolean? @default(false)
accounts Account[]
sessions Session[]
organizations UserOrganization[]
@@map("users")
}
enum Role {
pending
user
admin
}
model Organization extends Base {
slug String @unique
name String
projects Project[]
users UserOrganization[]
deleted Boolean? @default(false)
// Allow only users who are part of the organization
@@allow('all', users?[user == auth()])
@@map("organizations")
}
enum OrganizationRole {
member
admin
}
model UserOrganization extends Base {
userId String
organizationId String
user User @relation(fields: [userId], references: [id])
organization Organization @relation(fields: [organizationId], references: [id])
role OrganizationRole
@@unique([userId, organizationId])
@@map("users_organisations")
}
model Project extends Base {
slug String @unique
name String
isArchived Boolean @map("is_archived") @default(false)
organizationId String @map("organization_id")
organization Organization @relation(fields: [organizationId], references: [id])
databases Database[]
@@map("projects")
}
model Agent extends Base {
slug String @unique
name String
description String?
lastContact DateTime? @map("last_contact")
databases Database[]
// Restrict access to users with role "admin"
@@allow('all', auth().role == "admin")
@@map("agents")
}
enum Dbms {
postgresql
mysql
mongodb
}
model Database extends Base {
name String
dbms Dbms
generatedId String @unique
description String?
backupPolicy String? @map("backup_policy")
isWaitingForBackup Boolean? @default(false)
backupToRestore String?
agentId String @map("agent_id")
agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
lastContact DateTime? @map("last_contact")
backups Backup[]
restorations Restoration[]
projectId String? @map("project_id")
project Project? @relation(fields: [projectId], references: [id])
@@map("databases")
}
enum Status {
waiting
ongoing
failed
success
}
model Backup extends Base {
status Status @default(waiting)
file String?
databaseId String @map("database_id")
database Database @relation(fields: [databaseId], references: [id], onDelete: Cascade)
restorations Restoration[]
@@map("backups")
}
model Restoration extends Base {
status Status @default(waiting)
backupId String @map("backup_id")
backup Backup @relation(fields: [backupId], references: [id], onDelete: Cascade)
databaseId String? @map("database_id")
database Database? @relation(fields: [databaseId], references: [id], onDelete: Cascade)
@@map("restorations")
}
enum TypeStorage {
local
s3
}
model Settings extends Base {
storage TypeStorage @default(local)
name String @unique
s3EndPointUrl String?
s3AccessKeyId String?
s3SecretAccessKey String?
S3BucketName String?
smtpPassword String?
smtpFrom String?
smtpHost String?
smtpPort String?
smtpUser String?
// Restrict access to users with role "admin"
@@allow('all', auth().role == "admin")
@@map("settings")
}
-162
View File
@@ -1,162 +0,0 @@
import NextAuth, {CredentialsSignin} from "next-auth";
import {prisma} from "@/prisma";
import {PrismaAdapter} from "@auth/prisma-adapter";
import Credentials from "next-auth/providers/credentials";
import {env} from "@/env.mjs";
import GoogleProvider from "next-auth/providers/google";
export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({
adapter: PrismaAdapter(prisma),
theme: {
logo: "/logo.png"
},
secret: env.NEXT_PUBLIC_SECRET,
debug: process.env.NODE_ENV !== "production",
providers: [
Credentials({
name: "Credentials",
credentials: {
email: {label: "Email", type: "email"},
password: {label: "Password", type: "password"},
},
authorize: async (credentials) => {
let user = null
const argon2 = require('argon2');
console.log(credentials);
user = await prisma.user.findFirst({
where: {
email: credentials.email,
}
})
if (!user || user.role === "pending") {
return null
}
const isValid = await argon2.verify(user.password, credentials.password)
if (!isValid) {
return null
}
return user;
},
}),
GoogleProvider({
clientId: env.AUTH_GOOGLE_ID,
clientSecret: env.AUTH_GOOGLE_SECRET,
allowDangerousEmailAccountLinking: true,
async profile(profile) {
const users = await prisma.user.findMany({
where: {
deleted: {not: true},
}
})
console.log(users.length)
const role = users.length > 0 ? "pending" : "admin"
return {
role: role,
name: profile.name,
email: profile.email,
image: profile.picture,
email_verified: profile.email_verified,
authMethod: "google",
}
},
})
],
session: {
strategy: "jwt",
},
pages: {
signIn: "/login",
error: "/error"
},
callbacks: {
// session({session, user, token}) {
// // session.user.role = user.role
// // session.user.authMethod = user.authMethod;
// return session
// },
async jwt({token, trigger, session, user}) {
if (trigger === "update" && session) {
return {...token, ...session?.user};
}
return {...token, ...user};
},
async session({session, token, user}) {
session.user = token;
return session;
},
async signIn({account, user, profile}) {
const existingUser = await prisma.user.findFirst({
where: {email: user.email},
});
if (!existingUser) {
// Create a new user with a pending role
const users = await prisma.user.findMany({
where: {
deleted: {not: true},
}
})
const role = users.length > 0 ? "pending" : "admin"
const newUser = await prisma.user.create({
data: {
email: user.email,
name: user.name,
image: user.image,
role: role,
authMethod: account.provider,
},
});
const defaultOrganization = await prisma.organization.findUnique({
where: {slug: "default"},
include: {users: {
include:{
user: true
},
where:{
user: {
deleted: {not: true},
}
}
}}
});
const organizationRole = defaultOrganization.users.length > 0 ? "member" : "admin"
await prisma.userOrganization.create({
data: {
userId: newUser.id,
organizationId: defaultOrganization.id,
role: organizationRole
},
});
return role !== "pending";
}
if (existingUser.role === "pending") {
return false; // Prevent login if role is pending
}
// Update auth method if user exists
await prisma.user.update({
where: {id: existingUser.id},
data: {
authMethod: account.provider,
},
});
return true; // Allow login
},
}
});
-29
View File
@@ -1,29 +0,0 @@
"use server"
import {baseAuth} from "@/auth/auth";
import {User} from "@prisma/client";
import {prisma} from "@/prisma";
export const currentUser = async () => {
const session = await baseAuth();
if (!session?.user) {
return null;
}
return session.user as User;
}
export const requiredCurrentUser = async () => {
const user = await currentUser();
if (!user) {
return null
}
const userDb = await prisma.user.findFirst({
where: {id: user.id},
})
if(!userDb){
return null
}
return user;
}
+6 -13
View File
@@ -1,16 +1,9 @@
import {twx} from "@/lib/twx"; import { twx } from "@/lib/twx";
import {cn} from "@/lib/utils"; import { cn } from "@/lib/utils";
export const LayoutAdmin = twx.div((props) => [`w-full h-screen flex flex-col gap-4 mx-auto `]);
export const Layout = twx.div((props) => [`max-w-5xl w-full flex-col flex gap-4 mx-auto px-4 `]);
export const LayoutAdmin = twx.div((props) => [ export const LayoutTitle = twx.h1((props) => [cn(`text-4xl font-bold mt-5 `, props.className)]);
`w-full h-screen flex flex-col gap-4 mx-auto `
]);
export const Layout = twx.div((props)=>[
`max-w-5xl w-full flex-col flex gap-4 mx-auto px-4 `,
])
export const LayoutTitle = twx.h1((props)=>[ export const LayoutDescription = twx.p((props) => [`text-lg text-muted-foreground`]);
cn(`text-4xl font-bold mt-5 `, props.className)
])
export const LayoutDescription = twx.p((props)=>[`text-lg text-muted-foreground`])
+53 -44
View File
@@ -2,56 +2,65 @@
import * as React from "react" import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion" import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "@radix-ui/react-icons" import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
const AccordionItem = React.forwardRef< function AccordionItem({
React.ElementRef<typeof AccordionPrimitive.Item>, className,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
<AccordionPrimitive.Item return (
ref={ref} <AccordionPrimitive.Item
className={cn("border-b", className)} data-slot="accordion-item"
{...props} className={cn("border-b last:border-b-0", className)}
/> {...props}
)) />
AccordionItem.displayName = "AccordionItem" )
}
const AccordionTrigger = React.forwardRef< function AccordionTrigger({
React.ElementRef<typeof AccordionPrimitive.Trigger>, className,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> children,
>(({ className, children, ...props }, ref) => ( ...props
<AccordionPrimitive.Header className="flex"> }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
<AccordionPrimitive.Trigger return (
ref={ref} <AccordionPrimitive.Header className="flex">
className={cn( <AccordionPrimitive.Trigger
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180", data-slot="accordion-trigger"
className className={cn(
)} "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props} {...props}
> >
{children} <div className={cn("pt-0 pb-4", className)}>{children}</div>
<ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" /> </AccordionPrimitive.Content>
</AccordionPrimitive.Trigger> )
</AccordionPrimitive.Header> }
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
+117 -101
View File
@@ -6,125 +6,141 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button" import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
const AlertDialogTrigger = AlertDialogPrimitive.Trigger function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
const AlertDialogPortal = AlertDialogPrimitive.Portal function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
const AlertDialogOverlay = React.forwardRef< function AlertDialogOverlay({
React.ElementRef<typeof AlertDialogPrimitive.Overlay>, className,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
<AlertDialogPrimitive.Overlay return (
className={cn( <AlertDialogPrimitive.Overlay
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", data-slot="alert-dialog-overlay"
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "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}
/> />
</AlertDialogPortal> )
)) }
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({ function AlertDialogContent({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
<div return (
className={cn( <AlertDialogPortal>
"flex flex-col space-y-2 text-center sm:text-left", <AlertDialogOverlay />
className <AlertDialogPrimitive.Content
)} data-slot="alert-dialog-content"
{...props} className={cn(
/> "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
) className
AlertDialogHeader.displayName = "AlertDialogHeader" )}
{...props}
/>
</AlertDialogPortal>
)
}
const AlertDialogFooter = ({ function AlertDialogHeader({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.ComponentProps<"div">) {
<div return (
className={cn( <div
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", data-slot="alert-dialog-header"
className className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
)} {...props}
{...props} />
/> )
) }
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef< function AlertDialogFooter({
React.ElementRef<typeof AlertDialogPrimitive.Title>, className,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<"div">) {
<AlertDialogPrimitive.Title return (
ref={ref} <div
className={cn("text-lg font-semibold", className)} data-slot="alert-dialog-footer"
{...props} className={cn(
/> "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
)) className
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName )}
{...props}
/>
)
}
const AlertDialogDescription = React.forwardRef< function AlertDialogTitle({
React.ElementRef<typeof AlertDialogPrimitive.Description>, className,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
<AlertDialogPrimitive.Description return (
ref={ref} <AlertDialogPrimitive.Title
className={cn("text-sm text-muted-foreground", className)} data-slot="alert-dialog-title"
{...props} className={cn("text-lg font-semibold", className)}
/> {...props}
)) />
AlertDialogDescription.displayName = )
AlertDialogPrimitive.Description.displayName }
const AlertDialogAction = React.forwardRef< function AlertDialogDescription({
React.ElementRef<typeof AlertDialogPrimitive.Action>, className,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
<AlertDialogPrimitive.Action return (
ref={ref} <AlertDialogPrimitive.Description
className={cn(buttonVariants(), className)} data-slot="alert-dialog-description"
{...props} className={cn("text-muted-foreground text-sm", className)}
/> {...props}
)) />
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName )
}
const AlertDialogCancel = React.forwardRef< function AlertDialogAction({
React.ElementRef<typeof AlertDialogPrimitive.Cancel>, className,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
<AlertDialogPrimitive.Cancel return (
ref={ref} <AlertDialogPrimitive.Action
className={cn( className={cn(buttonVariants(), className)}
buttonVariants({ variant: "outline" }), {...props}
"mt-2 sm:mt-0", />
className )
)} }
{...props}
/> function AlertDialogCancel({
)) className,
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export { export {
AlertDialog, AlertDialog,
+44 -37
View File
@@ -4,13 +4,13 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const alertVariants = cva( const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-background text-foreground", default: "bg-card text-card-foreground",
destructive: destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -19,41 +19,48 @@ const alertVariants = cva(
} }
) )
const Alert = React.forwardRef< function Alert({
HTMLDivElement, className,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants> variant,
>(({ className, variant, ...props }, ref) => ( ...props
<div }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
ref={ref} return (
role="alert" <div
className={cn(alertVariants({ variant }), className)} data-slot="alert"
{...props} role="alert"
/> className={cn(alertVariants({ variant }), className)}
)) {...props}
Alert.displayName = "Alert" />
)
}
const AlertTitle = React.forwardRef< function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
HTMLParagraphElement, return (
React.HTMLAttributes<HTMLHeadingElement> <div
>(({ className, ...props }, ref) => ( data-slot="alert-title"
<h5 className={cn(
ref={ref} "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className={cn("mb-1 font-medium leading-none tracking-tight", className)} className
{...props} )}
/> {...props}
)) />
AlertTitle.displayName = "AlertTitle" )
}
const AlertDescription = React.forwardRef< function AlertDescription({
HTMLParagraphElement, className,
React.HTMLAttributes<HTMLParagraphElement> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<"div">) {
<div return (
ref={ref} <div
className={cn("text-sm [&_p]:leading-relaxed", className)} data-slot="alert-description"
{...props} className={cn(
/> "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
)) className
AlertDescription.displayName = "AlertDescription" )}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription } export { Alert, AlertTitle, AlertDescription }
+5 -1
View File
@@ -2,6 +2,10 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root function AspectRatio({
...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
}
export { AspectRatio } export { AspectRatio }
+42 -39
View File
@@ -5,46 +5,49 @@ import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const Avatar = React.forwardRef< function Avatar({
React.ElementRef<typeof AvatarPrimitive.Root>, className,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
<AvatarPrimitive.Root return (
ref={ref} <AvatarPrimitive.Root
className={cn( data-slot="avatar"
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className={cn(
className "relative flex size-8 shrink-0 overflow-hidden rounded-full",
)} className
{...props} )}
/> {...props}
)) />
Avatar.displayName = AvatarPrimitive.Root.displayName )
}
const AvatarImage = React.forwardRef< function AvatarImage({
React.ElementRef<typeof AvatarPrimitive.Image>, className,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
<AvatarPrimitive.Image return (
ref={ref} <AvatarPrimitive.Image
className={cn("aspect-square h-full w-full", className)} data-slot="avatar-image"
{...props} className={cn("aspect-square size-full", className)}
/> {...props}
)) />
AvatarImage.displayName = AvatarPrimitive.Image.displayName )
}
const AvatarFallback = React.forwardRef< function AvatarFallback({
React.ElementRef<typeof AvatarPrimitive.Fallback>, className,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
<AvatarPrimitive.Fallback return (
ref={ref} <AvatarPrimitive.Fallback
className={cn( data-slot="avatar-fallback"
"flex h-full w-full items-center justify-center rounded-full bg-muted", className={cn(
className "bg-muted flex size-full items-center justify-center rounded-full",
)} className
{...props} )}
/> {...props}
)) />
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName )
}
export { Avatar, AvatarImage, AvatarFallback } export { Avatar, AvatarImage, AvatarFallback }
+20 -10
View File
@@ -1,20 +1,22 @@
import * as React from "react" import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{ {
variants: { variants: {
variant: { variant: {
default: default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary: secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive: destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: "text-foreground", outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -23,13 +25,21 @@ const badgeVariants = cva(
} }
) )
export interface BadgeProps function Badge({
extends React.HTMLAttributes<HTMLDivElement>, className,
VariantProps<typeof badgeVariants> {} variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
function Badge({ className, variant, ...props }: BadgeProps) {
return ( return (
<div className={cn(badgeVariants({ variant }), className)} {...props} /> <Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
) )
} }
+76 -83
View File
@@ -1,108 +1,101 @@
import * as React from "react" import * as React from "react"
import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
const Breadcrumb = React.forwardRef< function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
HTMLElement, return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
React.ComponentPropsWithoutRef<"nav"> & { }
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef< function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
HTMLOListElement, return (
React.ComponentPropsWithoutRef<"ol"> <ol
>(({ className, ...props }, ref) => ( data-slot="breadcrumb-list"
<ol className={cn(
ref={ref} "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className={cn( className
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", )}
className {...props}
)} />
{...props} )
/> }
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef< function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
HTMLLIElement, return (
React.ComponentPropsWithoutRef<"li"> <li
>(({ className, ...props }, ref) => ( data-slot="breadcrumb-item"
<li className={cn("inline-flex items-center gap-1.5", className)}
ref={ref} {...props}
className={cn("inline-flex items-center gap-1.5", className)} />
{...props} )
/> }
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef< function BreadcrumbLink({
HTMLAnchorElement, asChild,
React.ComponentPropsWithoutRef<"a"> & { className,
asChild?: boolean ...props
} }: React.ComponentProps<"a"> & {
>(({ asChild, className, ...props }, ref) => { asChild?: boolean
}) {
const Comp = asChild ? Slot : "a" const Comp = asChild ? Slot : "a"
return ( return (
<Comp <Comp
ref={ref} data-slot="breadcrumb-link"
className={cn("transition-colors hover:text-foreground", className)} className={cn("hover:text-foreground transition-colors", className)}
{...props} {...props}
/> />
) )
}) }
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef< function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
HTMLSpanElement, return (
React.ComponentPropsWithoutRef<"span"> <span
>(({ className, ...props }, ref) => ( data-slot="breadcrumb-page"
<span role="link"
ref={ref} aria-disabled="true"
role="link" aria-current="page"
aria-disabled="true" className={cn("text-foreground font-normal", className)}
aria-current="page" {...props}
className={cn("font-normal text-foreground", className)} />
{...props} )
/> }
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({ function BreadcrumbSeparator({
children, children,
className, className,
...props ...props
}: React.ComponentProps<"li">) => ( }: React.ComponentProps<"li">) {
<li return (
role="presentation" <li
aria-hidden="true" data-slot="breadcrumb-separator"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)} role="presentation"
{...props} aria-hidden="true"
> className={cn("[&>svg]:size-3.5", className)}
{children ?? <ChevronRightIcon />} {...props}
</li> >
) {children ?? <ChevronRightIcon />}
BreadcrumbSeparator.displayName = "BreadcrumbSeparator" </li>
)
}
const BreadcrumbEllipsis = ({ function BreadcrumbEllipsis({
className, className,
...props ...props
}: React.ComponentProps<"span">) => ( }: React.ComponentProps<"span">) {
<span return (
role="presentation" <span
aria-hidden="true" data-slot="breadcrumb-ellipsis"
className={cn("flex h-9 w-9 items-center justify-center", className)} role="presentation"
{...props} aria-hidden="true"
> className={cn("flex size-9 items-center justify-center", className)}
<DotsHorizontalIcon className="h-4 w-4" /> {...props}
<span className="sr-only">More</span> >
</span> <DotsHorizontalIcon className="size-4" />
) <span className="sr-only">More</span>
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis" </span>
)
}
export { export {
Breadcrumb, Breadcrumb,
+30 -28
View File
@@ -5,26 +5,27 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{ {
variants: { variants: {
variant: { variant: {
default: default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90", "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive: destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary: secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground", ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2", default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md px-3 text-xs", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-8", lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "h-9 w-9", icon: "size-9",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -34,24 +35,25 @@ const buttonVariants = cva(
} }
) )
export interface ButtonProps function Button({
extends React.ButtonHTMLAttributes<HTMLButtonElement>, className,
VariantProps<typeof buttonVariants> { variant,
asChild?: boolean size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants } export { Button, buttonVariants }
+51 -66
View File
@@ -1,72 +1,57 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { DayPicker } from "react-day-picker" import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button" import { buttonVariants } from "@/components/ui/button";
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons" import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";
export type CalendarProps = React.ComponentProps<typeof DayPicker> export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({ function Calendar({ className, classNames, showOutsideDays = true, ...props }: React.ComponentProps<typeof DayPicker>) {
className, return (
classNames, <DayPicker
showOutsideDays = true, showOutsideDays={showOutsideDays}
...props className={cn("p-3", className)}
}: CalendarProps) { classNames={{
return ( months: "flex flex-col sm:flex-row gap-2",
<DayPicker month: "flex flex-col gap-4",
showOutsideDays={showOutsideDays} caption: "flex justify-center pt-1 relative items-center w-full",
className={cn("p-3", className)} caption_label: "text-sm font-medium",
classNames={{ nav: "flex items-center gap-1",
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", nav_button: cn(buttonVariants({ variant: "outline" }), "size-7 bg-transparent p-0 opacity-50 hover:opacity-100"),
month: "space-y-4", nav_button_previous: "absolute left-1",
caption: "flex justify-center pt-1 relative items-center", nav_button_next: "absolute right-1",
caption_label: "text-sm font-medium", table: "w-full border-collapse space-x-1",
nav: "space-x-1 flex items-center", head_row: "flex",
nav_button: cn( head_cell: "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
buttonVariants({ variant: "outline" }), row: "flex w-full mt-2",
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100" cell: cn(
), "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
nav_button_previous: "absolute left-1", props.mode === "range"
nav_button_next: "absolute right-1", ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
table: "w-full border-collapse space-y-1", : "[&:has([aria-selected])]:rounded-md"
head_row: "flex", ),
head_cell: day: cn(buttonVariants({ variant: "ghost" }), "size-8 p-0 font-normal aria-selected:opacity-100"),
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]", day_range_start: "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
row: "flex w-full mt-2", day_range_end: "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
cell: cn( day_selected:
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md", "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
props.mode === "range" day_today: "bg-accent text-accent-foreground",
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" day_outside: "day-outside text-muted-foreground aria-selected:text-muted-foreground",
: "[&:has([aria-selected])]:rounded-md" day_disabled: "text-muted-foreground opacity-50",
), day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
day: cn( day_hidden: "invisible",
buttonVariants({ variant: "ghost" }), ...classNames,
"h-8 w-8 p-0 font-normal aria-selected:opacity-100" }}
), components={{
day_range_start: "day-range-start", IconLeft: ({ className, children: _children, ...props }) => <ChevronLeftIcon className={cn("size-4", className)} {...props} />,
day_range_end: "day-range-end", IconRight: ({ className, children: _children, ...props }) => <ChevronRightIcon className={cn("size-4", className)} {...props} />,
day_selected: }}
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", {...props}
day_today: "bg-accent text-accent-foreground", />
day_outside: );
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />,
}}
{...props}
/>
)
} }
Calendar.displayName = "Calendar"
export { Calendar } export { Calendar };
+82 -66
View File
@@ -2,75 +2,91 @@ import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const Card = React.forwardRef< function Card({ className, ...props }: React.ComponentProps<"div">) {
HTMLDivElement, return (
React.HTMLAttributes<HTMLDivElement> <div
>(({ className, ...props }, ref) => ( data-slot="card"
<div className={cn(
ref={ref} "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className={cn( className
"rounded-xl border bg-card text-card-foreground shadow", )}
className {...props}
)} />
{...props} )
/> }
))
Card.displayName = "Card"
const CardHeader = React.forwardRef< function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
HTMLDivElement, return (
React.HTMLAttributes<HTMLDivElement> <div
>(({ className, ...props }, ref) => ( data-slot="card-header"
<div className={cn(
ref={ref} "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className={cn("flex flex-col space-y-1.5 p-6", className)} className
{...props} )}
/> {...props}
)) />
CardHeader.displayName = "CardHeader" )
}
const CardTitle = React.forwardRef< function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
HTMLDivElement, return (
React.HTMLAttributes<HTMLDivElement> <div
>(({ className, ...props }, ref) => ( data-slot="card-title"
<div className={cn("leading-none font-semibold", className)}
ref={ref} {...props}
className={cn("font-semibold leading-none tracking-tight", className)} />
{...props} )
/> }
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef< function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
HTMLDivElement, return (
React.HTMLAttributes<HTMLDivElement> <div
>(({ className, ...props }, ref) => ( data-slot="card-description"
<div className={cn("text-muted-foreground text-sm", className)}
ref={ref} {...props}
className={cn("text-sm text-muted-foreground", className)} />
{...props} )
/> }
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef< function CardAction({ className, ...props }: React.ComponentProps<"div">) {
HTMLDivElement, return (
React.HTMLAttributes<HTMLDivElement> <div
>(({ className, ...props }, ref) => ( data-slot="card-action"
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> className={cn(
)) "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
CardContent.displayName = "CardContent" className
)}
{...props}
/>
)
}
const CardFooter = React.forwardRef< function CardContent({ className, ...props }: React.ComponentProps<"div">) {
HTMLDivElement, return (
React.HTMLAttributes<HTMLDivElement> <div
>(({ className, ...props }, ref) => ( data-slot="card-content"
<div className={cn("px-6", className)}
ref={ref} {...props}
className={cn("flex items-center p-6 pt-0", className)} />
{...props} )
/> }
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
+113 -135
View File
@@ -1,13 +1,12 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"
import useEmblaCarousel, { import useEmblaCarousel, {
type UseEmblaCarouselType, type UseEmblaCarouselType,
} from "embla-carousel-react" } from "embla-carousel-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"
type CarouselApi = UseEmblaCarouselType[1] type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel> type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
@@ -42,124 +41,106 @@ function useCarousel() {
return context return context
} }
const Carousel = React.forwardRef< function Carousel({
HTMLDivElement, orientation = "horizontal",
React.HTMLAttributes<HTMLDivElement> & CarouselProps opts,
>( setApi,
( plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{ {
orientation = "horizontal", ...opts,
opts, axis: orientation === "horizontal" ? "x" : "y",
setApi,
plugins,
className,
children,
...props
}, },
ref plugins
) => { )
const [carouselRef, api] = useEmblaCarousel( const [canScrollPrev, setCanScrollPrev] = React.useState(false)
{ const [canScrollNext, setCanScrollNext] = React.useState(false)
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => { const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) { if (!api) return
return setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
} }
},
[scrollPrev, scrollNext]
)
setCanScrollPrev(api.canScrollPrev()) React.useEffect(() => {
setCanScrollNext(api.canScrollNext()) if (!api || !setApi) return
}, []) setApi(api)
}, [api, setApi])
const scrollPrev = React.useCallback(() => { React.useEffect(() => {
api?.scrollPrev() if (!api) return
}, [api]) onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
const scrollNext = React.useCallback(() => { return () => {
api?.scrollNext() api?.off("select", onSelect)
}, [api]) }
}, [api, onSelect])
const handleKeyDown = React.useCallback( return (
(event: React.KeyboardEvent<HTMLDivElement>) => { <CarouselContext.Provider
if (event.key === "ArrowLeft") { value={{
event.preventDefault() carouselRef,
scrollPrev() api: api,
} else if (event.key === "ArrowRight") { opts,
event.preventDefault() orientation:
scrollNext() orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
} scrollPrev,
}, scrollNext,
[scrollPrev, scrollNext] canScrollPrev,
) canScrollNext,
}}
React.useEffect(() => { >
if (!api || !setApi) { <div
return onKeyDownCapture={handleKeyDown}
} className={cn("relative", className)}
role="region"
setApi(api) aria-roledescription="carousel"
}, [api, setApi]) data-slot="carousel"
{...props}
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
> >
<div {children}
ref={ref} </div>
onKeyDownCapture={handleKeyDown} </CarouselContext.Provider>
className={cn("relative", className)} )
role="region" }
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef< function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel() const { carouselRef, orientation } = useCarousel()
return ( return (
<div ref={carouselRef} className="overflow-hidden"> <div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div <div
ref={ref}
className={cn( className={cn(
"flex", "flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
@@ -169,20 +150,16 @@ const CarouselContent = React.forwardRef<
/> />
</div> </div>
) )
}) }
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef< function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel() const { orientation } = useCarousel()
return ( return (
<div <div
ref={ref}
role="group" role="group"
aria-roledescription="slide" aria-roledescription="slide"
data-slot="carousel-item"
className={cn( className={cn(
"min-w-0 shrink-0 grow-0 basis-full", "min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4", orientation === "horizontal" ? "pl-4" : "pt-4",
@@ -191,24 +168,25 @@ const CarouselItem = React.forwardRef<
{...props} {...props}
/> />
) )
}) }
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef< function CarouselPrevious({
HTMLButtonElement, className,
React.ComponentProps<typeof Button> variant = "outline",
>(({ className, variant = "outline", size = "icon", ...props }, ref) => { size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel() const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return ( return (
<Button <Button
ref={ref} data-slot="carousel-previous"
variant={variant} variant={variant}
size={size} size={size}
className={cn( className={cn(
"absolute h-8 w-8 rounded-full", "absolute size-8 rounded-full",
orientation === "horizontal" orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2" ? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90", : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className className
)} )}
@@ -216,28 +194,29 @@ const CarouselPrevious = React.forwardRef<
onClick={scrollPrev} onClick={scrollPrev}
{...props} {...props}
> >
<ArrowLeftIcon className="h-4 w-4" /> <ArrowLeftIcon />
<span className="sr-only">Previous slide</span> <span className="sr-only">Previous slide</span>
</Button> </Button>
) )
}) }
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef< function CarouselNext({
HTMLButtonElement, className,
React.ComponentProps<typeof Button> variant = "outline",
>(({ className, variant = "outline", size = "icon", ...props }, ref) => { size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel() const { orientation, scrollNext, canScrollNext } = useCarousel()
return ( return (
<Button <Button
ref={ref} data-slot="carousel-next"
variant={variant} variant={variant}
size={size} size={size}
className={cn( className={cn(
"absolute h-8 w-8 rounded-full", "absolute size-8 rounded-full",
orientation === "horizontal" orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2" ? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className className
)} )}
@@ -245,12 +224,11 @@ const CarouselNext = React.forwardRef<
onClick={scrollNext} onClick={scrollNext}
{...props} {...props}
> >
<ArrowRightIcon className="h-4 w-4" /> <ArrowRightIcon />
<span className="sr-only">Next slide</span> <span className="sr-only">Next slide</span>
</Button> </Button>
) )
}) }
CarouselNext.displayName = "CarouselNext"
export { export {
type CarouselApi, type CarouselApi,
+190 -202
View File
@@ -34,25 +34,28 @@ function useChart() {
return context return context
} }
const ChartContainer = React.forwardRef< function ChartContainer({
HTMLDivElement, id,
React.ComponentProps<"div"> & { className,
config: ChartConfig children,
children: React.ComponentProps< config,
typeof RechartsPrimitive.ResponsiveContainer ...props
>["children"] }: React.ComponentProps<"div"> & {
} config: ChartConfig
>(({ id, className, children, config, ...props }, ref) => { children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}) {
const uniqueId = React.useId() const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return ( return (
<ChartContext.Provider value={{ config }}> <ChartContext.Provider value={{ config }}>
<div <div
data-slot="chart"
data-chart={chartId} data-chart={chartId}
ref={ref}
className={cn( className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none", "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className className
)} )}
{...props} {...props}
@@ -64,12 +67,11 @@ const ChartContainer = React.forwardRef<
</div> </div>
</ChartContext.Provider> </ChartContext.Provider>
) )
}) }
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter( const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color ([, config]) => config.theme || config.color
) )
if (!colorConfig.length) { if (!colorConfig.length) {
@@ -102,219 +104,205 @@ ${colorConfig
const ChartTooltip = RechartsPrimitive.Tooltip const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef< function ChartTooltipContent({
HTMLDivElement, active,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> & payload,
React.ComponentProps<"div"> & { className,
hideLabel?: boolean indicator = "dot",
hideIndicator?: boolean hideLabel = false,
indicator?: "line" | "dot" | "dashed" hideIndicator = false,
nameKey?: string label,
labelKey?: string labelFormatter,
} labelClassName,
>( formatter,
( color,
{ nameKey,
active, labelKey,
payload, }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
className, React.ComponentProps<"div"> & {
indicator = "dot", hideLabel?: boolean
hideLabel = false, hideIndicator?: boolean
hideIndicator = false, indicator?: "line" | "dot" | "dashed"
label, nameKey?: string
labelFormatter, labelKey?: string
labelClassName, }) {
formatter, const { config } = useChart()
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => { const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) { if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null return null
} }
const nestLabel = payload.length === 1 && indicator !== "dot" const [item] = payload
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
return ( if (labelFormatter) {
<div return (
ref={ref} <div className={cn("font-medium", labelClassName)}>
className={cn( {labelFormatter(value, payload)}
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div> </div>
</div> )
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
} }
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) { if (!value) {
return null return null
} }
return ( return <div className={cn("font-medium", labelClassName)}>{value}</div>
<div }, [
ref={ref} label,
className={cn( labelFormatter,
"flex items-center justify-center gap-4", payload,
verticalAlign === "top" ? "pb-3" : "pt-3", hideLabel,
className labelClassName,
)} config,
> labelKey,
{payload.map((item) => { ])
const key = `${nameKey || item.dataKey || "value"}`
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return ( return (
<div <div
key={item.value} key={item.dataKey}
className={cn( className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground" "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)} )}
> >
{itemConfig?.icon && !hideIcon ? ( {formatter && item?.value !== undefined && item.name ? (
<itemConfig.icon /> formatter(item.value, item.name, item, index, item.payload)
) : ( ) : (
<div <>
className="h-2 w-2 shrink-0 rounded-[2px]" {itemConfig?.icon ? (
style={{ <itemConfig.icon />
backgroundColor: item.color, ) : (
}} !hideIndicator && (
/> <div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)} )}
{itemConfig?.label}
</div> </div>
) )
})} })}
</div> </div>
) </div>
)
}
const ChartLegend = RechartsPrimitive.Legend
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}) {
const { config } = useChart()
if (!payload?.length) {
return null
} }
)
ChartLegendContent.displayName = "ChartLegend" return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
// Helper to extract item config from a payload. // Helper to extract item config from a payload.
function getPayloadConfigFromPayload( function getPayloadConfigFromPayload(
+22 -20
View File
@@ -2,29 +2,31 @@
import * as React from "react" import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "@radix-ui/react-icons" import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef< function Checkbox({
React.ElementRef<typeof CheckboxPrimitive.Root>, className,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
<CheckboxPrimitive.Root return (
ref={ref} <CheckboxPrimitive.Root
className={cn( data-slot="checkbox"
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", className={cn(
className "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
)} className
{...props} )}
> {...props}
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
> >
<CheckIcon className="h-4 w-4" /> <CheckboxPrimitive.Indicator
</CheckboxPrimitive.Indicator> data-slot="checkbox-indicator"
</CheckboxPrimitive.Root> className="flex items-center justify-center text-current transition-none"
)) >
Checkbox.displayName = CheckboxPrimitive.Root.displayName <CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox } export { Checkbox }
+25 -3
View File
@@ -2,10 +2,32 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent } export { Collapsible, CollapsibleTrigger, CollapsibleContent }
+2 -2
View File
@@ -46,7 +46,7 @@ const CommandInput = React.forwardRef<
<CommandPrimitive.Input <CommandPrimitive.Input
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
{...props} {...props}
@@ -117,7 +117,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item <CommandPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className className
)} )}
{...props} {...props}

Some files were not shown because too many files have changed in this diff Show More