Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { PermissionMode } from "@/components/shared/permission-mode";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
Expand Down Expand Up @@ -49,13 +51,35 @@ const mountSchema = z.object({
mountPath: z.string().min(1, "Mount path required"),
});

const uidSchema = z.preprocess(
(v) => (v === "" || v === null ? undefined : v),
z.coerce.number().int().positive().optional(),
);
const gidSchema = z.preprocess(
(v) => (v === "" || v === null ? undefined : v),
z.coerce.number().int().positive().optional(),
);
const modeSchema = z.preprocess(
(v) => (v === "" || v === null ? undefined : v),
z
.string()
.regex(/^[0-7]{3,4}$/, "Use octal digits like 644 or 0775")
.optional(),
);
const ownershipSchema = z.object({
uid: uidSchema,
gid: gidSchema,
mode: modeSchema,
});

const mySchema = z.discriminatedUnion("type", [
z
.object({
type: z.literal("bind"),
hostPath: z.string().min(1, "Host path required"),
})
.merge(mountSchema),
.merge(mountSchema)
.merge(ownershipSchema),
z
.object({
type: z.literal("volume"),
Expand All @@ -67,14 +91,16 @@ const mySchema = z.discriminatedUnion("type", [
"Invalid volume name. Use letters, numbers, '._-' and start with a letter/number.",
),
})
.merge(mountSchema),
.merge(mountSchema)
.merge(ownershipSchema),
z
.object({
type: z.literal("file"),
filePath: z.string().min(1, "File path required"),
content: z.string().optional(),
})
.merge(mountSchema),
.merge(mountSchema)
.merge(ownershipSchema),
]);

type AddMount = z.infer<typeof mySchema>;
Expand All @@ -92,6 +118,9 @@ export const AddVolumes = ({
type: serviceType === "compose" ? "file" : "bind",
hostPath: "",
mountPath: serviceType === "compose" ? "/" : "",
uid: undefined,
gid: undefined,
mode: "",
},
resolver: zodResolver(mySchema),
});
Expand All @@ -109,6 +138,9 @@ export const AddVolumes = ({
mountPath: data.mountPath,
type: data.type,
serviceType,
uid: data.uid,
gid: data.gid,
mode: data.mode,
})
.then(() => {
toast.success("Mount Created");
Expand All @@ -124,6 +156,9 @@ export const AddVolumes = ({
mountPath: data.mountPath,
type: data.type,
serviceType,
uid: data.uid,
gid: data.gid,
mode: data.mode,
})
.then(() => {
toast.success("Mount Created");
Expand All @@ -140,6 +175,9 @@ export const AddVolumes = ({
filePath: data.filePath,
type: data.type,
serviceType,
uid: data.uid,
gid: data.gid,
mode: data.mode,
})
.then(() => {
toast.success("Mount Created");
Expand All @@ -163,13 +201,13 @@ export const AddVolumes = ({
<DialogTitle>Volumes / Mounts</DialogTitle>
</DialogHeader>
{/* {isError && (
<div className="flex items-center flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)} */}
<div className="flex items-center flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)} */}

<Form {...form}>
<form
Expand Down Expand Up @@ -378,6 +416,72 @@ PORT=3000
)}
/>
)}

<div className="mt-4">
<FormLabel className="text-base font-medium">
Ownership / Permissions (optional)
</FormLabel>
<p className="text-sm text-muted-foreground">
If unset, mounts remain root-owned with default permissions.
</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mt-2">
<FormField
control={form.control}
name="uid"
render={({ field }) => (
<FormItem>
<FormLabel>UID</FormLabel>
<FormControl>
<Input placeholder="e.g. 1000" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="gid"
render={({ field }) => (
<FormItem>
<FormLabel>GID</FormLabel>
<FormControl>
<Input placeholder="e.g. 1000" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="mode"
render={({ field }) => (
<FormItem className="sm:col-span-3">
<FormLabel className="flex items-center gap-2">
<span>Mode</span>
<Badge variant="blank" className="text-sm h-5 px-2">
{(() => {
const v = field.value ?? "";
if (!/^\d{3,4}$/.test(v)) return "644";
if (v.length === 4 && v[0] === "0")
return v.slice(1);
return v.slice(-3);
})()}
</Badge>
</FormLabel>

<FormControl>
<PermissionMode
value={field.value ?? ""}
onChange={(v) => field.onChange(v)}
showAdvancedInput={false}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
</div>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Package, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
Expand Down Expand Up @@ -92,7 +93,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
>
{/* <Package className="size-8 self-center text-muted-foreground" /> */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 flex-col gap-4 sm:gap-8">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Type</span>
<span className="text-sm text-muted-foreground">
Expand Down Expand Up @@ -139,6 +140,69 @@ export const ShowVolumes = ({ id, type }: Props) => {
{mount.mountPath}
</span>
</div>

<div className="flex flex-col gap-1">
<span className="font-medium">Ownership</span>
<span className="text-sm text-muted-foreground">
{(() => {
const uid = mount.uid ?? null;
const gid = mount.gid ?? null;
if (uid === null && gid === null) return "—";
if (uid !== null && gid !== null)
return `${uid}:${gid}`;
if (uid !== null) return `${uid}`;
return `:${gid}`;
})()}
</span>
</div>

<div className="flex flex-col gap-1">
<span className="font-medium">Mode</span>
<div className="flex items-center gap-2">
<Badge variant="blank" className="text-sm h-5 px-2">
{(() => {
const v = mount.mode ?? "";
if (!/^\d{3,4}$/.test(v)) return "644";
if (v.length === 4 && v[0] === "0")
return v.slice(1);
return v.slice(-3);
})()}
</Badge>
<span className="text-xs text-muted-foreground">
{(() => {
const v = mount.mode ?? "";
const n = (() => {
if (!/^\d{3,4}$/.test(v)) return "644";
if (v.length === 4 && v[0] === "0")
return v.slice(1);
return v.slice(-3);
})();
const toBits = (d: number) => ({
r: !!(d & 4),
w: !!(d & 2),
x: !!(d & 1),
});
const [o, g, t] = n
.split("")
.map((d) => Number.parseInt(d, 10)) as [
number,
number,
number,
];
const fmt = (b: {
r: boolean;
w: boolean;
x: boolean;
}) =>
`${b.r ? "r" : "-"}${b.w ? "w" : "-"}${b.x ? "x" : "-"}`;
const b0 = toBits(o);
const b1 = toBits(g);
const b2 = toBits(t);
return `${fmt(b0)} ${fmt(b1)} ${fmt(b2)}`;
})()}
</span>
</div>
</div>
</div>
<div className="flex flex-row gap-1">
{canCreate && (
Expand Down
Loading
Loading