Compare commits

..

20 Commits

Author SHA1 Message Date
Mauricio Siu
6a7da40ffe Merge branch 'canary' into feat/add-custom-dokploy-loader 2026-03-01 15:53:33 -06:00
Mauricio Siu
39b40c58bb Merge pull request #3838 from lklacar/fix/service-card-behavior
fix: Fixed service card behavior #3837
2026-03-01 15:38:51 -06:00
Mauricio Siu
1861e10b2a Merge pull request #3859 from Dokploy/3851-ui-3-issues-in-1
feat: enhance request logging display with formatted status and duration
2026-03-01 15:23:49 -06:00
Mauricio Siu
964e3c4150 feat: enhance request logging display with formatted status and duration
Added helper functions to format status labels and execution durations in the requests dashboard. Updated the display logic to show "N/A" for zero status and improved duration representation in microseconds and milliseconds. This enhances the clarity and usability of the request logs for better monitoring and analysis.
2026-03-01 15:11:41 -06:00
Mauricio Siu
e05f31d8c6 Merge pull request #3857 from Dokploy/feat/improve-queries
feat: enhance project and environment services with additional column…
2026-03-01 14:24:16 -06:00
Mauricio Siu
cc3b902d1e feat: include project name in API response columns
Added the 'name' column to the project API response structure to enhance the data returned for project queries. This change improves the clarity and usability of the API by ensuring that project names are included in the response, facilitating better data handling for clients.
2026-03-01 14:20:08 -06:00
Mauricio Siu
6c1f2372ed refactor: clean up project dashboard and API response structure
Removed unused imports and redundant code in the project dashboard component to enhance readability. Updated the API project router to streamline the data structure by eliminating unnecessary domain retrievals, while ensuring essential application and compose details are still included. This refactor improves maintainability and optimizes data handling for the project management interface.
2026-03-01 14:15:47 -06:00
Mauricio Siu
7da69862e1 refactor: update project query to use permissions-aware endpoint
Replaced the existing project query with the new `allForPermissions` endpoint to enhance data retrieval for server monitoring settings. This change aligns with recent API enhancements aimed at improving permissions management.
2026-03-01 14:07:16 -06:00
autofix-ci[bot]
612e73bb80 [autofix.ci] apply automated fixes 2026-03-01 20:02:48 +00:00
Mauricio Siu
a360a259f5 feat: add admin-only endpoint for project permissions with detailed environment data
Introduced a new API endpoint `allForPermissions` to retrieve projects along with their environments and services specifically for admin users. This enhancement allows for a more comprehensive permissions UI by including detailed information about each environment and its associated applications, improving the overall user experience in managing permissions.
2026-03-01 14:02:00 -06:00
Mauricio Siu
149293f4d3 feat: enhance mysql configuration with specific column selections
Updated the mysql configuration in the environment service to include specific column selections for the server object. This change improves data structure clarity and allows for more precise data handling in future queries.
2026-03-01 13:57:17 -06:00
Mauricio Siu
a8a5e1c6f1 refactor: remove unused environment property in duplicateEnvironment function
Eliminated the 'env' property from the duplicateEnvironment function to streamline the code and improve clarity. This change enhances maintainability by removing unnecessary parameters.
2026-03-01 13:47:38 -06:00
Mauricio Siu
4ede21eda9 feat: enhance project and environment services with additional column selections
Updated project and environment services to include specific column selections for various database entities. This improves data retrieval efficiency and allows for more granular control over the returned data structure. Added columns for application, mariadb, mongo, mysql, postgres, redis, and compose entities, as well as enhancements to the environment query structure.
2026-03-01 13:42:34 -06:00
Mauricio Siu
e275e9162e Merge pull request #3846 from Dokploy/feat/add-more-endpoints-for-search
feat: add search functionality across multiple routers with member ac…
2026-03-01 01:27:38 -06:00
autofix-ci[bot]
60a6dc5fab [autofix.ci] apply automated fixes 2026-03-01 07:15:20 +00:00
Mauricio Siu
705c5bc1c9 feat: add search functionality across multiple routers with member access control
Implemented a search feature in application, compose, environment, mariadb, mongo, mysql, postgres, project, and redis routers. Each search allows filtering by various parameters and respects user permissions based on their role. The search queries utilize optimized conditions for efficient data retrieval.
2026-03-01 01:14:46 -06:00
Luka Klacar
f9dedd979e fix: Fixed service card behavior #3837 2026-02-28 23:12:31 +01:00
Mauricio Siu
7a1703a191 fix(show-registry): correct loading state condition for WhaleLoader display
- Updated the ShowRegistry component to display the WhaleLoader when loading is in progress, improving user experience during data fetching.
2026-02-08 02:27:01 -06:00
Mauricio Siu
f6af5daf5e refactor(animation): update whale-loader animations and styles
- Removed the deprecated "whale-draw" animation from the Tailwind CSS configuration.
- Added a new "whale-draw" keyframe animation in globals.css to enhance the whale-loader's visual effect.
- Updated the WhaleLoader component to utilize the new animation and adjusted stroke properties for improved rendering.
2026-02-08 02:26:49 -06:00
Mauricio Siu
452b9a3c78 feat(loader): add WhaleLoader component and animation
- Introduced a new WhaleLoader component that displays a loading animation using the Dokploy whale logo.
- Updated the Tailwind CSS configuration to include a new animation for the whale loader.
- Replaced the existing loading indicator in the ShowRegistry component with the new WhaleLoader for improved visual consistency.
2026-02-08 02:19:05 -06:00
22 changed files with 1440 additions and 273 deletions

View File

