mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be66856034 | ||
|
|
80e3027107 | ||
|
|
362ca53efb | ||
|
|
f0eafb1162 | ||
|
|
ad5baa7d6f | ||
|
|
5840e30afb | ||
|
|
8f95606dd7 | ||
|
|
ca9bc7ae94 | ||
|
|
288a40e697 | ||
|
|
fa91e8c289 | ||
|
|
4ab5fe4c6f | ||
|
|
d87782d180 | ||
|
|
1c97c97fa1 | ||
|
|
2426166676 | ||
|
|
6f6c1c7c52 | ||
|
|
7b9cb2ecb9 | ||
|
|
fc029af654 | ||
|
|
110a69dc4b | ||
|
|
a61791fdb2 | ||
|
|
c01440f9d4 | ||
|
|
39584dd7a1 | ||
|
|
d9badeacba | ||
|
|
d4cde2ba9c | ||
|
|
824ef4528e | ||
|
|
feb06a85ae | ||
|
|
cfad645e33 | ||
|
|
675f8dec20 | ||
|
|
d24e534828 | ||
|
|
80bdca138e | ||
|
|
0a2ca8c0b6 | ||
|
|
921a355ae0 | ||
|
|
a49b6d6573 | ||
|
|
baffd2efaf | ||
|
|
292693a1ed | ||
|
|
db1a0235ed | ||
|
|
2cd6a7cae0 | ||
|
|
bd80fdc960 |
@@ -40,6 +40,7 @@ public/uploads/*
|
||||
!public/uploads/
|
||||
|
||||
private/uploads/*
|
||||
private/keys/*
|
||||
|
||||
/.env
|
||||
|
||||
|
||||
+2
-2
@@ -26,5 +26,5 @@ keywords:
|
||||
- web-ui
|
||||
- agent
|
||||
license: Apache-2.0
|
||||
version: 1.2.5-rc.14
|
||||
date-released: "2026-02-10"
|
||||
version: 1.2.6-rc.1
|
||||
date-released: "2026-02-13"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
+17
-71
@@ -1,21 +1,11 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {notFound, redirect} from "next/navigation";
|
||||
import {Page, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
||||
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
|
||||
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi";
|
||||
import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button";
|
||||
import {Page} from "@/features/layout/page";
|
||||
import {db} from "@/db";
|
||||
import {eq, and, inArray} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {getOrganizationProjectDatabases} from "@/lib/services";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||
import {ImportModal} from "@/components/wrappers/dashboard/database/import/import-modal";
|
||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
||||
import {ChannelPoliciesModal} from "@/components/wrappers/dashboard/database/channels-policy/policy-modal";
|
||||
import {HardDrive, Megaphone} from "lucide-react";
|
||||
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
||||
import {DatabaseContent} from "@/components/wrappers/dashboard/projects/database/database-content";
|
||||
|
||||
@@ -93,12 +83,6 @@ export default async function RoutePage(props: PageParams<{
|
||||
notFound();
|
||||
}
|
||||
|
||||
const organizationChannels = await getOrganizationChannels(organization.id);
|
||||
const activeOrganizationChannels = organizationChannels.filter(channel => channel.enabled);
|
||||
|
||||
const organizationStorageChannels = await getOrganizationStorageChannels(organization.id);
|
||||
const activeOrganizationStorageChannels = organizationStorageChannels.filter(channel => channel.enabled);
|
||||
|
||||
|
||||
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||
|
||||
@@ -106,60 +90,22 @@ export default async function RoutePage(props: PageParams<{
|
||||
|
||||
return (
|
||||
<Page>
|
||||
|
||||
<div className="justify-between gap-2 sm:flex">
|
||||
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||
<div className="min-w-full md:min-w-fit ">
|
||||
{capitalizeFirstLetter(dbItem.name)}
|
||||
</div>
|
||||
{!isMember && (
|
||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||
<div className="flex items-center gap-2">
|
||||
<RetentionPolicySheet database={dbItem}/>
|
||||
<CronButton database={dbItem}/>
|
||||
<ChannelPoliciesModal
|
||||
database={dbItem}
|
||||
kind={"notification"}
|
||||
icon={<Megaphone/>}
|
||||
channels={activeOrganizationChannels}
|
||||
organizationId={organization.id}
|
||||
/>
|
||||
<ChannelPoliciesModal
|
||||
database={dbItem}
|
||||
icon={<HardDrive/>}
|
||||
kind={"storage"}
|
||||
channels={activeOrganizationStorageChannels}
|
||||
organizationId={organization.id}
|
||||
/>
|
||||
<ImportModal database={dbItem}/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PageTitle>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{dbItem.description && (
|
||||
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
|
||||
)}
|
||||
<PageContent className="flex flex-col w-full h-full">
|
||||
<DatabaseKpi successRate={successRate} database={dbItem} availableBackups={availableBackups}
|
||||
totalBackups={totalBackups}/>
|
||||
<BackupModalProvider>
|
||||
<DatabaseContent
|
||||
activeMember={activeMember}
|
||||
settings={settings}
|
||||
database={dbItem}
|
||||
isAlreadyRestore={isAlreadyRestore}
|
||||
restorations={restorations}
|
||||
backups={backups}
|
||||
/>
|
||||
</BackupModalProvider>
|
||||
</PageContent>
|
||||
<BackupModalProvider>
|
||||
<DatabaseContent
|
||||
activeMember={activeMember}
|
||||
settings={settings}
|
||||
database={dbItem}
|
||||
isAlreadyRestore={isAlreadyRestore}
|
||||
restorations={restorations}
|
||||
backups={backups}
|
||||
totalBackups={totalBackups}
|
||||
availableBackups={availableBackups}
|
||||
successRate={successRate}
|
||||
organizationId={organization.id}
|
||||
activeOrganizationChannels={[]}
|
||||
activeOrganizationStorageChannels={[]}
|
||||
/>
|
||||
</BackupModalProvider>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -66,7 +66,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
organizationSlug={organization.slug}
|
||||
data={projects}
|
||||
cardItem={ProjectCard}
|
||||
cardsPerPage={6}
|
||||
cardsPerPage={9}
|
||||
numberOfColumns={3}
|
||||
/>
|
||||
) : isMember ? (
|
||||
|
||||
@@ -19,7 +19,6 @@ export async function POST(
|
||||
) {
|
||||
|
||||
try {
|
||||
eventEmitter.emit('modification', {update: true});
|
||||
|
||||
const agentId = (await params).agentId
|
||||
const body: BodyResultRestore = await request.json();
|
||||
@@ -72,7 +71,6 @@ export async function POST(
|
||||
details: "Restoration successfully updated"
|
||||
}
|
||||
|
||||
eventEmitter.emit('modification', {update: true});
|
||||
|
||||
return Response.json(response, {status: 200})
|
||||
} catch (error) {
|
||||
|
||||
@@ -10,15 +10,16 @@ import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import type {StorageInput} from "@/features/storages/types";
|
||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
|
||||
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) {
|
||||
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date, settings: Setting) {
|
||||
const databasesResponse = [];
|
||||
|
||||
const formatDatabase = (database: DatabaseWith, backupAction: boolean, restoreAction: boolean, UrlBackup: string | null, storages: PingDatabaseStorageChannels[], urlMeta: string | null) => ({
|
||||
generatedId: database.agentDatabaseId,
|
||||
dbms: database.dbms,
|
||||
storages: storages,
|
||||
encrypt: false,
|
||||
encrypt: settings.encryption,
|
||||
data: {
|
||||
backup: {
|
||||
action: backupAction,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {eq} from "drizzle-orm";
|
||||
import {isUuidv4} from "@/utils/verify-uuid";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {eventEmitter} from "@/features/shared/event";
|
||||
import {notFound} from "next/navigation";
|
||||
|
||||
export type databaseAgent = {
|
||||
name: string,
|
||||
@@ -48,7 +49,14 @@ export async function POST(
|
||||
message = "Agent not found"
|
||||
return NextResponse.json({error: message}, {status: 404})
|
||||
}
|
||||
const databasesResponse = await handleDatabases(body, agent, lastContact)
|
||||
|
||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||
if (!settings) {
|
||||
return NextResponse.json({error: "An error occured"}, {status: 404})
|
||||
}
|
||||
|
||||
|
||||
const databasesResponse = await handleDatabases(body, agent, lastContact, settings)
|
||||
|
||||
await db
|
||||
.update(drizzleDb.schemas.agent)
|
||||
@@ -58,7 +66,6 @@ export async function POST(
|
||||
}))
|
||||
.where(eq(drizzleDb.schemas.agent.id, agentId));
|
||||
|
||||
eventEmitter.emit('modification', {update: true});
|
||||
|
||||
const response = {
|
||||
agent: {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import {EventEmitter} from 'events';
|
||||
import {auth} from "@/lib/auth/auth";
|
||||
import {headers} from "next/headers";
|
||||
import {NextResponse} from "next/server";
|
||||
import {eventEmitter} from "@/features/shared/event";
|
||||
|
||||
// export const eventEmitter = new EventEmitter();
|
||||
|
||||
export async function GET(request: Request) {
|
||||
|
||||
const session = await auth.api.getSession({
|
||||
@@ -28,7 +25,6 @@ export async function GET(request: Request) {
|
||||
|
||||
eventEmitter.on('modification', handleModification);
|
||||
|
||||
// Handle client disconnect
|
||||
request.signal.addEventListener('abort', () => {
|
||||
console.log('Client disconnected');
|
||||
controller.close();
|
||||
|
||||
+26
-11
@@ -6,8 +6,11 @@
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-sans: var(--font-poppins), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-title: var(--font-poppins), var(--font-poppins), sans-serif;
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-author: var(--font-author);
|
||||
--font-poppins: var(--font-poppins);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
@@ -71,7 +74,7 @@
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary: oklch(0.611 0.204 42.149);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
@@ -82,7 +85,7 @@
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--ring: oklch(0.611 0.204 42.149);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
@@ -90,12 +93,12 @@
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary: oklch(0.611 0.204 42.149);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--sidebar-ring: oklch(0.611 0.204 42.149);
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -105,8 +108,8 @@
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--primary: oklch(0.704 0.191 47.132);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
@@ -116,7 +119,7 @@
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--ring: oklch(0.704 0.191 47.132);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
@@ -124,12 +127,12 @@
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary: oklch(0.704 0.191 47.132);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
--sidebar-ring: oklch(0.704 0.191 47.132);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -137,6 +140,18 @@
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
@apply bg-background text-foreground font-sans;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@apply font-title;
|
||||
}
|
||||
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import "./globals.css";
|
||||
import {Providers} from "./providers";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer";
|
||||
import {inter} from "@/fonts/fonts";
|
||||
import {author, geistMono, poppins} from "@/fonts/fonts";
|
||||
|
||||
const title = process.env.PROJECT_NAME ?? "App Title";
|
||||
|
||||
@@ -27,7 +27,7 @@ export default async function RootLayout({
|
||||
<head>
|
||||
<meta name="apple-mobile-web-app-title" content={title}/>
|
||||
</head>
|
||||
<body className={cn(inter.className, "h-full")}>
|
||||
<body className={cn(poppins.variable, author.variable, geistMono.variable, "font-sans h-full")}>
|
||||
<ConsoleSilencer/>
|
||||
<Providers>
|
||||
{children}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {ThemeProvider} from "@/features/theme/theme-provider";
|
||||
import {Toaster} from "@/components/ui/sonner";
|
||||
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
|
||||
import {ThemeMetaUpdaterRoot} from "@/features/browser/theme-meta-updater-root";
|
||||
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
||||
|
||||
|
||||
export type ProviderProps = PropsWithChildren<{}>;
|
||||
|
||||
+79
-81
@@ -1,13 +1,11 @@
|
||||
{
|
||||
"name": "portabase",
|
||||
"version": "1.2.5-rc.14",
|
||||
"version": "1.2.6-rc.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 8887",
|
||||
"build": "next build --experimental-build-mode compile",
|
||||
"start": "next start",
|
||||
|
||||
|
||||
"lint": "next lint",
|
||||
"email": "email dev --dir ./src/components/emails",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
@@ -16,52 +14,52 @@
|
||||
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@radix-ui/react-accordion": "^1.2.10",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.13",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.6",
|
||||
"@radix-ui/react-avatar": "^1.1.9",
|
||||
"@radix-ui/react-checkbox": "^1.3.1",
|
||||
"@radix-ui/react-collapsible": "^1.1.10",
|
||||
"@radix-ui/react-context-menu": "^2.2.14",
|
||||
"@radix-ui/react-dialog": "^1.1.13",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||
"@radix-ui/react-hover-card": "^1.1.13",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.6",
|
||||
"@radix-ui/react-menubar": "^1.1.14",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.12",
|
||||
"@radix-ui/react-popover": "^1.1.13",
|
||||
"@radix-ui/react-progress": "^1.1.6",
|
||||
"@radix-ui/react-radio-group": "^1.3.6",
|
||||
"@radix-ui/react-scroll-area": "^1.2.8",
|
||||
"@radix-ui/react-select": "^2.2.4",
|
||||
"@radix-ui/react-separator": "^1.1.6",
|
||||
"@radix-ui/react-slider": "^1.3.4",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"@radix-ui/react-switch": "^1.2.4",
|
||||
"@radix-ui/react-tabs": "^1.1.11",
|
||||
"@radix-ui/react-toast": "^1.2.13",
|
||||
"@radix-ui/react-toggle": "^1.1.8",
|
||||
"@radix-ui/react-toggle-group": "^1.1.9",
|
||||
"@radix-ui/react-tooltip": "^1.2.6",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-menubar": "^1.1.16",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-email/components": "^0.0.41",
|
||||
"@t3-oss/env-nextjs": "^0.13.4",
|
||||
"@tanstack/react-query": "^5.76.1",
|
||||
"@t3-oss/env-nextjs": "^0.13.10",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/nodemailer": "^6.4.22",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@zenstackhq/runtime": "2.14.2",
|
||||
"argon2": "^0.43.0",
|
||||
"argon2": "^0.43.1",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-auth": "1.4.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dockerode": "^4.0.6",
|
||||
"dotenv": "^16.5.0",
|
||||
"dockerode": "^4.0.9",
|
||||
"dotenv": "^16.6.1",
|
||||
"drizzle-orm": "^0.43.1",
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
@@ -69,65 +67,65 @@
|
||||
"googleapis": "^170.1.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.553.0",
|
||||
"minio": "^8.0.5",
|
||||
"motion": "^12.23.24",
|
||||
"minio": "^8.0.6",
|
||||
"motion": "^12.34.0",
|
||||
"next": "16.1.5",
|
||||
"next-safe-action": "^7.10.8",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-cron": "^4.2.1",
|
||||
"node-forge": "^1.3.1",
|
||||
"nodemailer": "^7.0.3",
|
||||
"npm-check-updates": "^18.0.1",
|
||||
"pg": "^8.16.0",
|
||||
"prettier": "^3.8.0",
|
||||
"react": "^19.2.0",
|
||||
"node-forge": "^1.3.3",
|
||||
"nodemailer": "^7.0.13",
|
||||
"npm-check-updates": "^18.3.1",
|
||||
"pg": "^8.18.0",
|
||||
"prettier": "^3.8.1",
|
||||
"react": "^19.2.4",
|
||||
"react-day-picker": "9.7.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-email": "^4.0.13",
|
||||
"react-hook-form": "^7.56.3",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-dropzone": "^14.4.1",
|
||||
"react-email": "^4.3.2",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"react-qr-code": "^2.0.18",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"react-twc": "^1.4.2",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-twc": "^1.5.1",
|
||||
"react-use-measure": "^2.1.7",
|
||||
"recharts": "^2.15.3",
|
||||
"recharts": "^2.15.4",
|
||||
"sharp": "^0.34.5",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"sonner": "^2.0.3",
|
||||
"swiper": "^12.0.3",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"sonner": "^2.0.7",
|
||||
"swiper": "^12.1.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vaul": "^1.1.2",
|
||||
"ws": "^8.18.2",
|
||||
"zod": "^3.24.4"
|
||||
"ws": "^8.19.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/react": "^6.0.0",
|
||||
"@iconify/react": "^6.0.2",
|
||||
"@react-email/preview-server": "4.3.2",
|
||||
"@react-email/render": "^2.0.1",
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
"@react-email/render": "^2.0.4",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"@types/node": "^22.15.18",
|
||||
"@types/node-forge": "^1",
|
||||
"@types/pg": "^8.15.2",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@zenstackhq/openapi": "^2.14.2",
|
||||
"@zenstackhq/tanstack-query": "^2.14.2",
|
||||
"@types/node": "^22.19.11",
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"@types/pg": "^8.16.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@zenstackhq/openapi": "^2.22.1",
|
||||
"@zenstackhq/tanstack-query": "^2.22.2",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"esbuild": "^0.27.2",
|
||||
"eslint": "^9.39.0",
|
||||
"eslint-config-next": "^16.0.1",
|
||||
"eslint-plugin-tailwindcss": "^3.18.0",
|
||||
"framer-motion": "^12.24.7",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"tsx": "^4.19.4",
|
||||
"tw-animate-css": "^1.2.9",
|
||||
"typescript": "^5.8.3",
|
||||
"drizzle-kit": "^0.31.9",
|
||||
"esbuild": "^0.27.3",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-next": "^16.1.6",
|
||||
"eslint-plugin-tailwindcss": "^3.18.2",
|
||||
"framer-motion": "^12.34.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tsx": "^4.21.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"zenstack": "2.14.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316"
|
||||
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8"
|
||||
}
|
||||
|
||||
Generated
+923
-861
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ import { motion, useInView, type SpringOptions, type UseInViewOptions } from 'mo
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const githubButtonVariants = cva(
|
||||
'cursor-pointer relative overflow-hidden will-change-transform backface-visibility-hidden transform-gpu transition-transform duration-200 ease-out hover:scale-105 group whitespace-nowrap focus-visible:outline-hidden inline-flex items-center justify-center whitespace-nowrap font-medium ring-offset-background disabled:pointer-events-none disabled:opacity-60 [&_svg]:shrink-0',
|
||||
'cursor-pointer relative overflow-hidden will-change-transform backface-visibility-hidden transform-gpu group whitespace-nowrap focus-visible:outline-hidden inline-flex items-center justify-center whitespace-nowrap font-medium ring-offset-background disabled:pointer-events-none disabled:opacity-60 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -374,7 +374,7 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto scrollbar-hide group-data-[collapsible=icon]:overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -85,7 +85,7 @@ export const LoginForm = (props: loginFormProps) => {
|
||||
<FormItem>
|
||||
<FormLabel>Email Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input autoComplete="email" autoFocus placeholder="exemple@portabase.io" {...field} />
|
||||
<Input autoComplete="email" autoFocus placeholder="example@portabase.io" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
@@ -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>
|
||||
@@ -88,7 +88,7 @@ export const RegisterForm = (props: registerFormProps) => {
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="exemple@portabase.io" {...field} />
|
||||
<Input placeholder="example@portabase.io" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client"
|
||||
import {Button, ButtonVariantsProps} from "@/components/ui/button";
|
||||
import {useState} from "react";
|
||||
import {ReactNode, useState} from "react";
|
||||
import {Loader2} from "lucide-react";
|
||||
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
|
||||
export type ButtonWithConfirmProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
button: {
|
||||
button?: {
|
||||
main: {
|
||||
className?: string;
|
||||
type?: "button" | "submit" | "reset" | undefined;
|
||||
@@ -37,6 +37,11 @@ export type ButtonWithConfirmProps = {
|
||||
onClick?: () => void;
|
||||
};
|
||||
};
|
||||
children?: ReactNode;
|
||||
onConfirm?: (e: React.MouseEvent) => void;
|
||||
onCancel?: (e: React.MouseEvent) => void;
|
||||
confirmButtonText?: string;
|
||||
cancelButtonText?: string;
|
||||
isPending?: boolean;
|
||||
};
|
||||
|
||||
@@ -44,41 +49,86 @@ export type ButtonWithConfirmProps = {
|
||||
export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
|
||||
return (
|
||||
<Popover open={isConfirming} onOpenChange={!props.button.main.disabled ? setIsConfirming : undefined}>
|
||||
const isLegacy = !!props.button;
|
||||
const isDisabled = isLegacy ? !!props.button?.main.disabled : false;
|
||||
|
||||
const handleConfirm = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isLegacy) {
|
||||
props.button?.confirm.onClick?.();
|
||||
} else {
|
||||
props.onConfirm?.(e);
|
||||
}
|
||||
setIsConfirming(false);
|
||||
};
|
||||
|
||||
const handleCancel = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isLegacy) {
|
||||
props.button?.cancel.onClick?.();
|
||||
} else {
|
||||
props.onCancel?.(e);
|
||||
}
|
||||
setIsConfirming(false);
|
||||
};
|
||||
|
||||
const triggerContent = isLegacy ? (
|
||||
<Button
|
||||
type={props.button?.main.type}
|
||||
disabled={isDisabled}
|
||||
variant={props.button?.main.variant ?? "default"}
|
||||
size={props.button?.main.size ?? "default"}
|
||||
className={props.button?.main.className}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!isDisabled) setIsConfirming(true);
|
||||
}}
|
||||
>
|
||||
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
|
||||
{props.button?.main.icon}
|
||||
{props.button?.main.text && <span>{props.button.main.text}</span>}
|
||||
</Button>
|
||||
) : (
|
||||
<div onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsConfirming(true);
|
||||
}}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const withTooltip = isLegacy && props.button?.main.tooltipText && isDisabled ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<PopoverTrigger asChild>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex",
|
||||
props.button.main.disabled ? "cursor-not-allowed" : ""
|
||||
)}
|
||||
role="button">
|
||||
<Button
|
||||
type={props.button.main.type}
|
||||
disabled={!!props.button.main.disabled}
|
||||
variant={props.button.main.variant ?? "default"}
|
||||
size={props.button.main.size ?? "default"}
|
||||
className={props.button.main.className}
|
||||
onClick={() => {
|
||||
if (!props.button.main.disabled) setIsConfirming(true);
|
||||
}}
|
||||
>
|
||||
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
|
||||
{props.button.main.icon}
|
||||
{props.button.main.text && <span>{props.button.main.text}</span>}
|
||||
</Button>
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
<div>{triggerContent}</div>
|
||||
</TooltipTrigger>
|
||||
{props.button.main.tooltipText && props.button.main.disabled && (
|
||||
<TooltipContent>
|
||||
<p>{props.button.main.tooltipText}</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
<TooltipContent>
|
||||
<p>{props.button?.main.tooltipText}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent className="w-80">
|
||||
</TooltipProvider>
|
||||
) : triggerContent;
|
||||
|
||||
return (
|
||||
<Popover open={isConfirming} onOpenChange={setIsConfirming}>
|
||||
<PopoverTrigger asChild>
|
||||
{withTooltip}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-80"
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseMove={(e) => e.stopPropagation()}
|
||||
onMouseEnter={(e) => e.stopPropagation()}
|
||||
onMouseLeave={(e) => e.stopPropagation()}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium leading-none">{props.title}</h4>
|
||||
@@ -86,29 +136,22 @@ export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (isConfirming && props.button.confirm.onClick) {
|
||||
props.button.confirm.onClick();
|
||||
setIsConfirming(false);
|
||||
} else {
|
||||
setIsConfirming(true);
|
||||
}
|
||||
}}
|
||||
variant={props.button.confirm.variant ? props.button.confirm.variant : "default"}
|
||||
size={props.button.main.size ?? "default"}
|
||||
className={cn(props.button.main.className, "w-full")}
|
||||
onClick={handleConfirm}
|
||||
variant={isLegacy ? (props.button?.confirm.variant ?? "default") : "default"}
|
||||
size={isLegacy ? (props.button?.main.size ?? "default") : "default"}
|
||||
className={cn(isLegacy ? props.button?.main.className : "", "w-full")}
|
||||
>
|
||||
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
|
||||
{props.button.confirm.icon ? props.button.confirm.icon : null}
|
||||
<span>{props.button.confirm.text}</span>
|
||||
{isLegacy && props.button?.confirm.icon}
|
||||
<span>{isLegacy ? props.button?.confirm.text : (props.confirmButtonText ?? "Confirm")}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={props.button.cancel.variant ? props.button.cancel.variant : "default"}
|
||||
onClick={props.button.cancel.onClick ? () => props.button.cancel.onClick!() : () => setIsConfirming(false)}
|
||||
className={cn(props.button.main.className, "w-full")}
|
||||
size={props.button.main.size ?? "default"}
|
||||
variant={isLegacy ? (props.button?.cancel.variant ?? "outline") : "outline"}
|
||||
onClick={handleCancel}
|
||||
className={cn(isLegacy ? props.button?.main.className : "", "w-full")}
|
||||
size={isLegacy ? (props.button?.main.size ?? "default") : "default"}
|
||||
>
|
||||
Cancel
|
||||
{isLegacy ? (props.button?.cancel.text ?? "Cancel") : (props.cancelButtonText ?? "Cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,4 +159,3 @@ export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ export type ButtonWithLoadingProps = {
|
||||
icon?: ReactNode;
|
||||
variant?: keyof VariantButton;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
isPending?: boolean;
|
||||
size?: keyof SizeButton;
|
||||
} & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
@@ -100,7 +100,7 @@ export const ButtonWithLoading = ({
|
||||
}: ButtonWithLoadingProps) => {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => onClick?.()}
|
||||
onClick={(e) => onClick?.(e)}
|
||||
variant={variant}
|
||||
className={className}
|
||||
size={size}
|
||||
|
||||
@@ -26,7 +26,6 @@ export const CopyButton = (props: CopyButtonProps) => {
|
||||
|
||||
return (
|
||||
<Button
|
||||
// className={cn(buttonVariants({size: "sm"}))}
|
||||
onClick={() => {
|
||||
copyToClipboardWithMeta(value);
|
||||
setHasCopied(true);
|
||||
|
||||
@@ -1,21 +1,46 @@
|
||||
import { PropsWithChildren } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
"use client";
|
||||
|
||||
import {PropsWithChildren, useState} from "react";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Check, Copy, Terminal} from "lucide-react";
|
||||
import {copyToClipboardWithMeta} from "@/components/wrappers/common/button/copy-button";
|
||||
import {cn} from "@/lib/utils";
|
||||
|
||||
export type CodeSnippetProps = PropsWithChildren<{
|
||||
code: string;
|
||||
title?: string;
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
export const CodeSnippet = (props: CodeSnippetProps) => {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
await copyToClipboardWithMeta(props.code);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-background">
|
||||
<div className="flex items-center justify-between border-b bg-muted px-4 py-2">
|
||||
<div className="text-sm font-medium">.env</div>
|
||||
<Button variant="ghost" size="icon" className="hover:bg-muted/50 text-muted-foreground"></Button>
|
||||
</div>
|
||||
<div className="p-4 font-mono text-sm leading-6 text-foreground truncate">
|
||||
<pre className="language-javascript overflow-x-auto">
|
||||
<code className="mb-6">{props.code}</code>
|
||||
<div className={cn("relative group rounded-md bg-muted/50 border overflow-hidden", props.className)}>
|
||||
{props.title && (
|
||||
<div className="flex items-center px-4 py-2 text-xs font-medium text-muted-foreground border-b bg-muted/30">
|
||||
{props.title}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center p-4">
|
||||
<pre className="flex-1 overflow-x-auto font-mono text-sm leading-relaxed">
|
||||
<code className="break-all whitespace-pre-wrap">{props.code}</code>
|
||||
</pre>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-2 h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{isCopied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
|
||||
<span className="sr-only">Copy code</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ export const AgentRegistrationDialog = (props: agentRegistrationDialogProps) =>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">
|
||||
EDGE KEY
|
||||
CONNECTION KEY
|
||||
</Label>
|
||||
<Input id="name" value="Pedro Duarte" readOnly className="col-span-3" />
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@ export const GitHubStarsButtonCustom = () => {
|
||||
<GithubButton
|
||||
initialStars={0}
|
||||
targetStars={stars}
|
||||
label="Github Stars"
|
||||
label=""
|
||||
size="sm"
|
||||
separator={true}
|
||||
roundStars={true}
|
||||
|
||||
@@ -15,6 +15,8 @@ export type paginationNavigationProps = {
|
||||
export const PaginationNavigation = (props: paginationNavigationProps) => {
|
||||
const { className, totalPages, currentPage, goToPage, goToPrevPage, goToNextPage, maxVisiblePages = 3 } = props;
|
||||
|
||||
if (totalPages <= 1) return null;
|
||||
|
||||
return (
|
||||
<Pagination className={cn("", className)}>
|
||||
<PaginationContent>
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table";
|
||||
import {Input} from "@/components/ui/input";
|
||||
|
||||
import {ReactNode, useEffect, useState} from "react";
|
||||
import {ReactNode, useMemo, useState} from "react";
|
||||
import {TablePagination} from "./table-pagination";
|
||||
import {Checkbox} from "@/components/ui/checkbox";
|
||||
import {Button} from "@/components/ui/button";
|
||||
@@ -63,35 +63,38 @@ export function DataTable<TData, TValue>({
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
setRowSelection({});
|
||||
}, [data]);
|
||||
|
||||
if (enableSelect && data.length > 0) {
|
||||
const finalColumns = useMemo(() => {
|
||||
const selectColumnExists = columns.some((column) => column.id === "select");
|
||||
|
||||
if (!selectColumnExists) {
|
||||
columns.unshift({
|
||||
id: "select",
|
||||
header: ({table}) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({row}) => <Checkbox checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"/>,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
});
|
||||
if (enableSelect && data.length > 0 && !selectColumnExists) {
|
||||
return [
|
||||
{
|
||||
id: "select",
|
||||
header: ({table}: {table: any}) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({row}: {row: any}) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
...columns,
|
||||
];
|
||||
}
|
||||
}
|
||||
return columns;
|
||||
}, [columns, enableSelect, data.length]);
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
columns: finalColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
@@ -99,6 +102,10 @@ export function DataTable<TData, TValue>({
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getRowId: (row: any) => row.id || row.uuid,
|
||||
autoResetPageIndex: false,
|
||||
autoResetExpanded: false,
|
||||
enableRowSelection: true,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
ChannelOrganisationForm
|
||||
} from "@/components/wrappers/dashboard/admin/channels/organization/channels-organization-form";
|
||||
|
||||
|
||||
type ChannelAddModalProps = {
|
||||
channel?: NotificationChannelWith | StorageChannelWith
|
||||
organization?: OrganizationWithMembers;
|
||||
|
||||
+5
-4
@@ -34,6 +34,7 @@ import {
|
||||
addNotificationChannelAction, updateNotificationChannelAction
|
||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/action";
|
||||
|
||||
|
||||
type NotifierFormProps = {
|
||||
onSuccessAction?: () => void;
|
||||
organization?: OrganizationWithMembers;
|
||||
@@ -54,7 +55,7 @@ export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset(defaultValues ? {...defaultValues} : {});
|
||||
form.reset(defaultValues ? {...defaultValues} : {enabled: true});
|
||||
}, [defaultValues]);
|
||||
|
||||
const mutationAddNotificationChannel = useMutation({
|
||||
@@ -118,7 +119,7 @@ export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}
|
||||
}}
|
||||
>
|
||||
|
||||
{type.preview && (
|
||||
{type.preview && (
|
||||
<div
|
||||
className="absolute bottom-0 right-0 overflow-hidden w-20 h-20 pointer-events-none">
|
||||
<div
|
||||
@@ -223,11 +224,11 @@ export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}
|
||||
|
||||
<div className="flex justify-between mt-4">
|
||||
<div>
|
||||
{defaultValues && (
|
||||
{kind == "storage" && (
|
||||
<ChannelTestButton
|
||||
kind={kind}
|
||||
organizationId={organization?.id}
|
||||
channel={defaultValues}
|
||||
channel={form.getValues()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+12
-8
@@ -1,18 +1,21 @@
|
||||
"use client"
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {dispatchNotification} from "@/features/notifications/dispatch";
|
||||
import {EventPayload} from "@/features/notifications/types";
|
||||
import {Send, ShieldCheck} from "lucide-react";
|
||||
import {toast} from "sonner";
|
||||
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||
import {dispatchNotification} from "@/features/notifications/dispatch";
|
||||
import {EventPayload} from "@/features/notifications/types";
|
||||
import {useIsMobile} from "@/hooks/use-mobile";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {StorageChannel} from "@/db/schema/12_storage-channel";
|
||||
import {ChannelKind, getChannelTextBasedOnKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||
import {ChannelKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||
import type {StorageInput} from "@/features/storages/types";
|
||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||
|
||||
|
||||
type NotifierTestChannelButtonProps = {
|
||||
channel: NotificationChannel | StorageChannel;
|
||||
organizationId?: string;
|
||||
@@ -20,8 +23,9 @@ type NotifierTestChannelButtonProps = {
|
||||
}
|
||||
|
||||
export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestChannelButtonProps) => {
|
||||
const channelText = getChannelTextBasedOnKind(kind)
|
||||
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (kind === "notification") {
|
||||
@@ -30,7 +34,7 @@ export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestC
|
||||
message: `We are testing channel ${channel.name}`,
|
||||
level: 'info',
|
||||
};
|
||||
const result = await dispatchNotification(payload, undefined, channel.id, organizationId);
|
||||
const result = await dispatchNotification(payload, undefined, channel, organizationId);
|
||||
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
@@ -41,7 +45,7 @@ export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestC
|
||||
const input: StorageInput = {
|
||||
action: "ping",
|
||||
};
|
||||
const result = await dispatchStorage(input, undefined, channel.id);
|
||||
const result = await dispatchStorage(input, undefined, undefined, channel);
|
||||
|
||||
if (result.success) {
|
||||
toast.success("Successfully connected to storage channel");
|
||||
|
||||
+2
-2
@@ -47,7 +47,7 @@ export const NotifierSmtpForm = ({form}: NotifierSmtpFormProps) => {
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="contact@exemple.com" {...field} value={field.value ?? ""}/>
|
||||
<Input placeholder="contact@example.com" {...field} value={field.value ?? ""}/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
@@ -77,7 +77,7 @@ export const NotifierSmtpForm = ({form}: NotifierSmtpFormProps) => {
|
||||
<FormItem>
|
||||
<FormLabel>From Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={`"Portabase" <exemple@portabase.io>`} {...field}
|
||||
<Input placeholder={`"Portabase" <example@portabase.io>`} {...field}
|
||||
value={field.value ?? ""}/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
|
||||
<input type="hidden" {...form.register("config.refreshToken")} />
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button type="button" onClick={handleConnect}>
|
||||
<Button type="button" variant={"secondary"} className="hover:cursor-pointer" onClick={handleConnect}>
|
||||
{isConnected ? "Reconnect Google Drive" : "Connect Google Drive"}
|
||||
</Button>
|
||||
|
||||
|
||||
+15
-2
@@ -19,9 +19,22 @@ export const StorageS3Form = ({form}: StorageS3FormProps) => {
|
||||
name="config.endPointUrl"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Endpoind URL</FormLabel>
|
||||
<FormLabel>Endpoint URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="s3.exemple.com"/>
|
||||
<Input {...field} placeholder="s3.amazonaws.com"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.region"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Region</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="us-east-1"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
|
||||
@@ -110,7 +110,7 @@ export const EmailForm = (props: EmailFormProps) => {
|
||||
<FormItem>
|
||||
<FormLabel>From Email *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"exemple@portabase.com"} {...field} />
|
||||
<Input placeholder={"example@portabase.com"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"The email from where the email will be send"}</FormDescription>
|
||||
<FormMessage/>
|
||||
@@ -172,7 +172,7 @@ export const EmailForm = (props: EmailFormProps) => {
|
||||
<FormItem>
|
||||
<FormLabel>User Email *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"exemple@portabase.com"} {...field} />
|
||||
<Input placeholder={"example@portabase.com"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"The email server user"}</FormDescription>
|
||||
<FormMessage/>
|
||||
|
||||
@@ -41,7 +41,7 @@ export const SettingsTabs = ({settings, storageChannels}: SettingsTabsProps) =>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Default Storage',
|
||||
name: 'Storage',
|
||||
value: 'storage',
|
||||
icon: Save,
|
||||
content: (
|
||||
|
||||
+33
-10
@@ -4,7 +4,7 @@ import {Info} from "lucide-react";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {Form, FormField, FormItem, useZodForm} from "@/components/ui/form";
|
||||
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
updateStorageSettingsAction
|
||||
} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage.action";
|
||||
import {toast} from "sonner";
|
||||
import {Switch} from "@/components/ui/switch";
|
||||
|
||||
export type SettingsStorageSectionProps = {
|
||||
settings: Setting;
|
||||
@@ -29,7 +30,8 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
|
||||
const form = useZodForm({
|
||||
schema: DefaultStorageSchema,
|
||||
defaultValues: {
|
||||
storageChannelId: settings.defaultStorageChannelId ?? undefined
|
||||
storageChannelId: settings.defaultStorageChannelId ?? undefined,
|
||||
encryption: settings.encryption ?? false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -59,6 +61,7 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
|
||||
</Alert>
|
||||
<div className="flex flex-col h-full py-4 ">
|
||||
<Form
|
||||
className="space-y-4"
|
||||
form={form}
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
@@ -68,11 +71,12 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="storageChannelId"
|
||||
render={({ field }) => (
|
||||
render={({field}) => (
|
||||
<FormItem className="flex-grow min-w-[200px] sm:flex-grow-0 sm:w-64">
|
||||
<FormLabel>Default Storage Provider</FormLabel>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="w-full h-full mb-0">
|
||||
<SelectValue placeholder="Select a default storage channel" />
|
||||
<SelectValue placeholder="Select a default storage channel"/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{storageChannels.map((channel) => (
|
||||
@@ -80,9 +84,10 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
|
||||
<div className="flex items-center gap-2">
|
||||
{getChannelIcon(channel.provider)}
|
||||
<span className="font-medium">{channel.name}</span>
|
||||
<span className="text-[9px] uppercase bg-secondary px-1.5 py-0.5 rounded">
|
||||
{channel.provider}
|
||||
</span>
|
||||
<span
|
||||
className="text-[9px] uppercase bg-secondary px-1.5 py-0.5 rounded">
|
||||
{channel.provider}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -91,11 +96,29 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<ButtonWithLoading className="flex-shrink-0 w-full sm:w-auto" type="submit">
|
||||
Confirm
|
||||
</ButtonWithLoading>
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="encryption"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Backups Encryption</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ButtonWithLoading className="flex-shrink-0 w-full sm:w-auto" type="submit">
|
||||
Confirm
|
||||
</ButtonWithLoading>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,11 +20,11 @@ export const updateStorageSettingsAction = userAction
|
||||
const {name, data} = parsedInput;
|
||||
|
||||
try {
|
||||
|
||||
const [updatedSettings] = await db
|
||||
.update(drizzleDb.schemas.setting)
|
||||
.set({
|
||||
defaultStorageChannelId: data.storageChannelId,
|
||||
encryption: data.encryption,
|
||||
})
|
||||
.where(eq(drizzleDb.schemas.setting.name, name))
|
||||
.returning();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import {z} from "zod";
|
||||
|
||||
export const DefaultStorageSchema = z.object({
|
||||
storageChannelId: z.string(),
|
||||
encryption: z.boolean(),
|
||||
});
|
||||
|
||||
export type DefaultStorageType = z.infer<typeof DefaultStorageSchema>;
|
||||
|
||||
@@ -1,23 +1,145 @@
|
||||
"use client";
|
||||
import {PasswordInput} from "@/components/ui/password-input";
|
||||
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Copy, Check, Eye, EyeOff, Terminal, Key, Info} from "lucide-react";
|
||||
import {useState} from "react";
|
||||
import {CopyButton} from "@/components/wrappers/common/button/copy-button";
|
||||
import {copyToClipboardWithMeta} from "@/components/wrappers/common/button/copy-button";
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
|
||||
export type AgentCardKeyProps = {
|
||||
edgeKey: string;
|
||||
agentName: string;
|
||||
};
|
||||
|
||||
export const AgentCardKey = ({edgeKey}: AgentCardKeyProps) => {
|
||||
const [code, setCode] = useState<string>(`${edgeKey}`);
|
||||
export const AgentCardKey = ({edgeKey, agentName}: AgentCardKeyProps) => {
|
||||
const [isCopiedKey, setIsCopiedKey] = useState(false);
|
||||
const [isCopiedCommand, setIsCopiedCommand] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const command = `portabase agent "${agentName}" --key ${edgeKey}`;
|
||||
const maskedKey = "••••••••••••••••••••••••••••••••";
|
||||
|
||||
const handleCopy = async (text: string, setter: (v: boolean) => void) => {
|
||||
await copyToClipboardWithMeta(text);
|
||||
setter(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PasswordInput
|
||||
value={code}
|
||||
onChange={() => {
|
||||
setCode(edgeKey);
|
||||
}}
|
||||
/>
|
||||
<CopyButton className="mt-5" value={code}/>
|
||||
</>
|
||||
<div className="flex flex-col gap-4 py-2">
|
||||
<Card className="border-muted/60 shadow-none py-0">
|
||||
<CardHeader className="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">
|
||||
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">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
readOnly
|
||||
value={isVisible ? command : `portabase agent "${agentName}" --key ${maskedKey}`}
|
||||
onFocus={(e) => {
|
||||
setIsVisible(true);
|
||||
handleCopy(command, setIsCopiedCommand);
|
||||
e.currentTarget.select();
|
||||
}}
|
||||
onClick={(e) => e.currentTarget.select()}
|
||||
onBlur={() => {
|
||||
setIsVisible(false);
|
||||
setIsCopiedCommand(false);
|
||||
}}
|
||||
className="font-mono text-xs bg-muted/30 h-10 pr-10 cursor-pointer"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsVisible(!isVisible);
|
||||
}}
|
||||
className="absolute right-1 top-1.5 h-7 w-7"
|
||||
type="button"
|
||||
>
|
||||
{isVisible ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleCopy(command, setIsCopiedCommand)}
|
||||
className="shrink-0 gap-2 h-10 px-4"
|
||||
type="button"
|
||||
>
|
||||
{isCopiedCommand ? (
|
||||
<><Check className="h-4 w-4 text-green-500" /></>
|
||||
) : (
|
||||
<><Copy className="h-4 w-4" /></>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-muted/60 shadow-none py-0">
|
||||
<CardHeader className="px-4 pt-4">
|
||||
<CardTitle className="text-sm font-semibold flex items-center gap-2 uppercase tracking-tight">
|
||||
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">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
readOnly
|
||||
value={isVisible ? edgeKey : maskedKey}
|
||||
onFocus={(e) => {
|
||||
setIsVisible(true);
|
||||
handleCopy(edgeKey, setIsCopiedKey);
|
||||
e.currentTarget.select();
|
||||
}}
|
||||
onClick={(e) => e.currentTarget.select()}
|
||||
onBlur={() => {
|
||||
setIsVisible(false);
|
||||
setIsCopiedKey(false);
|
||||
}}
|
||||
className="font-mono text-xs bg-muted/30 h-10 pr-10 cursor-pointer"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsVisible(!isVisible);
|
||||
}}
|
||||
className="absolute right-1 top-1.5 h-7 w-7 hover:bg-transparent"
|
||||
type="button"
|
||||
>
|
||||
{isVisible ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleCopy(edgeKey, setIsCopiedKey)}
|
||||
className="shrink-0 gap-2 h-10 px-4 "
|
||||
type="button"
|
||||
>
|
||||
{isCopiedKey ? (
|
||||
<><Check className="h-4 w-4 text-green-500" /></>
|
||||
) : (
|
||||
<><Copy className="h-4 w-4" /></>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,10 +6,12 @@ import {Card} from "@/components/ui/card";
|
||||
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} from "lucide-react";
|
||||
import {Activity, ChevronRight, Copy, Check, Fingerprint, Server, Database, AlertCircle} from "lucide-react";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {truncateWords} from "@/utils/text";
|
||||
import {useIsMobile} from "@/hooks/use-mobile";
|
||||
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
import {useAgentUpdateCheck} from "@/features/agents/hooks/use-agent-update-check";
|
||||
|
||||
export type agentCardProps = {
|
||||
data: AgentWith;
|
||||
@@ -20,6 +22,8 @@ export const AgentCard = (props: agentCardProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const {newRelease, isUpdateAvailable} = useAgentUpdateCheck(agent.version);
|
||||
|
||||
const handleCopy = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -44,9 +48,23 @@ export const AgentCard = (props: agentCardProps) => {
|
||||
{isMobile ? truncateWords(agent.name, 2) : agent.name}
|
||||
</h3>
|
||||
{agent.version && (
|
||||
<Badge variant="secondary" className="h-5 px-1.5 text-[10px] font-bold tracking-wider">
|
||||
v{agent.version}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary" className="h-5 px-1.5 text-[10px] font-bold tracking-wider">
|
||||
v{agent.version}
|
||||
</Badge>
|
||||
{isUpdateAvailable && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center justify-center w-5 h-5 bg-yellow-500/10 rounded-full border border-yellow-500/20 text-yellow-600">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Update available: {newRelease?.tag_name}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,76 +4,111 @@ import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {Server} from "lucide-react";
|
||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/agent-card-key/agent-card-key";
|
||||
import {AgentWithDatabases} from "@/db/schema/08_agent";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {getAgentAction} from "@/features/agents/agents.action";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||
import {AgentDatabaseCard} from "@/components/wrappers/dashboard/agent/agent-database-card";
|
||||
import {AgentWithDatabases} from "@/db/schema/08_agent";
|
||||
import {eventUpdate} from "@/types/events";
|
||||
import {useAutoRefresh} from "@/hooks/use-auto-refresh";
|
||||
|
||||
type AgentContentPageProps = {
|
||||
edgeKey: string;
|
||||
agent: AgentWithDatabases
|
||||
|
||||
}
|
||||
|
||||
export const AgentContentPage = ({edgeKey, agent}: AgentContentPageProps) => {
|
||||
export const AgentContentPage = ({edgeKey, agent: initialAgent}: AgentContentPageProps) => {
|
||||
|
||||
useAutoRefresh({
|
||||
poll: {
|
||||
enabled: true,
|
||||
intervalMs: 5000,
|
||||
const {data} = useQuery({
|
||||
queryKey: ["agent-data", initialAgent.id],
|
||||
queryFn: async () => {
|
||||
const result = await getAgentAction(initialAgent.id);
|
||||
return result?.data;
|
||||
},
|
||||
sse: {
|
||||
enabled: true,
|
||||
url: "/api/events",
|
||||
eventName: "modification",
|
||||
shouldRefresh: (data) => {
|
||||
const update = data as eventUpdate;
|
||||
return Boolean(update?.update);
|
||||
},
|
||||
initialData: {
|
||||
data: initialAgent
|
||||
},
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
refetchInterval: 1000,
|
||||
});
|
||||
|
||||
const agent = data?.data ?? initialAgent;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-10">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between gap-6 ">
|
||||
<Card className="w-full sm:w-auto flex-1">
|
||||
<Card className="w-full sm:w-auto flex-1 border-none shadow-none bg-muted/30">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Databases</CardTitle>
|
||||
<Server className="h-4 w-4 text-muted-foreground"/>
|
||||
<CardTitle className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">Databases</CardTitle>
|
||||
<Server className="h-4 w-4 text-muted-foreground opacity-50"/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{agent.databases.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Databases linked to this agent</p>
|
||||
<div className="text-3xl font-bold tracking-tight">{agent.databases.length}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">Linked resources</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="w-full sm:w-auto flex-1">
|
||||
<Card className="w-full sm:w-auto flex-1 border-none shadow-none bg-muted/30">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Last contact</CardTitle>
|
||||
<Server className="h-4 w-4 text-muted-foreground"/>
|
||||
<CardTitle className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">Last contact</CardTitle>
|
||||
<Server className="h-4 w-4 text-muted-foreground opacity-50"/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{formatDateLastContact(agent.lastContact)}</div>
|
||||
<p className="text-xs text-muted-foreground">Last contact with agent</p>
|
||||
<div className="text-3xl font-bold tracking-tight">{formatDateLastContact(agent.lastContact)}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">Status heartbeat</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
<Card className="w-full sm:w-auto flex-1 ">
|
||||
<CardHeader className="font-bold text-xl">
|
||||
Edge Key
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AgentCardKey
|
||||
edgeKey={edgeKey}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<CardsWithPagination cardsPerPage={4} numberOfColumns={2} data={agent.databases}
|
||||
cardItem={AgentDatabaseCard}/>
|
||||
|
||||
</>
|
||||
<div className="space-y-6">
|
||||
<Accordion type="single" collapsible defaultValue={!agent.lastContact ? "registration" : undefined}>
|
||||
<AccordionItem value="registration" className="border rounded-xl px-6 bg-card shadow-sm overflow-hidden transition-all duration-300">
|
||||
<AccordionTrigger className="hover:no-underline py-4 group">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl font-bold tracking-tight">Registration & Setup</span>
|
||||
{!agent.lastContact && (
|
||||
<Badge variant="outline" className="bg-orange-500/10 text-orange-600 border-orange-500/20 animate-pulse">
|
||||
Action Required
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-6 pt-2 border-t border-dashed">
|
||||
<AgentCardKey
|
||||
edgeKey={edgeKey}
|
||||
agentName={agent.name}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Resources currently connected to this agent.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="opacity-50" />
|
||||
<CardsWithPagination
|
||||
cardsPerPage={4}
|
||||
numberOfColumns={2}
|
||||
data={agent.databases}
|
||||
cardItem={AgentDatabaseCard}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client"
|
||||
|
||||
import {ColumnDef} from "@tanstack/react-table"
|
||||
import {Database} from "@/db/schema/07_database"
|
||||
import Image from "next/image"
|
||||
import {ConnectionIndicator} from "@/components/wrappers/common/connection-indicator"
|
||||
import {formatDateLastContact} from "@/utils/date-formatting"
|
||||
import Link from "next/link"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {ChevronRight} from "lucide-react"
|
||||
|
||||
export const agentDatabaseColumns: ColumnDef<Database>[] = [
|
||||
{
|
||||
accessorKey: "dbms",
|
||||
header: "Type",
|
||||
cell: ({row}) => (
|
||||
<div className="flex items-center justify-center w-8 h-8 p-1.5 bg-muted/50 rounded-lg border border-border/50">
|
||||
<Image
|
||||
src={`/images/${row.original.dbms}.png`}
|
||||
alt={row.original.dbms}
|
||||
width={20}
|
||||
height={20}
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
cell: ({row}) => (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-sm">{row.original.name}</span>
|
||||
<span className="text-[10px] text-muted-foreground font-mono truncate max-w-[150px]">
|
||||
{row.original.agentDatabaseId}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "lastContact",
|
||||
header: "Last Contact",
|
||||
cell: ({row}) => (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs">
|
||||
{formatDateLastContact(row.original.lastContact)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
header: "Status",
|
||||
cell: ({row}) => (
|
||||
<div className="flex justify-center w-full">
|
||||
<ConnectionIndicator date={row.original.lastContact} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => null,
|
||||
cell: ({row}) => {
|
||||
const href = row.original.projectId
|
||||
? `/dashboard/projects/${row.original.projectId}/database/${row.original.id}`
|
||||
: "#";
|
||||
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<Button variant="ghost" size="icon" asChild className="hover:bg-accent group">
|
||||
<Link href={href}>
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-transform group-hover:translate-x-0.5" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, DialogClose } from "@/components/ui/dialog";
|
||||
|
||||
import { generateEdgeKey } from "@/utils/edge_key";
|
||||
import { PropsWithChildren } from "react";
|
||||
@@ -16,26 +16,29 @@ export type agentRegistrationDialogProps = PropsWithChildren<{
|
||||
|
||||
export function AgentModalKey(props: agentRegistrationDialogProps) {
|
||||
const edge_key = generateEdgeKey(getServerUrl(), props.agent.id);
|
||||
const code = `EDGE_KEY = ${edge_key}`;
|
||||
const command = `portabase agent "${props.agent.name}" --key ${edge_key}`;
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{props.children}</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px] w-full">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Agent Edge Key</DialogTitle>
|
||||
<DialogTitle>Agent Connection</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="sm:max-w-[375px] w-full">
|
||||
<div className="sm:max-w-[375px] w-full space-y-4">
|
||||
<CodeSnippet
|
||||
code={code}
|
||||
// className="w-full overflow-x-auto break-words"
|
||||
title="Installation Command"
|
||||
code={command}
|
||||
/>
|
||||
<CodeSnippet
|
||||
title="Agent Key (Manual)"
|
||||
code={edge_key}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<CopyButton value={code} />
|
||||
<Button type="submit">Save changes</Button>
|
||||
</div>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline">Close</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||
import {useRouter} from "next/navigation";
|
||||
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {toast} from "sonner";
|
||||
|
||||
import {backupButtonAction} from "@/components/wrappers/dashboard/backup/backup-button/backup-button.action";
|
||||
@@ -16,6 +15,7 @@ export type BackupButtonProps = {
|
||||
};
|
||||
|
||||
export const BackupButton = (props: BackupButtonProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
@@ -24,6 +24,7 @@ export const BackupButton = (props: BackupButtonProps) => {
|
||||
const backup = await backupButtonAction(databaseId);
|
||||
if (backup?.data?.success) {
|
||||
toast.success(backup.data.actionSuccess?.message || "Backup created successfully!");
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", props.databaseId]});
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.error(backup?.serverError || "Failed to create backup.");
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import {PropsWithChildren, ReactNode, useState} from "react";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { CircleUser, LogOut } from "lucide-react";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
|
||||
import { LogOut, User } from "lucide-react";
|
||||
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
|
||||
import { signOut } from "@/lib/auth/auth-client";
|
||||
|
||||
import {ProfileModal} from "@/components/wrappers/dashboard/common/profile/profile-modal";
|
||||
import {Account, Session, User} from "@/db/schema/02_user";
|
||||
|
||||
import {Account, Session, User as UserType} from "@/db/schema/02_user";
|
||||
|
||||
import {AuthProviderConfig} from "../../../../../../portabase.config";
|
||||
|
||||
export type LoggedInDropdownProps = PropsWithChildren<{
|
||||
user: User;
|
||||
user: UserType;
|
||||
sessions: Session[];
|
||||
currentSession: Session;
|
||||
accounts: Account[];
|
||||
children: ReactNode;
|
||||
providers: AuthProviderConfig[]
|
||||
|
||||
}>;
|
||||
|
||||
|
||||
|
||||
export const LoggedInDropdown = ({ user, sessions, currentSession, accounts, children, providers }: LoggedInDropdownProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -34,34 +44,50 @@ export const LoggedInDropdown = ({ user, sessions, currentSession, accounts, chi
|
||||
onOpenChange={setIsModalOpen}
|
||||
providers={providers}
|
||||
/>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" className="min-w-[var(--radix-popper-anchor-width)]">
|
||||
<DropdownMenuItem onClick={() => setIsModalOpen(!isModalOpen)}>
|
||||
<div className="flex justify-start items-center gap-2">
|
||||
<CircleUser size={16} />
|
||||
<span>Account</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
router.push("/login");
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-start items-center gap-2">
|
||||
<LogOut size={16} className="text-red-500" />
|
||||
<span className="text-red-500">Logout</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[var(--radix-popper-anchor-width)] rounded-xl border-2 border-border bg-popover shadow-none p-1"
|
||||
align="start"
|
||||
side="top"
|
||||
sideOffset={8}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setIsModalOpen(!isModalOpen)}
|
||||
className="group gap-2 p-1 cursor-pointer rounded-lg mb-1 transition-colors focus:bg-accent hover:bg-accent/50 border border-transparent"
|
||||
>
|
||||
<div className="flex size-9 items-center justify-center rounded-md border border-border bg-muted/50 shadow-sm transition-all group-hover:shadow-md group-hover:bg-background">
|
||||
<User size={18} className="text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium leading-none">Account Settings</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="group gap-2 p-1 cursor-pointer rounded-lg transition-colors focus:bg-red-50 dark:focus:bg-red-950/20 border border-transparent text-red-600 focus:text-red-600"
|
||||
onClick={async () => {
|
||||
await signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
router.push("/login");
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="flex size-9 items-center justify-center rounded-md border border-red-100 bg-red-50/50 dark:border-red-900/30 dark:bg-red-950/20 shadow-sm transition-all group-hover:shadow-md">
|
||||
<LogOut size={18} className="text-red-500" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium leading-none">Logout</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -7,20 +7,17 @@ import {
|
||||
Layers,
|
||||
ChartArea,
|
||||
ShieldHalf,
|
||||
Building, UserRoundCog, Mail, PackageOpen, Logs, Megaphone, Blocks, Warehouse
|
||||
Building, UserRoundCog, Mail, PackageOpen, Logs, Megaphone, Blocks, Warehouse, BookOpen
|
||||
} from "lucide-react";
|
||||
import {SidebarGroupItem, SidebarMenuCustomBase} from "@/components/wrappers/dashboard/common/sidebar/menu-sidebar";
|
||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
|
||||
|
||||
export const SidebarMenuCustomMain = () => {
|
||||
|
||||
const BASE_URL = `/dashboard`;
|
||||
|
||||
const {data: activeOrganization} = authClient.useActiveOrganization();
|
||||
const {data: organizations} = authClient.useListOrganizations();
|
||||
const {data: session, isPending, error} = authClient.useSession();
|
||||
const member = authClient.useActiveMember();
|
||||
|
||||
if (isPending) return null;
|
||||
|
||||
@@ -107,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}/>;
|
||||
};
|
||||
@@ -33,7 +33,7 @@ export function DatabaseActionsCell({backup, activeMember, isAlreadyRestore}: Da
|
||||
<div className={cn("flex items-center space-x-2")}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Button variant="ghost" size="icon" type="button" onClick={(e) => e.stopPropagation()}>
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="w-4 h-4"/>
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
import {Backup, BackupWith, Restoration} from "@/db/schema/07_database";
|
||||
import React from "react";
|
||||
import {Swiper, SwiperSlide} from "swiper/react";
|
||||
//@ts-ignore
|
||||
import "swiper/css";
|
||||
import "swiper/css/pagination";
|
||||
import {Pagination, Mousewheel} from "swiper/modules";
|
||||
@@ -12,14 +12,13 @@ import {
|
||||
BackupActionsType
|
||||
} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions.schema";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
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 {useRouter} from "next/navigation";
|
||||
import {
|
||||
createRestorationBackupAction, deleteBackupAction, deleteBackupStorageAction,
|
||||
downloadBackupAction
|
||||
@@ -29,8 +28,7 @@ import {SafeActionResult} from "next-safe-action";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {ZodString} from "zod";
|
||||
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
||||
import {AlertCircleIcon, Trash2} from "lucide-react";
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import {AlertCircleIcon} from "lucide-react";
|
||||
|
||||
type BackupActionsFormProps = {
|
||||
backup: BackupWith;
|
||||
@@ -40,8 +38,9 @@ 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();
|
||||
const router = useRouter();
|
||||
|
||||
const form = useZodForm({
|
||||
schema: BackupActionsSchema,
|
||||
@@ -74,8 +73,9 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
|
||||
|
||||
if (inner?.success) {
|
||||
toast.success(inner.actionSuccess?.message);
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", backup.databaseId]});
|
||||
router.refresh();
|
||||
if (action === "download") {
|
||||
|
||||
const url = inner.value
|
||||
if (typeof url === "string") {
|
||||
window.open(url, "_self");
|
||||
@@ -91,6 +91,8 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
|
||||
} else {
|
||||
if (action === "delete") {
|
||||
toast.success("Backup deleted successfully.")
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", backup.databaseId]});
|
||||
router.refresh();
|
||||
closeModal()
|
||||
} else {
|
||||
toast.error(inner?.actionError?.message ?? "An error occurred.");
|
||||
@@ -111,6 +113,8 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
|
||||
|
||||
if (inner?.success) {
|
||||
toast.success(inner.actionSuccess?.message);
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", backup.databaseId]});
|
||||
router.refresh();
|
||||
closeModal()
|
||||
} else {
|
||||
toast.error(inner?.actionError?.message);
|
||||
@@ -130,10 +134,7 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
{filteredBackupStorages.length > 0 ?
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="backupStorageId"
|
||||
@@ -176,24 +177,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>
|
||||
@@ -221,45 +224,15 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
|
||||
|
||||
<div className="flex flex-row items-center gap-x-4 w-full">
|
||||
{action === "delete" && (
|
||||
// <ButtonWithLoading
|
||||
// type="button"
|
||||
// variant="destructive"
|
||||
// onClick={() => mutationDeleteEntireBackup.mutateAsync()}
|
||||
// isPending={mutationDeleteEntireBackup.isPending}
|
||||
// disabled={mutationDeleteEntireBackup.isPending}
|
||||
// >
|
||||
// Delete entire backup
|
||||
// </ButtonWithLoading>
|
||||
|
||||
|
||||
<ButtonWithConfirm
|
||||
title={"Delete entire backup"}
|
||||
description={"Are you sure you want to delete this entire backup?"}
|
||||
button={{
|
||||
main: {
|
||||
type: "button",
|
||||
variant: "destructive",
|
||||
text: "Delete entire backup",
|
||||
},
|
||||
confirm: {
|
||||
className: "w-full",
|
||||
text: "Delete",
|
||||
icon: <Trash2/>,
|
||||
variant: "destructive",
|
||||
onClick: async () => {
|
||||
mutationDeleteEntireBackup.mutateAsync()
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
className: "w-full",
|
||||
text: "Cancel",
|
||||
icon: <Trash2/>,
|
||||
variant: "outline",
|
||||
},
|
||||
}}
|
||||
<ButtonWithLoading
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => mutationDeleteEntireBackup.mutateAsync()}
|
||||
isPending={mutationDeleteEntireBackup.isPending}
|
||||
/>
|
||||
|
||||
disabled={mutationDeleteEntireBackup.isPending}
|
||||
>
|
||||
Delete entire backup
|
||||
</ButtonWithLoading>
|
||||
)}
|
||||
|
||||
{filteredBackupStorages.length > 0 && (
|
||||
|
||||
@@ -58,9 +58,7 @@ export const downloadBackupAction = userAction.schema(
|
||||
};
|
||||
|
||||
|
||||
|
||||
const result = await dispatchStorage(input, undefined, backupStorage.storageChannelId);
|
||||
console.log(result);
|
||||
return {
|
||||
success: result.success,
|
||||
value: result.url,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"use server"
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {z} from "zod";
|
||||
import {db} from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {BackupWith, Restoration} from "@/db/schema/07_database";
|
||||
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
||||
|
||||
export const getDatabaseDataAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
databaseId: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}) => {
|
||||
const {databaseId} = parsedInput;
|
||||
|
||||
const database = await db.query.database.findFirst({
|
||||
where: eq(drizzleDb.schemas.database.id, databaseId),
|
||||
with: {
|
||||
project: true,
|
||||
retentionPolicy: true,
|
||||
alertPolicies: true,
|
||||
storagePolicies: true
|
||||
}
|
||||
});
|
||||
|
||||
const backups = await db.query.backup.findMany({
|
||||
where: eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||
with: {
|
||||
restorations: true,
|
||||
storages: {
|
||||
with: {
|
||||
storageChannel: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: (b, {desc}) => [desc(b.createdAt)],
|
||||
}) as BackupWith[];
|
||||
|
||||
const restorations = await db.query.restoration.findMany({
|
||||
where: eq(drizzleDb.schemas.restoration.databaseId, databaseId),
|
||||
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
||||
}) as Restoration[];
|
||||
|
||||
const totalBackups = backups.length;
|
||||
const availableBackups = backups.filter(b => !b.deletedAt).length;
|
||||
const successfulBackups = backups.filter(b => b.status === "success").length;
|
||||
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||
|
||||
let activeOrganizationChannels = [];
|
||||
let activeOrganizationStorageChannels = [];
|
||||
|
||||
if (database?.project?.organizationId) {
|
||||
const organizationChannels = await getOrganizationChannels(database.project.organizationId);
|
||||
activeOrganizationChannels = organizationChannels.filter(channel => channel.enabled);
|
||||
|
||||
const organizationStorageChannels = await getOrganizationStorageChannels(database.project.organizationId);
|
||||
activeOrganizationStorageChannels = organizationStorageChannels.filter(channel => channel.enabled);
|
||||
}
|
||||
|
||||
return {
|
||||
database,
|
||||
backups,
|
||||
restorations,
|
||||
activeOrganizationChannels,
|
||||
activeOrganizationStorageChannels,
|
||||
stats: {
|
||||
totalBackups,
|
||||
availableBackups,
|
||||
successRate
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import {createContext, useContext, useState, ReactNode} from "react";
|
||||
import {BackupWith} from "@/db/schema/07_database";
|
||||
import {useRouter} from "next/navigation";
|
||||
|
||||
export type DatabaseActionKind = "restore" | "download" | "delete";
|
||||
|
||||
@@ -32,7 +31,6 @@ const BackupModalContext = createContext<BackupModalContextType | undefined>(und
|
||||
|
||||
export const BackupModalProvider = ({children}: { children: ReactNode }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const [action, setAction] = useState<DatabaseActionKind | null>(null);
|
||||
const [backup, setBackup] = useState<BackupWith | null>(null);
|
||||
|
||||
@@ -46,7 +44,6 @@ export const BackupModalProvider = ({children}: { children: ReactNode }) => {
|
||||
setOpen(false);
|
||||
setAction(null);
|
||||
setBackup(null);
|
||||
router.refresh()
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,13 +8,13 @@ import {Button} from "@/components/ui/button";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||
import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {Switch} from "@/components/ui/switch";
|
||||
import {Card} from "@/components/ui/card";
|
||||
import Link from "next/link";
|
||||
import {useIsMobile} from "@/hooks/use-mobile";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {
|
||||
ChannelKind,
|
||||
getChannelIcon,
|
||||
@@ -48,6 +48,7 @@ export const ChannelPoliciesForm = ({
|
||||
onSuccess,
|
||||
kind
|
||||
}: ChannelPoliciesFormProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
const isMobile = useIsMobile();
|
||||
const channelText = getChannelTextBasedOnKind(kind);
|
||||
@@ -104,10 +105,6 @@ export const ChannelPoliciesForm = ({
|
||||
return existing &&
|
||||
(existing.eventKinds !== policy.eventKinds || existing.enabled !== policy.enabled);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
const promises = kind === "notification"
|
||||
? [
|
||||
policiesToAdd.length > 0 ? await createAlertPoliciesAction({databaseId: database.id, alertPolicies: policiesToAdd}) : null,
|
||||
@@ -132,7 +129,11 @@ export const ChannelPoliciesForm = ({
|
||||
if (failedActions.length > 0) throw new Error(failedActions[0].data.actionError?.message || "One or more operations failed");
|
||||
return {success: true};
|
||||
},
|
||||
onSuccess: () => { toast.success("Policies saved successfully"); router.refresh(); },
|
||||
onSuccess: () => {
|
||||
toast.success("Policies saved successfully");
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", database.id]});
|
||||
router.refresh();
|
||||
},
|
||||
onError: (error: any) => { toast.error(error.message || "Failed to save policies"); },
|
||||
});
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ export const ChannelPoliciesModal = ({icon, kind, database, channels, organizati
|
||||
|
||||
const channelsIds = channelsFiltered
|
||||
.map(channel => channel.id);
|
||||
console.log(channelsIds);
|
||||
const activeAlertPolicies = database.alertPolicies?.filter((policy) => channelsIds.includes(policy.notificationChannelId));
|
||||
const activeStoragePolicies = database.storagePolicies?.filter((policy) => channelsIds.includes(policy.storageChannelId));
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/cron-button/cron.action";
|
||||
import {Database} from "@/db/schema/07_database";
|
||||
|
||||
@@ -18,6 +18,7 @@ export type CronButtonProps = {
|
||||
};
|
||||
|
||||
export const CronButton = (props: CronButtonProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
const [isSwitched, setIsSwitched] = useState(props.database.backupPolicy !== null);
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -27,6 +28,7 @@ export const CronButton = (props: CronButtonProps) => {
|
||||
updateDatabaseBackupPolicyAction({ databaseId: props.database.id, backupPolicy: value }),
|
||||
onSuccess: () => {
|
||||
toast.success(`Method updated successfully.`);
|
||||
queryClient.invalidateQueries({ queryKey: ["database-data", props.database.id] });
|
||||
router.refresh();
|
||||
},
|
||||
onError: () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {AdvancedCronSelect} from "./advanced-cron-select";
|
||||
import {updateDatabaseBackupPolicyAction} from "@/components/wrappers/dashboard/database/cron-button/cron.action";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useState} from "react";
|
||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useState} from "react";
|
||||
import {toast} from "sonner";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
@@ -15,6 +15,7 @@ export type CronInputProps = {
|
||||
|
||||
export const CronInput = ({database, onSuccess}: CronInputProps) => {
|
||||
const [cron, setCron] = useState<string>(database.backupPolicy ?? "* * * * *");
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
|
||||
const updateBackupPolicy = useMutation({
|
||||
@@ -22,6 +23,7 @@ export const CronInput = ({database, onSuccess}: CronInputProps) => {
|
||||
onSuccess: () => {
|
||||
toast.success(`Cron updated successfully.`);
|
||||
onSuccess?.()
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", database.id]});
|
||||
router.refresh();
|
||||
},
|
||||
onError: () => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import {DropZoneFile} from "@/components/wrappers/common/dropzone/dropzone-file";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useState} from "react";
|
||||
import {Loader2} from "lucide-react";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {toast} from "sonner";
|
||||
import {uploadBackupAction} from "@/components/wrappers/dashboard/database/import/upload-backup.action";
|
||||
@@ -16,6 +16,7 @@ type UploadRetentionZoneProps = {
|
||||
};
|
||||
|
||||
export const UploadBackupZone = ({onSuccessAction, databaseId}: UploadRetentionZoneProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
@@ -37,6 +38,7 @@ export const UploadBackupZone = ({onSuccessAction, databaseId}: UploadRetentionZ
|
||||
if (inner?.success) {
|
||||
toast.success(inner.actionSuccess?.message);
|
||||
onSuccessAction?.()
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", databaseId]});
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.error(inner?.actionError?.message);
|
||||
@@ -45,7 +47,7 @@ export const UploadBackupZone = ({onSuccessAction, databaseId}: UploadRetentionZ
|
||||
console.error(err);
|
||||
toast.error("An error occurred while upload in the backup");
|
||||
} finally {
|
||||
router.refresh();
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", databaseId]});
|
||||
setIsProcessing(false);
|
||||
}
|
||||
},
|
||||
@@ -66,7 +68,7 @@ export const UploadBackupZone = ({onSuccessAction, databaseId}: UploadRetentionZ
|
||||
) : (
|
||||
<DropZoneFile
|
||||
accept={acceptDbImportFiles}
|
||||
maxSize={500 * 1024 * 1024}
|
||||
maxSize={2 * 1024 * 1024 * 1024}
|
||||
maxFiles={1}
|
||||
description="Import database backup"
|
||||
fileKind="Database file (.sql, .dump)"
|
||||
|
||||
+4
-2
@@ -10,11 +10,11 @@ import {
|
||||
useZodForm
|
||||
} from "@/components/ui/form";
|
||||
import {RetentionSettings, RetentionSettingsSchema} from "./backup-retention-settings.schema";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {updateOrCreateBackupRetentionPolicyAction} from "./backup-retention-settings.action";
|
||||
import {DatabaseWith, RetentionPolicy} from "@/db/schema/07_database";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {RadioGroup, RadioGroupItem} from "@/components/ui/radio-group";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
@@ -28,6 +28,7 @@ export type BackupRetentionSettingsFormProps = {
|
||||
};
|
||||
|
||||
export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRetentionSettingsFormProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
|
||||
const defaultValuesFormatted: RetentionSettings = {
|
||||
@@ -55,6 +56,7 @@ export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRet
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success("Retention policy updated successfully.");
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", database.id]});
|
||||
router.refresh();
|
||||
},
|
||||
onError: () => {
|
||||
|
||||
+2
-2
@@ -49,7 +49,7 @@ export function BackupRetentionSettings({database}: BackupRetentionSettingsProps
|
||||
yearly: 3,
|
||||
},
|
||||
})
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const updateRetentionPolicy = useMutation({
|
||||
mutationFn: async (payload: RetentionSettings) => await updateOrCreateBackupRetentionPolicyAction({
|
||||
@@ -58,7 +58,7 @@ export function BackupRetentionSettings({database}: BackupRetentionSettingsProps
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success("Retention policy updated successfully.")
|
||||
router.refresh()
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", database.id]})
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("An error occurred while updating retention policy.")
|
||||
|
||||
@@ -18,7 +18,7 @@ import {Skeleton} from "@/components/ui/skeleton"
|
||||
|
||||
export function OrganizationCombobox() {
|
||||
const router = useRouter()
|
||||
const {isMobile, state} = useSidebar()
|
||||
const {state} = useSidebar()
|
||||
const {data: organizations, isPending: isPendingList, refetch} = authClient.useListOrganizations()
|
||||
const {
|
||||
data: activeOrganization,
|
||||
@@ -75,7 +75,7 @@ export function OrganizationCombobox() {
|
||||
>
|
||||
<div className={cn(
|
||||
"flex aspect-square size-8 items-center justify-center rounded-lg text-white shadow-sm transition-transform duration-200",
|
||||
activeOrganization?.logo ? "bg-transparent" : "bg-orange-500",
|
||||
activeOrganization?.logo ? "bg-transparent" : "bg-primary",
|
||||
isOpen && "scale-105"
|
||||
)}>
|
||||
{activeOrganization?.logo ? (
|
||||
@@ -95,7 +95,7 @@ export function OrganizationCombobox() {
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="min-w-[var(--radix-popper-anchor-width)] rounded-xl border-2 border-border bg-popover shadow-none"
|
||||
className="w-[var(--radix-popper-anchor-width)] rounded-xl border-2 border-border bg-popover shadow-none"
|
||||
align="start"
|
||||
side="bottom"
|
||||
sideOffset={4}
|
||||
@@ -109,33 +109,33 @@ export function OrganizationCombobox() {
|
||||
className={cn(
|
||||
"group gap-2 p-1 cursor-pointer rounded-lg mb-1 last:mb-0 transition-colors",
|
||||
isActive
|
||||
? "bg-orange-500/10 text-orange-600 dark:text-orange-400 border border-orange-500/20"
|
||||
? "bg-primary/10 text-primary border border-primary/20"
|
||||
: "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-orange-500 text-white border-orange-500/30" : "bg-muted/50 border-border"
|
||||
isActive && !org.logo ? "bg-primary text-primary-foreground border-primary/30" : "bg-muted/50 border-border"
|
||||
)}>
|
||||
{org.logo ? (
|
||||
<img src={org.logo} alt={org.name} className="size-9 rounded-md object-cover"/>
|
||||
) : (
|
||||
<Building2 className={cn(
|
||||
"size-5",
|
||||
isActive ? "text-white" : "text-muted-foreground"
|
||||
isActive ? "text-primary-foreground" : "text-muted-foreground"
|
||||
)}/>
|
||||
)}
|
||||
</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-orange-600 dark:text-orange-400" : ""
|
||||
isActive ? "text-primary" : ""
|
||||
)}>{org.name}</span>
|
||||
</div>
|
||||
{isActive && (
|
||||
<div
|
||||
className="ml-auto flex size-5 items-center justify-center rounded-full bg-orange-500 shadow-sm">
|
||||
<Check className="size-3 text-white" strokeWidth={3}/>
|
||||
className="ml-auto flex size-5 items-center justify-center rounded-full bg-primary shadow-sm">
|
||||
<Check className="size-3 text-primary-foreground" strokeWidth={3}/>
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
@@ -150,7 +150,7 @@ export function OrganizationCombobox() {
|
||||
className="flex size-9 items-center justify-center rounded-md border border-dashed border-muted-foreground/30 bg-background transition-colors group-hover:border-primary/50 group-hover:bg-primary/5">
|
||||
<Plus className="size-4"/>
|
||||
</div>
|
||||
<div className="font-medium">Create new organization</div>
|
||||
<div className="font-medium">Create organization</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {useEffect} from "react";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {AlertCircle, Loader2} from "lucide-react";
|
||||
@@ -22,18 +21,6 @@ export function ProfileAccount({user}: ProfileAccountProps) {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
|
||||
interval = setInterval(async () => {
|
||||
router.refresh();
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
});
|
||||
|
||||
const emailForm = useZodForm({
|
||||
schema: EmailSchema,
|
||||
defaultValues: {
|
||||
|
||||
@@ -21,7 +21,7 @@ export function ProfileAppearance() {
|
||||
}
|
||||
|
||||
function ThemeSelector() {
|
||||
const {theme, setTheme} = useTheme();
|
||||
const {theme} = useTheme();
|
||||
|
||||
|
||||
return (
|
||||
@@ -39,7 +39,6 @@ function ThemeSelector() {
|
||||
isActive ? "border-primary bg-primary/5" : "border-muted/40"
|
||||
)}
|
||||
onClick={async () => {
|
||||
// setTheme(item.value)
|
||||
await authClient.updateUser({theme: item.value});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -8,11 +8,11 @@ import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||
import {useMemo, useState} from "react";
|
||||
import {Backup, BackupWith, DatabaseWith} from "@/db/schema/07_database";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
import {deleteBackupAction} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions.action";
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
|
||||
|
||||
type DatabaseBackupListProps = {
|
||||
@@ -32,7 +32,12 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
||||
]
|
||||
|
||||
const [selectedFilters, setSelectedFilters] = useState<FilterItem[]>([items[1]]);
|
||||
const router = useRouter();
|
||||
const [isActionsOpen, setIsActionsOpen] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return backupColumns(props.isAlreadyRestore, props.settings, props.database, props.activeMember);
|
||||
}, [props.isAlreadyRestore, props.activeMember.id, props.activeMember.role]);
|
||||
|
||||
const filteredBackups = useMemo(() => {
|
||||
if (!props.backups) return [];
|
||||
@@ -64,19 +69,12 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
||||
mutationFn: async (backups: Backup[]) => {
|
||||
const results = await Promise.all(
|
||||
backups.map(async (backup) => {
|
||||
if (backup.deletedAt == null) {
|
||||
if (backup.deletedAt == null || backup.status == "ongoing") {
|
||||
|
||||
const backupDeleted = await deleteBackupAction({
|
||||
databaseId: backup.databaseId,
|
||||
backupId: backup.id,
|
||||
})
|
||||
// const backupDeleted = await deleteBackupAction({
|
||||
// backupId: backup.id,
|
||||
// databaseId: backup.databaseId,
|
||||
// status: backup.status,
|
||||
// file: backup.file ?? "",
|
||||
// projectSlug: props.database?.project?.slug!
|
||||
// });
|
||||
return {
|
||||
success: backupDeleted?.data?.success,
|
||||
message: backupDeleted?.data?.success
|
||||
@@ -98,7 +96,7 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
||||
toast.error(result.message);
|
||||
}
|
||||
});
|
||||
router.refresh();
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", props.database.id]});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -107,7 +105,7 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
||||
return (
|
||||
<DataTable
|
||||
enableSelect={!isMember}
|
||||
columns={backupColumns(props.isAlreadyRestore, props.settings, props.database, props.activeMember)}
|
||||
columns={columns}
|
||||
data={filteredBackups}
|
||||
enablePagination
|
||||
selectedActions={(rows) => (
|
||||
@@ -115,28 +113,38 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
||||
<div className="flex justify-start md:justify-between gap-3 md:gap-0 items-center w-full ml-0">
|
||||
<div className="flex gap-2">
|
||||
{!isMember && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu open={isActionsOpen} onOpenChange={setIsActionsOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ButtonWithLoading
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsActionsOpen(true);
|
||||
}}
|
||||
disabled={rows.length === 0 || mutationDeleteBackups.isPending}
|
||||
icon={<MoreHorizontal/>}
|
||||
isPending={mutationDeleteBackups.isPending}
|
||||
size="sm"
|
||||
type="button"
|
||||
>Actions</ButtonWithLoading>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await mutationDeleteBackups.mutateAsync(rows)
|
||||
<ButtonWithConfirm
|
||||
onConfirm={() => {
|
||||
mutationDeleteBackups.mutate(rows);
|
||||
setIsActionsOpen(false);
|
||||
}}
|
||||
className="text-red-600 focus:text-red-700"
|
||||
onCancel={() => setIsActionsOpen(false)}
|
||||
title="Delete backups?"
|
||||
description="Are you sure you want to delete the selected backups? This action cannot be undone."
|
||||
confirmButtonText="Yes, delete"
|
||||
cancelButtonText="Cancel"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2"/>
|
||||
Delete Selected
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<Trash2 className="me-2 h-4 w-4 text-red-600"/>
|
||||
<span className="text-red-600">Delete Selected</span>
|
||||
</DropdownMenuItem>
|
||||
</ButtonWithConfirm>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,17 @@ import {Setting} from "@/db/schema/01_setting";
|
||||
import {BackupWith, DatabaseWith, Restoration} from "@/db/schema/07_database";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
import {useBackupModal} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
||||
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi";
|
||||
import {useQuery, useQueryClient} from "@tanstack/react-query";
|
||||
import {getDatabaseDataAction} from "@/components/wrappers/dashboard/database/backup/actions/get-data.action";
|
||||
import {PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
|
||||
import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button";
|
||||
import {ChannelPoliciesModal} from "@/components/wrappers/dashboard/database/channels-policy/policy-modal";
|
||||
import {HardDrive, Megaphone} from "lucide-react";
|
||||
import {ImportModal} from "@/components/wrappers/dashboard/database/import/import-modal";
|
||||
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
|
||||
|
||||
|
||||
export type DatabaseContentProps = {
|
||||
@@ -13,27 +24,121 @@ export type DatabaseContentProps = {
|
||||
restorations: Restoration[],
|
||||
isAlreadyRestore: boolean,
|
||||
database: DatabaseWith,
|
||||
activeMember: MemberWithUser
|
||||
activeMember: MemberWithUser,
|
||||
totalBackups: number,
|
||||
availableBackups: number,
|
||||
successRate: number | null,
|
||||
organizationId: string,
|
||||
activeOrganizationChannels: any[],
|
||||
activeOrganizationStorageChannels: any[]
|
||||
}
|
||||
|
||||
|
||||
export const DatabaseContent = ({
|
||||
settings,
|
||||
backups,
|
||||
activeMember,
|
||||
isAlreadyRestore,
|
||||
restorations,
|
||||
database
|
||||
}: DatabaseContentProps) => {
|
||||
export const DatabaseContent = (props: DatabaseContentProps) => {
|
||||
const {} = useBackupModal();
|
||||
|
||||
|
||||
const {data} = useQuery({
|
||||
queryKey: ["database-data", props.database.id],
|
||||
queryFn: async () => {
|
||||
const result = await getDatabaseDataAction({databaseId: props.database.id});
|
||||
return result?.data;
|
||||
},
|
||||
initialData: {
|
||||
// TODO : to be patched
|
||||
// @ts-ignore
|
||||
database: {
|
||||
...props.database,
|
||||
project: props.database.project ?? null,
|
||||
},
|
||||
backups: props.backups,
|
||||
restorations: props.restorations,
|
||||
activeOrganizationChannels: props.activeOrganizationChannels,
|
||||
activeOrganizationStorageChannels: props.activeOrganizationStorageChannels,
|
||||
stats: {
|
||||
totalBackups: props.totalBackups,
|
||||
availableBackups: props.availableBackups,
|
||||
successRate: props.successRate
|
||||
}
|
||||
},
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
refetchInterval: 1000,
|
||||
});
|
||||
|
||||
const database = data?.database ?? props.database;
|
||||
const backups = data?.backups ?? props.backups;
|
||||
const restorations = data?.restorations ?? props.restorations;
|
||||
const activeOrganizationChannels = data?.activeOrganizationChannels ?? props.activeOrganizationChannels;
|
||||
const activeOrganizationStorageChannels = data?.activeOrganizationStorageChannels ?? props.activeOrganizationStorageChannels;
|
||||
const stats = data?.stats ?? {
|
||||
totalBackups: props.totalBackups,
|
||||
availableBackups: props.availableBackups,
|
||||
successRate: props.successRate
|
||||
};
|
||||
|
||||
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
||||
const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing");
|
||||
|
||||
const isMember = props.activeMember.role === "member";
|
||||
|
||||
return (
|
||||
<>
|
||||
<DatabaseBackupActionsModal/>
|
||||
<DatabaseTabs activeMember={activeMember} settings={settings} database={database}
|
||||
isAlreadyRestore={isAlreadyRestore}
|
||||
backups={backups}
|
||||
restorations={restorations}/>
|
||||
<div className="justify-between gap-2 sm:flex">
|
||||
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||
<div className="min-w-full md:min-w-fit ">
|
||||
{capitalizeFirstLetter(database.name)}
|
||||
</div>
|
||||
{!isMember && (
|
||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||
<div className="flex items-center gap-2">
|
||||
<RetentionPolicySheet database={database}/>
|
||||
<CronButton database={database}/>
|
||||
<ChannelPoliciesModal
|
||||
database={database}
|
||||
kind={"notification"}
|
||||
icon={<Megaphone/>}
|
||||
channels={activeOrganizationChannels}
|
||||
organizationId={props.organizationId}
|
||||
/>
|
||||
<ChannelPoliciesModal
|
||||
database={database}
|
||||
icon={<HardDrive/>}
|
||||
kind={"storage"}
|
||||
channels={activeOrganizationStorageChannels}
|
||||
organizationId={props.organizationId}
|
||||
/>
|
||||
<ImportModal database={database}/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<BackupButton disable={isAlreadyBackup} databaseId={database.id}/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PageTitle>
|
||||
</div>
|
||||
|
||||
{database.description && (
|
||||
<PageDescription className="mt-5 sm:mt-0">{database.description}</PageDescription>
|
||||
)}
|
||||
|
||||
<PageContent className="flex flex-col w-full h-full">
|
||||
<DatabaseKpi
|
||||
successRate={stats.successRate}
|
||||
database={database}
|
||||
availableBackups={stats.availableBackups}
|
||||
totalBackups={stats.totalBackups}
|
||||
/>
|
||||
<DatabaseBackupActionsModal/>
|
||||
<DatabaseTabs
|
||||
activeMember={props.activeMember}
|
||||
settings={props.settings}
|
||||
database={database}
|
||||
isAlreadyRestore={isAlreadyRestore}
|
||||
backups={backups}
|
||||
restorations={restorations}
|
||||
/>
|
||||
</PageContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -11,7 +11,6 @@ export type DatabaseKpiPro = {
|
||||
totalBackups: number;
|
||||
availableBackups: number;
|
||||
};
|
||||
|
||||
export const DatabaseKpi = (props: DatabaseKpiPro) => {
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between gap-8 mb-6">
|
||||
@@ -42,7 +41,7 @@ export const DatabaseKpi = (props: DatabaseKpiPro) => {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{props.successRate ? `${props.successRate.toFixed(0)} %` : "Unavailable"}
|
||||
{typeof props.successRate === 'number' ? `${props.successRate.toFixed(0)} %` : "Unavailable"}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Backup success rate</p>
|
||||
</CardContent>
|
||||
|
||||
@@ -5,21 +5,28 @@ import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {MoreHorizontal, Trash2} from "lucide-react";
|
||||
import {Restoration} from "@/db/schema/07_database";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||
import {deleteRestoreAction} from "@/features/dashboard/restore/restore.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
import {useMemo, useState} from "react";
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
|
||||
|
||||
type DatabaseRestoreListProps = {
|
||||
isAlreadyRestore: boolean;
|
||||
restorations: Restoration[];
|
||||
activeMember: MemberWithUser
|
||||
activeMember: MemberWithUser;
|
||||
databaseId: string;
|
||||
}
|
||||
|
||||
export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const [isActionsOpen, setIsActionsOpen] = useState(false);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return restoreColumns(props.isAlreadyRestore, props.activeMember);
|
||||
}, [props.isAlreadyRestore, props.activeMember.id, props.activeMember.role]);
|
||||
|
||||
const mutationDeleteRestorations = useMutation({
|
||||
mutationFn: async (restorations: Restoration[]) => {
|
||||
@@ -46,7 +53,7 @@ export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
|
||||
toast.error(result.message);
|
||||
}
|
||||
});
|
||||
router.refresh();
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", props.databaseId]});
|
||||
},
|
||||
});
|
||||
const isMember = props.activeMember.role === "member";
|
||||
@@ -55,35 +62,48 @@ export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
|
||||
return (
|
||||
<DataTable
|
||||
enableSelect={!isMember}
|
||||
columns={restoreColumns(props.isAlreadyRestore, props.activeMember)}
|
||||
columns={columns}
|
||||
data={props.restorations}
|
||||
enablePagination
|
||||
selectedActions={(rows) => (
|
||||
<>
|
||||
{!isMember && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu open={isActionsOpen} onOpenChange={setIsActionsOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ButtonWithLoading
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsActionsOpen(true);
|
||||
}}
|
||||
disabled={rows.length === 0 || mutationDeleteRestorations.isPending}
|
||||
icon={<MoreHorizontal/>}
|
||||
isPending={mutationDeleteRestorations.isPending}
|
||||
size="sm"
|
||||
type="button"
|
||||
>Actions</ButtonWithLoading>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await mutationDeleteRestorations.mutateAsync(rows)
|
||||
<ButtonWithConfirm
|
||||
onConfirm={() => {
|
||||
mutationDeleteRestorations.mutate(rows)
|
||||
setIsActionsOpen(false);
|
||||
}}
|
||||
disabled={props.isAlreadyRestore}
|
||||
className="text-red-600 focus:text-red-700"
|
||||
onCancel={() => setIsActionsOpen(false)}
|
||||
title="Delete restorations?"
|
||||
description="Are you sure you want to delete the selected restorations? This action cannot be undone."
|
||||
confirmButtonText="Yes, delete"
|
||||
cancelButtonText="Cancel"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2"/>
|
||||
Delete Selected
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
disabled={props.isAlreadyRestore}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="text-red-600 focus:text-red-700"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2"/>
|
||||
Delete Selected
|
||||
</DropdownMenuItem>
|
||||
</ButtonWithConfirm>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useRouter, useSearchParams} from "next/navigation";
|
||||
import {eventUpdate} from "@/types/events";
|
||||
import {BackupWith, DatabaseWith, Restoration} from "@/db/schema/07_database";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {DatabaseBackupList} from "@/components/wrappers/dashboard/projects/database/database-backup-list";
|
||||
@@ -25,21 +24,6 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
|
||||
|
||||
const [tab, setTab] = useState<string>(() => searchParams.get("tab") ?? "backup");
|
||||
|
||||
useEffect(() => {
|
||||
const eventSource = new EventSource("/api/events");
|
||||
eventSource.addEventListener("modification", (event) => {
|
||||
const data: eventUpdate = JSON.parse(event.data);
|
||||
if (data.update) {
|
||||
router.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
eventSource.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const newTab = searchParams.get("tab") ?? "backup";
|
||||
setTab(newTab);
|
||||
@@ -69,6 +53,7 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
|
||||
isAlreadyRestore={props.isAlreadyRestore}
|
||||
restorations={props.restorations}
|
||||
activeMember={props.activeMember}
|
||||
databaseId={props.database.id}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -18,11 +18,11 @@ export const ProjectCard = (props: projectCardProps) => {
|
||||
return (
|
||||
<Link
|
||||
href={`/dashboard/projects/${project.id}`}
|
||||
className="group block transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl"
|
||||
className="group block transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-orange-500/50 focus-visible:ring-offset-2 rounded-xl"
|
||||
>
|
||||
<Card className="relative h-full flex flex-col p-4 transition-all border-border/50 bg-card hover:bg-accent/50 hover:border-primary/50 group-hover:shadow-lg overflow-hidden gap-0">
|
||||
<Card className="relative h-full flex flex-col p-4 transition-all duration-300 border-border/50 bg-card hover:bg-orange-500/[0.02] hover:border-orange-500/30 group-hover:shadow-md overflow-hidden gap-0">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary group-hover:bg-primary group-hover:text-primary-foreground transition-all duration-300 shadow-inner">
|
||||
<div className="flex items-center justify-center w-12 h-12 rounded-xl bg-orange-500/10 text-orange-600 dark:text-orange-400 group-hover:bg-orange-500/20 transition-all duration-300">
|
||||
<Folder className="w-8 h-8" />
|
||||
</div>
|
||||
<Badge className="text-[10px] font-medium px-2 py-1 rounded-lg bg-secondary/50 text-foreground">{dbCount} {dbCount === 1 ? "Database" : "Databases"}
|
||||
@@ -31,7 +31,7 @@ export const ProjectCard = (props: projectCardProps) => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<h3 className="text-lg font-black text-foreground group-hover:text-primary transition-colors line-clamp-1 tracking-tight">
|
||||
<h3 className="text-lg font-black text-foreground group-hover:text-orange-500 transition-colors line-clamp-1 tracking-tight">
|
||||
{project.name}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed">
|
||||
@@ -40,8 +40,8 @@ export const ProjectCard = (props: projectCardProps) => {
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between pt-3 border-t border-border/50">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-primary opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-x-[-10px] group-hover:translate-x-0">View Project</span>
|
||||
<div className="flex items-center gap-1 text-muted-foreground group-hover:text-primary transition-colors">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-orange-500 opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-x-[-10px] group-hover:translate-x-0">View Project</span>
|
||||
<div className="flex items-center gap-1 text-muted-foreground group-hover:text-orange-500 transition-colors">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest group-hover:hidden">Details</span>
|
||||
<ChevronRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform" />
|
||||
</div>
|
||||
|
||||
@@ -83,7 +83,7 @@ export function PercentageLineChart(props: percentageLineChartProps) {
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
domain={[0, 100]}
|
||||
tickFormatter={(tick) => `${tick}%`}
|
||||
tickFormatter={(tick) => `${Number(tick).toFixed(0)}%`}
|
||||
/>
|
||||
<ChartTooltip
|
||||
defaultIndex={1}
|
||||
@@ -112,7 +112,7 @@ function PourcentTooltip({
|
||||
<span className="h-2 w-2 rounded-full bg-[#fc6504]"/>
|
||||
<span className="text-muted-foreground">Success Rate :</span>
|
||||
<span className="ml-auto font-semibold">
|
||||
{data.successRate} %
|
||||
{data.successRate.toFixed(0)} %
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "settings" ADD COLUMN "encryption" boolean DEFAULT false;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -232,6 +232,13 @@
|
||||
"when": 1769203863305,
|
||||
"tag": "0032_sparkling_thunderbolt_ross",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 33,
|
||||
"version": "7",
|
||||
"when": 1770923665015,
|
||||
"tag": "0033_handy_valeria_richards",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,15 +1,10 @@
|
||||
import {boolean, pgTable, timestamp, uuid, varchar} from "drizzle-orm/pg-core";
|
||||
import {typeStorageEnum} from "./types";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
import {timestamps} from "@/db/schema/00_common";
|
||||
import {storageChannel} from "@/db/schema/12_storage-channel";
|
||||
import {relations} from "drizzle-orm";
|
||||
import {agent} from "@/db/schema/08_agent";
|
||||
import {project} from "@/db/schema/06_project";
|
||||
import {alertPolicy} from "@/db/schema/10_alert-policy";
|
||||
import {storagePolicy} from "@/db/schema/13_storage-policy";
|
||||
import {backup, database, restoration, retentionPolicy} from "@/db/schema/07_database";
|
||||
|
||||
|
||||
export const setting = pgTable("settings", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
@@ -22,6 +17,7 @@ export const setting = pgTable("settings", {
|
||||
smtpSecure: boolean("smtp_secure"),
|
||||
defaultStorageChannelId: uuid('default_storage_channel_id')
|
||||
.references(() => storageChannel.id, {onDelete: "set null"}),
|
||||
encryption: boolean("encryption").default(false),
|
||||
...timestamps
|
||||
});
|
||||
|
||||
|
||||
@@ -46,3 +46,15 @@ export const updateAgentAction = userAction
|
||||
data: updatedAgent,
|
||||
};
|
||||
});
|
||||
|
||||
export const getAgentAction = userAction.schema(z.string()).action(async ({parsedInput}) => {
|
||||
const agent = await db.query.agent.findFirst({
|
||||
where: eq(drizzleDb.schemas.agent.id, parsedInput),
|
||||
with: {
|
||||
databases: true
|
||||
}
|
||||
});
|
||||
return {
|
||||
data: agent,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {AgentType} from "@/features/agents/agents.schema";
|
||||
import {GearIcon} from "@radix-ui/react-icons";
|
||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||
import {useState} from "react";
|
||||
import {useRouter} from "next/navigation";
|
||||
|
||||
type AgentDialogProps = {
|
||||
agent?: AgentType & { id: string };
|
||||
@@ -24,6 +25,7 @@ type AgentDialogProps = {
|
||||
export const AgentDialog = ({agent, typeTrigger}: AgentDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const isEdit = !!agent;
|
||||
const router = useRouter();
|
||||
|
||||
const getTrigger = () => {
|
||||
switch (typeTrigger) {
|
||||
@@ -52,7 +54,10 @@ export const AgentDialog = ({agent, typeTrigger}: AgentDialogProps) => {
|
||||
<DialogTitle>{isEdit ? `Edit ${agent.name}` : "Create new agent"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<AgentForm
|
||||
onSuccess={() => setOpen(false)}
|
||||
onSuccess={() => {
|
||||
setOpen(false)
|
||||
router.refresh()
|
||||
}}
|
||||
defaultValues={agent}
|
||||
agentId={agent?.id}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {Input} from "@/components/ui/input";
|
||||
import {Form} from "@/components/ui/form";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import {AgentSchema, AgentType} from "@/features/agents/agents.schema";
|
||||
import {toast} from "sonner";
|
||||
@@ -27,6 +27,7 @@ export type agentFormProps = {
|
||||
|
||||
export const AgentForm = (props: agentFormProps) => {
|
||||
const isCreate = !Boolean(props.defaultValues);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const form = useZodForm({
|
||||
schema: AgentSchema,
|
||||
@@ -46,12 +47,16 @@ export const AgentForm = (props: agentFormProps) => {
|
||||
});
|
||||
|
||||
const data = createAgent?.data?.data;
|
||||
|
||||
if (createAgent?.serverError || !data) {
|
||||
toast.error(createAgent?.serverError);
|
||||
return;
|
||||
}
|
||||
toast.success(`Success ${isCreate ? "creating" : "updating"} agent`);
|
||||
router.refresh();
|
||||
|
||||
if (!isCreate && props.agentId) {
|
||||
queryClient.invalidateQueries({ queryKey: ["agent-data", props.agentId] });
|
||||
}
|
||||
|
||||
if (props.onSuccess) {
|
||||
props.onSuccess(data);
|
||||
@@ -94,7 +99,7 @@ export const AgentForm = (props: agentFormProps) => {
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="This agent is for the client exemple.com" {...field}
|
||||
<Input placeholder="This agent is for the client example.com" {...field}
|
||||
value={field.value ?? ""}/>
|
||||
</FormControl>
|
||||
<FormDescription>Enter your project agent description</FormDescription>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {getNewAgentRelease} from "@/features/updates/services/github";
|
||||
|
||||
export const useAgentUpdateCheck = (currentVersion?: string | null) => {
|
||||
const {data: newRelease, isLoading} = useQuery({
|
||||
queryKey: ["agent-new-release", currentVersion],
|
||||
queryFn: () => currentVersion ? getNewAgentRelease(currentVersion) : Promise.resolve(null),
|
||||
staleTime: 1000 * 60 * 60,
|
||||
enabled: !!currentVersion,
|
||||
});
|
||||
|
||||
return {
|
||||
newRelease,
|
||||
isLoading,
|
||||
isUpdateAvailable: !!newRelease,
|
||||
};
|
||||
};
|
||||
@@ -1,39 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import {ColumnDef} from "@tanstack/react-table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Download, MoreHorizontal, Trash2} from "lucide-react";
|
||||
import {ReloadIcon} from "@radix-ui/react-icons";
|
||||
import {
|
||||
getFileUrlPresignedLocal,
|
||||
getFileUrlPreSignedS3Action
|
||||
} from "@/features/upload/private/upload.action";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {createRestorationAction, deleteBackupAction} from "@/features/dashboard/restore/restore.action";
|
||||
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 {TooltipCustom} from "@/components/wrappers/common/tooltip-custom";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {SafeActionResult} from "next-safe-action";
|
||||
import {ZodString} from "zod";
|
||||
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";
|
||||
import {formatBytes, isImportedFilename} from "@/utils/text";
|
||||
import {formatBytes} from "@/utils/text";
|
||||
import {DatabaseActionsCell} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions-cell";
|
||||
|
||||
import { Badge as BadgeC } from "@/components/ui/badge";
|
||||
|
||||
export function backupColumns(
|
||||
isAlreadyRestore: boolean,
|
||||
@@ -45,7 +22,17 @@ export function backupColumns(
|
||||
{
|
||||
id: "availability",
|
||||
cell: ({row}) => {
|
||||
const colorStatus = row.original.deletedAt != null ? "bg-red-400 border-red-600" : "bg-green-400 border-green-600";
|
||||
const statusColors: Record<string, string> = {
|
||||
waiting: "bg-gray-400 border-gray-600",
|
||||
ongoing: "bg-orange-400 border-orange-600",
|
||||
success: "bg-green-400 border-green-600",
|
||||
};
|
||||
|
||||
const colorStatus =
|
||||
row.original.deletedAt != null
|
||||
? "bg-red-400 border-red-600"
|
||||
: statusColors[row.original.status] ?? "bg-gray-400 border-gray-600";
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@@ -68,7 +55,16 @@ export function backupColumns(
|
||||
cell: ({row}) => {
|
||||
const reference = row.original.id
|
||||
const isImported = row.original.imported
|
||||
return isImported ? `${reference} (imported)` : `${reference}`
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{reference}</span>
|
||||
{isImported && (
|
||||
<BadgeC variant="outline" className="bg-orange-400/10 border-orange-600/50 text-orange-600">
|
||||
Imported
|
||||
</BadgeC>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,13 +15,12 @@ import { ReloadIcon } from "@radix-ui/react-icons";
|
||||
import { StatusBadge } from "@/components/wrappers/common/status-badge";
|
||||
import {Restoration} from "@/db/schema/07_database";
|
||||
import {formatLocalizedDate} from "@/utils/date-formatting";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||
import {
|
||||
deleteRestoreAction,
|
||||
rerunRestorationAction
|
||||
} from "@/features/dashboard/restore/restore.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
|
||||
@@ -54,7 +53,7 @@ export function restoreColumns(
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue("status");
|
||||
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const rowData: Restoration = row.original;
|
||||
|
||||
|
||||
@@ -67,7 +66,7 @@ export function restoreColumns(
|
||||
if (restoration.data.success) {
|
||||
// @ts-ignore
|
||||
toast.success(restoration.data.actionSuccess.message);
|
||||
router.refresh();
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", rowData.databaseId]});
|
||||
} else {
|
||||
// @ts-ignore
|
||||
toast.error(restoration.data.actionError.message);
|
||||
@@ -84,7 +83,7 @@ export function restoreColumns(
|
||||
if (restoration.data.success) {
|
||||
// @ts-ignore
|
||||
toast.success(restoration.data.actionSuccess.message);
|
||||
router.refresh();
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", rowData.databaseId]});
|
||||
} else {
|
||||
// @ts-ignore
|
||||
toast.error(restoration.data.actionError.message);
|
||||
@@ -109,7 +108,7 @@ export function restoreColumns(
|
||||
{activeMember.role != "member" && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<Button variant="ghost" className="h-8 w-8 p-0" type="button" onClick={(e) => e.stopPropagation()}>
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4"/>
|
||||
</Button>
|
||||
|
||||
@@ -15,3 +15,19 @@ export function getPublicServerKeyContent() {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get Master server key
|
||||
*/
|
||||
export function getMasterServerKeyContent() {
|
||||
try {
|
||||
return fs.readFileSync("private/keys/master_key.bin");
|
||||
} catch (error: any) {
|
||||
console.error("Error :", error);
|
||||
return {
|
||||
success: false,
|
||||
message: `An error occurred while getting master server key`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessa
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { ProjectSchema, ProjectType } from "@/features/projects/projects.schema";
|
||||
import { createProjectAction, updateProjectAction } from "@/features/projects/projects.action";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -29,6 +29,7 @@ export type projectFormProps = {
|
||||
|
||||
export const ProjectForm = (props: projectFormProps) => {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const isCreate = !Boolean(props.defaultValues);
|
||||
const formatDatabasesList = (databases: DatabaseWith[]) => {
|
||||
return databases.map((database) => ({
|
||||
@@ -70,7 +71,13 @@ export const ProjectForm = (props: projectFormProps) => {
|
||||
if (project && project.data) {
|
||||
if (project.data.success) {
|
||||
project.data.actionSuccess && toast.success(project.data.actionSuccess.message);
|
||||
router.refresh();
|
||||
|
||||
if (!isCreate && values.databases) {
|
||||
values.databases.forEach(dbId => {
|
||||
queryClient.invalidateQueries({ queryKey: ["database-data", dbId] });
|
||||
});
|
||||
}
|
||||
|
||||
if (props.onSuccess) {
|
||||
props.onSuccess(project.data.value);
|
||||
} else {
|
||||
@@ -78,11 +85,9 @@ export const ProjectForm = (props: projectFormProps) => {
|
||||
}
|
||||
} else {
|
||||
project.data.actionError && toast.error(project.data.actionError.message || "Unknown error occurred.");
|
||||
router.refresh();
|
||||
}
|
||||
} else {
|
||||
toast.error("Failed to process request. No response received.");
|
||||
router.refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
"use server";
|
||||
|
||||
import {eq} from 'drizzle-orm';
|
||||
import {Json} from "drizzle-zod";
|
||||
|
||||
import * as drizzleDb from '@/db';
|
||||
import {db} from '@/db';
|
||||
import type {StorageInput, StorageProviderKind, StorageResult,} from './types';
|
||||
import {dispatchViaProvider} from "@/features/storages/providers";
|
||||
import {StorageChannel} from "@/db/schema/12_storage-channel";
|
||||
import {Json} from "drizzle-zod";
|
||||
import {
|
||||
StorageChannelFormType
|
||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.schema";
|
||||
|
||||
|
||||
export async function dispatchStorage(
|
||||
input: StorageInput,
|
||||
policyId?: string,
|
||||
channelId?: string,
|
||||
channelData?: StorageChannelFormType,
|
||||
organizationId?: string
|
||||
): Promise<StorageResult> {
|
||||
try {
|
||||
@@ -68,6 +74,8 @@ export async function dispatchStorage(
|
||||
};
|
||||
}
|
||||
|
||||
if (channelData) channel = {...channelData, config: channelData.config as Json};
|
||||
|
||||
if (!channel) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -90,7 +98,6 @@ export async function dispatchStorage(
|
||||
channel.provider as StorageProviderKind,
|
||||
channel.config,
|
||||
input,
|
||||
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {Readable} from "node:stream";
|
||||
|
||||
type S3Config = {
|
||||
endPointUrl: string;
|
||||
region?: string;
|
||||
accessKey: string;
|
||||
secretKey: string;
|
||||
bucketName: string;
|
||||
@@ -14,6 +15,7 @@ type S3Config = {
|
||||
async function getS3Client(config: S3Config) {
|
||||
return new Minio.Client({
|
||||
endPoint: config.endPointUrl,
|
||||
region: config.region ?? "us-east-1",
|
||||
accessKey: config.accessKey,
|
||||
secretKey: config.secretKey,
|
||||
port: config.port ?? 443,
|
||||
|
||||
@@ -1,41 +1,77 @@
|
||||
"use client"
|
||||
import { Moon, Sun, Check, SunMoon } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { authClient } from "@/lib/auth/auth-client"
|
||||
|
||||
import * as React from "react"
|
||||
import {Moon, Sun} from "lucide-react"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu"
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {toast} from "sonner";
|
||||
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: "dark", icon: Moon, label: "Dark" },
|
||||
{ id: "system", icon: SunMoon, label: "System" },
|
||||
] as const
|
||||
|
||||
export function ModeToggle() {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const setTheme = async (theme: "light" | "dark" | "system") => {
|
||||
toast.success("Your theme preference has been updated.");
|
||||
await authClient.updateUser({theme: theme});
|
||||
};
|
||||
const handleThemeChange = async (newTheme: "light" | "system" | "dark") => {
|
||||
await authClient.updateUser({ theme: newTheme })
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"/>
|
||||
<Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"/>
|
||||
<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">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuContent align="end" className="min-w-37.5 rounded-xl border-2 border-border p-1 shadow-none">
|
||||
{themes.map((t) => {
|
||||
const ThemeIcon = t.icon
|
||||
const isActive = theme === t.id
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={t.id}
|
||||
onClick={() => handleThemeChange(t.id)}
|
||||
className={cn(
|
||||
"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"
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
})}
|
||||
</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}
|
||||
|
||||
@@ -14,7 +14,7 @@ type ParsedVersion = {
|
||||
};
|
||||
|
||||
function parseVersion(version: string): ParsedVersion {
|
||||
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/);
|
||||
const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/);
|
||||
if (!match) return {
|
||||
major: 0,
|
||||
minor: 0,
|
||||
@@ -30,6 +30,49 @@ function parseVersion(version: string): ParsedVersion {
|
||||
};
|
||||
}
|
||||
|
||||
const findLatestVersion = (currentVersion: string, releases: GitHubRelease[]): GitHubRelease | null => {
|
||||
const cleanCurrentVersion = currentVersion.replace(/^v/, "");
|
||||
const isStable = /^\d+\.\d+\.\d+$/.test(cleanCurrentVersion);
|
||||
const isRc = /^\d+\.\d+\.\d+-rc\.\d+$/.test(cleanCurrentVersion);
|
||||
|
||||
const currentParsedVersion = parseVersion(cleanCurrentVersion);
|
||||
|
||||
if (isRc) {
|
||||
const latestStableVersion = releases.find(r => !r.prerelease);
|
||||
const latestRcVersion = releases.find(r => r.prerelease && (r.tag_name.includes("rc") || (r.name && r.name.includes("rc"))));
|
||||
|
||||
if (latestStableVersion) {
|
||||
const versionStr = latestStableVersion.tag_name || latestStableVersion.name;
|
||||
const latestStableParsedVersion = parseVersion(versionStr);
|
||||
if (latestStableParsedVersion.major > currentParsedVersion.major) return latestStableVersion;
|
||||
if (latestStableParsedVersion.major === currentParsedVersion.major && latestStableParsedVersion.minor > currentParsedVersion.minor) return latestStableVersion;
|
||||
if (latestStableParsedVersion.major === currentParsedVersion.major && latestStableParsedVersion.minor === currentParsedVersion.minor && latestStableParsedVersion.patch >= currentParsedVersion.patch) return latestStableVersion;
|
||||
}
|
||||
|
||||
if (latestRcVersion) {
|
||||
const versionStr = latestRcVersion.tag_name || latestRcVersion.name;
|
||||
const latestRcParsedVersion = parseVersion(versionStr);
|
||||
if (latestRcParsedVersion.major > currentParsedVersion.major) return latestRcVersion;
|
||||
if (latestRcParsedVersion.major === currentParsedVersion.major && latestRcParsedVersion.minor > currentParsedVersion.minor) return latestRcVersion;
|
||||
if (latestRcParsedVersion.major === currentParsedVersion.major && latestRcParsedVersion.minor === currentParsedVersion.minor && latestRcParsedVersion.patch > currentParsedVersion.patch) return latestRcVersion;
|
||||
if (latestRcParsedVersion.major === currentParsedVersion.major && latestRcParsedVersion.minor === currentParsedVersion.minor && latestRcParsedVersion.patch === currentParsedVersion.patch &&
|
||||
latestRcParsedVersion.rc !== undefined && currentParsedVersion.rc !== undefined && latestRcParsedVersion.rc > currentParsedVersion.rc) return latestRcVersion;
|
||||
}
|
||||
} else if (isStable) {
|
||||
const latestStableVersion = releases.find(r => !r.prerelease);
|
||||
if (latestStableVersion) {
|
||||
const versionStr = latestStableVersion.tag_name || latestStableVersion.name;
|
||||
const latestStableParsedVersion = parseVersion(versionStr);
|
||||
|
||||
if (latestStableParsedVersion.major > currentParsedVersion.major) return latestStableVersion;
|
||||
if (latestStableParsedVersion.major === currentParsedVersion.major && latestStableParsedVersion.minor > currentParsedVersion.minor) return latestStableVersion;
|
||||
if (latestStableParsedVersion.major === currentParsedVersion.major && latestStableParsedVersion.minor === currentParsedVersion.minor && latestStableParsedVersion.patch > currentParsedVersion.patch) return latestStableVersion;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getNewRelease = async (currentVersion: string): Promise<GitHubRelease | null> => {
|
||||
try {
|
||||
const response = await fetch("https://api.github.com/repos/Portabase/portabase/releases");
|
||||
@@ -38,49 +81,28 @@ export const getNewRelease = async (currentVersion: string): Promise<GitHubRelea
|
||||
}
|
||||
|
||||
const releases: GitHubRelease[] = await response.json();
|
||||
|
||||
const isStable = /^\d+\.\d+\.\d+$/.test(currentVersion);
|
||||
const isRc = /^\d+\.\d+\.\d+-rc\.\d+$/.test(currentVersion);
|
||||
|
||||
console.log("isStable", isStable)
|
||||
console.log("isRc", isRc)
|
||||
|
||||
|
||||
const currentParsedVersion = parseVersion(currentVersion)
|
||||
|
||||
const latestStableVersion = releases.find(r => !r.prerelease)
|
||||
const latestRcVersion = releases.find(r => r.name.includes("rc"))!
|
||||
|
||||
if (isRc) {
|
||||
if (!latestRcVersion) return null
|
||||
const latestRcParsedVersion = parseVersion(latestRcVersion.name)
|
||||
if (currentParsedVersion.major < latestRcParsedVersion.major) return latestRcVersion;
|
||||
if (currentParsedVersion.minor < latestRcParsedVersion.minor) return latestRcVersion;
|
||||
if (currentParsedVersion.patch < latestRcParsedVersion.patch) return latestRcVersion;
|
||||
if (currentParsedVersion.rc! < latestRcParsedVersion.rc!) return latestRcVersion;
|
||||
|
||||
if (!latestStableVersion) return null
|
||||
const latestStableParsedVersion = parseVersion(latestStableVersion.name)
|
||||
if (currentParsedVersion.major < latestStableParsedVersion.major) return latestStableVersion;
|
||||
if (currentParsedVersion.minor < latestStableParsedVersion.minor) return latestStableVersion;
|
||||
if (currentParsedVersion.patch < latestStableParsedVersion.patch) return latestStableVersion;
|
||||
|
||||
return null;
|
||||
|
||||
} else if (isStable) {
|
||||
if (!latestStableVersion) return null
|
||||
const latestStableParsedVersion = parseVersion(latestStableVersion.name)
|
||||
if (currentParsedVersion.major < latestStableParsedVersion.major) return latestStableVersion;
|
||||
if (currentParsedVersion.minor < latestStableParsedVersion.minor) return latestStableVersion;
|
||||
if (currentParsedVersion.patch < latestStableParsedVersion.patch) return latestStableVersion;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
return findLatestVersion(currentVersion, releases);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch latest release", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getNewAgentRelease = async (currentVersion: string): Promise<GitHubRelease | null> => {
|
||||
try {
|
||||
const response = await fetch("https://api.github.com/repos/Portabase/agent-rust/releases", {
|
||||
next: { revalidate: 3600 }
|
||||
});
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const releases: GitHubRelease[] = await response.json();
|
||||
return findLatestVersion(currentVersion, releases);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch latest agent release", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -75,10 +75,7 @@ export async function deleteFileS3Private(fileName: string, bucketName: string)
|
||||
*/
|
||||
export async function deleteLocalPrivate(fileName: string) {
|
||||
try {
|
||||
console.log(fileName);
|
||||
const filePath = path.join(process.cwd(), privateLocalDir, fileName);
|
||||
console.log(`Deleted ${filePath}`);
|
||||
// Delete locally
|
||||
await unlink(filePath);
|
||||
|
||||
return {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user