diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx
index 8227c1a0..be6a7742 100644
--- a/src/app/auth/login/page.tsx
+++ b/src/app/auth/login/page.tsx
@@ -6,8 +6,8 @@ import DashboardLoginForm from "./DashboardLoginForm";
import { Mail } from "lucide-react";
import { pullEnv } from "@app/lib/pullEnv";
import { cleanRedirect } from "@app/lib/cleanRedirect";
-import db from "@server/db";
-import { idp } from "@server/db/schemas";
+import { db } from "@server/db";
+import { idp } from "@server/db";
import { LoginFormIDP } from "@app/components/LoginForm";
export const dynamic = "force-dynamic";
diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx
index af31de98..9ed4ce87 100644
--- a/src/app/auth/resource/[resourceId]/page.tsx
+++ b/src/app/auth/resource/[resourceId]/page.tsx
@@ -14,8 +14,8 @@ import ResourceAccessDenied from "./ResourceAccessDenied";
import AccessToken from "./AccessToken";
import { pullEnv } from "@app/lib/pullEnv";
import { LoginFormIDP } from "@app/components/LoginForm";
-import db from "@server/db";
-import { idp } from "@server/db/schemas";
+import { db } from "@server/db";
+import { idp } from "@server/db";
export default async function ResourceAuthPage(props: {
params: Promise<{ resourceId: number }>;
diff --git a/src/app/favicon.ico b/src/app/favicon.ico
index 0ffb1c54..bcaab339 100644
Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ
diff --git a/src/components/ContainersSelector.tsx b/src/components/ContainersSelector.tsx
new file mode 100644
index 00000000..7e5dfd6f
--- /dev/null
+++ b/src/components/ContainersSelector.tsx
@@ -0,0 +1,670 @@
+import { useEffect, useState, FC, useCallback, useMemo } from "react";
+import {
+ ColumnDef,
+ getCoreRowModel,
+ useReactTable,
+ flexRender,
+ getFilteredRowModel,
+ VisibilityState
+} from "@tanstack/react-table";
+import { Button } from "@/components/ui/button";
+import {
+ Credenza,
+ CredenzaBody,
+ CredenzaClose,
+ CredenzaContent,
+ CredenzaDescription,
+ CredenzaFooter,
+ CredenzaHeader,
+ CredenzaTitle
+} from "@/components/Credenza";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow
+} from "@/components/ui/table";
+import { Badge } from "@/components/ui/badge";
+import { Input } from "@/components/ui/input";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger
+} from "@/components/ui/popover";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuCheckboxItem
+} from "@/components/ui/dropdown-menu";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Search, RefreshCw, Filter, Columns } from "lucide-react";
+import { GetSiteResponse, Container } from "@server/routers/site";
+import { useDockerSocket } from "@app/hooks/useDockerSocket";
+import { FaDocker } from "react-icons/fa";
+
+// Type definitions based on the JSON structure
+
+interface ContainerSelectorProps {
+ site: GetSiteResponse;
+ onContainerSelect?: (hostname: string, port?: number) => void;
+}
+
+export const ContainersSelector: FC
= ({
+ site,
+ onContainerSelect
+}) => {
+ const [open, setOpen] = useState(false);
+
+ const { isAvailable, containers, fetchContainers } = useDockerSocket(site);
+
+ useEffect(() => {
+ console.log("DockerSocket isAvailable:", isAvailable);
+ if (isAvailable) {
+ fetchContainers();
+ }
+ }, [isAvailable]);
+
+ if (!site || !isAvailable) {
+ return null;
+ }
+
+ const handleContainerSelect = (container: Container, port?: number) => {
+ // Extract hostname - prefer IP address from networks, fallback to container name
+ const hostname = getContainerHostname(container);
+ onContainerSelect?.(hostname, port);
+ setOpen(false);
+ };
+
+ return (
+ <>
+ setOpen(true)}
+ >
+ View Docker Containers
+
+
+
+
+ Containers in {site.name}
+
+ Select any container to use as a hostname for this
+ target. Click a port to use select a port.
+
+
+
+
+ fetchContainers()}
+ />
+
+
+
+
+ Close
+
+
+
+
+ >
+ );
+};
+
+const DockerContainersTable: FC<{
+ containers: Container[];
+ onContainerSelect: (container: Container, port?: number) => void;
+ onRefresh: () => void;
+}> = ({ containers, onContainerSelect, onRefresh }) => {
+ const [searchInput, setSearchInput] = useState("");
+ const [globalFilter, setGlobalFilter] = useState("");
+ const [hideContainersWithoutPorts, setHideContainersWithoutPorts] =
+ useState(true);
+ const [hideStoppedContainers, setHideStoppedContainers] = useState(false);
+ const [columnVisibility, setColumnVisibility] = useState({
+ labels: false
+ });
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setGlobalFilter(searchInput);
+ }, 100);
+
+ return () => clearTimeout(timer);
+ }, [searchInput]);
+
+ const getExposedPorts = useCallback((container: Container): number[] => {
+ const ports: number[] = [];
+
+ container.ports?.forEach((port) => {
+ if (port.privatePort) {
+ ports.push(port.privatePort);
+ }
+ });
+
+ return [...new Set(ports)]; // Remove duplicates
+ }, []);
+
+ const globalFilterFunction = useCallback(
+ (row: any, columnId: string, value: string) => {
+ const container = row.original as Container;
+ const searchValue = value.toLowerCase();
+
+ // Search across all relevant fields
+ const searchableFields = [
+ container.name,
+ container.image,
+ container.state,
+ container.status,
+ getContainerHostname(container),
+ ...Object.keys(container.networks),
+ ...Object.values(container.networks)
+ .map((n) => n.ipAddress)
+ .filter(Boolean),
+ ...getExposedPorts(container).map((p) => p.toString()),
+ ...Object.entries(container.labels).flat()
+ ];
+
+ return searchableFields.some((field) =>
+ field?.toString().toLowerCase().includes(searchValue)
+ );
+ },
+ [getExposedPorts]
+ );
+
+ const columns: ColumnDef[] = [
+ {
+ accessorKey: "name",
+ header: "Name",
+ cell: ({ row }) => (
+ {row.original.name}
+ )
+ },
+ {
+ accessorKey: "image",
+ header: "Image",
+ cell: ({ row }) => (
+
+ {row.original.image}
+
+ )
+ },
+ {
+ accessorKey: "state",
+ header: "State",
+ cell: ({ row }) => (
+
+ {row.original.state}
+
+ )
+ },
+ {
+ accessorKey: "networks",
+ header: "Networks",
+ cell: ({ row }) => {
+ const networks = Object.keys(row.original.networks);
+ return (
+
+ {networks.length > 0
+ ? networks.map((n) => (
+
+ {n}
+
+ ))
+ : "-"}
+
+ );
+ }
+ },
+ {
+ accessorKey: "hostname",
+ header: "Hostname/IP",
+ enableHiding: false,
+ cell: ({ row }) => (
+
+ {getContainerHostname(row.original)}
+
+ )
+ },
+ {
+ accessorKey: "labels",
+ header: "Labels",
+ cell: ({ row }) => {
+ const labels = row.original.labels || {};
+ const labelEntries = Object.entries(labels);
+
+ if (labelEntries.length === 0) {
+ return - ;
+ }
+
+ return (
+
+
+
+ {labelEntries.length} label
+ {labelEntries.length !== 1 ? "s" : ""}
+
+
+
+
+
+
+ Container Labels
+
+
+ {labelEntries.map(([key, value]) => (
+
+
+ {key}
+
+
+ {value || ""}
+
+
+ ))}
+
+
+
+
+
+ );
+ }
+ },
+ {
+ accessorKey: "ports",
+ header: "Ports",
+ enableHiding: false,
+ cell: ({ row }) => {
+ const ports = getExposedPorts(row.original);
+ return (
+
+ {ports.slice(0, 2).map((port) => (
+
+ onContainerSelect(row.original, port)
+ }
+ >
+ {port}
+
+ ))}
+ {ports.length > 2 && (
+
+
+
+ +{ports.length - 2} more
+
+
+
+ {ports.slice(2).map((port) => (
+
+ onContainerSelect(
+ row.original,
+ port
+ )
+ }
+ >
+ {port}
+
+ ))}
+
+
+ )}
+
+ );
+ }
+ },
+ {
+ id: "actions",
+ header: "Actions",
+ cell: ({ row }) => (
+ onContainerSelect(row.original)}
+ disabled={row.original.state !== "running"}
+ >
+ Select
+
+ )
+ }
+ ];
+
+ const initialFilters = useMemo(() => {
+ let filtered = containers;
+
+ // Filter by port visibility
+ if (hideContainersWithoutPorts) {
+ filtered = filtered.filter((container) => {
+ const ports = getExposedPorts(container);
+ return ports.length > 0; // Show only containers WITH ports
+ });
+ }
+
+ // Filter by container state
+ if (hideStoppedContainers) {
+ filtered = filtered.filter((container) => {
+ return container.state === "running";
+ });
+ }
+
+ return filtered;
+ }, [
+ containers,
+ hideContainersWithoutPorts,
+ hideStoppedContainers,
+ getExposedPorts
+ ]);
+
+ const table = useReactTable({
+ data: initialFilters,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ globalFilterFn: globalFilterFunction,
+ state: {
+ globalFilter,
+ columnVisibility
+ },
+ onGlobalFilterChange: setGlobalFilter,
+ onColumnVisibilityChange: setColumnVisibility
+ });
+
+ if (initialFilters.length === 0) {
+ return (
+
+
+
+ {(hideContainersWithoutPorts ||
+ hideStoppedContainers) &&
+ containers.length > 0 ? (
+ <>
+
+ No containers found matching the current
+ filters.
+
+
+ {hideContainersWithoutPorts && (
+
+ setHideContainersWithoutPorts(
+ false
+ )
+ }
+ >
+ Show containers without ports
+
+ )}
+ {hideStoppedContainers && (
+
+ setHideStoppedContainers(false)
+ }
+ >
+ Show stopped containers
+
+ )}
+
+ >
+ ) : (
+
+ No containers found. Make sure Docker containers
+ are running.
+
+ )}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ setSearchInput(event.target.value)
+ }
+ className="pl-8"
+ />
+ {searchInput &&
+ table.getFilteredRowModel().rows.length > 0 && (
+
+ {table.getFilteredRowModel().rows.length}{" "}
+ result
+ {table.getFilteredRowModel().rows.length !==
+ 1
+ ? "s"
+ : ""}
+
+ )}
+
+
+
+
+
+
+ Filters
+ {(hideContainersWithoutPorts ||
+ hideStoppedContainers) && (
+
+ {Number(
+ hideContainersWithoutPorts
+ ) + Number(hideStoppedContainers)}
+
+ )}
+
+
+
+
+ Filter Options
+
+
+
+ Ports
+
+
+ Stopped
+
+ {(hideContainersWithoutPorts ||
+ hideStoppedContainers) && (
+ <>
+
+
+ {
+ setHideContainersWithoutPorts(
+ false
+ );
+ setHideStoppedContainers(
+ false
+ );
+ }}
+ className="w-full text-xs"
+ >
+ Clear all filters
+
+
+ >
+ )}
+
+
+
+
+
+
+
+ Columns
+
+
+
+
+ Toggle Columns
+
+
+ {table
+ .getAllColumns()
+ .filter((column) => column.getCanHide())
+ .map((column) => {
+ return (
+
+ column.toggleVisibility(
+ !!value
+ )
+ }
+ >
+ {column.id === "hostname"
+ ? "Hostname/IP"
+ : column.id}
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef
+ .header,
+ header.getContext()
+ )}
+
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+
+ ))}
+
+ ))
+ ) : (
+
+
+ {searchInput && !globalFilter ? (
+
+ ) : (
+ `No containers found matching "${globalFilter}".`
+ )}
+
+
+ )}
+
+
+
+
+ );
+};
+
+function getContainerHostname(container: Container): string {
+ // First, try to get IP from networks
+ const networks = Object.values(container.networks);
+ for (const network of networks) {
+ if (network.ipAddress) {
+ return network.ipAddress;
+ }
+ }
+
+ // Fallback to container name (works in Docker networks)
+ return container.name;
+}
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index e0925525..b98e1886 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
import { SidebarNav } from "@app/components/SidebarNav";
import { OrgSelector } from "@app/components/OrgSelector";
import { cn } from "@app/lib/cn";
@@ -23,6 +23,7 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { useUserContext } from "@app/hooks/useUserContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
+import { useTheme } from "next-themes";
interface LayoutProps {
children: React.ReactNode;
@@ -61,6 +62,30 @@ export function Layout({
const { user } = useUserContext();
const { isUnlocked } = useLicenseStatusContext();
+ const { theme } = useTheme();
+ const [path, setPath] = useState(""); // Default logo path
+
+ useEffect(() => {
+ function getPath() {
+ let lightOrDark = theme;
+
+ if (theme === "system" || !theme) {
+ lightOrDark = window.matchMedia("(prefers-color-scheme: dark)")
+ .matches
+ ? "dark"
+ : "light";
+ }
+
+ if (lightOrDark === "light") {
+ return "/logo/word_mark_black.png";
+ }
+
+ return "/logo/word_mark_white.png";
+ }
+
+ setPath(getPath());
+ }, [theme, env]);
+
return (
{/* Full width header */}
@@ -139,12 +164,16 @@ export function Layout({
href="/"
className="flex items-center hidden md:block"
>
-
+ {path && (
+
+ )}
{showBreadcrumbs && (
diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx
new file mode 100644
index 00000000..704be637
--- /dev/null
+++ b/src/components/ui/scroll-area.tsx
@@ -0,0 +1,58 @@
+"use client"
+
+import * as React from "react"
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
+import { cn } from "@app/lib/cn"
+
+
+function ScrollArea({
+ className,
+ children,
+ ...props
+}: React.ComponentProps
) {
+ return (
+
+
+ {children}
+
+
+
+
+ )
+}
+
+function ScrollBar({
+ className,
+ orientation = "vertical",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export { ScrollArea, ScrollBar }
diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx
index 7bfec308..069e68b8 100644
--- a/src/components/ui/table.tsx
+++ b/src/components/ui/table.tsx
@@ -1,121 +1,138 @@
-import * as React from "react"
+import * as React from "react";
-import { cn } from "@app/lib/cn"
+import { cn } from "@app/lib/cn";
export function TableContainer({ children }: { children: React.ReactNode }) {
- return {children}
+ return {children}
;
}
const Table = React.forwardRef<
- HTMLTableElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-Table.displayName = "Table"
+ HTMLTableElement,
+ React.HTMLAttributes & { sticky?: boolean }
+>(({ className, sticky, ...props }, ref) => (
+
+));
+Table.displayName = "Table";
const TableHeader = React.forwardRef<
- HTMLTableSectionElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-TableHeader.displayName = "TableHeader"
+ HTMLTableSectionElement,
+ React.HTMLAttributes & { sticky?: boolean }
+>(({ className, sticky, ...props }, ref) => (
+
+));
+TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<
- HTMLTableSectionElement,
- React.HTMLAttributes
+ HTMLTableSectionElement,
+ React.HTMLAttributes
>(({ className, ...props }, ref) => (
-
-))
-TableBody.displayName = "TableBody"
+
+));
+TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<
- HTMLTableSectionElement,
- React.HTMLAttributes
+ HTMLTableSectionElement,
+ React.HTMLAttributes
>(({ className, ...props }, ref) => (
- tr]:last:border-b-0",
- className
- )}
- {...props}
- />
-))
-TableFooter.displayName = "TableFooter"
+ tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+));
+TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
- HTMLTableRowElement,
- React.HTMLAttributes
+ HTMLTableRowElement,
+ React.HTMLAttributes
>(({ className, ...props }, ref) => (
-
-))
-TableRow.displayName = "TableRow"
+
+));
+TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
- HTMLTableCellElement,
- React.ThHTMLAttributes
+ HTMLTableCellElement,
+ React.ThHTMLAttributes
>(({ className, ...props }, ref) => (
-
-))
-TableHead.displayName = "TableHead"
+
+));
+TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
- HTMLTableCellElement,
- React.TdHTMLAttributes
+ HTMLTableCellElement,
+ React.TdHTMLAttributes
>(({ className, ...props }, ref) => (
-
-))
-TableCell.displayName = "TableCell"
+
+));
+TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
- HTMLTableCaptionElement,
- React.HTMLAttributes
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
>(({ className, ...props }, ref) => (
-
-))
-TableCaption.displayName = "TableCaption"
+
+));
+TableCaption.displayName = "TableCaption";
export {
- Table,
- TableHeader,
- TableBody,
- TableFooter,
- TableHead,
- TableRow,
- TableCell,
- TableCaption,
-}
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption
+};
diff --git a/src/contexts/resourceContext.ts b/src/contexts/resourceContext.ts
index bb5501a6..d24a948b 100644
--- a/src/contexts/resourceContext.ts
+++ b/src/contexts/resourceContext.ts
@@ -1,9 +1,11 @@
import { GetResourceAuthInfoResponse } from "@server/routers/resource";
import { GetResourceResponse } from "@server/routers/resource/getResource";
+import { GetSiteResponse } from "@server/routers/site";
import { createContext } from "react";
interface ResourceContextType {
resource: GetResourceResponse;
+ site: GetSiteResponse | null;
authInfo: GetResourceAuthInfoResponse;
updateResource: (updatedResource: Partial) => void;
updateAuthInfo: (
diff --git a/src/hooks/useDockerSocket.ts b/src/hooks/useDockerSocket.ts
new file mode 100644
index 00000000..71eb9ca8
--- /dev/null
+++ b/src/hooks/useDockerSocket.ts
@@ -0,0 +1,167 @@
+import { createApiClient, formatAxiosError } from "@app/lib/api";
+import { useCallback, useEffect, useState } from "react";
+import { useEnvContext } from "./useEnvContext";
+import {
+ Container,
+ GetDockerStatusResponse,
+ ListContainersResponse,
+ TriggerFetchResponse
+} from "@server/routers/site";
+import { AxiosResponse } from "axios";
+import { toast } from "./useToast";
+import { Site } from "@server/db";
+
+const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
+
+export function useDockerSocket(site: Site) {
+ console.log(`useDockerSocket initialized for site ID: ${site.siteId}`);
+
+ const [dockerSocket, setDockerSocket] = useState();
+ const [containers, setContainers] = useState([]);
+
+ const api = createApiClient(useEnvContext());
+
+ const { dockerSocketEnabled: isEnabled = true } = site || {};
+ const { isAvailable = false, socketPath } = dockerSocket || {};
+
+ const checkDockerSocket = useCallback(async () => {
+ if (!isEnabled) {
+ console.warn("Docker socket is not enabled for this site.");
+ return;
+ }
+ try {
+ const res = await api.post(`/site/${site.siteId}/docker/check`);
+ console.log("Docker socket check response:", res);
+ } catch (error) {
+ console.error("Failed to check Docker socket:", error);
+ }
+ }, [api, site.siteId, isEnabled]);
+
+ const getDockerSocketStatus = useCallback(async () => {
+ if (!isEnabled) {
+ console.warn("Docker socket is not enabled for this site.");
+ return;
+ }
+
+ try {
+ const res = await api.get>(
+ `/site/${site.siteId}/docker/status`
+ );
+
+ if (res.status === 200) {
+ setDockerSocket(res.data.data);
+ } else {
+ console.error("Failed to get Docker status:", res);
+ toast({
+ variant: "destructive",
+ title: "Failed to get Docker status",
+ description:
+ "An error occurred while fetching Docker status."
+ });
+ }
+ } catch (error) {
+ console.error("Failed to get Docker status:", error);
+ toast({
+ variant: "destructive",
+ title: "Failed to get Docker status",
+ description: "An error occurred while fetching Docker status."
+ });
+ }
+ }, [api, site.siteId, isEnabled]);
+
+ const getContainers = useCallback(
+ async (maxRetries: number = 3) => {
+ if (!isEnabled || !isAvailable) {
+ console.warn("Docker socket is not enabled or available.");
+ return;
+ }
+
+ const fetchContainerList = async () => {
+ if (!isEnabled || !isAvailable) {
+ return;
+ }
+
+ let attempt = 0;
+ while (attempt < maxRetries) {
+ try {
+ const res = await api.get<
+ AxiosResponse
+ >(`/site/${site.siteId}/docker/containers`);
+ setContainers(res.data.data);
+ return res.data.data;
+ } catch (error: any) {
+ attempt++;
+
+ // Check if the error is a 425 (Too Early) status
+ if (error?.response?.status === 425) {
+ if (attempt < maxRetries) {
+ console.log(
+ `Containers not ready yet (attempt ${attempt}/${maxRetries}). Retrying in 250ms...`
+ );
+ await sleep(250);
+ continue;
+ } else {
+ console.warn(
+ "Max retry attempts reached. Containers may still be loading."
+ );
+ toast({
+ variant: "destructive",
+ title: "Containers not ready",
+ description:
+ "Containers are still loading. Please try again in a moment."
+ });
+ }
+ } else {
+ console.error(
+ "Failed to fetch Docker containers:",
+ error
+ );
+ toast({
+ variant: "destructive",
+ title: "Failed to fetch containers",
+ description: formatAxiosError(
+ error,
+ "An error occurred while fetching containers"
+ )
+ });
+ }
+ break;
+ }
+ }
+ };
+
+ try {
+ const res = await api.post>(
+ `/site/${site.siteId}/docker/trigger`
+ );
+ // TODO: identify a way to poll the server for latest container list periodically?
+ await fetchContainerList();
+ return res.data.data;
+ } catch (error) {
+ console.error("Failed to trigger Docker containers:", error);
+ }
+ },
+ [api, site.siteId, isEnabled, isAvailable]
+ );
+
+ // 2. Docker socket status monitoring
+ useEffect(() => {
+ if (!isEnabled || isAvailable) {
+ return;
+ }
+
+ checkDockerSocket();
+ getDockerSocketStatus();
+
+ }, [isEnabled, isAvailable, checkDockerSocket, getDockerSocketStatus]);
+
+ return {
+ isEnabled,
+ isAvailable: isEnabled && isAvailable,
+ socketPath,
+ containers,
+ check: checkDockerSocket,
+ status: getDockerSocketStatus,
+ fetchContainers: getContainers
+ };
+}
diff --git a/src/providers/ResourceProvider.tsx b/src/providers/ResourceProvider.tsx
index cd6229a4..37e30580 100644
--- a/src/providers/ResourceProvider.tsx
+++ b/src/providers/ResourceProvider.tsx
@@ -3,18 +3,21 @@
import ResourceContext from "@app/contexts/resourceContext";
import { GetResourceAuthInfoResponse } from "@server/routers/resource";
import { GetResourceResponse } from "@server/routers/resource/getResource";
+import { GetSiteResponse } from "@server/routers/site";
import { useState } from "react";
interface ResourceProviderProps {
children: React.ReactNode;
resource: GetResourceResponse;
+ site: GetSiteResponse | null;
authInfo: GetResourceAuthInfoResponse;
}
export function ResourceProvider({
children,
+ site,
resource: serverResource,
- authInfo: serverAuthInfo,
+ authInfo: serverAuthInfo
}: ResourceProviderProps) {
const [resource, setResource] =
useState(serverResource);
@@ -34,7 +37,7 @@ export function ResourceProvider({
return {
...prev,
- ...updatedResource,
+ ...updatedResource
};
});
};
@@ -53,14 +56,14 @@ export function ResourceProvider({
return {
...prev,
- ...updatedAuthInfo,
+ ...updatedAuthInfo
};
});
};
return (
{children}