mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
refactor: autoRemount as boolean
This commit is contained in:
@@ -13,7 +13,7 @@ export type ListVolumesResponses = {
|
|||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
volumes: Array<{
|
volumes: Array<{
|
||||||
autoRemount: 0 | 1;
|
autoRemount: boolean;
|
||||||
config:
|
config:
|
||||||
| {
|
| {
|
||||||
backend: "directory";
|
backend: "directory";
|
||||||
@@ -209,7 +209,7 @@ export type GetVolumeResponses = {
|
|||||||
used: number;
|
used: number;
|
||||||
};
|
};
|
||||||
volume: {
|
volume: {
|
||||||
autoRemount: 0 | 1;
|
autoRemount: boolean;
|
||||||
config:
|
config:
|
||||||
| {
|
| {
|
||||||
backend: "directory";
|
backend: "directory";
|
||||||
|
|||||||
25
apps/client/app/components/onoff.tsx
Normal file
25
apps/client/app/components/onoff.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { Switch } from "./ui/switch";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOn: boolean;
|
||||||
|
toggle: (v: boolean) => void;
|
||||||
|
enabledLabel: string;
|
||||||
|
disabledLabel: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OnOff = ({ isOn, toggle, enabledLabel, disabledLabel }: Props) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-semibold uppercase tracking-wide transition-colors",
|
||||||
|
isOn
|
||||||
|
? "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-200"
|
||||||
|
: "border-muted bg-muted/40 text-muted-foreground dark:border-muted/60 dark:bg-muted/10",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>{isOn ? enabledLabel : disabledLabel}</span>
|
||||||
|
<Switch checked={isOn} onCheckedChange={toggle} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,29 +1,26 @@
|
|||||||
import * as React from "react"
|
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
function Switch({
|
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||||
className,
|
return (
|
||||||
...props
|
<SwitchPrimitive.Root
|
||||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
data-slot="switch"
|
||||||
return (
|
className={cn(
|
||||||
<SwitchPrimitive.Root
|
"cursor-pointer peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
data-slot="switch"
|
className,
|
||||||
className={cn(
|
)}
|
||||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
{...props}
|
||||||
className
|
>
|
||||||
)}
|
<SwitchPrimitive.Thumb
|
||||||
{...props}
|
data-slot="switch-thumb"
|
||||||
>
|
className={cn(
|
||||||
<SwitchPrimitive.Thumb
|
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
|
||||||
data-slot="switch-thumb"
|
)}
|
||||||
className={cn(
|
/>
|
||||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
</SwitchPrimitive.Root>
|
||||||
)}
|
);
|
||||||
/>
|
|
||||||
</SwitchPrimitive.Root>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Switch }
|
export { Switch };
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { HeartIcon } from "lucide-react";
|
import { HeartIcon } from "lucide-react";
|
||||||
|
import { OnOff } from "~/components/onoff";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
import { Switch } from "~/components/ui/switch";
|
|
||||||
import type { Volume } from "~/lib/types";
|
import type { Volume } from "~/lib/types";
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
volume: Volume;
|
volume: Volume;
|
||||||
@@ -34,17 +33,7 @@ export const HealthchecksCard = ({ volume }: Props) => {
|
|||||||
|
|
||||||
<span className="flex justify-between items-center gap-2">
|
<span className="flex justify-between items-center gap-2">
|
||||||
<span className="text-sm">Remount on error</span>
|
<span className="text-sm">Remount on error</span>
|
||||||
<div
|
<OnOff isOn={volume.autoRemount} toggle={() => {}} enabledLabel="Enabled" disabledLabel="Paused" />
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-semibold uppercase tracking-wide transition-colors",
|
|
||||||
Boolean(volume.autoRemount)
|
|
||||||
? "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-200"
|
|
||||||
: "border-muted bg-muted/40 text-muted-foreground dark:border-muted/60 dark:bg-muted/10",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>{volume.autoRemount ? "Enabled" : "Paused"}</span>
|
|
||||||
<Switch checked={Boolean(volume.autoRemount)} onCheckedChange={() => {}} />
|
|
||||||
</div>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{volume.status !== "unmounted" && (
|
{volume.status !== "unmounted" && (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
import { OnOff } from "~/components/onoff";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card";
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
||||||
@@ -152,17 +153,12 @@ export const VolumeBackupsTabContent = ({ volume }: Props) => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col items-center space-y-2">
|
<FormItem className="flex flex-col items-center space-y-2">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div
|
<OnOff
|
||||||
className={cn(
|
isOn={field.value}
|
||||||
"flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-semibold uppercase tracking-wide transition-colors",
|
toggle={field.onChange}
|
||||||
field.value
|
enabledLabel="Enabled"
|
||||||
? "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-200"
|
disabledLabel="Paused"
|
||||||
: "border-muted bg-muted/40 text-muted-foreground dark:border-muted/60 dark:bg-muted/10",
|
/>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>{field.value ? "Enabled" : "Paused"}</span>
|
|
||||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { CodeBlock } from "~/components/ui/code-block";
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
|
||||||
import type { Volume } from "~/lib/types";
|
import type { Volume } from "~/lib/types";
|
||||||
import { getContainersUsingVolumeOptions } from "../../../api-client/@tanstack/react-query.gen";
|
import { getContainersUsingVolumeOptions } from "../../../api-client/@tanstack/react-query.gen";
|
||||||
|
import { Unplug } from "lucide-react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
volume: Volume;
|
volume: Volume;
|
||||||
@@ -78,39 +79,44 @@ export const DockerTabContent = ({ volume }: Props) => {
|
|||||||
<CardDescription>List of Docker containers mounting this volume.</CardDescription>
|
<CardDescription>List of Docker containers mounting this volume.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="space-y-4 text-sm">
|
<CardContent className="space-y-4 text-sm h-full">
|
||||||
{isLoading && <div>Loading containers...</div>}
|
{isLoading && <div>Loading containers...</div>}
|
||||||
{error && <div className="text-destructive">Failed to load containers: {String(error)}</div>}
|
{error && <div className="text-destructive">Failed to load containers: {String(error)}</div>}
|
||||||
{!isLoading && !error && containers.length === 0 && (
|
{!isLoading && !error && containers.length === 0 && (
|
||||||
<div>No containers are currently using this volume.</div>
|
<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 && (
|
{!isLoading && !error && containers.length > 0 && (
|
||||||
<Table>
|
<div className="max-h-130 overflow-y-auto">
|
||||||
<TableHeader>
|
<Table>
|
||||||
<TableRow>
|
<TableHeader>
|
||||||
<TableHead>Name</TableHead>
|
<TableRow>
|
||||||
<TableHead>ID</TableHead>
|
<TableHead>Name</TableHead>
|
||||||
<TableHead>State</TableHead>
|
<TableHead>ID</TableHead>
|
||||||
<TableHead>Image</TableHead>
|
<TableHead>State</TableHead>
|
||||||
</TableRow>
|
<TableHead>Image</TableHead>
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{containers.map((container) => (
|
|
||||||
<TableRow key={container.id}>
|
|
||||||
<TableCell>{container.name}</TableCell>
|
|
||||||
<TableCell>{container.id.slice(0, 12)}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<span
|
|
||||||
className={`px-2 py-1 rounded-full text-xs font-medium ${getStateClass(container.state)}`}
|
|
||||||
>
|
|
||||||
{container.state}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{container.image}</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableHeader>
|
||||||
</TableBody>
|
<TableBody className="text-sm">
|
||||||
</Table>
|
{containers.map((container) => (
|
||||||
|
<TableRow key={container.id}>
|
||||||
|
<TableCell>{container.name}</TableCell>
|
||||||
|
<TableCell>{container.id.slice(0, 12)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span
|
||||||
|
className={`px-2 py-1 rounded-full text-xs font-medium ${getStateClass(container.state)}`}
|
||||||
|
>
|
||||||
|
{container.state}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{container.image}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
20
apps/server/drizzle/0004_wealthy_tomas.sql
Normal file
20
apps/server/drizzle/0004_wealthy_tomas.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_volumes_table` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`path` text NOT NULL,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`status` text DEFAULT 'unmounted' NOT NULL,
|
||||||
|
`last_error` text,
|
||||||
|
`last_health_check` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`config` text NOT NULL,
|
||||||
|
`auto_remount` integer DEFAULT true NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_volumes_table`("id", "name", "path", "type", "status", "last_error", "last_health_check", "created_at", "updated_at", "config", "auto_remount") SELECT "id", "name", "path", "type", "status", "last_error", "last_health_check", "created_at", "updated_at", "config", "auto_remount" FROM `volumes_table`;--> statement-breakpoint
|
||||||
|
DROP TABLE `volumes_table`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_volumes_table` RENAME TO `volumes_table`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `volumes_table_name_unique` ON `volumes_table` (`name`);
|
||||||
118
apps/server/drizzle/meta/0004_snapshot.json
Normal file
118
apps/server/drizzle/meta/0004_snapshot.json
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "0b087a68-fbc6-4647-a6dc-e6322a3d4ee3",
|
||||||
|
"prevId": "b7f1ccb8-7bb3-486f-a103-b95b331a121f",
|
||||||
|
"tables": {
|
||||||
|
"volumes_table": {
|
||||||
|
"name": "volumes_table",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'unmounted'"
|
||||||
|
},
|
||||||
|
"last_error": {
|
||||||
|
"name": "last_error",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"last_health_check": {
|
||||||
|
"name": "last_health_check",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"name": "config",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"auto_remount": {
|
||||||
|
"name": "auto_remount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"volumes_table_name_unique": {
|
||||||
|
"name": "volumes_table_name_unique",
|
||||||
|
"columns": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,13 @@
|
|||||||
"when": 1758653407064,
|
"when": 1758653407064,
|
||||||
"tag": "0003_mature_hellcat",
|
"tag": "0003_mature_hellcat",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1758961535488,
|
||||||
|
"tag": "0004_wealthy_tomas",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ export const volumesTable = sqliteTable("volumes_table", {
|
|||||||
createdAt: int("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
createdAt: int("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
||||||
updatedAt: int("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
updatedAt: int("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
||||||
config: text("config", { mode: "json" }).$type<typeof volumeConfigSchema.inferOut>().notNull(),
|
config: text("config", { mode: "json" }).$type<typeof volumeConfigSchema.inferOut>().notNull(),
|
||||||
autoRemount: int("auto_remount").$type<1 | 0>().notNull().default(1),
|
autoRemount: int("auto_remount", { mode: "boolean" }).notNull().default(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Volume = typeof volumesTable.$inferSelect;
|
export type Volume = typeof volumesTable.$inferSelect;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const volumeSchema = type({
|
|||||||
updatedAt: "number",
|
updatedAt: "number",
|
||||||
lastHealthCheck: "number",
|
lastHealthCheck: "number",
|
||||||
config: volumeConfigSchema,
|
config: volumeConfigSchema,
|
||||||
autoRemount: "0 | 1",
|
autoRemount: "boolean",
|
||||||
});
|
});
|
||||||
|
|
||||||
export type VolumeDto = typeof volumeSchema.infer;
|
export type VolumeDto = typeof volumeSchema.infer;
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ const testConnection = async (backendConfig: BackendConfig) => {
|
|||||||
type: backendConfig.backend,
|
type: backendConfig.backend,
|
||||||
status: "unmounted" as const,
|
status: "unmounted" as const,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
autoRemount: 0 as const,
|
autoRemount: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const backend = createVolumeBackend(mockVolume);
|
const backend = createVolumeBackend(mockVolume);
|
||||||
|
|||||||
Reference in New Issue
Block a user