@@ -25,7 +25,6 @@ import {
import { api } from "@/utils/api";
export type Services = {
appName: string;
serverId?: string | null;
name: string;
type:

View File

@@ -2,7 +2,6 @@ import {
AlertTriangle,
ArrowUpDown,
BookIcon,
ExternalLinkIcon,
FolderInput,
Loader2,
MoreHorizontalIcon,
@@ -16,7 +15,6 @@ import { toast } from "sonner";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import {
AlertDialog,
AlertDialogAction,
@@ -40,10 +38,8 @@ import {
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
@@ -280,14 +276,6 @@ export const ShowProjects = () => {
)
.reduce((acc, curr) => acc + curr, 0);
const haveServicesWithDomains = project?.environments
.map(
(env) =>
env.applications.length > 0 ||
env.compose.length > 0,
)
.some(Boolean);
// Find default environment from accessible environments, or fall back to first accessible environment
const accessibleEnvironment =
project?.environments.find((env) => env.isDefault) ||
@@ -313,122 +301,6 @@ export const ShowProjects = () => {
}}
>
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
{haveServicesWithDomains ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<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"
size="sm"
variant="default"
>
<ExternalLinkIcon className="size-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
onClick={(e) => e.stopPropagation()}
>
{project.environments.some(
(env) => env.applications.length > 0,
) && (
<DropdownMenuGroup>
<DropdownMenuLabel>
Applications
</DropdownMenuLabel>
{project.environments.map((env) =>
env.applications.map((app) => (
<div key={app.applicationId}>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
{app.name}
<StatusTooltip
status={
app.applicationStatus
}
/>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{app.domains.map((domain) => (
<DropdownMenuItem
key={domain.domainId}
asChild
>
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${
domain.https
? "https"
: "http"
}://${domain.host}${
domain.path
}`}
>
<span className="truncate">
{domain.host}
</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</div>
)),
)}
</DropdownMenuGroup>
)}
{project.environments.some(
(env) => env.compose.length > 0,
) && (
<DropdownMenuGroup>
<DropdownMenuLabel>
Compose
</DropdownMenuLabel>
{project.environments.map((env) =>
env.compose.map((comp) => (
<div key={comp.composeId}>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
{comp.name}
<StatusTooltip
status={comp.composeStatus}
/>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{comp.domains.map((domain) => (
<DropdownMenuItem
key={domain.domainId}
asChild
>
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${
domain.https
? "https"
: "http"
}://${domain.host}${
domain.path
}`}
>
<span className="truncate">
{domain.host}
</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</div>
)),
)}
</DropdownMenuGroup>
)}
</DropdownMenuContent>
</DropdownMenu>
) : null}
<CardHeader>
<CardTitle className="flex items-center justify-between gap-2 overflow-clip">
<span className="flex flex-col gap-1.5 ">

View File

@@ -6,6 +6,9 @@ import { Button } from "@/components/ui/button";
import type { LogEntry } from "./show-requests";
export const getStatusColor = (status: number) => {
if (status === 0) {
return "secondary";
}
if (status >= 100 && status < 200) {
return "outline";
}
@@ -21,6 +24,24 @@ export const getStatusColor = (status: number) => {
return "destructive";
};
const formatStatusLabel = (status: number) => {
if (status === 0) {
return "N/A";
}
return status;
};
const formatDuration = (nanos: number) => {
const ms = nanos / 1000000;
if (ms < 1) {
return `${(nanos / 1000).toFixed(2)} µs`;
}
if (ms < 1000) {
return `${ms.toFixed(2)} ms`;
}
return `${(ms / 1000).toFixed(2)} s`;
};
export const columns: ColumnDef<LogEntry>[] = [
{
accessorKey: "level",
@@ -59,10 +80,10 @@ export const columns: ColumnDef<LogEntry>[] = [
</div>
<div className="flex flex-row gap-3 w-full">
<Badge variant={getStatusColor(log.OriginStatus)}>
Status: {log.OriginStatus}
Status: {formatStatusLabel(log.OriginStatus)}
</Badge>
<Badge variant={"secondary"}>
Exec Time: {`${log.Duration / 1000000000}s`}
Exec Time: {formatDuration(log.Duration)}
</Badge>
<Badge variant={"secondary"}>IP: {log.ClientAddr}</Badge>
</div>

View File

@@ -152,7 +152,15 @@ export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
return JSON.stringify(value, null, 2);
}
if (key === "Duration" || key === "OriginDuration" || key === "Overhead") {
return `${value / 1000000000} s`;
const nanos = Number(value);
const ms = nanos / 1000000;
if (ms < 1) {
return `${(nanos / 1000).toFixed(2)} µs`;
}
if (ms < 1000) {
return `${ms.toFixed(2)} ms`;
}
return `${(ms / 1000).toFixed(2)} s`;
}
if (key === "level") {
return <Badge variant="secondary">{value}</Badge>;
@@ -161,7 +169,11 @@ export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
return <Badge variant="outline">{value}</Badge>;
}
if (key === "DownstreamStatus" || key === "OriginStatus") {
return <Badge variant={getStatusColor(value)}>{value}</Badge>;
const num = Number(value);
if (num === 0) {
return <Badge variant="secondary">N/A</Badge>;
}
return <Badge variant={getStatusColor(num)}>{value}</Badge>;
}
return value;
};

View File

@@ -1,6 +1,7 @@
import { Loader2, Package, Trash2 } from "lucide-react";
import { Package, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { DialogAction } from "@/components/shared/dialog-action";
import { WhaleLoader } from "@/components/shared/whale-loader";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -31,10 +32,9 @@ export const ShowRegistry = () => {
</CardDescription>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
{isPending ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
{isLoading ? (
<div className="flex flex-row items-center justify-center min-h-[25vh]">
<WhaleLoader />
</div>
) : (
<>

View File

@@ -100,7 +100,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
const url = useUrl();
const { data: projects } = api.project.all.useQuery();
const { data: projects } = api.project.allForPermissions.useQuery();
const extractServicesFromProjects = () => {
if (!projects) return [];

View File

@@ -28,8 +28,12 @@ import {
import { Switch } from "@/components/ui/switch";
import { api, type RouterOutputs } from "@/utils/api";
type Project = RouterOutputs["project"]["all"][number];
type Environment = Project["environments"][number];
/** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */
type ProjectForPermissions =
RouterOutputs["project"]["allForPermissions"][number];
type EnvironmentForPermissions = ProjectForPermissions["environments"][number];
type Environment = EnvironmentForPermissions;
export type Services = {
appName: string;
@@ -173,7 +177,9 @@ interface Props {
export const AddUserPermissions = ({ userId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data: projects } = api.project.all.useQuery();
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
enabled: isOpen,
});
const { data, refetch } = api.user.one.useQuery(
{

View File

@@ -0,0 +1,57 @@
import { cn } from "@/lib/utils";
const WHALE_PATHS = [
"M390 56v12c.1 2.3.5 4 1 6a73 73 0 0 0 12 24c2 2.3 5.7 4 7 7 4 3.4 9.6 6.8 14 9 1.7.6 5.7 1.1 7 2 1.9 1.3 2.9 2.3 0 4v1c-.6 1.8-1.9 3.5-3 5q-3 4-7 7c-4.3 3.2-9.5 6.8-15 7h-1q-2 1.6-5 2h-4c-5.2.7-12.9 2.2-18 0h-6c-1.6 0-3-.8-4-1h-3a17 17 0 0 1-6-2h-1c-2.5-.1-4-1.2-6-2l-4-1c-8.4-2-20.3-6.6-27-12h-1c-4.6-1-9.5-4.3-13.7-6.3s-10.5-3-13.3-6.7h-1c-4-1-8.9-3.5-12-6h-1c-6.8-1.6-13.6-6-20-9-6.5-2.8-14.6-5.7-20-10h-1c-7-1.2-15.4-4-22-6h-97c-5.3 4.3-13.7 4.3-18.7 10.3S90.8 101 88 108c-.4 1.5-.8 2.3-1 4-.2 1.6-.8 4-1 5v51c.2 1.2.8 3.2 1 5 .2 2 .5 3.2 1 5a79 79 0 0 0 6 12c.8.7 1.4 2.2 2 3 1.8 2 4.9 3.4 6 6 9.5 8.3 23.5 10.3 33 18h1c5.1 1.2 12 4.8 16 8h1c4 1 8.9 3.5 12 6h1q4.6 1.2 8 4h1c2 .1 2.6 1.3 4 2 1.6.8 2.7.7 4 2h1q2.5.3 4 2h1c3 .7 6.7 2 9 4h1c4.7.8 13.4 3.1 17 6h1c2.5.1 4 1.3 6 2 1.8.4 3 .8 5 1q3 .4 5 1c1.6-.2 2 0 3 1h1q2.5-.5 4 1h1q2.5-.5 4 1h1c2.2-.2 4.5-.3 6 1h1q4-.4 7 1h45c1.2-.2 3.1-1 5-1h6c1.5-.6 2.9-1.3 5-1h1q1.5-1.4 4-1h1q1.5-1.4 4-1h1c2.4-1.3 5-1.6 8-2l5-1c2-.7 3.6-1.6 6-2 4-.7 7.2-1.7 11-3 2.3-1 4.2-2.5 7-3h1q1.5-1.7 4-2h1c1.9-1.5 3.9-2 6-3q2.9-1.6 6-3a95 95 0 0 0 11-5c4.4-2.8 8.9-6 14-8 0 0 .6.2 1 0 1.8-2.8 7-4.8 10-6 0 0 .6.2 1 0 1.5-2.4 5.3-4 8-5 0 0 .6.2 1 0 1.5-2.4 5.3-4 8-5 0 0 .6.2 1 0 1.3-2 3.8-3.1 6-4 0 0 .6.2 1 0 2-3 7.7-5.6 11-7l5-2c6.3-3.8 11.8-9.6 18-14v-1c0-1.9-.4-4.2 0-6-1-4.5-3.9-5.5-7-8h-1c-1.2 0-2.8-.2-4 0-8.9 1.7-16.5 11.3-25.2 14.8-8.8 3.4-16.9 10.7-25.8 14.2h-1c-10.9 10.6-29.2 16-42.7 23.3S343.7 234.6 328 235h-1q-1.5 1.4-4 1h-1q-1.5 1.4-4 1h-1c-1.5 1.3-3.9 1.2-6 1h-1c-1.7 1.3-4.6 1.2-7 1-1 .2-2.4 1-4 1h-5c-6.6 0-13.4.4-20 0-1.9-.1-2.7.3-4-1h-8c-2.8-.2-5.7-1.3-8-2h-2q-5.7.4-10-2h-1q-4.5 0-8-2h-1a10 10 0 0 1-6-2h-1c-5.9-.2-12-3.8-17-6l-4-1c-1.7-.5-2.8-.7-4-2h-1q-2.5-.2-4-2h-1q-3.4-.9-6-3h-1c-3.5-.8-7.3-2.9-10-5h-1c-1.7 0-2.2-.7-3-2h-1c-11.6-2.7-23.2-11.5-34.2-15.8-11-4.2-25.9-9.2-29.8-21.2h4c16.2 0 32.8-1 49 0 1.7.1 3 .8 4 1 2.1.4 3.4-.5 5 1h1c3.6.1 8.4 1.8 11 4h1a45 45 0 0 1 18 8h1q4.6 1.2 8 4h1c4.2 1 8.3 3.4 12 5q3.4 1.2 7 2c5.7 1.3 13 2.3 18 5h1c3.7-.2 7 1.1 10 2h9c1.6 0 3 .8 4 1h32c2.2-1.6 6-1 9-1h1a63 63 0 0 1 22-4 22 22 0 0 1 8-2c1.7-1.4 3.7-1.6 6-2a81 81 0 0 0 12-3c2.3-1 4.2-2.5 7-3h1q1.5-1.7 4-2h1c1.9-1.5 3.6-2.2 6-3l3-1c4.1-2.3 8.4-5.2 13-7 0 0 .6.2 1 0 1.5-2.4 6.3-5 9-6 0 0 .6.2 1 0 5.3-8.1 17.6-12.5 24.8-20.2C439.9 144 445 133 452 126v-1a12 12 0 0 1 2-5c2.1-2.2 8.9-1 12-1q2 .2 4 0c1-.2 2.3-1.2 4-1h1q2.1-1.5 5-2h1q2.1-1.9 5-3s.6.2 1 0c9-9.3 18-15.4 23-28 1.1-2.8 3.5-6.4 4-9 .2-1 .2-3 0-4-1.5-6-12.3-2.4-15.7 2.3S484.7 80 479 80h-7c-7.8 4.3-19.3 5.7-23 16a37 37 0 0 0-22-24c-1.5-.5-2.5-.7-4-1-2.1-.5-3.6-.2-5-2h-1a22 22 0 0 1-12-8c-2-2.9-3.4-6.5-6-9h-1c-3.9-.6-6.1 1-8 4m-181 45h1c2.2-.2 4.5-.3 6 1h1q2.5-.5 4 1h1a33 33 0 0 1 17 7h1c4.4 1 8.2 4.1 12 6 2.1 1 4.1 1.5 6 3h1c4 1 8.9 3.5 12 6h1c4 1 8.9 3.5 12 6h1c4 1 8.9 3.5 12 6h1a61 61 0 0 1 21 10h1c3.5.8 7.3 2.9 10 5h1c6.1 1.4 12.3 5 18 7 1.8.4 3 .8 5 1 1.8.2 3.7.8 5 1q2.5-.5 4 1h6c2.5 0 4 .3 6 1h3q-.7 2.1-3 2a46 46 0 0 1-16 7l-10 3c-2 .8-3.4 1.9-6 2h-1c-2.6 2.1-7.5 3-11 3h-1c-3.1 2.5-10.7 3.5-15 3h-1c-1.5 1.3-3.9 1.2-6 1-1 .2-2.4 1-4 1h-11c-3.8.4-8.3.4-12 0h-9c-2.3 0-4.3-.7-6-1h-3c-1.8 0-2.9-.7-4-1-3.5-.8-7-.7-10-2h-1c-4.1-.7-9.8-1.4-13-4h-1q-4-.6-7-3h-1q-2.5-.2-4-2h-1q-3.4-.9-6-3h-1c-7.2-1.7-13.3-5.9-20.2-8.8-7-2.8-16.2-4.3-22.8-7.2h-11c-14 0-28.9.3-42-1-2.3 0-4.8.3-7 0a6 6 0 0 1-5-5c-1.8-4.8-.4-10.4 0-15 0-4.3-.4-8.7 0-13 .2-3.2 2.2-7.3 4-10q2-3 5-5c2.1-2 5.4-2.3 8-3 15.6-3.9 36.3-1 53-1 5.2 0 12-.5 17 0s12.2-1.8 16 1Z",
"M162 132v1c1.8 2.9 4.5 5.3 8 6 .3-.2 3.7-.2 4 0 7-1.4 9.2-8.8 7-15v-1a14 14 0 0 0-7-4c-.3.2-3.7.2-4 0-6.5 1.3-8.6 6.8-8 13Z",
"M465 211h-1c-18.2 14.6-41.2 24.6-60 39-19 14.2-42.7 29.3-66 34l-4 1c-2.4 1-4 2-7 2h-1q-3.5 2-8 2h-1c-1.3 1.2-3 1.1-5 1h-2q-2.6 1.1-6 1h-2c-3 1.2-6.5 1-10 1-6.3.6-13.8.6-20 0-3.4 0-8.4.9-11-1h-1c-2.2.2-4.5.3-6-1h-1c-2 .2-3.7.2-5-1h-1c-7.6.5-16.5-3.4-23-6l-4-1a129 129 0 0 1-36.2-15.8c-10.4-6.6-23.2-12.8-32.5-20.5-9.2-7.7-23.8-12.8-30.3-22.7h-1c-2.3-1.4-4.5-2.7-6-5h-1c-4-2.5-8.5-5.2-12-8h-9a9 9 0 0 0-6 7c.3 3.3 0 6.7 0 10v9c.2 1.6 1 3.8 1 6v3c.2 1 1.2 2.2 1 4v1c1.2 1.2.8 2.2 1 4 .8 6.7 3 12.6 5 19 1.7 4.3 4.2 9.1 5 14v1q1.8 1.5 2 4v1a36 36 0 0 1 5 10c.7 2 1 3 2 5 8 12.7 15.7 25.5 25.8 37.3 10 11.7 20.8 20.6 32.4 30.4 11.7 9.9 28.3 14 39.8 23.3h1q2.5.3 4 2h1c2.8.4 4.8 2 7 3l7 2c5.7 1.3 13 2.3 18 5h1c2.1-.3 3.6.8 5 1h3c2.8.2 5.8 1 8 2h8c2.1 0 4.6.8 6 1h21c1.2-.2 3.2-1 5-1h9c3.3-1 7-2.4 11-2h1c2.7-2.2 7.4-2.4 11-3a55 55 0 0 0 8-2c6.5-2.6 13.9-6.3 21-8h1c8.5-6.8 20.6-9.7 29.2-16.8 8.7-7 18.3-12.8 26.8-20.2 4.4-3.8 9-9 13-13 14.8-14.8 20.7-34.6 33-50v-1q.9-3.4 3-6v-1q.3-2.5 2-4v-1c.5-3.3 2-8.6 4-11v-1q0-3.5 2-6v-1c1.1-6.7 2.4-15 5-21v-1c-.2-2-.2-3.7 1-5v-8c0-5.3-.5-10.8 0-16a14 14 0 0 0-4-6c-1-.5-1.1-.4-2-1h-6q-2.1 1.5-5 2m-6 38c-2.1 13.4-21.2 20.3-31 30-10 9.5-23.7 19-35 27-11.5 8-25.1 19.7-39 23h-1a22 22 0 0 1-10 4h-1a25 25 0 0 1-12 4h-1q-3.5 2-8 2h-1c-1.1 1.1-2.3 1-4 1h-2c-1.2.4-2.2 1-4 1h-2c-1.8.7-3.6 1.3-6 1h-1c-1.2 1.2-2.3 1-4 1h-5c-5.7.6-12.3.8-18 0h-4c-1.9 0-2.7-.6-4-1h-6c-1.9 0-2.7.3-4-1h-1q-2.5.5-4-1h-1c-8.1.5-16.8-3.6-24.2-5.8S210 329.8 204 325h-1c-12.8-5-27.1-15.6-37.7-24.3S138.8 284.2 131 273c-.3-.2-1 0-1 0-5.7-4.4-16.6-10-19-17-.9-2.6-1-5.4-2-8-.8-2.2-2.5-5-2-8a667 667 0 0 0 88 56h1q3.4.9 6 3h1c2.8.4 4.8 2 7 3q5 1.8 10 3l6 2q2.9.6 6 1 3 .4 5 1c1.6-.2 2 0 3 1h1c2-.2 3.7-.2 5 1h1c2.2-.3 3.4.4 5 1h8c1.6 0 3 .9 4 1h40c1.8-1.3 4.6-1.2 7-1h1c1.2-1.2 3.2-1.2 5-1h1c1.2-1.2 3.2-1.2 5-1h1c1.1-1.1 2.3-1 4-1h2c3.5-1.7 6.9-2.3 11-3l4-1c3.4-1.4 7.1-3 11-4 1.5-.4 2.5-.5 4-1 1.4-.7 2-1.9 4-2h1q2.6-2.1 6-3h1c2.5-2 6-3.8 9-5l3-1c1.4-.9 2-2.5 4-3h1q1.4-2.2 4-3h1c7.3-7.7 19-13.2 27.7-19.3 8.8-6.1 18.2-15 28.3-18.7.4-.2 1 0 1 0q3.8-3.9 9-6c1.3 2.5-.5 6.7-1 10m-20 55c-.2.4 0 1 0 1-3.4 9.6-12.7 19-19 27a88 88 0 0 1-12 12 214 214 0 0 1-26.7 20.3c-9.5 5.8-20 14.8-31.3 16.7h-1a22 22 0 0 1-10 4h-1c-3.2 2.6-8.9 3.3-13 4h-1q-1.5 1.4-4 1h-1q-1.5 1.4-4 1h-1c-4.9 2.3-10.5 1-16 2-1 .2-2.5 1-4 1-6.2.4-12.8.3-19 0-1.8 0-3.8-.8-5-1h-4c-1.6 0-3-.9-4-1h-4c-3.9-.3-8.8-1.3-12-3h-1c-3.3-.5-7.5-1-10-3h-1c-3.6-.1-8.4-1.8-11-4h-1c-3.9-.6-8-2.6-11-5h-1c-16.1-3.8-32.2-18.9-45-29a200 200 0 0 1-40-51c17.7 11.5 35 25.5 52 38h1c4 1.6 12.8 5.4 15 9h1c4.6 1 10.4 4.1 14 7h1q2.5.3 4 2h1c3.3.5 8.6 2 11 4h1q3.5 0 6 2h1q2.5-.5 4 1h1q2.5-.5 4 1h1c3.8-.2 7.9 1 11 2h9c1.6 0 3 .8 4 1h32c1.2-.2 3.2-1 5-1h8a139 139 0 0 1 20-4l5-1c2-.7 3.7-1.5 6-2l4-1c1.5-.6 3-1.7 5-2h1q3-2.4 7-3h1q2.6-2.1 6-3h1c11.7-9.4 27.6-14.6 39-25 11.6-10.3 25-18.5 37-28a15 15 0 0 1-5 10Z",
] as const;
interface WhaleLoaderProps {
className?: string;
}
/** Loader using the Dokploy whale logo: draws the whale gradually and bobs up/down like a tide. */
export const WhaleLoader = ({ className }: WhaleLoaderProps) => {
return (
<div
className={cn(
"flex flex-col items-center justify-center gap-3",
className,
)}
aria-label="Loading"
>
{/* animate-whale-tide */}
<div className="">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 559 446"
className="size-20 text-primary"
aria-hidden
>
<g
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
{WHALE_PATHS.map((d, i) => (
<path
key={i}
pathLength={1}
strokeDasharray={1}
strokeDashoffset={1}
className="animate-whale-draw"
style={{
animationDelay: `${i * 0.25}s`,
}}
d={d}
/>
))}
</g>
</svg>
</div>
<span className="text-sm text-muted-foreground">Loading...</span>
</div>
);
};

View File

@@ -13,7 +13,7 @@ const Command = React.forwardRef<
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground p-px",
className,
)}
{...props}
@@ -44,7 +44,7 @@ const CommandInput = React.forwardRef<
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 focus-visible:ring-inset",
className,
)}
{...props}

View File

@@ -23,7 +23,7 @@ import type {
InferGetServerSidePropsType,
} from "next";
import Head from "next/head";
import { useRouter } from "next/router";
import Link from "next/link";
import { type ReactElement, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
@@ -100,7 +100,6 @@ import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
export type Services = {
appName: string;
serverId?: string | null;
serverName?: string | null;
name: string;
@@ -146,7 +145,6 @@ export const extractServicesFromEnvironment = (
}
}
return {
appName: item.appName,
name: item.name,
type: "application",
id: item.applicationId,
@@ -161,7 +159,6 @@ export const extractServicesFromEnvironment = (
const mariadb: Services[] =
environment.mariadb?.map((item) => ({
appName: item.appName,
name: item.name,
type: "mariadb",
id: item.mariadbId,
@@ -174,7 +171,6 @@ export const extractServicesFromEnvironment = (
const postgres: Services[] =
environment.postgres?.map((item) => ({
appName: item.appName,
name: item.name,
type: "postgres",
id: item.postgresId,
@@ -187,7 +183,6 @@ export const extractServicesFromEnvironment = (
const mongo: Services[] =
environment.mongo?.map((item) => ({
appName: item.appName,
name: item.name,
type: "mongo",
id: item.mongoId,
@@ -200,7 +195,6 @@ export const extractServicesFromEnvironment = (
const redis: Services[] =
environment.redis?.map((item) => ({
appName: item.appName,
name: item.name,
type: "redis",
id: item.redisId,
@@ -213,7 +207,6 @@ export const extractServicesFromEnvironment = (
const mysql: Services[] =
environment.mysql?.map((item) => ({
appName: item.appName,
name: item.name,
type: "mysql",
id: item.mysqlId,
@@ -242,7 +235,6 @@ export const extractServicesFromEnvironment = (
}
}
return {
appName: item.appName,
name: item.name,
type: "compose",
id: item.composeId,
@@ -366,7 +358,6 @@ const EnvironmentPage = (
environmentId,
});
const { data: allProjects } = api.project.all.useQuery();
const router = useRouter();
const [isMoveDialogOpen, setIsMoveDialogOpen] = useState(false);
const [selectedTargetProject, setSelectedTargetProject] =
@@ -420,6 +411,7 @@ const EnvironmentPage = (
};
const handleServiceSelect = (serviceId: string, event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
setSelectedServices((prev) =>
prev.includes(serviceId)
@@ -1471,101 +1463,99 @@ const EnvironmentPage = (
<div className="flex w-full flex-col gap-4">
<div className="gap-5 pb-10 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
{filteredServices?.map((service) => (
<Card
<Link
key={service.id}
onClick={() => {
router.push(
`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`,
);
}}
className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border"
href={`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`}
className="block"
>
{service.serverId && (
<div className="absolute -left-1 -top-2">
<ServerIcon className="size-4 text-muted-foreground" />
</div>
)}
<div className="absolute -right-1 -top-2">
<StatusTooltip status={service.status} />
</div>
<div
className={cn(
"absolute -left-3 -bottom-3 size-9 translate-y-1 rounded-full p-0 transition-all duration-200 z-10 bg-background border",
selectedServices.includes(service.id)
? "opacity-100 translate-y-0"
: "opacity-0 group-hover:translate-y-0 group-hover:opacity-100",
)}
onClick={(e) =>
handleServiceSelect(service.id, e)
}
>
<div className="h-full w-full flex items-center justify-center">
<Checkbox
checked={selectedServices.includes(
service.id,
)}
className="data-[state=checked]:bg-primary"
/>
</div>
</div>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex flex-row items-center gap-2 justify-between w-full">
<div className="flex flex-col gap-2">
<span className="text-base flex items-center gap-2 font-medium leading-none flex-wrap">
{service.name}
</span>
{service.description && (
<span className="text-sm font-medium text-muted-foreground">
{service.description}
</span>
)}
</div>
<span className="text-sm font-medium text-muted-foreground self-start">
{service.type === "postgres" && (
<PostgresqlIcon className="h-7 w-7" />
)}
{service.type === "redis" && (
<RedisIcon className="h-7 w-7" />
)}
{service.type === "mariadb" && (
<MariadbIcon className="h-7 w-7" />
)}
{service.type === "mongo" && (
<MongodbIcon className="h-7 w-7" />
)}
{service.type === "mysql" && (
<MysqlIcon className="h-7 w-7" />
)}
{service.type === "application" && (
<GlobeIcon className="h-6 w-6" />
)}
{service.type === "compose" && (
<CircuitBoard className="h-6 w-6" />
)}
</span>
<Card className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border">
{service.serverId && (
<div className="absolute -left-1 -top-2">
<ServerIcon className="size-4 text-muted-foreground" />
</div>
</CardTitle>
</CardHeader>
<CardFooter className="mt-auto">
<div className="space-y-1 text-sm w-full">
{service.serverName && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1">
<ServerIcon className="size-3" />
<span className="truncate">
{service.serverName}
)}
<div className="absolute -right-1 -top-2">
<StatusTooltip status={service.status} />
</div>
<div
className={cn(
"absolute -left-3 -bottom-3 size-9 translate-y-1 rounded-full p-0 transition-all duration-200 z-10 bg-background border",
selectedServices.includes(service.id)
? "opacity-100 translate-y-0"
: "opacity-0 group-hover:translate-y-0 group-hover:opacity-100",
)}
onClick={(e) =>
handleServiceSelect(service.id, e)
}
>
<div className="h-full w-full flex items-center justify-center">
<Checkbox
checked={selectedServices.includes(
service.id,
)}
className="data-[state=checked]:bg-primary"
/>
</div>
</div>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex flex-row items-center gap-2 justify-between w-full">
<div className="flex flex-col gap-2">
<span className="text-base flex items-center gap-2 font-medium leading-none flex-wrap">
{service.name}
</span>
{service.description && (
<span className="text-sm font-medium text-muted-foreground">
{service.description}
</span>
)}
</div>
<span className="text-sm font-medium text-muted-foreground self-start">
{service.type === "postgres" && (
<PostgresqlIcon className="h-7 w-7" />
)}
{service.type === "redis" && (
<RedisIcon className="h-7 w-7" />
)}
{service.type === "mariadb" && (
<MariadbIcon className="h-7 w-7" />
)}
{service.type === "mongo" && (
<MongodbIcon className="h-7 w-7" />
)}
{service.type === "mysql" && (
<MysqlIcon className="h-7 w-7" />
)}
{service.type === "application" && (
<GlobeIcon className="h-6 w-6" />
)}
{service.type === "compose" && (
<CircuitBoard className="h-6 w-6" />
)}
</span>
</div>
)}
<DateTooltip date={service.createdAt}>
Created
</DateTooltip>
</div>
</CardFooter>
</Card>
</CardTitle>
</CardHeader>
<CardFooter className="mt-auto">
<div className="space-y-1 text-sm w-full">
{service.serverName && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1">
<ServerIcon className="size-3" />
<span className="truncate">
{service.serverName}
</span>
</div>
)}
<DateTooltip date={service.createdAt}>
Created
</DateTooltip>
</div>
</CardFooter>
</Card>
</Link>
))}
</div>
</div>

View File

@@ -7,6 +7,7 @@ import {
findApplicationById,
findEnvironmentById,
findGitProviderById,
findMemberById,
findProjectById,
getApplicationStats,
IS_CLOUD,
@@ -32,7 +33,7 @@ import {
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { nanoid } from "nanoid";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
@@ -53,6 +54,8 @@ import {
apiSaveGitProvider,
apiUpdateApplication,
applications,
environments,
projects,
} from "@/server/db/schema";
import { deploymentWorker } from "@/server/queues/deployments-queue";
import type { DeploymentJob } from "@/server/queues/queue-types";
@@ -1002,4 +1005,138 @@ export const applicationRouter = createTRPCRouter({
message: "Deployment cancellation only available in cloud version",
});
}),
search: protectedProcedure
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
appName: z.string().optional(),
description: z.string().optional(),
repository: z.string().optional(),
owner: z.string().optional(),
dockerImage: z.string().optional(),
projectId: z.string().optional(),
environmentId: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.projectId) {
baseConditions.push(eq(environments.projectId, input.projectId));
}
if (input.environmentId) {
baseConditions.push(
eq(applications.environmentId, input.environmentId),
);
}
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(applications.name, term),
ilike(applications.appName, term),
ilike(applications.description ?? "", term),
ilike(applications.repository ?? "", term),
ilike(applications.owner ?? "", term),
ilike(applications.dockerImage ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(applications.name, `%${input.name.trim()}%`));
}
if (input.appName?.trim()) {
baseConditions.push(
ilike(applications.appName, `%${input.appName.trim()}%`),
);
}
if (input.description?.trim()) {
baseConditions.push(
ilike(
applications.description ?? "",
`%${input.description.trim()}%`,
),
);
}
if (input.repository?.trim()) {
baseConditions.push(
ilike(applications.repository ?? "", `%${input.repository.trim()}%`),
);
}
if (input.owner?.trim()) {
baseConditions.push(
ilike(applications.owner ?? "", `%${input.owner.trim()}%`),
);
}
if (input.dockerImage?.trim()) {
baseConditions.push(
ilike(
applications.dockerImage ?? "",
`%${input.dockerImage.trim()}%`,
),
);
}
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${applications.applicationId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db
.select({
applicationId: applications.applicationId,
name: applications.name,
appName: applications.appName,
description: applications.description,
environmentId: applications.environmentId,
applicationStatus: applications.applicationStatus,
sourceType: applications.sourceType,
createdAt: applications.createdAt,
})
.from(applications)
.innerJoin(
environments,
eq(applications.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where)
.orderBy(desc(applications.createdAt))
.limit(input.limit)
.offset(input.offset),
db
.select({ count: sql<number>`count(*)::int` })
.from(applications)
.innerJoin(
environments,
eq(applications.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return {
items,
total: countResult[0]?.count ?? 0,
};
}),
});

View File

@@ -16,6 +16,7 @@ import {
findDomainsByComposeId,
findEnvironmentById,
findGitProviderById,
findMemberById,
findProjectById,
findServerById,
getComposeContainer,
@@ -41,7 +42,7 @@ import {
} from "@dokploy/server/templates/github";
import { processTemplate } from "@dokploy/server/templates/processors";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import _ from "lodash";
import { nanoid } from "nanoid";
import { parse } from "toml";
@@ -58,6 +59,8 @@ import {
apiRedeployCompose,
apiUpdateCompose,
compose as composeTable,
environments,
projects,
} from "@/server/db/schema";
import { deploymentWorker } from "@/server/queues/deployments-queue";
import type { DeploymentJob } from "@/server/queues/queue-types";
@@ -1054,4 +1057,114 @@ export const composeRouter = createTRPCRouter({
message: "Deployment cancellation only available in cloud version",
});
}),
search: protectedProcedure
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
appName: z.string().optional(),
description: z.string().optional(),
projectId: z.string().optional(),
environmentId: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.projectId) {
baseConditions.push(eq(environments.projectId, input.projectId));
}
if (input.environmentId) {
baseConditions.push(
eq(composeTable.environmentId, input.environmentId),
);
}
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(composeTable.name, term),
ilike(composeTable.appName, term),
ilike(composeTable.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(composeTable.name, `%${input.name.trim()}%`));
}
if (input.appName?.trim()) {
baseConditions.push(
ilike(composeTable.appName, `%${input.appName.trim()}%`),
);
}
if (input.description?.trim()) {
baseConditions.push(
ilike(
composeTable.description ?? "",
`%${input.description.trim()}%`,
),
);
}
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${composeTable.composeId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db
.select({
composeId: composeTable.composeId,
name: composeTable.name,
appName: composeTable.appName,
description: composeTable.description,
environmentId: composeTable.environmentId,
composeStatus: composeTable.composeStatus,
sourceType: composeTable.sourceType,
createdAt: composeTable.createdAt,
})
.from(composeTable)
.innerJoin(
environments,
eq(composeTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where)
.orderBy(desc(composeTable.createdAt))
.limit(input.limit)
.offset(input.offset),
db
.select({ count: sql<number>`count(*)::int` })
.from(composeTable)
.innerJoin(
environments,
eq(composeTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return {
items,
total: countResult[0]?.count ?? 0,
};
}),
});

View File

@@ -11,7 +11,9 @@ import {
findMemberById,
updateEnvironmentById,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
@@ -21,6 +23,7 @@ import {
apiRemoveEnvironment,
apiUpdateEnvironment,
} from "@/server/db/schema";
import { environments, projects } from "@/server/db/schema";
// Helper function to filter services within an environment based on user permissions
const filterEnvironmentServices = (
@@ -358,4 +361,92 @@ export const environmentRouter = createTRPCRouter({
});
}
}),
search: protectedProcedure
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
description: z.string().optional(),
projectId: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.projectId) {
baseConditions.push(eq(environments.projectId, input.projectId));
}
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(environments.name, term),
ilike(environments.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(environments.name, `%${input.name.trim()}%`));
}
if (input.description?.trim()) {
baseConditions.push(
ilike(
environments.description ?? "",
`%${input.description.trim()}%`,
),
);
}
if (ctx.user.role === "member") {
const { accessedEnvironments } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedEnvironments.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${environments.environmentId} IN (${sql.join(
accessedEnvironments.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db
.select({
environmentId: environments.environmentId,
name: environments.name,
description: environments.description,
createdAt: environments.createdAt,
env: environments.env,
projectId: environments.projectId,
isDefault: environments.isDefault,
})
.from(environments)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where)
.orderBy(desc(environments.createdAt))
.limit(input.limit)
.offset(input.offset),
db
.select({ count: sql<number>`count(*)::int` })
.from(environments)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return {
items,
total: countResult[0]?.count ?? 0,
};
}),
});

View File

@@ -8,6 +8,7 @@ import {
findBackupsByDbId,
findEnvironmentById,
findMariadbById,
findMemberById,
findProjectById,
IS_CLOUD,
rebuildDatabase,
@@ -22,7 +23,7 @@ import {
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
@@ -37,6 +38,7 @@ import {
apiUpdateMariaDB,
mariadb as mariadbTable,
} from "@/server/db/schema";
import { environments, projects } from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
export const mariadbRouter = createTRPCRouter({
create: protectedProcedure
@@ -446,4 +448,102 @@ export const mariadbRouter = createTRPCRouter({
await rebuildDatabase(mariadb.mariadbId, "mariadb");
return true;
}),
search: protectedProcedure
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
appName: z.string().optional(),
description: z.string().optional(),
projectId: z.string().optional(),
environmentId: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.projectId) {
baseConditions.push(eq(environments.projectId, input.projectId));
}
if (input.environmentId) {
baseConditions.push(
eq(mariadbTable.environmentId, input.environmentId),
);
}
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(mariadbTable.name, term),
ilike(mariadbTable.appName, term),
ilike(mariadbTable.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(mariadbTable.name, `%${input.name.trim()}%`));
}
if (input.appName?.trim()) {
baseConditions.push(
ilike(mariadbTable.appName, `%${input.appName.trim()}%`),
);
}
if (input.description?.trim()) {
baseConditions.push(
ilike(
mariadbTable.description ?? "",
`%${input.description.trim()}%`,
),
);
}
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${mariadbTable.mariadbId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db
.select({
mariadbId: mariadbTable.mariadbId,
name: mariadbTable.name,
appName: mariadbTable.appName,
description: mariadbTable.description,
environmentId: mariadbTable.environmentId,
applicationStatus: mariadbTable.applicationStatus,
createdAt: mariadbTable.createdAt,
})
.from(mariadbTable)
.innerJoin(
environments,
eq(mariadbTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where)
.orderBy(desc(mariadbTable.createdAt))
.limit(input.limit)
.offset(input.offset),
db
.select({ count: sql<number>`count(*)::int` })
.from(mariadbTable)
.innerJoin(
environments,
eq(mariadbTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return { items, total: countResult[0]?.count ?? 0 };
}),
});

View File

@@ -7,6 +7,7 @@ import {
deployMongo,
findBackupsByDbId,
findEnvironmentById,
findMemberById,
findMongoById,
findProjectById,
IS_CLOUD,
@@ -21,7 +22,7 @@ import {
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
@@ -36,6 +37,7 @@ import {
apiUpdateMongo,
mongo as mongoTable,
} from "@/server/db/schema";
import { environments, projects } from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
export const mongoRouter = createTRPCRouter({
create: protectedProcedure
@@ -476,4 +478,97 @@ export const mongoRouter = createTRPCRouter({
return true;
}),
search: protectedProcedure
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
appName: z.string().optional(),
description: z.string().optional(),
projectId: z.string().optional(),
environmentId: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.projectId) {
baseConditions.push(eq(environments.projectId, input.projectId));
}
if (input.environmentId) {
baseConditions.push(eq(mongoTable.environmentId, input.environmentId));
}
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(mongoTable.name, term),
ilike(mongoTable.appName, term),
ilike(mongoTable.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(mongoTable.name, `%${input.name.trim()}%`));
}
if (input.appName?.trim()) {
baseConditions.push(
ilike(mongoTable.appName, `%${input.appName.trim()}%`),
);
}
if (input.description?.trim()) {
baseConditions.push(
ilike(mongoTable.description ?? "", `%${input.description.trim()}%`),
);
}
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${mongoTable.mongoId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db
.select({
mongoId: mongoTable.mongoId,
name: mongoTable.name,
appName: mongoTable.appName,
description: mongoTable.description,
environmentId: mongoTable.environmentId,
applicationStatus: mongoTable.applicationStatus,
createdAt: mongoTable.createdAt,
})
.from(mongoTable)
.innerJoin(
environments,
eq(mongoTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where)
.orderBy(desc(mongoTable.createdAt))
.limit(input.limit)
.offset(input.offset),
db
.select({ count: sql<number>`count(*)::int` })
.from(mongoTable)
.innerJoin(
environments,
eq(mongoTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return { items, total: countResult[0]?.count ?? 0 };
}),
});

View File

@@ -7,6 +7,7 @@ import {
deployMySql,
findBackupsByDbId,
findEnvironmentById,
findMemberById,
findMySqlById,
findProjectById,
IS_CLOUD,
@@ -21,7 +22,7 @@ import {
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
@@ -34,7 +35,9 @@ import {
apiSaveEnvironmentVariablesMySql,
apiSaveExternalPortMySql,
apiUpdateMySql,
environments,
mysql as mysqlTable,
projects,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
@@ -471,4 +474,97 @@ export const mysqlRouter = createTRPCRouter({
return true;
}),
search: protectedProcedure
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
appName: z.string().optional(),
description: z.string().optional(),
projectId: z.string().optional(),
environmentId: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.projectId) {
baseConditions.push(eq(environments.projectId, input.projectId));
}
if (input.environmentId) {
baseConditions.push(eq(mysqlTable.environmentId, input.environmentId));
}
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(mysqlTable.name, term),
ilike(mysqlTable.appName, term),
ilike(mysqlTable.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(mysqlTable.name, `%${input.name.trim()}%`));
}
if (input.appName?.trim()) {
baseConditions.push(
ilike(mysqlTable.appName, `%${input.appName.trim()}%`),
);
}
if (input.description?.trim()) {
baseConditions.push(
ilike(mysqlTable.description ?? "", `%${input.description.trim()}%`),
);
}
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${mysqlTable.mysqlId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db
.select({
mysqlId: mysqlTable.mysqlId,
name: mysqlTable.name,
appName: mysqlTable.appName,
description: mysqlTable.description,
environmentId: mysqlTable.environmentId,
applicationStatus: mysqlTable.applicationStatus,
createdAt: mysqlTable.createdAt,
})
.from(mysqlTable)
.innerJoin(
environments,
eq(mysqlTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where)
.orderBy(desc(mysqlTable.createdAt))
.limit(input.limit)
.offset(input.offset),
db
.select({ count: sql<number>`count(*)::int` })
.from(mysqlTable)
.innerJoin(
environments,
eq(mysqlTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return { items, total: countResult[0]?.count ?? 0 };
}),
});

View File

@@ -7,6 +7,7 @@ import {
deployPostgres,
findBackupsByDbId,
findEnvironmentById,
findMemberById,
findPostgresById,
findProjectById,
getMountPath,
@@ -22,7 +23,7 @@ import {
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
@@ -37,6 +38,7 @@ import {
apiUpdatePostgres,
postgres as postgresTable,
} from "@/server/db/schema";
import { environments, projects } from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
export const postgresRouter = createTRPCRouter({
@@ -483,4 +485,104 @@ export const postgresRouter = createTRPCRouter({
return true;
}),
search: protectedProcedure
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
appName: z.string().optional(),
description: z.string().optional(),
projectId: z.string().optional(),
environmentId: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.projectId) {
baseConditions.push(eq(environments.projectId, input.projectId));
}
if (input.environmentId) {
baseConditions.push(
eq(postgresTable.environmentId, input.environmentId),
);
}
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(postgresTable.name, term),
ilike(postgresTable.appName, term),
ilike(postgresTable.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(
ilike(postgresTable.name, `%${input.name.trim()}%`),
);
}
if (input.appName?.trim()) {
baseConditions.push(
ilike(postgresTable.appName, `%${input.appName.trim()}%`),
);
}
if (input.description?.trim()) {
baseConditions.push(
ilike(
postgresTable.description ?? "",
`%${input.description.trim()}%`,
),
);
}
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${postgresTable.postgresId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db
.select({
postgresId: postgresTable.postgresId,
name: postgresTable.name,
appName: postgresTable.appName,
description: postgresTable.description,
environmentId: postgresTable.environmentId,
applicationStatus: postgresTable.applicationStatus,
createdAt: postgresTable.createdAt,
})
.from(postgresTable)
.innerJoin(
environments,
eq(postgresTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where)
.orderBy(desc(postgresTable.createdAt))
.limit(input.limit)
.offset(input.offset),
db
.select({ count: sql<number>`count(*)::int` })
.from(postgresTable)
.innerJoin(
environments,
eq(postgresTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return { items, total: countResult[0]?.count ?? 0 };
}),
});

View File

@@ -34,10 +34,14 @@ import {
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, sql } from "drizzle-orm";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import type { AnyPgColumn } from "drizzle-orm/pg-core";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
} from "@/server/api/trpc";
import {
apiCreateProject,
apiFindOneProject,
@@ -219,31 +223,69 @@ export const projectRouter = createTRPCRouter({
applications.applicationId,
accessedServices,
),
with: { domains: true },
columns: {
applicationId: true,
name: true,
applicationStatus: true,
},
},
mariadb: {
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
columns: {
mariadbId: true,
name: true,
applicationStatus: true,
},
},
mongo: {
where: buildServiceFilter(mongo.mongoId, accessedServices),
columns: {
mongoId: true,
name: true,
applicationStatus: true,
},
},
mysql: {
where: buildServiceFilter(mysql.mysqlId, accessedServices),
columns: {
mysqlId: true,
name: true,
applicationStatus: true,
},
},
postgres: {
where: buildServiceFilter(
postgres.postgresId,
accessedServices,
),
columns: {
postgresId: true,
name: true,
applicationStatus: true,
},
},
redis: {
where: buildServiceFilter(redis.redisId, accessedServices),
columns: {
redisId: true,
name: true,
applicationStatus: true,
},
},
compose: {
where: buildServiceFilter(compose.composeId, accessedServices),
with: { domains: true },
columns: {
composeId: true,
name: true,
composeStatus: true,
},
},
},
columns: {
environmentId: true,
isDefault: true,
name: true,
},
},
},
orderBy: desc(projects.createdAt),
@@ -255,21 +297,50 @@ export const projectRouter = createTRPCRouter({
environments: {
with: {
applications: {
with: {
domains: true,
columns: {
applicationId: true,
name: true,
applicationStatus: true,
},
},
mariadb: {
columns: {
mariadbId: true,
},
},
mongo: {
columns: {
mongoId: true,
},
},
mysql: {
columns: {
mysqlId: true,
},
},
postgres: {
columns: {
postgresId: true,
},
},
redis: {
columns: {
redisId: true,
},
},
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
compose: {
with: {
domains: true,
columns: {
composeId: true,
name: true,
composeStatus: true,
},
},
},
columns: {
name: true,
environmentId: true,
isDefault: true,
},
},
},
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
@@ -277,6 +348,183 @@ export const projectRouter = createTRPCRouter({
});
}),
/** All projects with full environments and services for the admin permissions UI. Admin only. */
allForPermissions: adminProcedure.query(async ({ ctx }) => {
return await db.query.projects.findMany({
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
orderBy: desc(projects.createdAt),
columns: {
projectId: true,
name: true,
},
with: {
environments: {
columns: {
environmentId: true,
name: true,
isDefault: true,
},
with: {
applications: {
columns: {
applicationId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
mariadb: {
columns: {
mariadbId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
postgres: {
columns: {
postgresId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
mysql: {
columns: {
mysqlId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
mongo: {
columns: {
mongoId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
redis: {
columns: {
redisId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
compose: {
columns: {
composeId: true,
appName: true,
name: true,
createdAt: true,
composeStatus: true,
description: true,
serverId: true,
},
},
},
},
},
});
}),
search: protectedProcedure
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
description: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(projects.name, term),
ilike(projects.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(projects.name, `%${input.name.trim()}%`));
}
if (input.description?.trim()) {
baseConditions.push(
ilike(projects.description ?? "", `%${input.description.trim()}%`),
);
}
if (ctx.user.role === "member") {
const { accessedProjects } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedProjects.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${projects.projectId} IN (${sql.join(
accessedProjects.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db.query.projects.findMany({
where,
limit: input.limit,
offset: input.offset,
orderBy: desc(projects.createdAt),
columns: {
projectId: true,
name: true,
description: true,
createdAt: true,
organizationId: true,
env: true,
},
}),
db
.select({ count: sql<number>`count(*)::int` })
.from(projects)
.where(where),
]);
return {
items,
total: countResult[0]?.count ?? 0,
};
}),
remove: protectedProcedure
.input(apiRemoveProject)
.mutation(async ({ input, ctx }) => {

View File

@@ -6,6 +6,7 @@ import {
createRedis,
deployRedis,
findEnvironmentById,
findMemberById,
findProjectById,
findRedisById,
IS_CLOUD,
@@ -20,7 +21,7 @@ import {
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
@@ -35,6 +36,7 @@ import {
apiUpdateRedis,
redis as redisTable,
} from "@/server/db/schema";
import { environments, projects } from "@/server/db/schema";
export const redisRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateRedis)
@@ -450,4 +452,97 @@ export const redisRouter = createTRPCRouter({
await rebuildDatabase(redis.redisId, "redis");
return true;
}),
search: protectedProcedure
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
appName: z.string().optional(),
description: z.string().optional(),
projectId: z.string().optional(),
environmentId: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.projectId) {
baseConditions.push(eq(environments.projectId, input.projectId));
}
if (input.environmentId) {
baseConditions.push(eq(redisTable.environmentId, input.environmentId));
}
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(redisTable.name, term),
ilike(redisTable.appName, term),
ilike(redisTable.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(redisTable.name, `%${input.name.trim()}%`));
}
if (input.appName?.trim()) {
baseConditions.push(
ilike(redisTable.appName, `%${input.appName.trim()}%`),
);
}
if (input.description?.trim()) {
baseConditions.push(
ilike(redisTable.description ?? "", `%${input.description.trim()}%`),
);
}
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${redisTable.redisId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db
.select({
redisId: redisTable.redisId,
name: redisTable.name,
appName: redisTable.appName,
description: redisTable.description,
environmentId: redisTable.environmentId,
applicationStatus: redisTable.applicationStatus,
createdAt: redisTable.createdAt,
})
.from(redisTable)
.innerJoin(
environments,
eq(redisTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where)
.orderBy(desc(redisTable.createdAt))
.limit(input.limit)
.offset(input.offset),
db
.select({ count: sql<number>`count(*)::int` })
.from(redisTable)
.innerJoin(
environments,
eq(redisTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return { items, total: countResult[0]?.count ?? 0 };
}),
});

View File

@@ -201,6 +201,30 @@
.animate-heartbeat {
animation: heartbeat 2.5s infinite;
}
@keyframes whale-draw {
0% {
stroke-dashoffset: 1;
fill-opacity: 0;
}
45% {
stroke-dashoffset: 0;
fill-opacity: 1;
}
55% {
stroke-dashoffset: 0;
fill-opacity: 1;
}
100% {
stroke-dashoffset: 1;
fill-opacity: 0;
}
}
.animate-whale-draw {
animation: whale-draw 2.8s ease-in-out infinite;
}
@media (prefers-color-scheme: dark) {
.swagger-ui {
background-color: white;

View File

@@ -106,11 +106,16 @@ const config = {
height: "0",
},
},
"whale-tide": {
"0%, 100%": { transform: "translateY(0)" },
"50%": { transform: "translateY(-8px)" },
},
},
animation: {
"caret-blink": "caret-blink 1.25s ease-out infinite",
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"whale-tide": "whale-tide 2.2s ease-in-out infinite",
},
},
},

View File

@@ -34,42 +34,139 @@ export const createEnvironment = async (
export const findEnvironmentById = async (environmentId: string) => {
const environment = await db.query.environments.findFirst({
where: eq(environments.environmentId, environmentId),
columns: {
name: true,
description: true,
environmentId: true,
isDefault: true,
projectId: true,
env: true,
},
with: {
applications: {
with: {
deployments: true,
server: true,
server: {
columns: {
name: true,
serverId: true,
},
},
},
columns: {
name: true,
applicationId: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
mariadb: {
with: {
server: true,
server: {
columns: {
name: true,
serverId: true,
},
},
},
columns: {
mariadbId: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
mongo: {
with: {
server: true,
server: {
columns: {
name: true,
serverId: true,
},
},
},
columns: {
mongoId: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
mysql: {
with: {
server: true,
server: {
columns: {
name: true,
serverId: true,
},
},
},
columns: {
mysqlId: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
postgres: {
with: {
server: true,
server: {
columns: {
name: true,
serverId: true,
},
},
},
columns: {
postgresId: true,
name: true,
description: true,
createdAt: true,
applicationStatus: true,
serverId: true,
},
},
redis: {
with: {
server: true,
server: {
columns: {
name: true,
serverId: true,
},
},
},
columns: {
redisId: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
compose: {
with: {
deployments: true,
server: true,
server: {
columns: {
name: true,
serverId: true,
},
},
},
columns: {
composeId: true,
name: true,
createdAt: true,
composeStatus: true,
description: true,
serverId: true,
},
},
project: true,
@@ -98,6 +195,12 @@ export const findEnvironmentsByProjectId = async (projectId: string) => {
compose: true,
project: true,
},
columns: {
name: true,
description: true,
environmentId: true,
isDefault: true,
},
});
return projectEnvironments;
};
@@ -169,6 +272,7 @@ export const duplicateEnvironment = async (
name: input.name,
description: input.description || originalEnvironment.description,
projectId: originalEnvironment.projectId,
env: originalEnvironment.env,
})
.returning()
.then((value) => value[0]);