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:
charlesgauthereau
2026-01-18 14:40:26 +01:00
12 changed files with 103 additions and 57 deletions
+9 -9
View File
@@ -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
View File
@@ -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>
);
};
@@ -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;
+2 -1
View File
@@ -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