mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: system optional capabilities
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
||||
updateBackupSchedule,
|
||||
getBackupScheduleForVolume,
|
||||
runBackupNow,
|
||||
getSystemInfo,
|
||||
} from "../sdk.gen";
|
||||
import { queryOptions, type UseMutationOptions, type DefaultError } from "@tanstack/react-query";
|
||||
import type {
|
||||
@@ -90,6 +91,7 @@ import type {
|
||||
GetBackupScheduleForVolumeData,
|
||||
RunBackupNowData,
|
||||
RunBackupNowResponse,
|
||||
GetSystemInfoData,
|
||||
} from "../types.gen";
|
||||
import { client as _heyApiClient } from "../client.gen";
|
||||
|
||||
@@ -1078,3 +1080,23 @@ export const runBackupNowMutation = (
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
export const getSystemInfoQueryKey = (options?: Options<GetSystemInfoData>) => createQueryKey("getSystemInfo", options);
|
||||
|
||||
/**
|
||||
* Get system information including available capabilities
|
||||
*/
|
||||
export const getSystemInfoOptions = (options?: Options<GetSystemInfoData>) => {
|
||||
return queryOptions({
|
||||
queryFn: async ({ queryKey, signal }) => {
|
||||
const { data } = await getSystemInfo({
|
||||
...options,
|
||||
...queryKey[0],
|
||||
signal,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
queryKey: getSystemInfoQueryKey(options),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -72,6 +72,8 @@ import type {
|
||||
GetBackupScheduleForVolumeResponses,
|
||||
RunBackupNowData,
|
||||
RunBackupNowResponses,
|
||||
GetSystemInfoData,
|
||||
GetSystemInfoResponses,
|
||||
} from "./types.gen";
|
||||
import { client as _heyApiClient } from "./client.gen";
|
||||
|
||||
@@ -513,3 +515,15 @@ export const runBackupNow = <ThrowOnError extends boolean = false>(
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get system information including available capabilities
|
||||
*/
|
||||
export const getSystemInfo = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<GetSystemInfoData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).get<GetSystemInfoResponses, unknown, ThrowOnError>({
|
||||
url: "/api/v1/system/info",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1418,6 +1418,26 @@ export type RunBackupNowResponses = {
|
||||
|
||||
export type RunBackupNowResponse = RunBackupNowResponses[keyof RunBackupNowResponses];
|
||||
|
||||
export type GetSystemInfoData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: "/api/v1/system/info";
|
||||
};
|
||||
|
||||
export type GetSystemInfoResponses = {
|
||||
/**
|
||||
* System information with enabled capabilities
|
||||
*/
|
||||
200: {
|
||||
capabilities: {
|
||||
docker: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type GetSystemInfoResponse = GetSystemInfoResponses[keyof GetSystemInfoResponses];
|
||||
|
||||
export type ClientOptions = {
|
||||
baseUrl: "http://192.168.2.42:4096" | (string & {});
|
||||
};
|
||||
|
||||
43
apps/client/app/components/ui/alert.tsx
Normal file
43
apps/client/app/components/ui/alert.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
|
||||
));
|
||||
Alert.displayName = "Alert";
|
||||
|
||||
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />
|
||||
),
|
||||
);
|
||||
AlertTitle.displayName = "AlertTitle";
|
||||
|
||||
const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
|
||||
),
|
||||
);
|
||||
AlertDescription.displayName = "AlertDescription";
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Unplug } from "lucide-react";
|
||||
import { AlertCircle, Unplug } from "lucide-react";
|
||||
import * as YML from "yaml";
|
||||
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { CodeBlock } from "~/components/ui/code-block";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
|
||||
import type { Volume } from "~/lib/types";
|
||||
import { getContainersUsingVolumeOptions } from "../../../api-client/@tanstack/react-query.gen";
|
||||
import { getContainersUsingVolumeOptions, getSystemInfoOptions } from "../../../api-client/@tanstack/react-query.gen";
|
||||
|
||||
type Props = {
|
||||
volume: Volume;
|
||||
@@ -28,6 +29,11 @@ export const DockerTabContent = ({ volume }: Props) => {
|
||||
|
||||
const dockerRunCommand = `docker run -v im-${volume.name}:/path/in/container nginx:latest`;
|
||||
|
||||
const { data: systemInfo } = useQuery({
|
||||
...getSystemInfoOptions(),
|
||||
staleTime: 60000, // Cache for 1 minute
|
||||
});
|
||||
|
||||
const {
|
||||
data: containersData,
|
||||
isLoading,
|
||||
@@ -39,6 +45,7 @@ export const DockerTabContent = ({ volume }: Props) => {
|
||||
});
|
||||
|
||||
const containers = containersData || [];
|
||||
const dockerAvailable = systemInfo?.data?.capabilities?.docker ?? true;
|
||||
|
||||
const getStateClass = (state: string) => {
|
||||
switch (state) {
|
||||
@@ -53,6 +60,28 @@ export const DockerTabContent = ({ volume }: Props) => {
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,_1fr)_minmax(0,_1fr)]">
|
||||
{!dockerAvailable && (
|
||||
<div className="xl:col-span-2">
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Docker Integration Unavailable</AlertTitle>
|
||||
<AlertDescription>
|
||||
Docker integration features are currently disabled. To enable them, you need to mount the following paths
|
||||
in your docker-compose.yml:
|
||||
<ul className="mt-2 list-inside list-disc space-y-1 text-sm">
|
||||
<li>
|
||||
<code className="rounded bg-muted px-1 py-0.5">/var/run/docker.sock:/var/run/docker.sock</code>
|
||||
</li>
|
||||
<li>
|
||||
<code className="rounded bg-muted px-1 py-0.5">/run/docker/plugins:/run/docker/plugins</code>
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-2 text-sm">After adding these mounts, restart the Ironmount container.</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Plug-and-play Docker integration</CardTitle>
|
||||
@@ -83,19 +112,31 @@ export const DockerTabContent = ({ volume }: Props) => {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Containers Using This Volume</CardTitle>
|
||||
<CardDescription>List of Docker containers mounting this volume.</CardDescription>
|
||||
<CardDescription>
|
||||
{dockerAvailable
|
||||
? "List of Docker containers mounting this volume."
|
||||
: "Docker integration is unavailable - enable it to see container information."}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4 text-sm h-full">
|
||||
{isLoading && <div>Loading containers...</div>}
|
||||
{error && <div className="text-destructive">Failed to load containers: {String(error)}</div>}
|
||||
{!isLoading && !error && containers.length === 0 && (
|
||||
{!dockerAvailable && (
|
||||
<div className="flex flex-col items-center justify-center text-center h-full">
|
||||
<AlertCircle className="mb-4 h-5 w-5 text-muted-foreground" />
|
||||
<p className="text-muted-foreground">Docker integration is not available.</p>
|
||||
</div>
|
||||
)}
|
||||
{dockerAvailable && isLoading && <div>Loading containers...</div>}
|
||||
{dockerAvailable && error && (
|
||||
<div className="text-destructive">Failed to load containers: {String(error)}</div>
|
||||
)}
|
||||
{dockerAvailable && !isLoading && !error && containers.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center text-center h-full">
|
||||
<Unplug className="mb-4 h-5 w-5 text-muted-foreground" />
|
||||
<p className="text-muted-foreground">No Docker containers are currently using this volume.</p>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && containers.length > 0 && (
|
||||
{dockerAvailable && !isLoading && !error && containers.length > 0 && (
|
||||
<div className="max-h-130 overflow-y-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
|
||||
Reference in New Issue
Block a user