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:
@@ -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
|
||||
key={type.value}
|
||||
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={() => {
|
||||
if (type.preview) return;
|
||||
form.setValue("provider", type.value as any);
|
||||
form.setValue("config", {});
|
||||
}}
|
||||
>
|
||||
<div className="h-10 w-10 bg-secondary rounded-full flex items-center justify-center">
|
||||
<Icon className="h-6 w-6 text-foreground"/>
|
||||
{type.preview && (
|
||||
<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>
|
||||
<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>
|
||||
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -46,14 +46,14 @@ export function usersListColumns(): ColumnDef<User>[] {
|
||||
<TableCell
|
||||
className="text-right text-muted-foreground">{formatLocalizedDate(row.original.createdAt)}</TableCell>
|
||||
</TableRow>
|
||||
{/*{row.original.lastConnectedAt && (*/}
|
||||
{/* <TableRow>*/}
|
||||
{/* <TableCell>Last connected</TableCell>*/}
|
||||
{/* <TableCell className="text-right text-muted-foreground">*/}
|
||||
{/* {formatLocalizedDate(row.original.lastConnectedAt)} ({timeAgo(row.original.lastConnectedAt, locale)})*/}
|
||||
{/* </TableCell>*/}
|
||||
{/* </TableRow>*/}
|
||||
{/*)}*/}
|
||||
{row.original.lastConnectedAt && (
|
||||
<TableRow>
|
||||
<TableCell>Last connected</TableCell>
|
||||
<TableCell className="text-right text-muted-foreground">
|
||||
{formatLocalizedDate(row.original.lastConnectedAt)} ({timeAgo(row.original.lastConnectedAt)})
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{row.original.lastChangedPasswordAt && (
|
||||
<TableRow>
|
||||
<TableCell>Last password change</TableCell>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import {useState} from "react";
|
||||
import Link from "next/link";
|
||||
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 {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 {truncateWords} from "@/utils/text";
|
||||
import {useIsMobile} from "@/hooks/use-mobile";
|
||||
@@ -86,7 +86,7 @@ export const AgentCard = (props: agentCardProps) => {
|
||||
</div>
|
||||
|
||||
<div className="scale-110">
|
||||
<ConnectionCircle date={agent.lastContact}/>
|
||||
<ConnectionIndicator date={agent.lastContact}/>
|
||||
</div>
|
||||
|
||||
<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) => {
|
||||
return (
|
||||
<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">
|
||||
<DialogTitle>Settings</DialogTitle>
|
||||
<DialogDescription>Manage your account settings</DialogDescription>
|
||||
|
||||
@@ -73,7 +73,7 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
||||
fileInput.onchange = handleImageUpload;
|
||||
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"/>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import {useState} from "react";
|
||||
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 {Database} from "@/db/schema/07_database";
|
||||
import {ChevronRight, Activity, Fingerprint, Copy, Check} from "lucide-react";
|
||||
@@ -60,7 +60,7 @@ export const DatabaseCard = (props: databaseCardProps) => {
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-3">
|
||||
<div className="scale-100 origin-right">
|
||||
<ConnectionCircle date={database.lastContact}/>
|
||||
<ConnectionIndicator date={database.lastContact}/>
|
||||
</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 {z} from "zod";
|
||||
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 {organization} from "@/db/schema/03_organization";
|
||||
import {Account as BetterAuthAccount} from "better-auth";
|
||||
@@ -23,6 +23,7 @@ export const user = pgTable("user", {
|
||||
banned: boolean("banned"),
|
||||
banReason: text("ban_reason"),
|
||||
banExpires: timestamp("ban_expires"),
|
||||
lastConnectedAt: timestamp(),
|
||||
lastChangedPasswordAt: timestamp(),
|
||||
twoFactorEnabled: boolean("two_factor_enabled").default(false),
|
||||
...timestamps
|
||||
|
||||
Reference in New Issue
Block a user