mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #856 from DJKnaeckebrot/feat/project-search
feat: add global search command
This commit is contained in:
@@ -1,35 +1,35 @@
|
|||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
BookIcon,
|
BookIcon,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
FolderInput,
|
FolderInput,
|
||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
@@ -38,253 +38,257 @@ import { ProjectEnviroment } from "./project-enviroment";
|
|||||||
import { UpdateProject } from "./update";
|
import { UpdateProject } from "./update";
|
||||||
|
|
||||||
export const ShowProjects = () => {
|
export const ShowProjects = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data } = api.project.all.useQuery();
|
const { data } = api.project.all.useQuery();
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.auth.get.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
const { data: user } = api.user.byAuthId.useQuery(
|
||||||
{
|
{
|
||||||
authId: auth?.id || "",
|
authId: auth?.id || "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
enabled: !!auth?.id && auth?.rol === "user",
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
const { mutateAsync } = api.project.remove.useMutation();
|
const { mutateAsync } = api.project.remove.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data?.length === 0 && (
|
{data?.length === 0 && (
|
||||||
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4">
|
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4">
|
||||||
<FolderInput className="size-10 md:size-28 text-muted-foreground" />
|
<FolderInput className="size-10 md:size-28 text-muted-foreground" />
|
||||||
<span className="text-center font-medium text-muted-foreground">
|
<span className="text-center font-medium text-muted-foreground">
|
||||||
No projects added yet. Click on Create project.
|
No projects added yet. Click on Create project.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-6 w-full grid sm:grid-cols-2 lg:grid-cols-3 flex-wrap gap-5 pb-10">
|
<div className="mt-6 w-full grid sm:grid-cols-2 lg:grid-cols-3 flex-wrap gap-5 pb-10">
|
||||||
{data?.map((project) => {
|
{data?.map((project) => {
|
||||||
const emptyServices =
|
const emptyServices =
|
||||||
project?.mariadb.length === 0 &&
|
project?.mariadb.length === 0 &&
|
||||||
project?.mongo.length === 0 &&
|
project?.mongo.length === 0 &&
|
||||||
project?.mysql.length === 0 &&
|
project?.mysql.length === 0 &&
|
||||||
project?.postgres.length === 0 &&
|
project?.postgres.length === 0 &&
|
||||||
project?.redis.length === 0 &&
|
project?.redis.length === 0 &&
|
||||||
project?.applications.length === 0 &&
|
project?.applications.length === 0 &&
|
||||||
project?.compose.length === 0;
|
project?.compose.length === 0;
|
||||||
|
|
||||||
const totalServices =
|
const totalServices =
|
||||||
project?.mariadb.length +
|
project?.mariadb.length +
|
||||||
project?.mongo.length +
|
project?.mongo.length +
|
||||||
project?.mysql.length +
|
project?.mysql.length +
|
||||||
project?.postgres.length +
|
project?.postgres.length +
|
||||||
project?.redis.length +
|
project?.redis.length +
|
||||||
project?.applications.length +
|
project?.applications.length +
|
||||||
project?.compose.length;
|
project?.compose.length;
|
||||||
|
|
||||||
const flattedDomains = [
|
const flattedDomains = [
|
||||||
...project.applications.flatMap((a) => a.domains),
|
...project.applications.flatMap((a) => a.domains),
|
||||||
...project.compose.flatMap((a) => a.domains),
|
...project.compose.flatMap((a) => a.domains),
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderDomainsDropdown = (
|
const renderDomainsDropdown = (
|
||||||
item: typeof project.compose | typeof project.applications,
|
item: typeof project.compose | typeof project.applications
|
||||||
) =>
|
) =>
|
||||||
item[0] ? (
|
item[0] ? (
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
{"applicationId" in item[0] ? "Applications" : "Compose"}
|
{"applicationId" in item[0] ? "Applications" : "Compose"}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{item.map((a) => (
|
{item.map((a) => (
|
||||||
<Fragment
|
<Fragment
|
||||||
key={"applicationId" in a ? a.applicationId : a.composeId}
|
key={"applicationId" in a ? a.applicationId : a.composeId}
|
||||||
>
|
>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuLabel className="font-normal capitalize text-xs ">
|
<DropdownMenuLabel className="font-normal capitalize text-xs ">
|
||||||
{a.name}
|
{a.name}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{a.domains.map((domain) => (
|
{a.domains.map((domain) => (
|
||||||
<DropdownMenuItem key={domain.domainId} asChild>
|
<DropdownMenuItem key={domain.domainId} asChild>
|
||||||
<Link
|
<Link
|
||||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
href={`${domain.https ? "https" : "http"}://${
|
||||||
>
|
domain.host
|
||||||
<span>{domain.host}</span>
|
}${domain.path}`}
|
||||||
<ExternalLink className="size-4 shrink-0" />
|
>
|
||||||
</Link>
|
<span>{domain.host}</span>
|
||||||
</DropdownMenuItem>
|
<ExternalLink className="size-4 shrink-0" />
|
||||||
))}
|
</Link>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuItem>
|
||||||
</Fragment>
|
))}
|
||||||
))}
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuGroup>
|
</Fragment>
|
||||||
) : null;
|
))}
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={project.projectId} className="w-full lg:max-w-md">
|
<div key={project.projectId} className="w-full lg:max-w-md">
|
||||||
<Link href={`/dashboard/project/${project.projectId}`}>
|
<Link href={`/dashboard/project/${project.projectId}`}>
|
||||||
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
|
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
|
||||||
{flattedDomains.length > 1 ? (
|
{flattedDomains.length > 1 ? (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
<ExternalLinkIcon className="size-3.5" />
|
<ExternalLinkIcon className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className="w-[200px] space-y-2"
|
className="w-[200px] space-y-2"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{renderDomainsDropdown(project.applications)}
|
{renderDomainsDropdown(project.applications)}
|
||||||
{renderDomainsDropdown(project.compose)}
|
{renderDomainsDropdown(project.compose)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : flattedDomains[0] ? (
|
) : flattedDomains[0] ? (
|
||||||
<Button
|
<Button
|
||||||
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={`${flattedDomains[0].https ? "https" : "http"}://${flattedDomains[0].host}${flattedDomains[0].path}`}
|
href={`${
|
||||||
target="_blank"
|
flattedDomains[0].https ? "https" : "http"
|
||||||
>
|
}://${flattedDomains[0].host}${flattedDomains[0].path}`}
|
||||||
<ExternalLinkIcon className="size-3.5" />
|
target="_blank"
|
||||||
</Link>
|
>
|
||||||
</Button>
|
<ExternalLinkIcon className="size-3.5" />
|
||||||
) : null}
|
</Link>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center justify-between gap-2">
|
<CardTitle className="flex items-center justify-between gap-2">
|
||||||
<span className="flex flex-col gap-1.5">
|
<span className="flex flex-col gap-1.5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<BookIcon className="size-4 text-muted-foreground" />
|
<BookIcon className="size-4 text-muted-foreground" />
|
||||||
<span className="text-base font-medium leading-none">
|
<span className="text-base font-medium leading-none">
|
||||||
{project.name}
|
{project.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
{project.description}
|
{project.description}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div className="flex self-start space-x-1">
|
<div className="flex self-start space-x-1">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="px-2"
|
className="px-2"
|
||||||
>
|
>
|
||||||
<MoreHorizontalIcon className="size-5" />
|
<MoreHorizontalIcon className="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-[200px] space-y-2">
|
<DropdownMenuContent className="w-[200px] space-y-2">
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
Actions
|
Actions
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<ProjectEnviroment
|
<ProjectEnviroment
|
||||||
projectId={project.projectId}
|
projectId={project.projectId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<UpdateProject projectId={project.projectId} />
|
<UpdateProject projectId={project.projectId} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
{(auth?.rol === "admin" ||
|
{(auth?.rol === "admin" ||
|
||||||
user?.canDeleteProjects) && (
|
user?.canDeleteProjects) && (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger className="w-full">
|
<AlertDialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<TrashIcon className="size-4" />
|
<TrashIcon className="size-4" />
|
||||||
<span>Delete</span>
|
<span>Delete</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>
|
<AlertDialogTitle>
|
||||||
Are you sure to delete this project?
|
Are you sure to delete this project?
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
{!emptyServices ? (
|
{!emptyServices ? (
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950">
|
<div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950">
|
||||||
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
||||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||||
You have active services, please
|
You have active services, please
|
||||||
delete them first
|
delete them first
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This action cannot be undone
|
This action cannot be undone
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
)}
|
)}
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>
|
<AlertDialogCancel>
|
||||||
Cancel
|
Cancel
|
||||||
</AlertDialogCancel>
|
</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
disabled={!emptyServices}
|
disabled={!emptyServices}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
projectId: project.projectId,
|
projectId: project.projectId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(
|
toast.success(
|
||||||
"Project delete succesfully",
|
"Project delete succesfully"
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error(
|
toast.error(
|
||||||
"Error to delete this project",
|
"Error to delete this project"
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
utils.project.all.invalidate();
|
utils.project.all.invalidate();
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="pt-4">
|
<CardFooter className="pt-4">
|
||||||
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
||||||
<DateTooltip date={project.createdAt}>
|
<DateTooltip date={project.createdAt}>
|
||||||
Created
|
Created
|
||||||
</DateTooltip>
|
</DateTooltip>
|
||||||
<span>
|
<span>
|
||||||
{totalServices}{" "}
|
{totalServices}{" "}
|
||||||
{totalServices === 1 ? "service" : "services"}
|
{totalServices === 1 ? "service" : "services"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
189
apps/dokploy/components/dashboard/search-command.tsx
Normal file
189
apps/dokploy/components/dashboard/search-command.tsx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandList,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandDialog,
|
||||||
|
CommandSeparator,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import {
|
||||||
|
extractServices,
|
||||||
|
type Services,
|
||||||
|
} from "@/pages/dashboard/project/[projectId]";
|
||||||
|
import type { findProjectById } from "@dokploy/server/services/project";
|
||||||
|
import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
MariadbIcon,
|
||||||
|
MongodbIcon,
|
||||||
|
MysqlIcon,
|
||||||
|
PostgresqlIcon,
|
||||||
|
RedisIcon,
|
||||||
|
} from "@/components/icons/data-tools-icons";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { StatusTooltip } from "../shared/status-tooltip";
|
||||||
|
|
||||||
|
type Project = Awaited<ReturnType<typeof findProjectById>>;
|
||||||
|
|
||||||
|
export const SearchCommand = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const [search, setSearch] = React.useState("");
|
||||||
|
|
||||||
|
const { data } = api.project.all.useQuery();
|
||||||
|
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const down = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "j" && (e.metaKey || e.ctrlKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen((open) => !open);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", down);
|
||||||
|
return () => document.removeEventListener("keydown", down);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||||
|
<CommandInput
|
||||||
|
placeholder={"Search projects or settings"}
|
||||||
|
value={search}
|
||||||
|
onValueChange={setSearch}
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>
|
||||||
|
No projects added yet. Click on Create project.
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup heading={"Projects"}>
|
||||||
|
<CommandList>
|
||||||
|
{data?.map((project) => (
|
||||||
|
<CommandItem
|
||||||
|
key={project.projectId}
|
||||||
|
onSelect={() => {
|
||||||
|
router.push(`/dashboard/project/${project.projectId}`);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BookIcon className="size-4 text-muted-foreground mr-2" />
|
||||||
|
{project.name}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandList>
|
||||||
|
</CommandGroup>
|
||||||
|
<CommandSeparator />
|
||||||
|
<CommandGroup heading={"Services"}>
|
||||||
|
<CommandList>
|
||||||
|
{data?.map((project) => {
|
||||||
|
const applications: Services[] = extractServices(project);
|
||||||
|
return applications.map((application) => (
|
||||||
|
<CommandItem
|
||||||
|
key={application.id}
|
||||||
|
onSelect={() => {
|
||||||
|
router.push(
|
||||||
|
`/dashboard/project/${project.projectId}/services/${application.type}/${application.id}`
|
||||||
|
);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{application.type === "postgres" && (
|
||||||
|
<PostgresqlIcon className="h-6 w-6 mr-2" />
|
||||||
|
)}
|
||||||
|
{application.type === "redis" && (
|
||||||
|
<RedisIcon className="h-6 w-6 mr-2" />
|
||||||
|
)}
|
||||||
|
{application.type === "mariadb" && (
|
||||||
|
<MariadbIcon className="h-6 w-6 mr-2" />
|
||||||
|
)}
|
||||||
|
{application.type === "mongo" && (
|
||||||
|
<MongodbIcon className="h-6 w-6 mr-2" />
|
||||||
|
)}
|
||||||
|
{application.type === "mysql" && (
|
||||||
|
<MysqlIcon className="h-6 w-6 mr-2" />
|
||||||
|
)}
|
||||||
|
{application.type === "application" && (
|
||||||
|
<GlobeIcon className="h-6 w-6 mr-2" />
|
||||||
|
)}
|
||||||
|
{application.type === "compose" && (
|
||||||
|
<CircuitBoard className="h-6 w-6 mr-2" />
|
||||||
|
)}
|
||||||
|
<span className="flex-grow">
|
||||||
|
{project.name} / {application.name}{" "}
|
||||||
|
<div style={{ display: "none" }}>{application.id}</div>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<StatusTooltip status={application.status} />
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
));
|
||||||
|
})}
|
||||||
|
</CommandList>
|
||||||
|
</CommandGroup>
|
||||||
|
<CommandSeparator />
|
||||||
|
<CommandGroup heading={"Application"} hidden={true}>
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => {
|
||||||
|
router.push("/dashboard/projects");
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Projects
|
||||||
|
</CommandItem>
|
||||||
|
{!isCloud && (
|
||||||
|
<>
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => {
|
||||||
|
router.push("/dashboard/monitoring");
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Monitoring
|
||||||
|
</CommandItem>
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => {
|
||||||
|
router.push("/dashboard/traefik");
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Traefik
|
||||||
|
</CommandItem>
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => {
|
||||||
|
router.push("/dashboard/docker");
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Docker
|
||||||
|
</CommandItem>
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => {
|
||||||
|
router.push("/dashboard/requests");
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Requests
|
||||||
|
</CommandItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => {
|
||||||
|
router.push("/dashboard/settings/server");
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</CommandDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -11,72 +11,74 @@ import { Inter } from "next/font/google";
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import type { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
|
import { SearchCommand } from "@/components/dashboard/search-command";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
||||||
getLayout?: (page: ReactElement) => ReactNode;
|
getLayout?: (page: ReactElement) => ReactNode;
|
||||||
// session: Session | null;
|
// session: Session | null;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AppPropsWithLayout = AppProps & {
|
type AppPropsWithLayout = AppProps & {
|
||||||
Component: NextPageWithLayout;
|
Component: NextPageWithLayout;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MyApp = ({
|
const MyApp = ({
|
||||||
Component,
|
Component,
|
||||||
pageProps: { ...pageProps },
|
pageProps: { ...pageProps },
|
||||||
}: AppPropsWithLayout) => {
|
}: AppPropsWithLayout) => {
|
||||||
const getLayout = Component.getLayout ?? ((page) => page);
|
const getLayout = Component.getLayout ?? ((page) => page);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
:root {
|
:root {
|
||||||
--font-inter: ${inter.style.fontFamily};
|
--font-inter: ${inter.style.fontFamily};
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Dokploy</title>
|
<title>Dokploy</title>
|
||||||
</Head>
|
</Head>
|
||||||
{process.env.NEXT_PUBLIC_UMAMI_HOST &&
|
{process.env.NEXT_PUBLIC_UMAMI_HOST &&
|
||||||
process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
|
process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
|
||||||
<Script
|
<Script
|
||||||
src={process.env.NEXT_PUBLIC_UMAMI_HOST}
|
src={process.env.NEXT_PUBLIC_UMAMI_HOST}
|
||||||
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
|
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
forcedTheme={Component.theme}
|
forcedTheme={Component.theme}
|
||||||
>
|
>
|
||||||
<Toaster richColors />
|
<Toaster richColors />
|
||||||
{getLayout(<Component {...pageProps} />)}
|
<SearchCommand />
|
||||||
</ThemeProvider>
|
{getLayout(<Component {...pageProps} />)}
|
||||||
</>
|
</ThemeProvider>
|
||||||
);
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api.withTRPC(
|
export default api.withTRPC(
|
||||||
appWithTranslation(
|
appWithTranslation(
|
||||||
MyApp,
|
MyApp,
|
||||||
// keep this in sync with next-i18next.config.js
|
// keep this in sync with next-i18next.config.js
|
||||||
// if you want to know why don't just import the config file, this because next-i18next.config.js must be a CJS, but the rest of the code is ESM.
|
// if you want to know why don't just import the config file, this because next-i18next.config.js must be a CJS, but the rest of the code is ESM.
|
||||||
// Add the config here is due to the issue: https://github.com/i18next/next-i18next/issues/2259
|
// Add the config here is due to the issue: https://github.com/i18next/next-i18next/issues/2259
|
||||||
// if one day every page is translated, we can safely remove this config.
|
// if one day every page is translated, we can safely remove this config.
|
||||||
{
|
{
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: "en",
|
defaultLocale: "en",
|
||||||
locales: Object.values(Languages),
|
locales: Object.values(Languages),
|
||||||
localeDetection: false,
|
localeDetection: false,
|
||||||
},
|
},
|
||||||
fallbackLng: "en",
|
fallbackLng: "en",
|
||||||
keySeparator: false,
|
keySeparator: false,
|
||||||
},
|
}
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user