mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Working on the README.md
This commit is contained in:
Binary file not shown.
@@ -5,31 +5,37 @@
|
||||
</a>
|
||||
|
||||
<h3 align="center">Portabase</h3>
|
||||
|
||||
<p align="center">
|
||||
Easily backup and restore your database instances with Portabase
|
||||
<br />
|
||||
<a href="https://portabase.io"><strong>Explore the Docs »</strong></a>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://www.youtube.com/watch?v=D9uFrGxLc4s">View Demo</a>
|
||||
·
|
||||
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
|
||||
·
|
||||
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
||||
|
||||

|
||||
|
||||
<p>
|
||||
Free, open-source, and self-hosted solution for automated backup and restoration of your database instances.
|
||||
</p>
|
||||
|
||||
[](LICENSE)
|
||||
[](https://hub.docker.com/r/solucetechnologies/portabase)
|
||||
[](https://github.com/RostislavDugin/postgresus)
|
||||
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://www.mysql.com/)
|
||||
[](https://mariadb.org/)
|
||||
[](https://github.com/RostislavDugin/postgresus)
|
||||
|
||||
<p>
|
||||
<strong>
|
||||
<a href="https://portabase.io">Documentation</a> •
|
||||
<a href="https://www.youtube.com/watch?v=D9uFrGxLc4s">Demo</a> •
|
||||
<a href="#installation">Installation</a> •
|
||||
<a href="#contributing">Contributing</a> •
|
||||
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a> •
|
||||
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
||||
</strong>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
## 📚 Table of Contents
|
||||
|
||||
- [About The Project](#about-the-project)
|
||||
- [Built With](#built-with)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation](#installation)
|
||||
@@ -44,17 +50,17 @@
|
||||
|
||||
## About The Project
|
||||
|
||||
**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.
|
||||
**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.
|
||||
|
||||
GitHub Repository: [Portabase](https://github.com/Soluce-Technologies/portabase)
|
||||
|
||||
### 🔧 Built With
|
||||
|
||||
- [![NextJS][NextJS]][NextJS-url] (v15 with App Router)
|
||||
- [![NextJS][NextJS]][NextJS-url] (v16 with App Router)
|
||||
- [![Drizzle][Drizzle]][Drizzle-url]
|
||||
- [![ShadcnUI][ShadcnUI]][ShadcnUI-url]
|
||||
- BetterAuth
|
||||
- React Email
|
||||
- [![BetterAuth][BetterAuth]][BetterAuth-url]
|
||||
|
||||
---
|
||||
|
||||
@@ -147,29 +153,29 @@ Give the project a ⭐ if you like it!
|
||||
# Environment
|
||||
NODE_ENV=production
|
||||
|
||||
# Database
|
||||
# Database
|
||||
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
||||
|
||||
# Project Info
|
||||
# Project Info
|
||||
PROJECT_NAME="Portabase"
|
||||
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
||||
PROJECT_URL=http://app.portabase.io
|
||||
PROJECT_SECRET=
|
||||
|
||||
# SMTP (Email)
|
||||
# SMTP (Email)
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM=
|
||||
|
||||
# Google OAuth
|
||||
# Google OAuth
|
||||
AUTH_GOOGLE_ID=
|
||||
AUTH_GOOGLE_SECRET=
|
||||
AUTH_GOOGLE_METHOD=
|
||||
|
||||
|
||||
# S3/MinIO Configuration
|
||||
# S3/MinIO Configuration
|
||||
S3_ENDPOINT=http://app.s3.portabase.io
|
||||
S3_ACCESS_KEY=
|
||||
S3_SECRET_KEY=
|
||||
@@ -177,10 +183,10 @@ S3_BUCKET_NAME=portabase
|
||||
S3_PORT=9000
|
||||
S3_USE_SSL=true
|
||||
|
||||
# Storage Backend: 'local' or 's3'
|
||||
# Storage Backend: 'local' or 's3'
|
||||
STORAGE_TYPE=local
|
||||
|
||||
# Retention
|
||||
# Retention
|
||||
RETENTION_CRON="* * * * *"
|
||||
```
|
||||
|
||||
@@ -219,8 +225,17 @@ 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
|
||||
|
||||
[BetterAuth]: https://img.shields.io/badge/Better%20Auth-FFF?logo=betterauth&logoColor=000&style=for-the-badge
|
||||
|
||||
[Drizzle]: https://img.shields.io/badge/Drizzle-111?style=for-the-badge&logo=Drizzle&logoColor=c5f74f
|
||||
|
||||
[ShadcnUI]: https://img.shields.io/badge/shadcn/ui-000000?style=for-the-badge&logo=shadcn/ui&logoColor=white
|
||||
|
||||
[NextJS-url]: https://nextjs.org/
|
||||
|
||||
[BetterAuth-url]: https://www.better-auth.com/
|
||||
|
||||
[Drizzle-url]: https://orm.drizzle.team/
|
||||
|
||||
[ShadcnUI-url]: https://ui.shadcn.com/
|
||||
|
||||
+5
-1
@@ -10,11 +10,15 @@
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "radix",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {
|
||||
"@reui": "https://reui.io/r/{name}.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ services:
|
||||
image: solucetechnologies/portabase:1.1.3-rc.3
|
||||
ports:
|
||||
- '8887:80'
|
||||
environment:
|
||||
TZ: "Europe/Paris"
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
|
||||
+37
-16
@@ -1,21 +1,42 @@
|
||||
name: portabase-dev
|
||||
|
||||
services:
|
||||
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
|
||||
|
||||
# app:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: docker/dockerfile/Dockerfile
|
||||
# target: dev
|
||||
# ports:
|
||||
# - "8887:8887"
|
||||
# environment:
|
||||
# - TZ=Europe/Paris
|
||||
# - NODE_ENV=development
|
||||
# depends_on:
|
||||
# db:
|
||||
# condition: service_healthy
|
||||
# volumes:
|
||||
# - .:/app
|
||||
# - /app/node_modules
|
||||
# container_name: portabase-app
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
postgres-data:
|
||||
|
||||
@@ -3,6 +3,7 @@ FROM node:22-alpine AS base
|
||||
ENV YARN_VERSION=4.9.1
|
||||
|
||||
RUN apk add --update --no-cache \
|
||||
tzdata \
|
||||
build-base \
|
||||
libc6-compat \
|
||||
openssl \
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -n "$TZ" ]; then
|
||||
if [ -f "/usr/share/zoneinfo/$TZ" ]; then
|
||||
ln -sf /usr/share/zoneinfo/$TZ /etc/localtime
|
||||
echo "$TZ" > /etc/timezone
|
||||
echo "[INFO] Timezone set to $TZ"
|
||||
else
|
||||
echo "[WARN] Timezone '$TZ' not found. Using default."
|
||||
fi
|
||||
fi
|
||||
|
||||
node server.js
|
||||
|
||||
exec "$@"
|
||||
+3
-1
@@ -63,8 +63,9 @@
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.510.0",
|
||||
"lucide-react": "^0.553.0",
|
||||
"minio": "^8.0.5",
|
||||
"motion": "^12.23.24",
|
||||
"next": "16.0.0",
|
||||
"next-safe-action": "^7.10.8",
|
||||
"next-themes": "^0.4.6",
|
||||
@@ -81,6 +82,7 @@
|
||||
"react-hook-form": "^7.56.3",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"react-twc": "^1.4.2",
|
||||
"react-use-measure": "^2.1.7",
|
||||
"recharts": "^2.15.3",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
|
||||
@@ -43,6 +43,7 @@ export const PORTABASE_DEFAULT_SETTINGS = {
|
||||
"'self'",
|
||||
"https://api.iconify.design",
|
||||
"https://code.iconify.design",
|
||||
"https://api.github.com",
|
||||
|
||||
],
|
||||
OBJECT_SRC: ["'none'"],
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
useSpring,
|
||||
useTransform,
|
||||
motion,
|
||||
useInView,
|
||||
type MotionValue,
|
||||
type SpringOptions,
|
||||
type UseInViewOptions,
|
||||
} from 'motion/react';
|
||||
import useMeasure from 'react-use-measure';
|
||||
import {cn} from "@/lib/utils";
|
||||
|
||||
|
||||
type SlidingNumberRollerProps = {
|
||||
prevValue: number;
|
||||
value: number;
|
||||
place: number;
|
||||
transition: SpringOptions;
|
||||
};
|
||||
|
||||
function SlidingNumberRoller({
|
||||
prevValue,
|
||||
value,
|
||||
place,
|
||||
transition,
|
||||
}: SlidingNumberRollerProps) {
|
||||
const startNumber = Math.floor(prevValue / place) % 10;
|
||||
const targetNumber = Math.floor(value / place) % 10;
|
||||
const animatedValue = useSpring(startNumber, transition);
|
||||
|
||||
React.useEffect(() => {
|
||||
animatedValue.set(targetNumber);
|
||||
}, [targetNumber, animatedValue]);
|
||||
|
||||
const [measureRef, { height }] = useMeasure();
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={measureRef}
|
||||
data-slot="sliding-number-roller"
|
||||
className="relative inline-block w-[1ch] overflow-x-visible overflow-y-clip leading-none tabular-nums"
|
||||
>
|
||||
<span className="invisible">0</span>
|
||||
{Array.from({ length: 10 }, (_, i) => (
|
||||
<SlidingNumberDisplay
|
||||
key={i}
|
||||
motionValue={animatedValue}
|
||||
number={i}
|
||||
height={height}
|
||||
transition={transition}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
type SlidingNumberDisplayProps = {
|
||||
motionValue: MotionValue<number>;
|
||||
number: number;
|
||||
height: number;
|
||||
transition: SpringOptions;
|
||||
};
|
||||
|
||||
function SlidingNumberDisplay({
|
||||
motionValue,
|
||||
number,
|
||||
height,
|
||||
transition,
|
||||
}: SlidingNumberDisplayProps) {
|
||||
const y = useTransform(motionValue, (latest) => {
|
||||
if (!height) return 0;
|
||||
const currentNumber = latest % 10;
|
||||
const offset = (10 + number - currentNumber) % 10;
|
||||
let translateY = offset * height;
|
||||
if (offset > 5) translateY -= 10 * height;
|
||||
return translateY;
|
||||
});
|
||||
|
||||
if (!height) {
|
||||
return <span className="invisible absolute">{number}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.span
|
||||
data-slot="sliding-number-display"
|
||||
style={{ y }}
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
transition={{ ...transition, type: 'spring' }}
|
||||
>
|
||||
{number}
|
||||
</motion.span>
|
||||
);
|
||||
}
|
||||
|
||||
type SlidingNumberProps = React.ComponentProps<'span'> & {
|
||||
number: number | string;
|
||||
inView?: boolean;
|
||||
inViewMargin?: UseInViewOptions['margin'];
|
||||
inViewOnce?: boolean;
|
||||
padStart?: boolean;
|
||||
decimalSeparator?: string;
|
||||
decimalPlaces?: number;
|
||||
transition?: SpringOptions;
|
||||
};
|
||||
|
||||
function SlidingNumber({
|
||||
ref,
|
||||
number,
|
||||
className,
|
||||
inView = false,
|
||||
inViewMargin = '0px',
|
||||
inViewOnce = true,
|
||||
padStart = false,
|
||||
decimalSeparator = '.',
|
||||
decimalPlaces = 0,
|
||||
transition = {
|
||||
stiffness: 200,
|
||||
damping: 20,
|
||||
mass: 0.4,
|
||||
},
|
||||
...props
|
||||
}: SlidingNumberProps) {
|
||||
const localRef = React.useRef<HTMLSpanElement>(null);
|
||||
React.useImperativeHandle(ref, () => localRef.current!);
|
||||
|
||||
const inViewResult = useInView(localRef, {
|
||||
once: inViewOnce,
|
||||
margin: inViewMargin,
|
||||
});
|
||||
const isInView = !inView || inViewResult;
|
||||
|
||||
const prevNumberRef = React.useRef<number>(0);
|
||||
|
||||
const effectiveNumber = React.useMemo(
|
||||
() => (!isInView ? 0 : Math.abs(Number(number))),
|
||||
[number, isInView],
|
||||
);
|
||||
|
||||
const formatNumber = React.useCallback(
|
||||
(num: number) =>
|
||||
decimalPlaces != null ? num.toFixed(decimalPlaces) : num.toString(),
|
||||
[decimalPlaces],
|
||||
);
|
||||
|
||||
const numberStr = formatNumber(effectiveNumber);
|
||||
const [newIntStrRaw, newDecStrRaw = ''] = numberStr.split('.');
|
||||
const newIntStr =
|
||||
padStart && newIntStrRaw?.length === 1 ? '0' + newIntStrRaw : newIntStrRaw;
|
||||
|
||||
const prevFormatted = formatNumber(prevNumberRef.current);
|
||||
const [prevIntStrRaw = '', prevDecStrRaw = ''] = prevFormatted.split('.');
|
||||
const prevIntStr =
|
||||
padStart && prevIntStrRaw.length === 1
|
||||
? '0' + prevIntStrRaw
|
||||
: prevIntStrRaw;
|
||||
|
||||
const adjustedPrevInt = React.useMemo(() => {
|
||||
return prevIntStr.length > (newIntStr?.length ?? 0)
|
||||
? prevIntStr.slice(-(newIntStr?.length ?? 0))
|
||||
: prevIntStr.padStart(newIntStr?.length ?? 0, '0');
|
||||
}, [prevIntStr, newIntStr]);
|
||||
|
||||
const adjustedPrevDec = React.useMemo(() => {
|
||||
if (!newDecStrRaw) return '';
|
||||
return prevDecStrRaw.length > newDecStrRaw.length
|
||||
? prevDecStrRaw.slice(0, newDecStrRaw.length)
|
||||
: prevDecStrRaw.padEnd(newDecStrRaw.length, '0');
|
||||
}, [prevDecStrRaw, newDecStrRaw]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isInView) prevNumberRef.current = effectiveNumber;
|
||||
}, [effectiveNumber, isInView]);
|
||||
|
||||
const intDigitCount = newIntStr?.length ?? 0;
|
||||
const intPlaces = React.useMemo(
|
||||
() =>
|
||||
Array.from({ length: intDigitCount }, (_, i) =>
|
||||
Math.pow(10, intDigitCount - i - 1),
|
||||
),
|
||||
[intDigitCount],
|
||||
);
|
||||
const decPlaces = React.useMemo(
|
||||
() =>
|
||||
newDecStrRaw
|
||||
? Array.from({ length: newDecStrRaw.length }, (_, i) =>
|
||||
Math.pow(10, newDecStrRaw.length - i - 1),
|
||||
)
|
||||
: [],
|
||||
[newDecStrRaw],
|
||||
);
|
||||
|
||||
const newDecValue = newDecStrRaw ? parseInt(newDecStrRaw, 10) : 0;
|
||||
const prevDecValue = adjustedPrevDec ? parseInt(adjustedPrevDec, 10) : 0;
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={localRef}
|
||||
data-slot="sliding-number"
|
||||
className={cn('flex items-center', className)}
|
||||
{...props}
|
||||
>
|
||||
{isInView && Number(number) < 0 && <span className="mr-1">-</span>}
|
||||
|
||||
{intPlaces.map((place) => (
|
||||
<SlidingNumberRoller
|
||||
key={`int-${place}`}
|
||||
prevValue={parseInt(adjustedPrevInt, 10)}
|
||||
value={parseInt(newIntStr ?? '0', 10)}
|
||||
place={place}
|
||||
transition={transition}
|
||||
/>
|
||||
))}
|
||||
|
||||
{newDecStrRaw && (
|
||||
<>
|
||||
<span>{decimalSeparator}</span>
|
||||
{decPlaces.map((place) => (
|
||||
<SlidingNumberRoller
|
||||
key={`dec-${place}`}
|
||||
prevValue={prevDecValue}
|
||||
value={newDecValue}
|
||||
place={place}
|
||||
transition={transition}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export { SlidingNumber, type SlidingNumberProps };
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import {GithubButton} from "@/components/ui/github-button";
|
||||
import * as React from "react";
|
||||
import {useEffect} from "react";
|
||||
|
||||
export const GitHubStarsButtonCustom = () => {
|
||||
|
||||
const [stars, setStars] = React.useState(0);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
|
||||
const username = "Soluce-Technologies"
|
||||
const repo = "portabase"
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`https://api.github.com/repos/${username}/${repo}`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data && typeof data.stargazers_count === 'number') {
|
||||
setStars(data.stargazers_count);
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [username, repo]);
|
||||
|
||||
if (isLoading) return null;
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className=" gap-4 flex-wrap hidden md:block">
|
||||
<GithubButton
|
||||
initialStars={0}
|
||||
targetStars={stars}
|
||||
label="Github Stars"
|
||||
size="sm"
|
||||
separator={true}
|
||||
roundStars={true}
|
||||
repoUrl={`https://github.com/${username}/${repo}`}
|
||||
variant="outline"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default GitHubStarsButtonCustom;
|
||||
@@ -17,7 +17,7 @@ export const TablePaginationSize = (props: tablePaginationSizeProps) => {
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center justify-end sm:justify-center space-x-2", className)}>
|
||||
<p className="whitespace-nowrap text-sm font-medium">Rows per page</p>
|
||||
<p className="whitespace-nowrap text-sm font-medium hidden md:block">Rows per page</p>
|
||||
<Select
|
||||
value={`${table.getState().pagination.pageSize}`}
|
||||
onValueChange={(value) => {
|
||||
|
||||
@@ -15,9 +15,9 @@ export function TablePagination(props: tablePaginationProps) {
|
||||
const { className, table, maxVisiblePages = 3, pageSizeOptions = [10, 20, 30, 40, 50] } = props;
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-x-4 sm:flex-row", className)}>
|
||||
<div className={cn("flex gap-x-4 flex-row w-full", className)}>
|
||||
<TablePaginationSize table={table} pageSizeOptions={pageSizeOptions} />
|
||||
<TablePaginationNavigation table={table} maxVisiblePages={maxVisiblePages} className="justify-end mt-5 sm:mt-0" />
|
||||
<TablePaginationNavigation table={table} maxVisiblePages={maxVisiblePages} className="justify-end mt-0" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,10 +50,10 @@ export const AdminTabs = ({users, settings, organizations}: AdminTabsProps) => {
|
||||
Storage
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="users">
|
||||
<TabsContent className="h-full" value="users">
|
||||
<AdminUsersTable users={users}/>
|
||||
</TabsContent>
|
||||
<TabsContent value="organizations">
|
||||
<TabsContent className="h-full" value="organizations">
|
||||
<AdminOrganizationsTable organizations={organizations}/>
|
||||
</TabsContent>
|
||||
<TabsContent value="email">
|
||||
|
||||
+3
-3
@@ -11,13 +11,13 @@ export type AdminOrganizationsTableProps = {
|
||||
export const AdminOrganizationsTable = (props: AdminOrganizationsTableProps) => {
|
||||
const {organizations} = props;
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4 h-full py-4">
|
||||
<Card>
|
||||
<div className="flex flex-col gap-y-4 h-full py-4 ">
|
||||
<Card className="h-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Active organizations</CardTitle>
|
||||
<CardDescription>Manage all system organizations</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="h-full">
|
||||
<AdminOrganizationList organizations={organizations} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
-1
@@ -11,7 +11,6 @@ import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-d
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||
import {formatFrenchDate} from "@/utils/date-formatting";
|
||||
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
||||
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/button-delete-use";
|
||||
import {Organization} from "@/db/schema/03_organization";
|
||||
|
||||
@@ -12,12 +12,12 @@ export const AdminUsersTable = (props: AdminUsersTableProps) => {
|
||||
const {users} = props;
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4 h-full py-4">
|
||||
<Card>
|
||||
<Card className="h-full ">
|
||||
<CardHeader>
|
||||
<CardTitle>Active users</CardTitle>
|
||||
<CardDescription>Manage your users</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="h-full">
|
||||
<DataTable
|
||||
enableSelect={false}
|
||||
columns={usersColumnsAdmin}
|
||||
|
||||
@@ -6,14 +6,11 @@ import {useMutation} from "@tanstack/react-query";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useState} from "react";
|
||||
import {Trash2} from "lucide-react";
|
||||
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||
import {formatFrenchDate} from "@/utils/date-formatting";
|
||||
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
||||
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/button-delete-use";
|
||||
import {formatLocalizedDate} from "@/utils/date-formatting";
|
||||
|
||||
export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
||||
{
|
||||
@@ -88,7 +85,7 @@ export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
||||
accessorKey: "updatedAt",
|
||||
header: "Updated At",
|
||||
cell: ({row}) => {
|
||||
return formatFrenchDate(row.getValue("updatedAt"))
|
||||
return formatLocalizedDate(row.getValue("updatedAt"))
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ export const schemas = {
|
||||
|
||||
export const db = drizzle({
|
||||
client: pool,
|
||||
logger: process.env.NODE_ENV != 'production',
|
||||
// logger: process.env.NODE_ENV != 'production',
|
||||
schema: schemas,
|
||||
});
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export const env = createEnv({
|
||||
RETENTION_CRON: z
|
||||
.string()
|
||||
.default(process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *"),
|
||||
|
||||
},
|
||||
client: {
|
||||
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
|
||||
|
||||
@@ -22,7 +22,6 @@ import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {StatusBadge} from "@/components/wrappers/common/status-badge";
|
||||
import {Backup, DatabaseWith} from "@/db/schema/07_database";
|
||||
import {formatFrenchDate} from "@/utils/date-formatting";
|
||||
import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {SafeActionResult} from "next-safe-action";
|
||||
@@ -31,6 +30,7 @@ import {ServerActionResult} from "@/types/action-type";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
import {formatLocalizedDate} from "@/utils/date-formatting";
|
||||
|
||||
|
||||
export function backupColumns(
|
||||
@@ -52,7 +52,7 @@ export function backupColumns(
|
||||
</TooltipTrigger>
|
||||
{row.original.deletedAt != null && (
|
||||
<TooltipContent>
|
||||
<p>{formatFrenchDate(row.original.deletedAt)}</p>
|
||||
<p>{formatLocalizedDate(row.original.deletedAt)}</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
@@ -68,7 +68,7 @@ export function backupColumns(
|
||||
accessorKey: "createdAt",
|
||||
header: "Created At",
|
||||
cell: ({row}) => {
|
||||
return formatFrenchDate(row.getValue("createdAt"))
|
||||
return formatLocalizedDate(row.getValue("createdAt"))
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,11 +13,10 @@ import { Button } from "@/components/ui/button";
|
||||
import {MoreHorizontal, Trash2} from "lucide-react";
|
||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||
import { StatusBadge } from "@/components/wrappers/common/status-badge";
|
||||
import {Backup, Restoration} from "@/db/schema/07_database";
|
||||
import {formatFrenchDate} from "@/utils/date-formatting";
|
||||
import {Restoration} from "@/db/schema/07_database";
|
||||
import {formatLocalizedDate} from "@/utils/date-formatting";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {
|
||||
deleteBackupAction,
|
||||
deleteRestoreAction,
|
||||
rerunRestorationAction
|
||||
} from "@/features/dashboard/restore/restore.action";
|
||||
@@ -40,7 +39,7 @@ export function restoreColumns(
|
||||
accessorKey: "createdAt",
|
||||
header: "Created At",
|
||||
cell: ({ row }) => {
|
||||
return formatFrenchDate(row.getValue("createdAt"))
|
||||
return formatLocalizedDate(row.getValue("createdAt"))
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,6 +5,8 @@ import {ModeToggle} from "@/features/theme/ModeToggle";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
import {LoggedInButton} from "@/components/wrappers/dashboard/common/logged-in/logged-in-button";
|
||||
import {BreadCrumbsWrapper} from "@/components/wrappers/common/bread-crumbs/bread-crumbs";
|
||||
import GitHubStarsButtonCustom from "@/components/wrappers/common/github/github-button";
|
||||
// import GitHubStarsButtonCustom from "@/components/wrappers/common/github-button";
|
||||
|
||||
export const Header = async () => {
|
||||
const user = await currentUser();
|
||||
@@ -19,6 +21,7 @@ export const Header = async () => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<GitHubStarsButtonCustom/>
|
||||
<ModeToggle/>
|
||||
<LoggedInButton/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
import {format, formatDistanceToNow} from "date-fns";
|
||||
import {formatDistanceToNow} from "date-fns";
|
||||
|
||||
/**
|
||||
* Get user's locale and timezone from the browser
|
||||
*/
|
||||
const TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const LOCALE = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
|
||||
/**
|
||||
* Format a date in DD/MM/YYYY HH:mm 24-hour format
|
||||
*/
|
||||
export function formatLocalizedDate(date: string | number | Date) {
|
||||
const d = new Date(date);
|
||||
return new Intl.DateTimeFormat(LOCALE, {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false, // 24-hour format
|
||||
timeZone: TIMEZONE,
|
||||
}).format(d);
|
||||
}
|
||||
|
||||
export function humanReadableDate(rawDate: string | number | Date) {
|
||||
return formatFrenchDate(rawDate);
|
||||
return formatLocalizedDate(rawDate);
|
||||
}
|
||||
|
||||
export function timeAgo(rawDate: string | number | Date) {
|
||||
@@ -10,14 +32,7 @@ export function timeAgo(rawDate: string | number | Date) {
|
||||
}
|
||||
|
||||
export function formatDateLastContact(lastContact: string | number | Date | null) {
|
||||
console.log(lastContact)
|
||||
return lastContact
|
||||
? formatFrenchDate(lastContact)
|
||||
: "Never connected."
|
||||
? formatLocalizedDate(lastContact)
|
||||
: "Never connected.";
|
||||
}
|
||||
|
||||
export function formatFrenchDate(date: string | number | Date) {
|
||||
return new Date(date).toLocaleString("fr-FR", {
|
||||
timeZone: "Europe/Paris",
|
||||
});
|
||||
}
|
||||
@@ -7784,6 +7784,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"framer-motion@npm:^12.23.24":
|
||||
version: 12.23.24
|
||||
resolution: "framer-motion@npm:12.23.24"
|
||||
dependencies:
|
||||
motion-dom: "npm:^12.23.23"
|
||||
motion-utils: "npm:^12.23.6"
|
||||
tslib: "npm:^2.4.0"
|
||||
peerDependencies:
|
||||
"@emotion/is-prop-valid": "*"
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
"@emotion/is-prop-valid":
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
checksum: 10c0/716addd9fa85dd2c1475ab848e14c4e6d3246ced041de84afc19b1d6534c897256c8f2878864cf057039116e56f5ca1b87a4cbb943f0b1cf37199ef7174c3cf8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fs-constants@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "fs-constants@npm:1.0.0"
|
||||
@@ -9113,12 +9135,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lucide-react@npm:^0.510.0":
|
||||
version: 0.510.0
|
||||
resolution: "lucide-react@npm:0.510.0"
|
||||
"lucide-react@npm:^0.553.0":
|
||||
version: 0.553.0
|
||||
resolution: "lucide-react@npm:0.553.0"
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
checksum: 10c0/00b3efed64c370cd5ae97023583996ff83939baf6f4af11393d536ca9e520b0cc1a82a81aa6d0350625bff398daaa69357df74383631f2c3a0aeb6974f4237c7
|
||||
checksum: 10c0/b1283f1a2302a50d6a16bd67b9f7417ae08a74cba59ae5ca9953527cb9724247df12ba8c72ca2fc6a70b745f8315cc709ac7573643c4088728766da151999ef6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9406,6 +9428,43 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"motion-dom@npm:^12.23.23":
|
||||
version: 12.23.23
|
||||
resolution: "motion-dom@npm:12.23.23"
|
||||
dependencies:
|
||||
motion-utils: "npm:^12.23.6"
|
||||
checksum: 10c0/139705731085063519b88f23fcc5b1c13e15707a4ff3365da02ef9a4bf2a2d8ebed9a151c57e7f215ccd9e822365d93c16e28e619fbf25611f61dcff5ee81d75
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"motion-utils@npm:^12.23.6":
|
||||
version: 12.23.6
|
||||
resolution: "motion-utils@npm:12.23.6"
|
||||
checksum: 10c0/c058e8ba6423b3baa63e985bcc669877ee7d9579d938f5348b4e60c5ea1b4b33dd7f4877434436a4a5807f3cf00370d3fd4079a6fdd6309c5c87aa62b311a897
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"motion@npm:^12.23.24":
|
||||
version: 12.23.24
|
||||
resolution: "motion@npm:12.23.24"
|
||||
dependencies:
|
||||
framer-motion: "npm:^12.23.24"
|
||||
tslib: "npm:^2.4.0"
|
||||
peerDependencies:
|
||||
"@emotion/is-prop-valid": "*"
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
"@emotion/is-prop-valid":
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
checksum: 10c0/d937ddaf50816f97e4e7121f323104fd655045ef122bd03d433370649c00129a5dd928cbe54efe9add22a05b2a9b84b7cfda1fd5c5b1e9c6245cd94e1c24e823
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ms@npm:^2.1.1, ms@npm:^2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "ms@npm:2.1.3"
|
||||
@@ -10237,8 +10296,9 @@ __metadata:
|
||||
eslint-config-next: "npm:^16.0.1"
|
||||
eslint-plugin-tailwindcss: "npm:^3.18.0"
|
||||
input-otp: "npm:^1.4.2"
|
||||
lucide-react: "npm:^0.510.0"
|
||||
lucide-react: "npm:^0.553.0"
|
||||
minio: "npm:^8.0.5"
|
||||
motion: "npm:^12.23.24"
|
||||
next: "npm:16.0.0"
|
||||
next-safe-action: "npm:^7.10.8"
|
||||
next-themes: "npm:^0.4.6"
|
||||
@@ -10256,6 +10316,7 @@ __metadata:
|
||||
react-hook-form: "npm:^7.56.3"
|
||||
react-resizable-panels: "npm:^3.0.2"
|
||||
react-twc: "npm:^1.4.2"
|
||||
react-use-measure: "npm:^2.1.7"
|
||||
recharts: "npm:^2.15.3"
|
||||
socket.io: "npm:^4.8.1"
|
||||
socket.io-client: "npm:^4.8.1"
|
||||
@@ -10689,6 +10750,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-use-measure@npm:^2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "react-use-measure@npm:2.1.7"
|
||||
peerDependencies:
|
||||
react: ">=16.13"
|
||||
react-dom: ">=16.13"
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
checksum: 10c0/ff24130e6f95e853feb6892fb74af08dbc5aae3574b701169e3bc3adb392c3162f51a58ddfe39bb7337db13ae609bbec0bb51a9de8b5fae5420f9d17e1f8b542
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react@npm:^19.2.0":
|
||||
version: 19.2.0
|
||||
resolution: "react@npm:19.2.0"
|
||||
|
||||
Reference in New Issue
Block a user