This commit is contained in:
Théo LAGACHE
2026-02-12 15:48:51 +01:00
parent 7b9cb2ecb9
commit 6f6c1c7c52
21 changed files with 133 additions and 169 deletions
+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024 Soluce Technologies
Copyright 2024 Portabase
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+1 -1
View File
@@ -16,7 +16,7 @@ export default async function GuardPage() {
return (
<TooltipProvider>
<CardAuth className="w-full max-w-md">
<CardAuth className="w-full">
<CardHeader>
<div className="grid gap-2 text-center mb-2">
<h1 className="text-3xl font-bold">Two-factor verification</h1>
+13 -6
View File
@@ -2,6 +2,8 @@ import React from "react";
import {redirect} from "next/navigation";
import {currentUser} from "@/lib/auth/current-user";
import {AuthLogoSection} from "@/components/wrappers/auth/auth-logo-section";
import {env} from "@/env.mjs";
import {Heart} from "lucide-react";
export default async function Layout({children}: { children: React.ReactNode }) {
@@ -12,13 +14,18 @@ export default async function Layout({children}: { children: React.ReactNode })
}
return (
<div className="flex min-h-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8 ">
<div className="mx-auto w-full max-w-md">
<AuthLogoSection/>
<div>{children}</div>
<div className="flex min-h-screen flex-col justify-between py-10 sm:px-6 lg:px-8 ">
<div className="flex flex-col items-center justify-center flex-1">
<div className="mx-auto w-full max-w-md">
<AuthLogoSection/>
<div className="mt-4">{children}</div>
</div>
</div>
<footer className="py-4 text-center text-xs justify-items-end text-muted-foreground">
Powered by <span className="font-medium">Soluce Technologies</span>
<footer className="mt-8 text-center text-xs text-muted-foreground flex flex-col gap-1">
<p className="flex items-center justify-center gap-1">
Made with <Heart className="size-3 fill-red-500 text-red-500" /> by <span className="font-medium text-foreground">Portabase</span>
</p>
<p>v{env.NEXT_PUBLIC_PROJECT_VERSION}</p>
</footer>
</div>
)
+4 -3
View File
@@ -1,9 +1,10 @@
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { CardContent, CardHeader } from "@/components/ui/card";
import { TooltipProvider } from "@/components/ui/tooltip";
import { ResetPasswordForm } from "@/components/wrappers/auth/login/reset-password-form/reset-password-form";
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth/auth";
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
import {CardAuth} from "@/features/layout/card-auth";
export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) {
@@ -23,7 +24,7 @@ export default async function RoutePage(props: { searchParams: Promise<{ token:
return (
<TooltipProvider>
<Card className="w-full max-w-md shadow-lg">
<CardAuth className="w-full">
<CardHeader className="space-y-4">
<div className="space-y-1 text-center">
<h1 className="text-2xl font-bold tracking-tight">Set a new password</h1>
@@ -53,7 +54,7 @@ export default async function RoutePage(props: { searchParams: Promise<{ token:
<CardContent>
<ResetPasswordForm />
</CardContent>
</Card>
</CardAuth>
</TooltipProvider>
);
}
+1 -1
View File
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex w-full min-w-0 rounded-md border bg-transparent px-4 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"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",
className
@@ -41,9 +41,6 @@ export const AuthLogoSection = () => {
/>
)}
<span className="absolute bottom-8 right-5 text-sm text-muted-foreground" style={style}>
v{env.NEXT_PUBLIC_PROJECT_VERSION}
</span>
</div>
);
};
@@ -68,7 +68,7 @@ export const ForgotPasswordForm = (props: ForgotPasswordFormProps) => {
/>
<div className="flex flex-col items-center gap-y-6 w-full">
<ButtonWithLoading className="mt-2 w-full" isPending={mutation.isPending}>
<ButtonWithLoading className="mt-2 w-full h-11" isPending={mutation.isPending}>
Send reset link
</ButtonWithLoading>
<Link href="/login" className="group flex items-center text-sm hover:underline">
@@ -113,7 +113,7 @@ export const LoginForm = (props: loginFormProps) => {
</FormItem>
)}
/>
<ButtonWithLoading className="mt-2" isPending={mutation.isPending}>
<ButtonWithLoading className="mt-2 h-11" isPending={mutation.isPending}>
Login
</ButtonWithLoading>
</Form>
@@ -82,7 +82,7 @@ export const ResetPasswordForm = (props: ResetPasswordFormProps) => {
/>
<div className="flex flex-col items-center gap-y-6 w-full">
<ButtonWithLoading className="mt-2 w-full" isPending={mutation.isPending}>
<ButtonWithLoading className="mt-2 w-full h-11" isPending={mutation.isPending}>
Reset
</ButtonWithLoading>
<Link href="/login" className="group flex items-center text-sm hover:underline">
@@ -55,7 +55,7 @@ export const RegisterForm = (props: registerFormProps) => {
<CardHeader>
<div className="grid gap-2 text-center mb-2">
<h1 className="text-3xl font-bold">Create an account</h1>
<p className="text-balance text-muted-foreground">Enter your informations bellow to register</p>
<p className="text-balance text-muted-foreground">Enter your informations below to register</p>
</div>
</CardHeader>
<CardContent>
@@ -138,7 +138,7 @@ export const RegisterForm = (props: registerFormProps) => {
</FormItem>
)}
/>
<Button type="submit" disabled={mutation.isPending}>
<Button type="submit" className="h-11" disabled={mutation.isPending}>
Sign up
</Button>
<div className="mt-4 text-center text-sm">
@@ -47,7 +47,7 @@ export const ResetPasswordForm = ({token}: ResetPasswordFormProps) => {
<CardHeader>
<div className="grid gap-2 text-center mb-2">
<h1 className="text-3xl font-bold">Reset Password</h1>
<p className="text-balance text-muted-foreground">Fill information bellow to change your
<p className="text-balance text-muted-foreground">Fill information below to change your
password</p>
</div>
</CardHeader>
@@ -14,6 +14,10 @@ interface tablePaginationProps {
export function TablePagination(props: tablePaginationProps) {
const {className, table, maxVisiblePages = 3, pageSizeOptions = [10, 20, 30, 40, 50]} = props;
const totalPages = table.getPageCount();
if (totalPages <= 1) return null;
return (
<div
className={cn("flex gap-x-4", className)}
@@ -5,9 +5,7 @@ import {Button} from "@/components/ui/button";
import {Copy, Check, Eye, EyeOff, Terminal, Key, Info} from "lucide-react";
import {useState} from "react";
import {copyToClipboardWithMeta} from "@/components/wrappers/common/button/copy-button";
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
import { cn } from "@/lib/utils";
export type AgentCardKeyProps = {
edgeKey: string;
@@ -29,26 +27,14 @@ export const AgentCardKey = ({edgeKey, agentName}: AgentCardKeyProps) => {
return (
<div className="flex flex-col gap-4 py-2">
<Tabs defaultValue="automatic" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="automatic" className="gap-2">
<Terminal className="h-4 w-4" />
<span>Automatic</span>
</TabsTrigger>
<TabsTrigger value="manual" className="gap-2">
<Key className="h-4 w-4" />
<span>Manual</span>
</TabsTrigger>
</TabsList>
<TabsContent value="automatic" className="mt-4 focus-visible:outline-none">
<Card className="border-muted/60 shadow-none py-0">
<CardHeader className="pb-3 px-4 pt-4">
<CardTitle className="text-sm font-semibold flex items-center gap-2 uppercase tracking-tight">
CLI Setup
</CardTitle>
<CardDescription className="text-xs leading-relaxed">
Click the input below to reveal the key and copy it to your terminal.
To setup your agent using the CLI, copy the command below and paste it in your terminal.
Make sure you have the PortaBase CLI installed. If not, you can <a href="https://portabase.io/docs/cli" target="_blank" className="underline hover:text-primary">install it here</a>.
</CardDescription>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-4">
@@ -95,37 +81,17 @@ export const AgentCardKey = ({edgeKey, agentName}: AgentCardKeyProps) => {
)}
</Button>
</div>
<div className="p-3 rounded-md bg-orange-500/10 dark:bg-orange-500/5 border border-orange-500/20">
<div className="flex gap-2.5">
<Terminal className="h-4 w-4 text-orange-600 dark:text-orange-400 shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="text-xs font-bold text-orange-900 dark:text-orange-100">CLI Installation</p>
<p className="text-[11px] text-orange-800/80 dark:text-orange-400/80 leading-normal">
This command requires the <strong>portabase cli</strong>. If you haven't installed it yet, you can find the instructions at{" "}
<a
href="https://portabase.io/docs/cli"
target="_blank"
rel="noopener noreferrer"
className="font-bold underline underline-offset-2 hover:text-orange-700 dark:hover:text-orange-300 transition-colors"
>
portabase.io/docs/cli
</a>
</p>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="manual" className="mt-4 focus-visible:outline-none">
<Card className="border-muted/60 shadow-none py-0">
<Card className="border-muted/60 shadow-none py-0">
<CardHeader className="pb-3 px-4 pt-4">
<CardTitle className="text-sm font-semibold flex items-center gap-2 uppercase tracking-tight">
Registration Key
Manual Setup
</CardTitle>
<CardDescription className="text-xs leading-relaxed">
Use this key to manually configure your agent in your configuration file.
If you need help for manual configuration, you can follow our guide in the <a href="https://portabase.io/docs" target="_blank" className="underline hover:text-primary">documentation</a>.
</CardDescription>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-4">
@@ -172,44 +138,8 @@ export const AgentCardKey = ({edgeKey, agentName}: AgentCardKeyProps) => {
)}
</Button>
</div>
<div className="flex flex-col gap-3">
<div className="p-3 rounded-md bg-orange-500/10 dark:bg-orange-500/5 border border-orange-500/20">
<div className="flex gap-2.5">
<Info className="h-4 w-4 text-orange-600 dark:text-orange-400 shrink-0 mt-0.5" />
<div className="space-y-0.5">
<p className="text-xs font-bold text-orange-900 dark:text-orange-100">Important Security Note</p>
<p className="text-[11px] text-orange-800/80 dark:text-orange-400/80 leading-normal">
Never share this key. Anyone with access to it can register as this agent.
</p>
</div>
</div>
</div>
<div className="p-3 rounded-md bg-muted/30 border border-muted/50">
<div className="flex gap-2.5">
<Terminal className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="text-xs font-bold text-foreground">Need help with manual setup?</p>
<p className="text-[11px] text-muted-foreground leading-normal">
Check out our guide for manual configuration and Docker deployment at{" "}
<a
href="https://portabase.io/docs/agent/setup#docker"
target="_blank"
rel="noopener noreferrer"
className="font-bold underline underline-offset-2 hover:text-orange-500 transition-colors text-foreground"
>
portabase.io/docs/setup
</a>
</p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
};
@@ -90,7 +90,8 @@ export const AgentContentPage = ({edgeKey, agent: initialAgent}: AgentContentPag
</Accordion>
</div>
<div className="space-y-6">
{agent.databases.length > 0 && (
<div className="space-y-6">
<div className="flex items-center justify-between px-1">
<div className="space-y-1">
<h2 className="text-2xl font-bold tracking-tight">Managed Databases</h2>
@@ -107,6 +108,7 @@ export const AgentContentPage = ({edgeKey, agent: initialAgent}: AgentContentPag
cardItem={AgentDatabaseCard}
/>
</div>
)}
</div>
)
}
@@ -47,20 +47,6 @@ export const SidebarMenuCustomMain = () => {
type: "list",
group_content: groupContent,
},
{
label: "Resources",
type: "list",
group_content: [
{
title: "Documentation",
url: "https://portabase.io/docs",
icon: BookOpen,
type: "item",
redirect: true,
not_from_base_url: true,
}
],
},
];
@@ -118,6 +104,23 @@ export const SidebarMenuCustomMain = () => {
});
}
items.push(
{
label: "Resources",
type: "list",
group_content: [
{
title: "Documentation",
url: "https://portabase.io/docs",
icon: BookOpen,
type: "item",
redirect: true,
not_from_base_url: true,
}
],
},
)
return <SidebarMenuCustomBase baseUrl={BASE_URL} items={items}/>;
};
@@ -16,8 +16,6 @@ import {useMutation, useQueryClient} from "@tanstack/react-query";
import {BackupStorageWith} from "@/db/schema/14_storage-backup";
import {TooltipProvider} from "@/components/ui/tooltip";
import {getChannelIcon} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
import {truncateWords} from "@/utils/text";
import {useIsMobile} from "@/hooks/use-mobile";
import {Badge} from "@/components/ui/badge";
import {getStatusColor, getStatusIcon} from "@/components/wrappers/dashboard/admin/notifications/logs/columns";
import {
@@ -39,7 +37,6 @@ type BackupActionsFormProps = {
export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
const filteredBackupStorages = backup.storages?.filter((storage) => storage.deletedAt === null) ?? []
const isMobile = useIsMobile();
const {closeModal} = useBackupModal();
const queryClient = useQueryClient();
@@ -175,24 +172,26 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0 flex flex-col gap-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{getChannelIcon(storage.storageChannel?.provider || "")}
<h3 className="font-medium text-foreground">
{isMobile ? truncateWords(storage?.storageChannel?.name ?? "", 2) : storage.storageChannel?.name}
</h3>
<Badge variant="secondary"
className="text-xs font-mono">
{storage.storageChannel?.provider}
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<div className="shrink-0">
{getChannelIcon(storage.storageChannel?.provider || "")}
</div>
<h3 className="font-medium text-foreground truncate">
{storage.storageChannel?.name}
</h3>
<Badge variant="secondary"
className="text-xs font-mono shrink-0">
{storage.storageChannel?.provider}
</Badge>
</div>
<Badge variant="outline"
className={`gap-1.5 shrink-0 ${getStatusColor(storage.status)}`}>
{getStatusIcon(storage.status === "success")}
<span
className="capitalize">{storage.status.toUpperCase()}</span>
</Badge>
</div>
<Badge variant="outline"
className={`gap-1.5 ${getStatusColor(storage.status)}`}>
{getStatusIcon(storage.status === "success")}
<span
className="capitalize">{storage.status.toUpperCase()}</span>
</Badge>
</div>
</div>
</div>
</div>
@@ -113,7 +113,7 @@ export function OrganizationCombobox() {
: "focus:bg-accent hover:bg-accent/50 border border-transparent"
)}>
<div className={cn(
"flex size-9 items-center justify-center rounded-md border shadow-sm transition-all group-hover:shadow-md",
"flex size-9 shrink-0 items-center justify-center rounded-md border shadow-sm transition-all group-hover:shadow-md",
org.logo ? "bg-transparent border-transparent" : "",
isActive && !org.logo ? "bg-primary text-primary-foreground border-primary/30" : "bg-muted/50 border-border"
)}>
@@ -126,7 +126,7 @@ export function OrganizationCombobox() {
)}/>
)}
</div>
<div className="flex flex-col gap-0.5">
<div className="flex flex-col gap-0.5 flex-1 min-w-0">
<span className={cn(
"text-sm max-w-42.5 truncate font-medium leading-none",
isActive ? "text-primary" : ""
@@ -145,7 +145,7 @@ export default function TwoFactorForm({ onSuccess, onSuccessData }: TwoFactorFor
/>
<div className="flex flex-col gap-3 pt-2">
<Button type="submit" disabled={isPending}>
<Button type="submit" className="h-11" disabled={isPending}>
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Verify
</Button>
@@ -21,7 +21,7 @@ export function ProfileAppearance() {
}
function ThemeSelector() {
const {theme, setTheme} = useTheme();
const {theme} = useTheme();
return (
+48 -29
View File
@@ -1,58 +1,77 @@
"use client"
import { Moon, Sun, Monitor } from "lucide-react"
import { authClient } from "@/lib/auth/auth-client"
import { motion } from "motion/react"
import { Moon, Sun, Check, SunMoon } from "lucide-react"
import { useTheme } from "next-themes"
import { authClient } from "@/lib/auth/auth-client"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { cn } from "@/lib/utils"
const themes = [
{ id: "light", icon: Sun, label: "Light" },
{ id: "system", icon: Monitor, label: "System" },
{ id: "dark", icon: Moon, label: "Dark" },
{ id: "system", icon: SunMoon, label: "System" },
] as const
export function ModeToggle() {
const { theme } = useTheme()
const currentIndex = themes.findIndex((t) => t.id === theme)
const activeIndex = currentIndex === -1 ? 1 : currentIndex
const handleThemeChange = async (newTheme: "light" | "system" | "dark") => {
await authClient.updateUser({ theme: newTheme })
}
return (
<div className="flex items-center justify-center rounded-full bg-muted/50 p-0.5 border border-border/50 relative w-fit">
<motion.div
className="absolute h-6 w-7 rounded-full bg-background shadow-sm z-0 border border-border/20"
initial={false}
animate={{ x: activeIndex * 28 }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
style={{ left: "2px" }}
/>
<div className="flex gap-0 relative z-10">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-8 rounded-full border border-input bg-transparent shadow-xs transition-transform active:scale-95">
{theme === "light" ? <Sun className="size-4" /> : theme === "dark" ? <Moon className="size-4" /> : <SunMoon className="size-4" />}
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-37.5 rounded-xl border-2 border-border p-1 shadow-none">
{themes.map((t) => {
const Icon = t.icon
const ThemeIcon = t.icon
const isActive = theme === t.id
return (
<button
<DropdownMenuItem
key={t.id}
onClick={() => handleThemeChange(t.id)}
className={cn(
"flex h-6 w-7 items-center justify-center rounded-full transition-colors duration-200 hover:text-foreground outline-none",
isActive ? "text-primary" : "text-muted-foreground"
"group gap-2 p-2 cursor-pointer rounded-lg mb-1 last:mb-0 transition-colors",
isActive
? "bg-primary/10 text-primary border border-primary/20"
: "focus:bg-accent hover:bg-accent/50 border border-transparent"
)}
aria-label={t.label}
>
<Icon className="h-3.5 w-3.5" />
</button>
<div className={cn(
"flex size-7 shrink-0 items-center justify-center rounded-md border shadow-sm transition-all group-hover:shadow-md",
isActive ? "bg-primary text-primary-foreground border-primary/30" : "bg-muted/50 border-border"
)}>
<ThemeIcon className={cn(
"size-4",
isActive ? "text-primary-foreground" : "text-muted-foreground"
)} />
</div>
<span className={cn(
"text-sm font-medium leading-none flex-1",
isActive ? "text-primary" : ""
)}>
{t.label}
</span>
{isActive && (
<div className="flex size-4 items-center justify-center rounded-full bg-primary shadow-sm ml-auto">
<Check className="size-2.5 text-primary-foreground" strokeWidth={3} />
</div>
)}
</DropdownMenuItem>
)
})}
</div>
</div>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -18,9 +18,9 @@ export const UpdateNotification = () => {
<SidebarGroup className="py-0">
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem className="px-2">
<SidebarMenuItem>
<div
className="relative flex flex-col gap-2 rounded-lg border bg-primary/5 p-3 text-sidebar-foreground border-primary/20">
className="w-[var(--radix-popper-anchor-width)] relative flex flex-col gap-2 rounded-lg border bg-primary/5 p-3 text-sidebar-foreground border-primary/20">
<button
onClick={dismissUpdate}
className="absolute right-2 top-2 rounded-md p-0.5 text-muted-foreground/50 hover:bg-sidebar-accent hover:text-foreground transition-colors"
@@ -37,10 +37,12 @@ export const UpdateNotification = () => {
<div className="flex flex-col min-w-0">
<div className="flex items-center gap-1">
<span className="text-[10px] font-semibold leading-none">Update available</span>
<span
className="text-[10px] text-muted-foreground font-medium px-1 py-0.5 bg-primary/10 rounded-full">
v{newRelease.tag_name.replace(/^v/, "")}
</span>
{newRelease.tag_name && (
<span
className="text-[10px] text-muted-foreground font-medium px-1 py-0.5 bg-primary/10 rounded-full">
v{newRelease.tag_name.replace(/^v/, "")}
</span>
)}
</div>
<Link
href={newRelease.html_url}