mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Merge remote-tracking branch 'origin/main' into feat/storage
# Conflicts: # src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.tsx # src/components/wrappers/dashboard/admin/notifications/helpers.tsx # src/db/migrations/meta/0020_snapshot.json # src/db/migrations/meta/_journal.json
This commit is contained in:
+9
-9
@@ -1,18 +1,18 @@
|
|||||||
# Environnement
|
# Environnement
|
||||||
NODE_ENV=production
|
NODE_ENV=development
|
||||||
|
|
||||||
# Base de données
|
# Base de données
|
||||||
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
DATABASE_URL=postgresql://devuser:changeme@localhost:5432/devdb?schema=public
|
||||||
|
|
||||||
# Projet
|
# Projet
|
||||||
PROJECT_NAME="Portabase"
|
PROJECT_NAME="Portabase"
|
||||||
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
||||||
PROJECT_URL=http://app.portabase.io
|
PROJECT_URL=http://localhost:8887
|
||||||
PROJECT_SECRET=
|
PROJECT_SECRET=
|
||||||
|
|
||||||
# SMTP (email)
|
# SMTP (email)
|
||||||
SMTP_HOST=
|
SMTP_HOST=
|
||||||
SMTP_PORT=587
|
SMTP_PORT=
|
||||||
SMTP_USER=
|
SMTP_USER=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
SMTP_FROM=
|
SMTP_FROM=
|
||||||
@@ -23,14 +23,14 @@ AUTH_GOOGLE_SECRET=
|
|||||||
AUTH_GOOGLE_METHOD=
|
AUTH_GOOGLE_METHOD=
|
||||||
|
|
||||||
# S3
|
# S3
|
||||||
S3_ENDPOINT=http://app.s3.portabase.io
|
S3_ENDPOINT=
|
||||||
S3_ACCESS_KEY=
|
S3_ACCESS_KEY=
|
||||||
S3_SECRET_KEY=
|
S3_SECRET_KEY=
|
||||||
S3_BUCKET_NAME=portabase
|
S3_BUCKET_NAME=
|
||||||
S3_PORT=9000
|
S3_PORT=
|
||||||
S3_USE_SSL=true
|
S3_USE_SSL=
|
||||||
|
|
||||||
# Storage Type (s3, local)
|
# Storage type (local, S3)
|
||||||
STORAGE_TYPE=local
|
STORAGE_TYPE=local
|
||||||
|
|
||||||
# Retention
|
# Retention
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ services:
|
|||||||
db:
|
db:
|
||||||
image: postgres:17-alpine
|
image: postgres:17-alpine
|
||||||
ports:
|
ports:
|
||||||
- "5433:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres-data:/var/lib/postgresql/data
|
- postgres-data:/var/lib/postgresql/data
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import {cn} from "@/lib/utils";
|
|
||||||
|
|
||||||
export type ConnectionCircleProps = {
|
|
||||||
date?: Date | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ConnectionCircle = ({date}: ConnectionCircleProps) => {
|
|
||||||
let style = "bg-gray-300 border-gray-400";
|
|
||||||
|
|
||||||
if (date instanceof Date && !isNaN(date.getTime())) {
|
|
||||||
const now = Date.now();
|
|
||||||
const timestamp = date.getTime();
|
|
||||||
const interval_seconds = (now - timestamp) / 1000;
|
|
||||||
|
|
||||||
if (interval_seconds < 55) {
|
|
||||||
style = "bg-green-400 border-green-600";
|
|
||||||
} else if (interval_seconds <= 60) {
|
|
||||||
style = "bg-orange-400 border-orange-600";
|
|
||||||
} else {
|
|
||||||
style = "bg-red-400 border-red-600";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn("Invalid date passed to ConnectionCircle:", date);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={cn("w-5 h-5 rounded-full border-4", style)}/>;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import {cn} from "@/lib/utils";
|
||||||
|
|
||||||
|
export type ConnectionIndicatorProps = {
|
||||||
|
date?: Date | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConnectionIndicator = ({date}: ConnectionIndicatorProps) => {
|
||||||
|
let style = "bg-gray-300";
|
||||||
|
|
||||||
|
if (date instanceof Date && !isNaN(date.getTime())) {
|
||||||
|
const intervalSeconds = (Date.now() - date.getTime()) / 1000;
|
||||||
|
|
||||||
|
if (intervalSeconds < 55) {
|
||||||
|
style = "bg-green-500";
|
||||||
|
} else if (intervalSeconds <= 60) {
|
||||||
|
style = "bg-orange-400";
|
||||||
|
} else {
|
||||||
|
style = "bg-red-500";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-3 h-3">
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute -inset-0.25 rounded-full opacity-60 animate-ping",
|
||||||
|
style
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
animationDuration: "2s",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative w-3 h-3 rounded-full shadow-sm animate-pulse",
|
||||||
|
style
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
animationDuration: "2s",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+29
-4
@@ -106,18 +106,43 @@ export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}
|
|||||||
<Card
|
<Card
|
||||||
key={type.value}
|
key={type.value}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-center justify-center gap-3 p-4 cursor-pointer hover:bg-accent/50 hover:border-primary/50 transition-all",
|
"relative flex flex-col items-center justify-center gap-3 p-4 transition-all",
|
||||||
|
type.preview
|
||||||
|
? "cursor-not-allowed bg-gray-200 border-gray-300 opacity-70"
|
||||||
|
: "cursor-pointer hover:bg-accent/50 hover:border-primary/50"
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (type.preview) return;
|
||||||
form.setValue("provider", type.value as any);
|
form.setValue("provider", type.value as any);
|
||||||
form.setValue("config", {});
|
form.setValue("config", {});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="h-10 w-10 bg-secondary rounded-full flex items-center justify-center">
|
{type.preview && (
|
||||||
<Icon className="h-6 w-6 text-foreground"/>
|
<div
|
||||||
|
className="absolute bottom-0 right-0 overflow-hidden w-20 h-20 pointer-events-none">
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 right-0 w-38 h-6 flex items-center justify-center
|
||||||
|
transform -rotate-45 translate-x-14 -translate-y-4"
|
||||||
|
style={{backgroundColor: "#FE6702"}}
|
||||||
|
>
|
||||||
|
<span className="text-white text-[8px] pr-3 font-medium text-center w-full">coming soon</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={cn(
|
||||||
|
"h-10 w-10 rounded-full flex items-center justify-center",
|
||||||
|
type.preview ? "bg-gray-400" : "bg-secondary"
|
||||||
|
)}>
|
||||||
|
<Icon className={cn("h-6 w-6 text-foreground", type.preview && "text-gray-700")}/>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-sm">{type.label}</span>
|
<span className={cn(
|
||||||
|
"font-medium text-sm align-middle text-center",
|
||||||
|
type.preview ? "text-gray-500" : "text-foreground"
|
||||||
|
)}>
|
||||||
|
{type.label}
|
||||||
|
</span>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ export function usersListColumns(): ColumnDef<User>[] {
|
|||||||
<TableCell
|
<TableCell
|
||||||
className="text-right text-muted-foreground">{formatLocalizedDate(row.original.createdAt)}</TableCell>
|
className="text-right text-muted-foreground">{formatLocalizedDate(row.original.createdAt)}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{/*{row.original.lastConnectedAt && (*/}
|
{row.original.lastConnectedAt && (
|
||||||
{/* <TableRow>*/}
|
<TableRow>
|
||||||
{/* <TableCell>Last connected</TableCell>*/}
|
<TableCell>Last connected</TableCell>
|
||||||
{/* <TableCell className="text-right text-muted-foreground">*/}
|
<TableCell className="text-right text-muted-foreground">
|
||||||
{/* {formatLocalizedDate(row.original.lastConnectedAt)} ({timeAgo(row.original.lastConnectedAt, locale)})*/}
|
{formatLocalizedDate(row.original.lastConnectedAt)} ({timeAgo(row.original.lastConnectedAt)})
|
||||||
{/* </TableCell>*/}
|
</TableCell>
|
||||||
{/* </TableRow>*/}
|
</TableRow>
|
||||||
{/*)}*/}
|
)}
|
||||||
{row.original.lastChangedPasswordAt && (
|
{row.original.lastChangedPasswordAt && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Last password change</TableCell>
|
<TableCell>Last password change</TableCell>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {Card} from "@/components/ui/card";
|
import {Card} from "@/components/ui/card";
|
||||||
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
|
import {ConnectionIndicator} from "@/components/wrappers/common/connection-indicator";
|
||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||||
import {AgentWith} from "@/db/schema/08_agent";
|
import {AgentWith} from "@/db/schema/08_agent";
|
||||||
import {Activity, ChevronRight, Copy, Check, Fingerprint, Server, Database, ShieldCheck} from "lucide-react";
|
import {Activity, ChevronRight, Copy, Check, Fingerprint, Server, Database} from "lucide-react";
|
||||||
import {Badge} from "@/components/ui/badge";
|
import {Badge} from "@/components/ui/badge";
|
||||||
import {truncateWords} from "@/utils/text";
|
import {truncateWords} from "@/utils/text";
|
||||||
import {useIsMobile} from "@/hooks/use-mobile";
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
@@ -86,7 +86,7 @@ export const AgentCard = (props: agentCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="scale-110">
|
<div className="scale-110">
|
||||||
<ConnectionCircle date={agent.lastContact}/>
|
<ConnectionIndicator date={agent.lastContact}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ChevronRight className="w-5 h-5 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
|
<ChevronRight className="w-5 h-5 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type ProfileModalProps = {
|
|||||||
export const ProfileModal = ({ user, sessions, currentSession, accounts, open, onOpenChange, providers }: ProfileModalProps) => {
|
export const ProfileModal = ({ user, sessions, currentSession, accounts, open, onOpenChange, providers }: ProfileModalProps) => {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="w-[95vw] h-[90vh] max-w-md lg:max-w-[1000px] lg:h-[800px] p-0 overflow-hidden flex flex-col outline-none gap-0 rounded-xl bg-background">
|
<DialogContent className="w-[95vw] h-[90vh] max-w-md lg:max-w-[1000px] lg:h-[800px] pb-6 overflow-hidden flex flex-col outline-none gap-0 rounded-xl bg-background">
|
||||||
<DialogHeader className="sr-only">
|
<DialogHeader className="sr-only">
|
||||||
<DialogTitle>Settings</DialogTitle>
|
<DialogTitle>Settings</DialogTitle>
|
||||||
<DialogDescription>Manage your account settings</DialogDescription>
|
<DialogDescription>Manage your account settings</DialogDescription>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
|||||||
fileInput.onchange = handleImageUpload;
|
fileInput.onchange = handleImageUpload;
|
||||||
fileInput.click();
|
fileInput.click();
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-100 hover:bg-gray-500 hover:bg-opacity-50 rounded-full w-24 h-24 lg:w-32 lg:h-32"
|
className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-30 hover:bg-gray-500 hover:bg-opacity-50 rounded-full w-24 h-24 lg:w-32 lg:h-32"
|
||||||
>
|
>
|
||||||
<UploadIcon className="w-12 h-12 lg:w-16 lg:h-16 text-primary"/>
|
<UploadIcon className="w-12 h-12 lg:w-16 lg:h-16 text-primary"/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Link from "next/link";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {Card} from "@/components/ui/card";
|
import {Card} from "@/components/ui/card";
|
||||||
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
|
import {ConnectionIndicator} from "@/components/wrappers/common/connection-indicator";
|
||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||||
import {Database} from "@/db/schema/07_database";
|
import {Database} from "@/db/schema/07_database";
|
||||||
import {ChevronRight, Activity, Fingerprint, Copy, Check} from "lucide-react";
|
import {ChevronRight, Activity, Fingerprint, Copy, Check} from "lucide-react";
|
||||||
@@ -60,7 +60,7 @@ export const DatabaseCard = (props: databaseCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-3">
|
<div className="flex flex-col items-end gap-3">
|
||||||
<div className="scale-100 origin-right">
|
<div className="scale-100 origin-right">
|
||||||
<ConnectionCircle date={database.lastContact}/>
|
<ConnectionIndicator date={database.lastContact}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user" ADD COLUMN "lastConnectedAt" timestamp;
|
||||||
@@ -3,7 +3,7 @@ import {boolean, integer, pgEnum, pgTable, text, timestamp, uuid} from "drizzle-
|
|||||||
import {createSelectSchema} from "drizzle-zod";
|
import {createSelectSchema} from "drizzle-zod";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {project} from "./06_project";
|
import {project} from "./06_project";
|
||||||
import {member, OrganizationMember} from "@/db/schema/04_member";
|
import {member} from "@/db/schema/04_member";
|
||||||
import {invitation} from "@/db/schema/05_invitation";
|
import {invitation} from "@/db/schema/05_invitation";
|
||||||
import {organization} from "@/db/schema/03_organization";
|
import {organization} from "@/db/schema/03_organization";
|
||||||
import {Account as BetterAuthAccount} from "better-auth";
|
import {Account as BetterAuthAccount} from "better-auth";
|
||||||
@@ -23,6 +23,7 @@ export const user = pgTable("user", {
|
|||||||
banned: boolean("banned"),
|
banned: boolean("banned"),
|
||||||
banReason: text("ban_reason"),
|
banReason: text("ban_reason"),
|
||||||
banExpires: timestamp("ban_expires"),
|
banExpires: timestamp("ban_expires"),
|
||||||
|
lastConnectedAt: timestamp(),
|
||||||
lastChangedPasswordAt: timestamp(),
|
lastChangedPasswordAt: timestamp(),
|
||||||
twoFactorEnabled: boolean("two_factor_enabled").default(false),
|
twoFactorEnabled: boolean("two_factor_enabled").default(false),
|
||||||
...timestamps
|
...timestamps
|
||||||
|
|||||||
Reference in New Issue
Block a user