mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-28 01:05:25 +02:00
Compare commits
13 Commits
feat/compo
...
feat/add-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b060f80932 | ||
|
|
04b9f56333 | ||
|
|
599b97da51 | ||
|
|
415298fddb | ||
|
|
ddff8b9de7 | ||
|
|
90f97912a4 | ||
|
|
9af745ce67 | ||
|
|
d99f2cd460 | ||
|
|
d234558822 | ||
|
|
7f25ddca44 | ||
|
|
638b3dd546 | ||
|
|
1a8fd8396d | ||
|
|
9f07f8e9e1 |
42
.github/workflows/sync-openapi-docs.yml
vendored
42
.github/workflows/sync-openapi-docs.yml
vendored
@@ -68,3 +68,45 @@ jobs:
|
|||||||
|
|
||||||
echo "✅ OpenAPI synced to website successfully"
|
echo "✅ OpenAPI synced to website successfully"
|
||||||
|
|
||||||
|
- name: Sync to MCP repository
|
||||||
|
run: |
|
||||||
|
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git mcp-repo
|
||||||
|
|
||||||
|
cd mcp-repo
|
||||||
|
|
||||||
|
cp -f ../openapi.json openapi.json
|
||||||
|
|
||||||
|
git config user.name "Dokploy Bot"
|
||||||
|
git config user.email "bot@dokploy.com"
|
||||||
|
|
||||||
|
git add openapi.json
|
||||||
|
git commit -m "chore: sync OpenAPI specification [skip ci]" \
|
||||||
|
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
||||||
|
-m "Updated: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" \
|
||||||
|
--allow-empty
|
||||||
|
|
||||||
|
git push
|
||||||
|
|
||||||
|
echo "✅ OpenAPI synced to MCP repository successfully"
|
||||||
|
|
||||||
|
- name: Sync to CLI repository
|
||||||
|
run: |
|
||||||
|
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git cli-repo
|
||||||
|
|
||||||
|
cd cli-repo
|
||||||
|
|
||||||
|
cp -f ../openapi.json openapi.json
|
||||||
|
|
||||||
|
git config user.name "Dokploy Bot"
|
||||||
|
git config user.email "bot@dokploy.com"
|
||||||
|
|
||||||
|
git add openapi.json
|
||||||
|
git commit -m "chore: sync OpenAPI specification [skip ci]" \
|
||||||
|
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
||||||
|
-m "Updated: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" \
|
||||||
|
--allow-empty
|
||||||
|
|
||||||
|
git push
|
||||||
|
|
||||||
|
echo "✅ OpenAPI synced to CLI repository successfully"
|
||||||
|
|
||||||
|
|||||||
79
.github/workflows/sync-version.yml
vendored
Normal file
79
.github/workflows/sync-version.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
name: Sync version to MCP and CLI repos
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync-version:
|
||||||
|
name: Sync version to external repos
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Dokploy repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: get_version
|
||||||
|
run: |
|
||||||
|
VERSION=$(jq -r .version apps/dokploy/package.json)
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
|
- name: Sync version to MCP repository
|
||||||
|
run: |
|
||||||
|
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git mcp-repo
|
||||||
|
cd mcp-repo
|
||||||
|
|
||||||
|
# Bump version
|
||||||
|
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
|
||||||
|
mv package.json.tmp package.json
|
||||||
|
|
||||||
|
# Regenerate tools from latest OpenAPI spec
|
||||||
|
npm install -g pnpm
|
||||||
|
pnpm install
|
||||||
|
pnpm run fetch-openapi
|
||||||
|
pnpm run generate
|
||||||
|
|
||||||
|
git config user.name "Dokploy Bot"
|
||||||
|
git config user.email "bot@dokploy.com"
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore: bump version to ${{ steps.get_version.outputs.version }}" \
|
||||||
|
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
||||||
|
-m "Release: ${{ github.event.release.html_url }}" \
|
||||||
|
--allow-empty
|
||||||
|
|
||||||
|
git push
|
||||||
|
|
||||||
|
|
||||||
|
- name: Sync version to CLI repository
|
||||||
|
run: |
|
||||||
|
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git cli-repo
|
||||||
|
|
||||||
|
cd cli-repo
|
||||||
|
|
||||||
|
# Bump version
|
||||||
|
if [ -f package.json ]; then
|
||||||
|
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
|
||||||
|
mv package.json.tmp package.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy latest openapi spec and regenerate commands
|
||||||
|
cp ../openapi.json ./openapi.json
|
||||||
|
npm install -g pnpm
|
||||||
|
pnpm install
|
||||||
|
pnpm run generate
|
||||||
|
|
||||||
|
git config user.name "Dokploy Bot"
|
||||||
|
git config user.email "bot@dokploy.com"
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore: bump version to ${{ steps.get_version.outputs.version }}" \
|
||||||
|
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
||||||
|
-m "Release: ${{ github.event.release.html_url }}" \
|
||||||
|
--allow-empty
|
||||||
|
|
||||||
|
git push
|
||||||
|
|
||||||
|
echo "CLI repo synced to version ${{ steps.get_version.outputs.version }}"
|
||||||
|
|
||||||
@@ -36,6 +36,10 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ShowContainerConfig } from "@/components/dashboard/docker/config/show-container-config";
|
||||||
|
import { ShowContainerMounts } from "@/components/dashboard/docker/mounts/show-container-mounts";
|
||||||
|
import { ShowContainerNetworks } from "@/components/dashboard/docker/networks/show-container-networks";
|
||||||
|
import { DockerTerminalModal } from "@/components/dashboard/docker/terminal/docker-terminal-modal";
|
||||||
|
|
||||||
const DockerLogsId = dynamic(
|
const DockerLogsId = dynamic(
|
||||||
() =>
|
() =>
|
||||||
@@ -217,6 +221,24 @@ const ContainerRow = ({
|
|||||||
View Logs
|
View Logs
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
<ShowContainerConfig
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={serverId || ""}
|
||||||
|
/>
|
||||||
|
<ShowContainerMounts
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={serverId || ""}
|
||||||
|
/>
|
||||||
|
<ShowContainerNetworks
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={serverId || ""}
|
||||||
|
/>
|
||||||
|
<DockerTerminalModal
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={serverId || ""}
|
||||||
|
>
|
||||||
|
Terminal
|
||||||
|
</DockerTerminalModal>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
containerId: string;
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Mount {
|
||||||
|
Type: string;
|
||||||
|
Source: string;
|
||||||
|
Destination: string;
|
||||||
|
Mode: string;
|
||||||
|
RW: boolean;
|
||||||
|
Propagation: string;
|
||||||
|
Name?: string;
|
||||||
|
Driver?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowContainerMounts = ({ containerId, serverId }: Props) => {
|
||||||
|
const { data } = api.docker.getConfig.useQuery(
|
||||||
|
{
|
||||||
|
containerId,
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!containerId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const mounts: Mount[] = data?.Mounts ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
View Mounts
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="w-full md:w-[70vw] min-w-[70vw]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Container Mounts</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Volume and bind mounts for this container
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="overflow-auto max-h-[70vh]">
|
||||||
|
{mounts.length === 0 ? (
|
||||||
|
<div className="text-center text-muted-foreground py-8">
|
||||||
|
No mounts found for this container.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Type</TableHead>
|
||||||
|
<TableHead>Source</TableHead>
|
||||||
|
<TableHead>Destination</TableHead>
|
||||||
|
<TableHead>Mode</TableHead>
|
||||||
|
<TableHead>Read/Write</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{mounts.map((mount, index) => (
|
||||||
|
<TableRow key={index}>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline">{mount.Type}</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs max-w-[250px] truncate">
|
||||||
|
{mount.Name || mount.Source}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs max-w-[250px] truncate">
|
||||||
|
{mount.Destination}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs">
|
||||||
|
{mount.Mode || "-"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={mount.RW ? "default" : "secondary"}>
|
||||||
|
{mount.RW ? "RW" : "RO"}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
containerId: string;
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Network {
|
||||||
|
IPAMConfig: unknown;
|
||||||
|
Links: unknown;
|
||||||
|
Aliases: string[] | null;
|
||||||
|
MacAddress: string;
|
||||||
|
NetworkID: string;
|
||||||
|
EndpointID: string;
|
||||||
|
Gateway: string;
|
||||||
|
IPAddress: string;
|
||||||
|
IPPrefixLen: number;
|
||||||
|
IPv6Gateway: string;
|
||||||
|
GlobalIPv6Address: string;
|
||||||
|
GlobalIPv6PrefixLen: number;
|
||||||
|
DriverOpts: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowContainerNetworks = ({ containerId, serverId }: Props) => {
|
||||||
|
const { data } = api.docker.getConfig.useQuery(
|
||||||
|
{
|
||||||
|
containerId,
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!containerId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const networks: Record<string, Network> =
|
||||||
|
data?.NetworkSettings?.Networks ?? {};
|
||||||
|
const entries = Object.entries(networks);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
View Networks
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="w-full md:w-[70vw] min-w-[70vw]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Container Networks</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Networks attached to this container
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="overflow-auto max-h-[70vh]">
|
||||||
|
{entries.length === 0 ? (
|
||||||
|
<div className="text-center text-muted-foreground py-8">
|
||||||
|
No networks found for this container.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Network</TableHead>
|
||||||
|
<TableHead>IP Address</TableHead>
|
||||||
|
<TableHead>Gateway</TableHead>
|
||||||
|
<TableHead>MAC Address</TableHead>
|
||||||
|
<TableHead>Aliases</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{entries.map(([name, network]) => (
|
||||||
|
<TableRow key={name}>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline">{name}</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs">
|
||||||
|
{network.IPAddress
|
||||||
|
? `${network.IPAddress}/${network.IPPrefixLen}`
|
||||||
|
: "-"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs">
|
||||||
|
{network.Gateway || "-"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs">
|
||||||
|
{network.MacAddress || "-"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs">
|
||||||
|
{network.Aliases?.join(", ") || "-"}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { ShowContainerConfig } from "../config/show-container-config";
|
import { ShowContainerConfig } from "../config/show-container-config";
|
||||||
import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs";
|
import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs";
|
||||||
|
import { ShowContainerMounts } from "../mounts/show-container-mounts";
|
||||||
|
import { ShowContainerNetworks } from "../networks/show-container-networks";
|
||||||
import { RemoveContainerDialog } from "../remove/remove-container";
|
import { RemoveContainerDialog } from "../remove/remove-container";
|
||||||
import { DockerTerminalModal } from "../terminal/docker-terminal-modal";
|
import { DockerTerminalModal } from "../terminal/docker-terminal-modal";
|
||||||
import { UploadFileModal } from "../upload/upload-file-modal";
|
import { UploadFileModal } from "../upload/upload-file-modal";
|
||||||
@@ -123,6 +125,14 @@ export const columns: ColumnDef<Container>[] = [
|
|||||||
containerId={container.containerId}
|
containerId={container.containerId}
|
||||||
serverId={container.serverId || ""}
|
serverId={container.serverId || ""}
|
||||||
/>
|
/>
|
||||||
|
<ShowContainerMounts
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={container.serverId || ""}
|
||||||
|
/>
|
||||||
|
<ShowContainerNetworks
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={container.serverId || ""}
|
||||||
|
/>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
containerId={container.containerId}
|
containerId={container.containerId}
|
||||||
serverId={container.serverId || ""}
|
serverId={container.serverId || ""}
|
||||||
|
|||||||
@@ -116,6 +116,14 @@ export function TagSelector({
|
|||||||
<HandleTag />
|
<HandleTag />
|
||||||
</div>
|
</div>
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
|
{tags.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center gap-2 py-4">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
No tags created yet.
|
||||||
|
</span>
|
||||||
|
<HandleTag />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{tags.map((tag) => {
|
{tags.map((tag) => {
|
||||||
const isSelected = selectedTags.includes(tag.id);
|
const isSelected = selectedTags.includes(tag.id);
|
||||||
|
|||||||
198
apps/dokploy/components/ui/context-menu.tsx
Normal file
198
apps/dokploy/components/ui/context-menu.tsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const ContextMenu = ContextMenuPrimitive.Root;
|
||||||
|
|
||||||
|
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
||||||
|
|
||||||
|
const ContextMenuGroup = ContextMenuPrimitive.Group;
|
||||||
|
|
||||||
|
const ContextMenuPortal = ContextMenuPrimitive.Portal;
|
||||||
|
|
||||||
|
const ContextMenuSub = ContextMenuPrimitive.Sub;
|
||||||
|
|
||||||
|
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
||||||
|
|
||||||
|
const ContextMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
|
));
|
||||||
|
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
|
const ContextMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
|
const ContextMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.Portal>
|
||||||
|
<ContextMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</ContextMenuPrimitive.Portal>
|
||||||
|
));
|
||||||
|
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const ContextMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
const ContextMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
|
));
|
||||||
|
ContextMenuCheckboxItem.displayName =
|
||||||
|
ContextMenuPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
|
const ContextMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.RadioItem>
|
||||||
|
));
|
||||||
|
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
|
||||||
|
|
||||||
|
const ContextMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
|
||||||
|
|
||||||
|
const ContextMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
const ContextMenuShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
ContextMenuShortcut.displayName = "ContextMenuShortcut";
|
||||||
|
|
||||||
|
export {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuCheckboxItem,
|
||||||
|
ContextMenuRadioItem,
|
||||||
|
ContextMenuLabel,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuShortcut,
|
||||||
|
ContextMenuGroup,
|
||||||
|
ContextMenuPortal,
|
||||||
|
ContextMenuSub,
|
||||||
|
ContextMenuSubContent,
|
||||||
|
ContextMenuSubTrigger,
|
||||||
|
ContextMenuRadioGroup,
|
||||||
|
};
|
||||||
@@ -67,6 +67,7 @@
|
|||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
"@radix-ui/react-collapsible": "^1.1.11",
|
"@radix-ui/react-collapsible": "^1.1.11",
|
||||||
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
Play,
|
Play,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
|
RefreshCw,
|
||||||
Search,
|
Search,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
SquareTerminal,
|
SquareTerminal,
|
||||||
@@ -68,6 +69,14 @@ import {
|
|||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuLabel,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from "@/components/ui/context-menu";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -424,6 +433,7 @@ const EnvironmentPage = (
|
|||||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||||
const [deleteVolumes, setDeleteVolumes] = useState(false);
|
const [deleteVolumes, setDeleteVolumes] = useState(false);
|
||||||
const [selectedServerId, setSelectedServerId] = useState<string>("all");
|
const [selectedServerId, setSelectedServerId] = useState<string>("all");
|
||||||
|
const [serviceToDelete, setServiceToDelete] = useState<Services | null>(null);
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
if (selectedServices.length === filteredServices.length) {
|
if (selectedServices.length === filteredServices.length) {
|
||||||
@@ -814,6 +824,110 @@ const EnvironmentPage = (
|
|||||||
setIsBulkActionLoading(false);
|
setIsBulkActionLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getServiceActions = (service: Services) => {
|
||||||
|
switch (service.type) {
|
||||||
|
case "application":
|
||||||
|
return applicationActions;
|
||||||
|
case "compose":
|
||||||
|
return composeActions;
|
||||||
|
case "postgres":
|
||||||
|
return postgresActions;
|
||||||
|
case "mysql":
|
||||||
|
return mysqlActions;
|
||||||
|
case "mariadb":
|
||||||
|
return mariadbActions;
|
||||||
|
case "redis":
|
||||||
|
return redisActions;
|
||||||
|
case "mongo":
|
||||||
|
return mongoActions;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getServiceIdKey = (service: Services) => {
|
||||||
|
switch (service.type) {
|
||||||
|
case "application":
|
||||||
|
return "applicationId";
|
||||||
|
case "compose":
|
||||||
|
return "composeId";
|
||||||
|
case "postgres":
|
||||||
|
return "postgresId";
|
||||||
|
case "mysql":
|
||||||
|
return "mysqlId";
|
||||||
|
case "mariadb":
|
||||||
|
return "mariadbId";
|
||||||
|
case "redis":
|
||||||
|
return "redisId";
|
||||||
|
case "mongo":
|
||||||
|
return "mongoId";
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleServiceAction = async (
|
||||||
|
service: Services,
|
||||||
|
action: "start" | "stop" | "deploy",
|
||||||
|
) => {
|
||||||
|
const actions = getServiceActions(service);
|
||||||
|
const idKey = getServiceIdKey(service);
|
||||||
|
if (!actions || !idKey) return;
|
||||||
|
|
||||||
|
const actionLabels = {
|
||||||
|
start: { loading: "Starting", success: "started", error: "starting" },
|
||||||
|
stop: { loading: "Stopping", success: "stopped", error: "stopping" },
|
||||||
|
deploy: {
|
||||||
|
loading: "Deploying",
|
||||||
|
success: "queued for deployment",
|
||||||
|
error: "deploying",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const labels = actionLabels[action];
|
||||||
|
|
||||||
|
toast.promise(
|
||||||
|
(async () => {
|
||||||
|
await actions[action].mutateAsync({
|
||||||
|
[idKey]: service.id,
|
||||||
|
} as any);
|
||||||
|
})(),
|
||||||
|
{
|
||||||
|
loading: `${labels.loading} ${service.name}...`,
|
||||||
|
success: () => {
|
||||||
|
utils.environment.one.invalidate({ environmentId });
|
||||||
|
return `${service.name} ${labels.success} successfully`;
|
||||||
|
},
|
||||||
|
error: (error) =>
|
||||||
|
`Error ${labels.error} ${service.name}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleServiceDelete = async (service: Services) => {
|
||||||
|
const actions = getServiceActions(service);
|
||||||
|
const idKey = getServiceIdKey(service);
|
||||||
|
if (!actions || !idKey) return;
|
||||||
|
|
||||||
|
toast.promise(
|
||||||
|
(async () => {
|
||||||
|
await actions.delete.mutateAsync({
|
||||||
|
[idKey]: service.id,
|
||||||
|
} as any);
|
||||||
|
})(),
|
||||||
|
{
|
||||||
|
loading: `Deleting ${service.name}...`,
|
||||||
|
success: () => {
|
||||||
|
utils.environment.one.invalidate({ environmentId });
|
||||||
|
return `${service.name} deleted successfully`;
|
||||||
|
},
|
||||||
|
error: (error) =>
|
||||||
|
`Error deleting ${service.name}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setServiceToDelete(null);
|
||||||
|
};
|
||||||
|
|
||||||
// Get unique servers from services
|
// Get unique servers from services
|
||||||
const availableServers = useMemo(() => {
|
const availableServers = useMemo(() => {
|
||||||
if (!applications) return [];
|
if (!applications) return [];
|
||||||
@@ -1472,110 +1586,156 @@ const EnvironmentPage = (
|
|||||||
<div className="flex w-full flex-col gap-4">
|
<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">
|
<div className="gap-5 pb-10 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
{filteredServices?.map((service) => (
|
{filteredServices?.map((service) => (
|
||||||
<Link
|
<ContextMenu key={service.id}>
|
||||||
key={service.id}
|
<ContextMenuTrigger asChild>
|
||||||
href={`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`}
|
<Link
|
||||||
className="block"
|
href={`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`}
|
||||||
>
|
className="block"
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
<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">
|
<Card className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border">
|
||||||
<Checkbox
|
{service.serverId && (
|
||||||
checked={selectedServices.includes(
|
<div className="absolute -left-1 -top-2">
|
||||||
service.id,
|
<ServerIcon className="size-4 text-muted-foreground" />
|
||||||
)}
|
|
||||||
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" &&
|
|
||||||
(service.icon ? (
|
|
||||||
// biome-ignore lint/performance/noImgElement: application icon is data URL
|
|
||||||
<img
|
|
||||||
src={service.icon}
|
|
||||||
alt={service.name}
|
|
||||||
className="size-7 object-contain"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<GlobeIcon className="h-6 w-6" />
|
|
||||||
))}
|
|
||||||
{service.type === "compose" && (
|
|
||||||
<CircuitBoard className="h-6 w-6" />
|
|
||||||
)}
|
|
||||||
{service.type === "libsql" && (
|
|
||||||
<LibsqlIcon className="h-6 w-6" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</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}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<DateTooltip date={service.createdAt}>
|
<div className="absolute -right-1 -top-2">
|
||||||
Created
|
<StatusTooltip status={service.status} />
|
||||||
</DateTooltip>
|
</div>
|
||||||
</div>
|
|
||||||
</CardFooter>
|
<div
|
||||||
</Card>
|
className={cn(
|
||||||
</Link>
|
"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" &&
|
||||||
|
(service.icon ? (
|
||||||
|
// biome-ignore lint/performance/noImgElement: application icon is data URL
|
||||||
|
<img
|
||||||
|
src={service.icon}
|
||||||
|
alt={service.name}
|
||||||
|
className="size-7 object-contain"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<GlobeIcon className="h-6 w-6" />
|
||||||
|
))}
|
||||||
|
{service.type === "compose" && (
|
||||||
|
<CircuitBoard className="h-6 w-6" />
|
||||||
|
)}
|
||||||
|
{service.type === "libsql" && (
|
||||||
|
<LibsqlIcon className="h-6 w-6" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</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}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DateTooltip date={service.createdAt}>
|
||||||
|
Created
|
||||||
|
</DateTooltip>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
{service.type !== "libsql" && (
|
||||||
|
<ContextMenuContent className="w-48">
|
||||||
|
<ContextMenuLabel className="truncate">
|
||||||
|
{service.name}
|
||||||
|
</ContextMenuLabel>
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
<ContextMenuItem
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() =>
|
||||||
|
handleServiceAction(service, "start")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Play className="size-4" />
|
||||||
|
Start
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() =>
|
||||||
|
handleServiceAction(service, "deploy")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4" />
|
||||||
|
Deploy
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
className="flex items-center gap-2 text-orange-500 focus:text-orange-500"
|
||||||
|
onClick={() =>
|
||||||
|
handleServiceAction(service, "stop")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Ban className="size-4" />
|
||||||
|
Stop
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
<ContextMenuItem
|
||||||
|
className="flex items-center gap-2 text-red-500 focus:text-red-500"
|
||||||
|
onClick={() => setServiceToDelete(service)}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4" />
|
||||||
|
Delete
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
)}
|
||||||
|
</ContextMenu>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1586,6 +1746,38 @@ const EnvironmentPage = (
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Single Service Delete Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={!!serviceToDelete}
|
||||||
|
onOpenChange={(open) => !open && setServiceToDelete(null)}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete Service</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to delete{" "}
|
||||||
|
<span className="font-semibold">{serviceToDelete?.name}</span>?
|
||||||
|
This action cannot be undone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setServiceToDelete(null)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => {
|
||||||
|
if (serviceToDelete) {
|
||||||
|
handleServiceDelete(serviceToDelete);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ const { handler, api } = betterAuth({
|
|||||||
where: eq(schema.member.role, "owner"),
|
where: eq(schema.member.role, "owner"),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!IS_CLOUD) {
|
if (!IS_CLOUD && !isAdminPresent) {
|
||||||
await updateWebServerSettings({
|
await updateWebServerSettings({
|
||||||
serverIp: await getPublicIpWithFallback(),
|
serverIp: await getPublicIpWithFallback(),
|
||||||
});
|
});
|
||||||
|
|||||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@@ -176,6 +176,9 @@ importers:
|
|||||||
'@radix-ui/react-collapsible':
|
'@radix-ui/react-collapsible':
|
||||||
specifier: ^1.1.11
|
specifier: ^1.1.11
|
||||||
version: 1.1.12(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 1.1.12(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
|
'@radix-ui/react-context-menu':
|
||||||
|
specifier: ^2.2.16
|
||||||
|
version: 2.2.16(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
'@radix-ui/react-dialog':
|
'@radix-ui/react-dialog':
|
||||||
specifier: ^1.1.14
|
specifier: ^1.1.14
|
||||||
version: 1.1.15(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 1.1.15(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
@@ -2801,6 +2804,19 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-context-menu@2.2.16':
|
||||||
|
resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': 18.3.5
|
||||||
|
'@types/react-dom': 18.3.0
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-context@1.0.0':
|
'@radix-ui/react-context@1.0.0':
|
||||||
resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==}
|
resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -10633,6 +10649,20 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.5
|
'@types/react': 18.3.5
|
||||||
|
|
||||||
|
'@radix-ui/react-context-menu@2.2.16(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@18.3.5)(react@18.2.0)
|
||||||
|
'@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.5)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.5)(react@18.2.0)
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.5
|
||||||
|
'@types/react-dom': 18.3.0
|
||||||
|
|
||||||
'@radix-ui/react-context@1.0.0(react@18.2.0)':
|
'@radix-ui/react-context@1.0.0(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.28.6
|
'@babel/runtime': 7.28.6
|
||||||
|
|||||||
Reference in New Issue
Block a user