Compare commits

..
Author SHA1 Message Date
charlesgauthereau 2bd25353da chore(release): 1.1.12-rc.3 2026-01-11 14:13:58 +01:00
charlesgauthereau e1bee19c18 fix(ci): docker.yml 2026-01-11 14:13:51 +01:00
charlesgauthereau 0ec49af72a chore(release): 1.1.12-rc.2 2026-01-11 14:12:43 +01:00
charlesgauthereau 6d4eb836ab fix(ci): working on github actions to add new docker repository 2026-01-11 14:12:40 +01:00
charlesgauthereau 0c3cfecb1d chore(release): 1.1.12-rc.1 2026-01-11 13:54:35 +01:00
charlesgauthereau f9186e7354 fix: overflow in agent-card.tsx 2026-01-11 13:54:22 +01:00
charlesgauthereau bc603a08cf fix: Remove card in some forms. 2026-01-11 13:52:02 +01:00
charlesgauthereau 077aada056 chore(release): 1.1.11-rc.2 2026-01-11 13:36:57 +01:00
charlesgauthereau 4ed5f9872d fix: delete backup crash if no file. 2026-01-11 13:36:11 +01:00
charlesgauthereau 2d6be20b30 fix: improving organization-form.tsx UX/UI and removed the card. 2026-01-11 12:41:39 +01:00
charlesgauthereau be8f3bd98c fix: organization settings page responsive on mobile. 2026-01-11 12:37:57 +01:00
charlesgauthereau a3a20bee4a fix: agent-card.tsx overflow on mobile. 2026-01-11 12:23:32 +01:00
charlesgauthereau 09a1e75acf fix: notifier-card.tsx overflow on mobile. 2026-01-11 12:12:14 +01:00
charlesgauthereau ce6bdd01ef fix: overflow mobile view on the notification provider card in detabase details page. 2026-01-11 11:57:18 +01:00
charlesgauthereau a03a1d635f fix: responsive in project details. 2026-01-11 10:59:42 +01:00
charlesgauthereau 419b6d4891 chore(release): 1.1.11-rc.1 2026-01-09 19:16:17 +01:00
charlesgauthereau b4840318d5 fix: database page Backup button and route api. 2026-01-09 19:15:27 +01:00
Théo LAGACHE bf81f437d1 chore(release): 1.1.10 2026-01-07 15:29:53 +01:00
Théo LAGACHEandGitHub 81a4d33df1 Merge pull request #7 from Portabase/feat/webhooks
Feat/webhooks
2026-01-07 15:29:30 +01:00
27 changed files with 402 additions and 403 deletions
+35 -8
View File
@@ -1,3 +1,4 @@
name: Docker Publish name: Docker Publish
on: on:
@@ -7,6 +8,10 @@ on:
required: false required: false
type: string type: string
default: 'solucetechnologies/portabase' default: 'solucetechnologies/portabase'
image_name2:
required: false
type: string
default: 'portabase/portabase'
add_latest: add_latest:
required: false required: false
type: boolean type: boolean
@@ -24,6 +29,10 @@ on:
required: true required: true
DOCKER_PASSWORD: DOCKER_PASSWORD:
required: true required: true
DOCKER_USERNAME_2:
required: true
DOCKER_PASSWORD_2:
required: true
jobs: jobs:
build-and-push: build-and-push:
@@ -37,27 +46,45 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Log in to Docker Hub
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set tags - name: Set tags
id: set-tags id: set-tags
run: | run: |
REF_NAME=${GITHUB_REF#refs/tags/} REF_NAME=${GITHUB_REF#refs/tags/}
TAGS="${{ inputs.image_name }}:$REF_NAME" TAGS="${{ inputs.image_name }}:$REF_NAME"
TAGS2="${{ inputs.image_name2 }}:$REF_NAME"
if [[ "${{ inputs.add_latest }}" == "true" ]]; then if [[ "${{ inputs.add_latest }}" == "true" ]]; then
TAGS="$TAGS,${{ inputs.image_name }}:latest" TAGS="$TAGS,${{ inputs.image_name }}:latest"
fi fi
echo "tags=$TAGS" >> $GITHUB_OUTPUT echo "tags=$TAGS" >> $GITHUB_OUTPUT
echo "tags=$TAGS2" >> $GITHUB_OUTPUT
- name: Build and push Docker image
- name: Log in to Docker Hub account 1
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push to solucetechnologies/portabase
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: ${{ inputs.dockerfile }} file: ${{ inputs.dockerfile }}
push: true push: true
tags: ${{ steps.set-tags.outputs.tags }} tags: ${{ steps.set-tags.outputs.tags1 }}
target: ${{ inputs.target }}
- name: Log in to Docker Hub account 2
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
with:
username: ${{ secrets.DOCKER_USERNAME_2 }}
password: ${{ secrets.DOCKER_PASSWORD_2 }}
- name: Build and push to portabase/portabase
uses: docker/build-push-action@v6
with:
context: .
file: ${{ inputs.dockerfile }}
push: true
tags: ${{ steps.set-tags.outputs.tags2 }}
target: ${{ inputs.target }} target: ${{ inputs.target }}
+2
View File
@@ -17,6 +17,8 @@ jobs:
secrets: secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME_2: ${{ secrets.DOCKER_USERNAME_2 }}
DOCKER_PASSWORD_2: ${{ secrets.DOCKER_PASSWORD_2 }}
github_release: github_release:
needs: docker_publish needs: docker_publish
+2
View File
@@ -18,6 +18,8 @@ jobs:
secrets: secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME_2: ${{ secrets.DOCKER_USERNAME_2 }}
DOCKER_PASSWORD_2: ${{ secrets.DOCKER_PASSWORD_2 }}
github_release: github_release:
needs: docker_publish needs: docker_publish
+2 -2
View File
@@ -26,5 +26,5 @@ keywords:
- web-ui - web-ui
- agent - agent
license: Apache-2.0 license: Apache-2.0
version: 1.1.10-rc.2 version: 1.1.12-rc.3
date-released: "2026-01-07" date-released: "2026-01-11"
@@ -33,16 +33,22 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
return ( return (
<Page> <Page>
<div className="justify-between gap-2 sm:flex"> <div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center"> <PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
{capitalizeFirstLetter(agent.name)} <div className="min-w-full md:min-w-fit ">
<Link className={buttonVariants({variant: "outline"})} {capitalizeFirstLetter(agent.name)}
href={`/dashboard/agents/${agent.id}/edit`}> </div>
<GearIcon className="w-7 h-7"/> <div className="flex items-center gap-2 md:justify-between w-full ">
</Link> <div className="flex items-center gap-2">
<Link className={buttonVariants({variant: "outline"})}
href={`/dashboard/agents/${agent.id}/edit`}>
<GearIcon className="w-7 h-7"/>
</Link>
</div>
<div className="flex items-center gap-2">
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
</div>
</div>
</PageTitle> </PageTitle>
<PageActions className="justify-between">
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
</PageActions>
</div> </div>
{agent.description && ( {agent.description && (
@@ -22,7 +22,7 @@ export default async function RoutePage(props: PageParams<{}>) {
with: { with: {
databases: true databases: true
}, },
orderBy: (fields) => desc(fields.createdAt), orderBy: (fields) => desc(fields.lastContact),
}); });
@@ -60,7 +60,7 @@ export default async function RoutePage(props: PageParams<{
orderBy: (r, {desc}) => [desc(r.createdAt)], orderBy: (r, {desc}) => [desc(r.createdAt)],
}); });
const isAlreadyBackup = backups.some((b) => b.status === "waiting"); const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing");
const isAlreadyRestore = restorations.some((r) => r.status === "waiting"); const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
const totalBackups = await db.select({count: drizzleDb.schemas.backup.id}) const totalBackups = await db.select({count: drizzleDb.schemas.backup.id})
@@ -15,6 +15,11 @@ import {eq} from "drizzle-orm";
import {getActiveMember, getOrganization} from "@/lib/auth/auth"; import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {capitalizeFirstLetter} from "@/utils/text"; 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 {AlertPolicyModal} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy-modal";
import {ImportModal} from "@/components/wrappers/dashboard/database/import/import-modal";
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
export default async function RoutePage(props: PageParams<{ export default async function RoutePage(props: PageParams<{
projectId: string projectId: string
@@ -55,22 +60,25 @@ export default async function RoutePage(props: PageParams<{
return ( return (
<Page> <Page>
<div className="justify-between gap-2 sm:flex"> <div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center"> <PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
{capitalizeFirstLetter(proj.name)} <div className="min-w-full md:min-w-fit ">
{capitalizeFirstLetter(proj.name)}
</div>
{!isMember && ( {!isMember && (
<Link className={buttonVariants({variant: "outline"})} <div className="flex items-center gap-2 md:justify-between w-full ">
href={`/dashboard/projects/${proj.id}/edit`}> <div className="flex items-center gap-2">
<GearIcon className="w-7 h-7"/> <Link className={buttonVariants({variant: "outline"})}
</Link> href={`/dashboard/projects/${proj.id}/edit`}>
<GearIcon className="w-7 h-7"/>
</Link>
</div>
<div className="flex items-center gap-2">
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
</div>
</div>
)} )}
</PageTitle> </PageTitle>
{!isMember && (
<PageActions className="justify-between">
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
</PageActions>
)}
</div> </div>
<PageDescription>The list of associated databases</PageDescription>
<PageContent className="flex flex-col w-full h-full"> <PageContent className="flex flex-col w-full h-full">
{proj.databases.length > 0 ? ( {proj.databases.length > 0 ? (
<CardsWithPagination <CardsWithPagination
@@ -11,6 +11,13 @@ import {Metadata} from "next";
import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs"; import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
import {getOrganizationChannels} from "@/db/services/notification-channel"; import {getOrganizationChannels} from "@/db/services/notification-channel";
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl"; import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
import {capitalizeFirstLetter} from "@/utils/text";
import Link from "next/link";
import {buttonVariants} from "@/components/ui/button";
import {GearIcon} from "@radix-ui/react-icons";
import {
ButtonDeleteProject
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Settings", title: "Settings",
@@ -26,25 +33,29 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
} }
const notificationChannels = await getOrganizationChannels(organization.id) const notificationChannels = await getOrganizationChannels(organization.id)
const permissions = computeOrganizationPermissions(activeMember); const permissions = computeOrganizationPermissions(activeMember);
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle className="flex items-center"> <PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
Organization settings <div className="min-w-full md:min-w-fit ">
{permissions.canManageSettings && organization.slug !== "default" && ( Organization settings
<EditButtonSettings/> </div>
)} <div className="flex items-center gap-2 md:justify-between w-full ">
<div className="flex items-center gap-2">
{permissions.canManageSettings && organization.slug !== "default" && (
<EditButtonSettings/>
)}
</div>
<div className="flex items-center gap-2">
{permissions.canManageDangerZone && organization.slug !== "default" && (
<DeleteOrganizationButton organizationSlug={organization.slug}/>
)}
</div>
</div>
</PageTitle> </PageTitle>
<PageActions>
{permissions.canManageDangerZone && organization.slug !== "default" && (
<DeleteOrganizationButton organizationSlug={organization.slug}/>
)}
</PageActions>
</PageHeader> </PageHeader>
<PageContent> <PageContent>
<OrganizationTabs <OrganizationTabs
+12 -5
View File
@@ -6,7 +6,7 @@ import {Agent} from "@/db/schema/08_agent";
import {Database} from "@/db/schema/07_database"; import {Database} from "@/db/schema/07_database";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {db as dbClient} from "@/db"; import {db as dbClient} from "@/db";
import {and, eq} from "drizzle-orm"; import {and, eq, inArray} from "drizzle-orm";
import {EDbmsSchema} from "@/db/schema/types"; import {EDbmsSchema} from "@/db/schema/types";
import {ServerActionResult} from "@/types/action-type"; import {ServerActionResult} from "@/types/action-type";
import {SafeActionResult} from "next-safe-action"; import {SafeActionResult} from "next-safe-action";
@@ -75,8 +75,15 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
.returning(); .returning();
const backup = await dbClient.query.backup.findFirst({ // const backup = await dbClient.query.backup.findFirst({
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting")) // where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting"))
// })
const activeBackup = await dbClient.query.backup.findFirst({
where: and(
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
inArray(drizzleDb.schemas.backup.status, ["waiting", "ongoing"])
)
}) })
@@ -85,13 +92,13 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
}) })
if (backup) { if (activeBackup && activeBackup.status == "waiting") {
backupAction = true backupAction = true
await dbClient await dbClient
.update(drizzleDb.schemas.backup) .update(drizzleDb.schemas.backup)
.set(withUpdatedAt({status: "ongoing"})) .set(withUpdatedAt({status: "ongoing"}))
.where(eq(drizzleDb.schemas.backup.id, backup.id)); .where(eq(drizzleDb.schemas.backup.id, activeBackup.id));
} }
if (restoration) { if (restoration) {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "portabase", "name": "portabase",
"version": "1.1.10-rc.2", "version": "1.1.12-rc.3",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack -p 8887", "dev": "next dev --turbopack -p 8887",
@@ -95,8 +95,6 @@ export const EmailForm = (props: EmailFormProps) => {
return ( return (
<TooltipProvider> <TooltipProvider>
<Card>
<CardContent>
<Form <Form
form={form} form={form}
className="flex flex-col gap-4 mt-3" className="flex flex-col gap-4 mt-3"
@@ -182,23 +180,6 @@ export const EmailForm = (props: EmailFormProps) => {
)} )}
/> />
<div className="flex justify-between gap-4"> <div className="flex justify-between gap-4">
{/*{props.defaultValues?.smtpFrom && (*/}
{/* <ButtonWithLoading*/}
{/* type="button"*/}
{/* isPending={mutationSendEmailTest.isPending}*/}
{/* onClick={async () => {*/}
{/* await mutationSendEmailTest.mutateAsync();*/}
{/* }}*/}
{/* icon={<Send/>}*/}
{/* size="default"*/}
{/* className="bg-green-600 hover:bg-green-700 text-white font-medium shadow-sm transition-all"*/}
{/* >Send email test</ButtonWithLoading>*/}
{/*)}*/}
{props.defaultValues?.smtpFrom && ( {props.defaultValues?.smtpFrom && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@@ -227,14 +208,13 @@ export const EmailForm = (props: EmailFormProps) => {
)} )}
</Tooltip> </Tooltip>
)} )}
<div className="flex justify-end">
<Button <Button type="submit">
type="submit" Save
>Save</Button> </Button>
</div>
</div> </div>
</Form> </Form>
</CardContent>
</Card>
</TooltipProvider> </TooltipProvider>
); );
}; };
@@ -8,6 +8,8 @@ import {formatDateLastContact} from "@/utils/date-formatting";
import {AgentWith} from "@/db/schema/08_agent"; import {AgentWith} from "@/db/schema/08_agent";
import {Activity, ChevronRight, Copy, Check, Fingerprint, Server, Database, ShieldCheck} from "lucide-react"; import {Activity, ChevronRight, Copy, Check, Fingerprint, Server, Database, ShieldCheck} from "lucide-react";
import {Badge} from "@/components/ui/badge"; import {Badge} from "@/components/ui/badge";
import {truncateWords} from "@/utils/text";
import {useIsMobile} from "@/hooks/use-mobile";
export type agentCardProps = { export type agentCardProps = {
data: AgentWith; data: AgentWith;
@@ -15,6 +17,7 @@ export type agentCardProps = {
export const AgentCard = (props: agentCardProps) => { export const AgentCard = (props: agentCardProps) => {
const {data: agent} = props; const {data: agent} = props;
const isMobile = useIsMobile();
const [isCopied, setIsCopied] = useState(false); const [isCopied, setIsCopied] = useState(false);
const handleCopy = (e: React.MouseEvent) => { const handleCopy = (e: React.MouseEvent) => {
@@ -37,8 +40,8 @@ export const AgentCard = (props: agentCardProps) => {
<div className="flex-1 min-w-0 flex flex-col gap-1.5"> <div className="flex-1 min-w-0 flex flex-col gap-1.5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<h3 className="text-lg font-black text-foreground group-hover:text-primary transition-colors truncate tracking-tight leading-none"> <h3 className="text-lg font-black text-foreground group-hover:text-primary transition-colors truncate tracking-tight ">
{agent.name} {isMobile ? truncateWords(agent.name, 2) : agent.name}
</h3> </h3>
{agent.version && ( {agent.version && (
<Badge variant="secondary" className="h-5 px-1.5 text-[10px] font-bold tracking-wider"> <Badge variant="secondary" className="h-5 px-1.5 text-[10px] font-bold tracking-wider">
@@ -47,8 +50,8 @@ export const AgentCard = (props: agentCardProps) => {
)} )}
</div> </div>
<div className="flex items-center gap-4 text-muted-foreground group-hover:text-foreground transition-colors"> <div className="flex items-center gap-4 text-muted-foreground group-hover:text-foreground transition-colors">
<div className="flex items-center gap-1.5 min-w-0"> <div className="hidden sm:flex items-center gap-1.5 min-w-0">
<Fingerprint className="w-3.5 h-3.5 shrink-0" /> <Fingerprint className="w-3.5 h-3.5 shrink-0" />
<span className="font-mono text-xs font-bold truncate opacity-80"> <span className="font-mono text-xs font-bold truncate opacity-80">
{agent.id} {agent.id}
@@ -62,7 +65,7 @@ export const AgentCard = (props: agentCardProps) => {
</button> </button>
</div> </div>
<div className="hidden sm:flex items-center gap-1.5 shrink-0"> <div className="flex items-center gap-1.5 shrink-0">
<Database className="w-3.5 h-3.5" /> <Database className="w-3.5 h-3.5" />
<span className="text-xs font-bold uppercase tracking-tight opacity-80"> <span className="text-xs font-bold uppercase tracking-tight opacity-80">
{agent.databases?.length ?? 0} DBs {agent.databases?.length ?? 0} DBs
@@ -10,9 +10,9 @@ export type agentDatabaseCardProps = {
export const AgentDatabaseCard = (props: agentDatabaseCardProps) => { export const AgentDatabaseCard = (props: agentDatabaseCardProps) => {
const {data: database} = props; const {data: database} = props;
return <DatabaseCard data={database}/>; return <DatabaseCard withDetails={false} data={database}/>;
//todo: à remettre quand on aura fait l'impersonation des admins //Todo: Add again when impersonation system will be implemented
/* if (!database.projectId) { /* if (!database.projectId) {
return <DatabaseCard data={database}/>; return <DatabaseCard data={database}/>;
} }
@@ -57,54 +57,53 @@ export const AgentForm = (props: agentFormProps) => {
}); });
return ( return (
<TooltipProvider> <TooltipProvider>
<Card> <Form
<CardContent> form={form}
<Form className="flex flex-col gap-4"
form={form} onSubmit={async (values) => {
className="flex flex-col gap-4 mt-3" await mutation.mutateAsync(values);
onSubmit={async (values) => { }}
await mutation.mutateAsync(values); >
}} <FormField
> control={form.control}
<FormField name="name"
control={form.control} defaultValue=""
name="name" render={({field}) => (
defaultValue="" <FormItem>
render={({field}) => ( <FormLabel>Name</FormLabel>
<FormItem> <FormControl>
<FormLabel>Name</FormLabel> <Input placeholder="Agent 1" {...field} />
<FormControl> </FormControl>
<Input placeholder="Agent 1" {...field} /> <FormDescription>Your agent project name</FormDescription>
</FormControl> <FormMessage/>
<FormDescription>Your agent project name</FormDescription> </FormItem>
<FormMessage/> )}
</FormItem> />
)} <FormField
/> control={form.control}
<FormField defaultValue=""
control={form.control} name="description"
defaultValue="" render={({field}) => (
name="description" <FormItem>
render={({field}) => ( <FormLabel>Description</FormLabel>
<FormItem> <FormControl>
<FormLabel>Description</FormLabel> <Input placeholder="This agent is for the client exemple.com" {...field}
<FormControl> value={field.value ?? ""}/>
<Input placeholder="This agent is for the client exemple.com" {...field} </FormControl>
value={field.value ?? ""}/> <FormDescription>Enter your project agent description</FormDescription>
</FormControl> <FormMessage/>
<FormDescription>Enter your project agent description</FormDescription> </FormItem>
<FormMessage/> )}
</FormItem> />
)} <div className="flex justify-end">
/> <Button type="submit">
<Button>{isCreate ? `Create agent` : `Save agent`}</Button> {isCreate ? "Create" : "Update"}
</Form> </Button>
</CardContent> </div>
</Card> </Form>
</TooltipProvider> </TooltipProvider>
); );
}; };
@@ -6,6 +6,7 @@ import {useMutation} from "@tanstack/react-query";
import {useRouter} from "next/navigation"; import {useRouter} from "next/navigation";
import {toast} from "sonner"; import {toast} from "sonner";
import {deleteAgentAction} from "@/components/wrappers/dashboard/agent/button-delete-agent/delete-agent.action"; import {deleteAgentAction} from "@/components/wrappers/dashboard/agent/button-delete-agent/delete-agent.action";
import {useIsMobile} from "@/hooks/use-mobile";
export type ButtonDeleteAgentProps = { export type ButtonDeleteAgentProps = {
text?: string; text?: string;
@@ -14,6 +15,7 @@ export type ButtonDeleteAgentProps = {
export const ButtonDeleteAgent = (props: ButtonDeleteAgentProps) => { export const ButtonDeleteAgent = (props: ButtonDeleteAgentProps) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile();
const mutation = useMutation({ const mutation = useMutation({
mutationFn: () => deleteAgentAction(props.agentId), mutationFn: () => deleteAgentAction(props.agentId),
@@ -33,7 +35,7 @@ export const ButtonDeleteAgent = (props: ButtonDeleteAgentProps) => {
description="Are you sure you want to remove this agent? This action cannot be undone." description="Are you sure you want to remove this agent? This action cannot be undone."
button={{ button={{
main: { main: {
text: props.text ? props.text : "", text: props.text ? !isMobile ? props.text: "" : "",
variant: "outline", variant: "outline",
icon: <Trash2 color="red"/>, icon: <Trash2 color="red"/>,
}, },
@@ -9,6 +9,8 @@ import {
import {EditNotifierButton} from "@/components/wrappers/dashboard/common/notifier/notifier-card/button-edit-notifier"; import {EditNotifierButton} from "@/components/wrappers/dashboard/common/notifier/notifier-card/button-edit-notifier";
import {OrganizationWithMembers} from "@/db/schema/03_organization"; import {OrganizationWithMembers} from "@/db/schema/03_organization";
import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers"; import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers";
import {truncateWords} from "@/utils/text";
import {useIsMobile} from "@/hooks/use-mobile";
export type NotifierCardProps = { export type NotifierCardProps = {
data: NotificationChannelWith; data: NotificationChannelWith;
@@ -19,6 +21,7 @@ export type NotifierCardProps = {
export const NotifierCard = (props: NotifierCardProps) => { export const NotifierCard = (props: NotifierCardProps) => {
const {data, organization} = props; const {data, organization} = props;
const isMobile = useIsMobile()
return ( return (
<div className="block transition-all duration-200 rounded-xl"> <div className="block transition-all duration-200 rounded-xl">
@@ -33,13 +36,14 @@ export const NotifierCard = (props: NotifierCardProps) => {
<div className="flex justify-start w-full"> <div className="flex justify-start w-full">
<div className="flex flex-col items-start md:flex-row md:items-center gap-2 "> <div className="flex flex-col items-start md:flex-row md:items-center gap-2 ">
<h3 className="font-medium text-foreground">{data.name}</h3> <h3 className="font-medium text-foreground">{isMobile ? truncateWords(data.name, 2) : data.name}</h3>
<Badge variant="secondary" className="text-xs font-mono"> <Badge variant="secondary" className="text-xs font-mono">
{data.provider} {data.provider}
</Badge> </Badge>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<EditNotifierButton <EditNotifierButton
organizations={props.organizations} organizations={props.organizations}
@@ -23,8 +23,8 @@ import {useRouter} from "next/navigation";
import {Switch} from "@/components/ui/switch"; import {Switch} from "@/components/ui/switch";
import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers"; import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers";
import {Card} from "@/components/ui/card"; import {Card} from "@/components/ui/card";
import {cn} from "@/lib/utils";
import Link from "next/link"; import Link from "next/link";
import {useIsMobile} from "@/hooks/use-mobile";
type AlertPolicyFormProps = { type AlertPolicyFormProps = {
onSuccess?: () => void; onSuccess?: () => void;
@@ -35,6 +35,7 @@ type AlertPolicyFormProps = {
export const AlertPolicyForm = ({database, notificationChannels, organizationId, onSuccess}: AlertPolicyFormProps) => { export const AlertPolicyForm = ({database, notificationChannels, organizationId, onSuccess}: AlertPolicyFormProps) => {
const router = useRouter() const router = useRouter()
const isMobile = useIsMobile()
const organizationNotificationChannels = notificationChannels.map(channel => channel.id) ?? []; const organizationNotificationChannels = notificationChannels.map(channel => channel.id) ?? [];
@@ -159,9 +160,11 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<Label className="text-base font-medium">Configure Alerts</Label> <Label className="text-base font-medium">Configure Alerts</Label>
<p className="text-xs text-muted-foreground mt-1"> {!isMobile && (
Choose which channels receive notifications for specific events. <p className="text-xs text-muted-foreground mt-1">
</p> Choose which channels receive notifications for specific events.
</p>
)}
</div> </div>
<Button <Button
disabled={ disabled={
@@ -184,7 +187,9 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
<InfoIcon className="h-8 w-8 text-muted-foreground/50"/> <InfoIcon className="h-8 w-8 text-muted-foreground/50"/>
<p className="font-medium text-sm text-foreground">No notification channels</p> <p className="font-medium text-sm text-foreground">No notification channels</p>
<p className="text-xs text-muted-foreground max-w-xs"> <p className="text-xs text-muted-foreground max-w-xs">
Please <Link href={`/dashboard/settings`} className="underline underline-offset-4 hover:text-primary transition-colors">configure notification channels</Link> in your organization settings first. Please <Link href={`/dashboard/settings`}
className="underline underline-offset-4 hover:text-primary transition-colors">configure
notification channels</Link> in your organization settings first.
</p> </p>
</div> </div>
) : fields.length === 0 ? ( ) : fields.length === 0 ? (
@@ -201,17 +206,17 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
) : ( ) : (
<div className="grid gap-4"> <div className="grid gap-4">
{fields.map((field, index) => ( {fields.map((field, index) => (
<Card key={field.id} className="p-4 transition-all hover:border-primary/50 relative group"> <Card key={field.id} className="p-4 transition-all hover:border-primary/50 relative group min-w-0 overflow-hidden">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="grid grid-cols-[1fr_auto_auto] gap-3 items-end"> <div className="flex flex-row gap-2 items-start md:items-end flex-nowrap min-w-0 ">
<div className="flex flex-col gap-1.5"> <div className="flex-1 min-w-0 flex flex-col gap-1.5">
<Label className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest pl-0.5"> <Label className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest pl-0.5">
Notification Channel Notification Channel
</Label> </Label>
<FormField <FormField
control={form.control} control={form.control}
name={`alertPolicies.${index}.notificationChannelId`} name={`alertPolicies.${index}.notificationChannelId`}
render={({field}) => { render={({ field }) => {
const selectedIds = form const selectedIds = form
.watch("alertPolicies") .watch("alertPolicies")
.map((a: AlertPolicyType) => a.notificationChannelId) .map((a: AlertPolicyType) => a.notificationChannelId)
@@ -226,36 +231,38 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
const selectedChannel = notificationChannels.find(c => c.id === field.value); const selectedChannel = notificationChannels.find(c => c.id === field.value);
return ( return (
<FormItem className="space-y-0"> <FormItem className="space-y-0 min-w-0">
<Select onValueChange={field.onChange} <Select onValueChange={field.onChange} value={field.value?.toString() || ""}>
value={field.value?.toString() || ""}
>
<FormControl> <FormControl>
<SelectTrigger className="h-9 w-full bg-background border-input"> <SelectTrigger className="h-9 w-full bg-background border-input min-w-0">
<SelectValue placeholder="Select channel"> <SelectValue placeholder="Select channel">
{selectedChannel ? ( {selectedChannel && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 min-w-0 w-full">
<div className={cn("flex items-center justify-center h-4 w-4 rounded")}> <div className="flex items-center justify-center h-4 w-4 shrink-0">
{getNotificationChannelIcon(selectedChannel.provider)} {getNotificationChannelIcon(selectedChannel.provider)}
</div> </div>
<span className="truncate font-medium text-sm">{selectedChannel.name}</span> <span className="truncate font-medium text-sm min-w-0">
<span className="text-[9px] bg-secondary px-1.5 py-0.5 rounded text-muted-foreground ml-auto font-mono uppercase tracking-tighter">{selectedChannel.provider}</span> {selectedChannel.name}
</span>
<span className="shrink-0 text-[9px] bg-secondary px-1.5 py-0.5 rounded text-muted-foreground font-mono uppercase">
{selectedChannel.provider}
</span>
</div> </div>
) : "Select channel"} )}
</SelectValue> </SelectValue>
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{availableNotificationChannels.map((channel) => ( {availableNotificationChannels.map(channel => (
<SelectItem key={channel.id.toString()} <SelectItem key={channel.id.toString()} value={channel.id.toString()}>
value={channel.id.toString()}> <div className="flex items-center gap-2 w-full min-w-0">
<div className="flex items-center gap-2 w-full"> <div className="text-muted-foreground scale-90 shrink-0">
<div className="text-muted-foreground scale-90">
{getNotificationChannelIcon(channel.provider)} {getNotificationChannelIcon(channel.provider)}
</div> </div>
<span className="font-medium">{channel.name}</span> <span className="font-medium truncate min-w-0">{channel.name}</span>
<span <span className="text-xs text-muted-foreground ml-2 capitalize shrink-0">
className="text-xs text-muted-foreground ml-2 capitalize">({channel.provider})</span> ({channel.provider})
</span>
</div> </div>
</SelectItem> </SelectItem>
))} ))}
@@ -267,63 +274,61 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
}} }}
/> />
</div> </div>
<div className="flex flex-col gap-1.5 shrink-0">
<div className="flex flex-col gap-1.5"> <Label className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest pl-0.5">
<Label className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest pl-0.5">
Status Status
</Label> </Label>
<FormField <FormField
control={form.control} control={form.control}
name={`alertPolicies.${index}.enabled`} name={`alertPolicies.${index}.enabled`}
render={({field}) => ( render={({ field }) => (
<FormItem className="space-y-0"> <FormItem className="space-y-0">
<FormControl> <FormControl>
<div className="flex items-center h-9 px-3 rounded-md border border-input bg-background min-w-[90px] justify-between"> <div className="flex items-center h-9 px-1 md:px-3 rounded-md border border-input bg-background justify-between min-w-0">
<Label htmlFor={`switch-${index}`} className="text-xs cursor-pointer font-medium text-foreground mr-2"> {!isMobile && (
{field.value ? "Active" : "Off"} <Label htmlFor={`switch-${index}`} className="text-xs cursor-pointer font-medium text-foreground mr-2">
</Label> {field.value ? "Active" : "Off"}
<Switch </Label>
checked={field.value} )}
onCheckedChange={field.onChange} <Switch checked={field.value} onCheckedChange={field.onChange} id={`switch-${index}`} className="scale-75 origin-right"/>
id={`switch-${index}`}
className="scale-75 origin-right"
/>
</div> </div>
</FormControl> </FormControl>
</FormItem> </FormItem>
)} )}
/> />
</div> </div>
<div className="flex flex-col gap-1.5 shrink-0 mt-auto">
<div className="flex flex-col gap-1.5">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
size="icon" size="icon"
className="h-9 w-9 text-muted-foreground hover:text-destructive hover:border-destructive/50 hover:bg-destructive/10 transition-colors border-input bg-background" className="h-9 w-9 text-muted-foreground hover:text-destructive hover:border-destructive/50 hover:bg-destructive/10 transition-colors border-input bg-background"
onClick={() => removeAlertPolicy(index)}> onClick={() => removeAlertPolicy(index)}
>
<Trash2 className="w-4 h-4"/> <Trash2 className="w-4 h-4"/>
</Button> </Button>
</div> </div>
</div> </div>
<FormField <FormField
control={form.control} control={form.control}
name={`alertPolicies.${index}.eventKinds`} name={`alertPolicies.${index}.eventKinds`}
render={({field}) => ( render={({ field }) => (
<FormItem className="space-y-1.5"> <FormItem className="space-y-1.5 min-w-0">
<FormLabel className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Trigger Events</FormLabel> <FormLabel className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Trigger Events
</FormLabel>
<FormControl> <FormControl>
<MultiSelect <div className="max-w-full overflow-hidden">
options={EVENT_KIND_OPTIONS} <MultiSelect
onValueChange={field.onChange} options={EVENT_KIND_OPTIONS}
defaultValue={field.value ?? []} onValueChange={field.onChange}
placeholder="Select events to trigger notifications..." defaultValue={field.value ?? []}
variant="inverted" placeholder={isMobile ? "Select events...": "Select events to trigger notifications..."}
animation={0} variant="inverted"
className="bg-background/50" animation={0}
/> className="bg-background/50 w-full min-w-0 flex-wrap"
/>
</div>
</FormControl> </FormControl>
<FormMessage/> <FormMessage/>
</FormItem> </FormItem>
@@ -6,6 +6,7 @@ import {useRouter} from "next/navigation";
import {toast} from "sonner"; import {toast} from "sonner";
import {authClient} from "@/lib/auth/auth-client"; import {authClient} from "@/lib/auth/auth-client";
import {Trash2} from "lucide-react"; import {Trash2} from "lucide-react";
import {useIsMobile} from "@/hooks/use-mobile";
export type DeleteOrganizationButtonProps = { export type DeleteOrganizationButtonProps = {
organizationSlug: string; organizationSlug: string;
@@ -13,6 +14,7 @@ export type DeleteOrganizationButtonProps = {
export const DeleteOrganizationButton = (props: DeleteOrganizationButtonProps) => { export const DeleteOrganizationButton = (props: DeleteOrganizationButtonProps) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile();
const {data: organizations, refetch} = authClient.useListOrganizations(); const {data: organizations, refetch} = authClient.useListOrganizations();
const mutation = useMutation({ const mutation = useMutation({
@@ -47,7 +49,7 @@ export const DeleteOrganizationButton = (props: DeleteOrganizationButtonProps) =
description="Are you sure you want to remove this organization? This action cannot be undone." description="Are you sure you want to remove this organization? This action cannot be undone."
button={{ button={{
main: { main: {
text: "Delete Organization", text: !isMobile ? "Delete Organization" : "",
variant: "outline", variant: "outline",
icon: <Trash2 color="red"/>, icon: <Trash2 color="red"/>,
}, },
@@ -1,6 +1,5 @@
"use client"; "use client";
import {Card, CardContent, CardHeader} from "@/components/ui/card";
import { import {
FormControl, FormControl,
FormDescription, FormDescription,
@@ -22,7 +21,6 @@ import {
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema"; } from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization"; import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
import { import {
deleteOrganizationAction,
updateOrganizationAction updateOrganizationAction
} from "@/components/wrappers/dashboard/organization/organization.action"; } from "@/components/wrappers/dashboard/organization/organization.action";
import {toast} from "sonner"; import {toast} from "sonner";
@@ -95,76 +93,75 @@ export const OrganizationForm = (props: organizationFormProps) => {
return ( return (
<Card> <Form
<CardHeader></CardHeader> form={form}
<CardContent> className="flex flex-col gap-4"
<Form onSubmit={async (values) => {
form={form} await mutation.mutateAsync(values);
className="flex flex-col gap-4" }}
onSubmit={async (values) => { >
await mutation.mutateAsync(values); <FormField
}} control={form.control}
> name="name"
<FormField defaultValue=""
control={form.control} render={({field}) => (
name="name" <FormItem>
defaultValue="" <FormLabel>Name</FormLabel>
render={({field}) => ( <FormControl>
<FormItem> <Input placeholder="Organization 1" {...field} />
<FormLabel>Name</FormLabel> </FormControl>
<FormControl> <FormMessage/>
<Input placeholder="Organization 1" {...field} /> </FormItem>
</FormControl> )}
<FormMessage/> />
</FormItem> <FormField
)} control={form.control}
/> name="slug"
<FormField defaultValue=""
control={form.control} render={({field}) => (
name="slug" <FormItem>
defaultValue="" <FormLabel>Slug</FormLabel>
render={({field}) => ( <FormControl>
<FormItem> <Input
<FormLabel>Slug</FormLabel> placeholder="project-1"
<FormControl> {...field}
<Input onChange={(e) => {
placeholder="project-1" const value = e.target.value.replaceAll(" ", "-").toLowerCase();
{...field} field.onChange(value);
onChange={(e) => { }}
const value = e.target.value.replaceAll(" ", "-").toLowerCase(); />
field.onChange(value); </FormControl>
}} <FormMessage/>
/> </FormItem>
</FormControl> )}
<FormMessage/> />
</FormItem> <FormField
)} control={form.control}
/> name="users"
<FormField render={({field}) => (
control={form.control} <FormItem>
name="users" <FormLabel>Users</FormLabel>
render={({field}) => ( <FormControl>
<FormItem> <MultiSelect
<FormLabel>Users</FormLabel> options={formatUsersList(props.users)}
<FormControl> onValueChange={field.onChange}
<MultiSelect defaultValue={field.value ?? []}
options={formatUsersList(props.users)} placeholder="Select users"
onValueChange={field.onChange} variant="inverted"
defaultValue={field.value ?? []} animation={2}
placeholder="Select users" // maxCount={100}
variant="inverted" />
animation={2} </FormControl>
// maxCount={100} <FormDescription>Select users you want to add to this organization</FormDescription>
/> <FormMessage/>
</FormControl> </FormItem>
<FormDescription>Select users you want to add to this organization</FormDescription> )}
<FormMessage/> />
</FormItem> <div className="flex justify-end">
)} <Button type="submit">
/> {isCreate ? "Create" : "Update"}
<Button>{isCreate ? `Create Organization` : `Update Organization`}</Button> </Button>
</Form> </div>
</CardContent> </Form>
</Card>
); );
}; };
@@ -28,10 +28,6 @@ export const OrganizationNotifiersTab = ({
<h3 className="text-xl font-semibold text-balance mb-1"> <h3 className="text-xl font-semibold text-balance mb-1">
Notification Settings Notification Settings
</h3> </h3>
<p className="text-muted-foreground leading-relaxed ">
Configure how and when you receive alerts about your services and
infrastructure.
</p>
</div> </div>
<NotifierAddEditModal <NotifierAddEditModal
organization={organization} organization={organization}
@@ -8,6 +8,7 @@ import {
} from "@/components/wrappers/dashboard/projects/button-delete-project/delete-project.action"; } from "@/components/wrappers/dashboard/projects/button-delete-project/delete-project.action";
import {useRouter} from "next/navigation"; import {useRouter} from "next/navigation";
import {toast} from "sonner"; import {toast} from "sonner";
import {useIsMobile} from "@/hooks/use-mobile";
export type ButtonDeleteProjectProps = { export type ButtonDeleteProjectProps = {
text?: string; text?: string;
@@ -16,6 +17,8 @@ export type ButtonDeleteProjectProps = {
export const ButtonDeleteProject = (props: ButtonDeleteProjectProps) => { export const ButtonDeleteProject = (props: ButtonDeleteProjectProps) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile()
const mutation = useMutation({ const mutation = useMutation({
mutationFn: () => deleteProjectAction(props.projectId), mutationFn: () => deleteProjectAction(props.projectId),
onSuccess: async (result: any) => { onSuccess: async (result: any) => {
@@ -35,7 +38,7 @@ export const ButtonDeleteProject = (props: ButtonDeleteProjectProps) => {
description="Are you sure you want to delete this project ? This action cannot be undone." description="Are you sure you want to delete this project ? This action cannot be undone."
button={{ button={{
main: { main: {
text: props.text ? props.text : "", text: props.text ? !isMobile ? props.text: "" : "",
variant: "outline", variant: "outline",
icon: <Trash2 color="red"/>, icon: <Trash2 color="red"/>,
}, },
@@ -19,8 +19,9 @@ export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
const {data: database, extendedProps: extendedProps} = props; const {data: database, extendedProps: extendedProps} = props;
return ( return (
<Link className="group block transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl" <Link
href={`/dashboard/projects/${extendedProps.id}/database/${database.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"
href={`/dashboard/projects/${extendedProps.id}/database/${database.id}`}>
<DatabaseCard data={database}/> <DatabaseCard data={database}/>
</Link> </Link>
); );
@@ -28,10 +29,11 @@ export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
export type databaseCardProps = { export type databaseCardProps = {
data: Database; data: Database;
withDetails?: boolean;
}; };
export const DatabaseCard = (props: databaseCardProps) => { export const DatabaseCard = (props: databaseCardProps) => {
const {data: database} = props; const {data: database, withDetails = true} = props;
const [isCopied, setIsCopied] = useState(false); const [isCopied, setIsCopied] = useState(false);
const handleCopy = (e: React.MouseEvent) => { const handleCopy = (e: React.MouseEvent) => {
@@ -43,9 +45,11 @@ export const DatabaseCard = (props: databaseCardProps) => {
}; };
return ( return (
<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 border-border/50 bg-card hover:bg-accent/50 hover:border-primary/50 group-hover:shadow-lg overflow-hidden gap-0">
<div className="flex items-start justify-between mb-2"> <div className="flex items-start justify-between mb-2">
<div className="relative w-12 h-12 p-2 bg-background rounded-xl border border-border/50 shadow-sm flex items-center justify-center group-hover:border-primary/30 transition-all duration-300 group-hover:scale-105"> <div
className="relative w-12 h-12 p-2 bg-background rounded-xl border border-border/50 shadow-sm flex items-center justify-center group-hover:border-primary/30 transition-all duration-300 group-hover:scale-105">
<Image <Image
src={`/images/${database.dbms}.png`} src={`/images/${database.dbms}.png`}
alt={`${database.dbms} icon`} alt={`${database.dbms} icon`}
@@ -67,9 +71,10 @@ export const DatabaseCard = (props: databaseCardProps) => {
</h3> </h3>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex items-center gap-2 text-muted-foreground group-hover:text-foreground transition-colors"> <div
className="flex items-center gap-2 text-muted-foreground group-hover:text-foreground transition-colors">
<div className="p-1 bg-muted/50 rounded-lg"> <div className="p-1 bg-muted/50 rounded-lg">
<Fingerprint className="w-3.5 h-3.5" /> <Fingerprint className="w-3.5 h-3.5"/>
</div> </div>
<span className="font-mono text-[10px] font-bold truncate"> <span className="font-mono text-[10px] font-bold truncate">
{database.agentDatabaseId} {database.agentDatabaseId}
@@ -79,12 +84,14 @@ export const DatabaseCard = (props: databaseCardProps) => {
className="ml-1 p-0.5 hover:bg-muted rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 z-10" className="ml-1 p-0.5 hover:bg-muted rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 z-10"
title="Copy ID" title="Copy ID"
> >
{isCopied ? <Check className="w-2.5 h-2.5 text-green-500" /> : <Copy className="w-2.5 h-2.5" />} {isCopied ? <Check className="w-2.5 h-2.5 text-green-500"/> :
<Copy className="w-2.5 h-2.5"/>}
</button> </button>
</div> </div>
<div className="flex items-center gap-2 text-muted-foreground group-hover:text-foreground transition-colors"> <div
className="flex items-center gap-2 text-muted-foreground group-hover:text-foreground transition-colors">
<div className="p-1 bg-muted/50 rounded-lg"> <div className="p-1 bg-muted/50 rounded-lg">
<Activity className="w-3.5 h-3.5" /> <Activity className="w-3.5 h-3.5"/>
</div> </div>
<span className="text-[10px] font-bold uppercase tracking-tight"> <span className="text-[10px] font-bold uppercase tracking-tight">
{formatDateLastContact(database.lastContact)} {formatDateLastContact(database.lastContact)}
@@ -93,13 +100,18 @@ export const DatabaseCard = (props: databaseCardProps) => {
</div> </div>
</div> </div>
<div className="mt-4 flex items-center justify-between pt-3 border-t border-border/50"> {withDetails && (
<span className="text-[10px] font-black uppercase tracking-widest text-primary opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-x-[-10px] group-hover:translate-x-0">Open</span> <div className="mt-4 flex items-center justify-between pt-3 border-t border-border/50">
<div className="flex items-center gap-1 text-muted-foreground group-hover:text-primary transition-colors"> <span
<span className="text-[10px] font-bold uppercase tracking-widest group-hover:hidden">Details</span> className="text-[10px] font-black uppercase tracking-widest text-primary opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-x-[-10px] group-hover:translate-x-0">Open</span>
<ChevronRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform" /> <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 group-hover:hidden">Details</span>
<ChevronRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform"/>
</div>
</div> </div>
</div> )}
</Card> </Card>
); );
}; };
@@ -78,9 +78,6 @@ export const ProjectForm = (props: projectFormProps) => {
}); });
return ( return (
<Card>
<CardHeader></CardHeader>
<CardContent>
<Form <Form
form={form} form={form}
className="flex flex-col gap-4" className="flex flex-col gap-4"
@@ -124,9 +121,11 @@ export const ProjectForm = (props: projectFormProps) => {
</FormItem> </FormItem>
)} )}
/> />
<Button>{isCreate ? `Create Project` : `Update Project`}</Button> <div className="flex justify-end">
<Button type="submit">
{isCreate ? "Create" : "Update"}
</Button>
</div>
</Form> </Form>
</CardContent>
</Card>
); );
}; };
@@ -7,12 +7,9 @@ import * as drizzleDb from "@/db";
import {db} from "@/db"; import {db} from "@/db";
import {and, eq} from "drizzle-orm"; import {and, eq} from "drizzle-orm";
import {Backup, Restoration} from "@/db/schema/07_database"; import {Backup, Restoration} from "@/db/schema/07_database";
import {NextResponse} from "next/server";
import { import {
deleteFileS3Private, deleteFileS3Private,
deleteLocalPrivate, deleteLocalPrivate,
uploadLocalPrivate,
uploadS3Private
} from "@/features/upload/private/upload.action"; } from "@/features/upload/private/upload.action";
import {env} from "@/env.mjs"; import {env} from "@/env.mjs";
import {withUpdatedAt} from "@/db/utils"; import {withUpdatedAt} from "@/db/utils";
@@ -89,98 +86,32 @@ export const deleteBackupAction = userAction
let success: boolean, message: string; let success: boolean, message: string;
const result = if (parsedInput.file) {
settings.storage === "local" const result =
? await deleteLocalPrivate(parsedInput.file) settings.storage === "local"
: await deleteFileS3Private(`${parsedInput.projectSlug}/${parsedInput.file}`, env.S3_BUCKET_NAME!); ? await deleteLocalPrivate(parsedInput.file)
: await deleteFileS3Private(`${parsedInput.projectSlug}/${parsedInput.file}`, env.S3_BUCKET_NAME!);
({success, message} = result); ({success, message} = result);
if (!success) { if (!success) {
return { return {
success: false, success: false,
actionError: { actionError: {
message: message, message: message,
status: 404, status: 404,
cause: "Unable to delete backup from storage", cause: "Unable to delete backup from storage",
messageParams: {message: "Error deleting the backup"}, messageParams: {message: "Error deleting the backup"},
}, },
}; };
}
} }
return { return {
success: true, success: true,
actionSuccess: { actionSuccess: {
message: `Backup deleted successfully (ref: ${parsedInput.backupId}).`, message: `Backup deleted successfully (ref: ${parsedInput.backupId}).`,
}, },
}; };
// const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
// if (!settings) {
// return {
// success: false,
// actionError: {
// message: "No settings found.",
// status: 404,
// cause: "No settings found.",
// messageParams: {message: "Error deleting the backup"},
// },
// };
// }
//
//
// let success: boolean, message: string;
//
// const result =
// settings.storage === "local"
// ? await deleteLocalPrivate(parsedInput.file)
// : await deleteFileS3Private(`${parsedInput.projectSlug}/${parsedInput.file}`, env.S3_BUCKET_NAME!);
//
// ({success, message} = result);
//
// if (!success) {
// return {
// success: false,
// actionError: {
// message: message,
// status: 404,
// cause: "Unable to delete backup from storage",
// messageParams: {message: "Error deleting the backup"},
// },
// };
// }
//
// await db
// .delete(drizzleDb.schemas.backup)
// .where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
// .execute();
//
// const backupExists = await db
// .select()
// .from(drizzleDb.schemas.backup)
// .where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
// .execute();
//
//
// if (backupExists.length === 0) {
// return {
// success: true,
// actionSuccess: {
// message: "Backup deleted successfully.",
// },
// };
// } else {
// return {
// success: false,
// actionError: {
// message: "Backup not found or already deleted.",
// status: 404,
// cause: "Backup could not be deleted (from database or remote storage).",
// messageParams: {message: "Error deleting the backup"},
// },
// };
// }
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
@@ -246,7 +177,6 @@ export const rerunRestorationAction = userAction
}); });
// Create Restoration Action (Drizzle version)
export const createRestorationAction = userAction export const createRestorationAction = userAction
.schema( .schema(
z.object({ z.object({
@@ -256,7 +186,6 @@ export const createRestorationAction = userAction
) )
.action(async ({parsedInput}): Promise<ServerActionResult<Restoration>> => { .action(async ({parsedInput}): Promise<ServerActionResult<Restoration>> => {
try { try {
// Insert new restoration into the database
const restorationData = await db const restorationData = await db
.insert(drizzleDb.schemas.restoration) .insert(drizzleDb.schemas.restoration)
.values({ .values({
+2 -1
View File
@@ -75,8 +75,9 @@ export async function deleteFileS3Private(fileName: string, bucketName: string)
*/ */
export async function deleteLocalPrivate(fileName: string) { export async function deleteLocalPrivate(fileName: string) {
try { try {
console.log(fileName);
const filePath = path.join(process.cwd(), privateLocalDir, fileName); const filePath = path.join(process.cwd(), privateLocalDir, fileName);
console.log(`Deleted ${filePath}`);
// Delete locally // Delete locally
await unlink(filePath); await unlink(filePath);
+20 -16
View File
@@ -43,27 +43,31 @@ export const deleteBackupCronAction = action
})) }))
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId))) .where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
let success: boolean, message: string;
const result = if (parsedInput.file) {
settings.storage === "local" let success: boolean, message: string;
? await deleteLocalPrivate(parsedInput.file)
: await deleteFileS3Private(`${parsedInput.projectSlug}/${parsedInput.file}`, env.S3_BUCKET_NAME!);
({success, message} = result); const result =
settings.storage === "local"
? await deleteLocalPrivate(parsedInput.file)
: await deleteFileS3Private(`${parsedInput.projectSlug}/${parsedInput.file}`, env.S3_BUCKET_NAME!);
if (!success) { ({success, message} = result);
return {
success: false, if (!success) {
actionError: { return {
message: message, success: false,
status: 404, actionError: {
cause: "Unable to delete backup from storage", message: message,
messageParams: {message: "Error deleting the backup"}, status: 404,
}, cause: "Unable to delete backup from storage",
}; messageParams: {message: "Error deleting the backup"},
},
};
}
} }
return { return {
success: true, success: true,
actionSuccess: { actionSuccess: {