mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bd25353da | ||
|
|
e1bee19c18 | ||
|
|
0ec49af72a | ||
|
|
6d4eb836ab | ||
|
|
0c3cfecb1d | ||
|
|
f9186e7354 | ||
|
|
bc603a08cf | ||
|
|
077aada056 | ||
|
|
4ed5f9872d | ||
|
|
2d6be20b30 | ||
|
|
be8f3bd98c | ||
|
|
a3a20bee4a | ||
|
|
09a1e75acf | ||
|
|
ce6bdd01ef | ||
|
|
a03a1d635f | ||
|
|
419b6d4891 | ||
|
|
b4840318d5 | ||
|
|
bf81f437d1 | ||
|
|
81a4d33df1 |
@@ -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 }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
+3
-1
@@ -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"/>,
|
||||||
},
|
},
|
||||||
|
|||||||
+70
-73
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
-4
@@ -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}
|
||||||
|
|||||||
+4
-1
@@ -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({
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user