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: {
|
||||
volumes: Array<{
|
||||
autoRemount: 0 | 1;
|
||||
autoRemount: boolean;
|
||||
config:
|
||||
| {
|
||||
backend: "directory";
|
||||
@@ -209,7 +209,7 @@ export type GetVolumeResponses = {
|
||||
used: number;
|
||||
};
|
||||
volume: {
|
||||
autoRemount: 0 | 1;
|
||||
autoRemount: boolean;
|
||||
config:
|
||||
| {
|
||||
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({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
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>
|
||||
)
|
||||
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"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",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
export { Switch };
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { HeartIcon } from "lucide-react";
|
||||
import { OnOff } from "~/components/onoff";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Switch } from "~/components/ui/switch";
|
||||
import type { Volume } from "~/lib/types";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
type Props = {
|
||||
volume: Volume;
|
||||
@@ -34,17 +33,7 @@ export const HealthchecksCard = ({ volume }: Props) => {
|
||||
|
||||
<span className="flex justify-between items-center gap-2">
|
||||
<span className="text-sm">Remount on error</span>
|
||||
<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",
|
||||
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>
|
||||
<OnOff isOn={volume.autoRemount} toggle={() => {}} enabledLabel="Enabled" disabledLabel="Paused" />
|
||||
</span>
|
||||
</div>
|
||||
{volume.status !== "unmounted" && (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { OnOff } from "~/components/onoff";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
||||
@@ -152,17 +153,12 @@ export const VolumeBackupsTabContent = ({ volume }: Props) => {
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-center space-y-2">
|
||||
<FormControl>
|
||||
<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",
|
||||
field.value
|
||||
? "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>{field.value ? "Enabled" : "Paused"}</span>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</div>
|
||||
<OnOff
|
||||
isOn={field.value}
|
||||
toggle={field.onChange}
|
||||
enabledLabel="Enabled"
|
||||
disabledLabel="Paused"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 { Unplug } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
volume: Volume;
|
||||
@@ -78,39 +79,44 @@ export const DockerTabContent = ({ volume }: Props) => {
|
||||
<CardDescription>List of Docker containers mounting this volume.</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4 text-sm">
|
||||
<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 && (
|
||||
<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 && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>State</TableHead>
|
||||
<TableHead>Image</TableHead>
|
||||
</TableRow>
|
||||
</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>
|
||||
<div className="max-h-130 overflow-y-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>State</TableHead>
|
||||
<TableHead>Image</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHeader>
|
||||
<TableBody className="text-sm">
|
||||
{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>
|
||||
</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,
|
||||
"tag": "0003_mature_hellcat",
|
||||
"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())`),
|
||||
updatedAt: int("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
||||
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;
|
||||
|
||||
@@ -13,7 +13,7 @@ const volumeSchema = type({
|
||||
updatedAt: "number",
|
||||
lastHealthCheck: "number",
|
||||
config: volumeConfigSchema,
|
||||
autoRemount: "0 | 1",
|
||||
autoRemount: "boolean",
|
||||
});
|
||||
|
||||
export type VolumeDto = typeof volumeSchema.infer;
|
||||
|
||||
@@ -155,7 +155,7 @@ const testConnection = async (backendConfig: BackendConfig) => {
|
||||
type: backendConfig.backend,
|
||||
status: "unmounted" as const,
|
||||
lastError: null,
|
||||
autoRemount: 0 as const,
|
||||
autoRemount: true,
|
||||
};
|
||||
|
||||
const backend = createVolumeBackend(mockVolume);
|
||||
|
||||
Reference in New Issue
Block a user