mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat(volumes): read only mount mode
This commit is contained in:
@@ -4,10 +4,8 @@ import type { Options as ClientOptions, TDataShape, Client } from "./client";
|
||||
import type {
|
||||
RegisterData,
|
||||
RegisterResponses,
|
||||
RegisterErrors,
|
||||
LoginData,
|
||||
LoginResponses,
|
||||
LoginErrors,
|
||||
LogoutData,
|
||||
LogoutResponses,
|
||||
GetMeData,
|
||||
@@ -16,7 +14,6 @@ import type {
|
||||
GetStatusResponses,
|
||||
ChangePasswordData,
|
||||
ChangePasswordResponses,
|
||||
ChangePasswordErrors,
|
||||
ListVolumesData,
|
||||
ListVolumesResponses,
|
||||
CreateVolumeData,
|
||||
@@ -97,7 +94,7 @@ export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends
|
||||
* Register a new user
|
||||
*/
|
||||
export const register = <ThrowOnError extends boolean = false>(options?: Options<RegisterData, ThrowOnError>) => {
|
||||
return (options?.client ?? _heyApiClient).post<RegisterResponses, RegisterErrors, ThrowOnError>({
|
||||
return (options?.client ?? _heyApiClient).post<RegisterResponses, unknown, ThrowOnError>({
|
||||
url: "/api/v1/auth/register",
|
||||
...options,
|
||||
headers: {
|
||||
@@ -111,7 +108,7 @@ export const register = <ThrowOnError extends boolean = false>(options?: Options
|
||||
* Login with username and password
|
||||
*/
|
||||
export const login = <ThrowOnError extends boolean = false>(options?: Options<LoginData, ThrowOnError>) => {
|
||||
return (options?.client ?? _heyApiClient).post<LoginResponses, LoginErrors, ThrowOnError>({
|
||||
return (options?.client ?? _heyApiClient).post<LoginResponses, unknown, ThrowOnError>({
|
||||
url: "/api/v1/auth/login",
|
||||
...options,
|
||||
headers: {
|
||||
@@ -157,7 +154,7 @@ export const getStatus = <ThrowOnError extends boolean = false>(options?: Option
|
||||
export const changePassword = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<ChangePasswordData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).post<ChangePasswordResponses, ChangePasswordErrors, ThrowOnError>({
|
||||
return (options?.client ?? _heyApiClient).post<ChangePasswordResponses, unknown, ThrowOnError>({
|
||||
url: "/api/v1/auth/change-password",
|
||||
...options,
|
||||
headers: {
|
||||
|
||||
@@ -10,13 +10,6 @@ export type RegisterData = {
|
||||
url: "/api/v1/auth/register";
|
||||
};
|
||||
|
||||
export type RegisterErrors = {
|
||||
/**
|
||||
* Invalid request or username already exists
|
||||
*/
|
||||
400: unknown;
|
||||
};
|
||||
|
||||
export type RegisterResponses = {
|
||||
/**
|
||||
* User created successfully
|
||||
@@ -43,13 +36,6 @@ export type LoginData = {
|
||||
url: "/api/v1/auth/login";
|
||||
};
|
||||
|
||||
export type LoginErrors = {
|
||||
/**
|
||||
* Invalid credentials
|
||||
*/
|
||||
401: unknown;
|
||||
};
|
||||
|
||||
export type LoginResponses = {
|
||||
/**
|
||||
* Login successful
|
||||
@@ -135,17 +121,6 @@ export type ChangePasswordData = {
|
||||
url: "/api/v1/auth/change-password";
|
||||
};
|
||||
|
||||
export type ChangePasswordErrors = {
|
||||
/**
|
||||
* Invalid current password or validation error
|
||||
*/
|
||||
400: unknown;
|
||||
/**
|
||||
* Not authenticated
|
||||
*/
|
||||
401: unknown;
|
||||
};
|
||||
|
||||
export type ChangePasswordResponses = {
|
||||
/**
|
||||
* Password changed successfully
|
||||
|
||||
@@ -202,6 +202,31 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="readOnly"
|
||||
defaultValue={false}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Read-only Mode</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.value ?? false}
|
||||
onChange={(e) => field.onChange(e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<span className="text-sm">Mount volume as read-only</span>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Prevent any modifications to the volume. Recommended for backup sources and sensitive data.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -301,6 +326,31 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="readOnly"
|
||||
defaultValue={false}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Read-only Mode</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.value ?? false}
|
||||
onChange={(e) => field.onChange(e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<span className="text-sm">Mount volume as read-only</span>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Prevent any modifications to the volume. Recommended for backup sources and sensitive data.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -422,6 +472,31 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="readOnly"
|
||||
defaultValue={false}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Read-only Mode</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.value ?? false}
|
||||
onChange={(e) => field.onChange(e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<span className="text-sm">Mount volume as read-only</span>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Prevent any modifications to the volume. Recommended for backup sources and sensitive data.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -17,16 +17,20 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "~/components/ui/alert-dialog";
|
||||
import type { Snapshot } from "~/lib/types";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip";
|
||||
import type { Snapshot, Volume } from "~/lib/types";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
snapshot: Snapshot;
|
||||
repositoryName: string;
|
||||
volume?: Volume;
|
||||
}
|
||||
|
||||
export const SnapshotFileBrowser = (props: Props) => {
|
||||
const { snapshot, repositoryName } = props;
|
||||
const { snapshot, repositoryName, volume } = props;
|
||||
|
||||
const isReadOnly = volume?.config && "readOnly" in volume.config && volume.config.readOnly === true;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||
@@ -195,11 +199,28 @@ export const SnapshotFileBrowser = (props: Props) => {
|
||||
<CardDescription>{`Viewing snapshot from ${new Date(snapshot?.time ?? 0).toLocaleString()}`}</CardDescription>
|
||||
</div>
|
||||
{selectedPaths.size > 0 && (
|
||||
<Button onClick={handleRestoreClick} variant="primary" size="sm" disabled={isRestoring}>
|
||||
{isRestoring
|
||||
? "Restoring..."
|
||||
: `Restore ${selectedPaths.size} selected ${selectedPaths.size === 1 ? "item" : "items"}`}
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span tabIndex={isReadOnly ? 0 : undefined}>
|
||||
<Button
|
||||
onClick={handleRestoreClick}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
disabled={isRestoring || isReadOnly}
|
||||
>
|
||||
{isRestoring
|
||||
? "Restoring..."
|
||||
: `Restore ${selectedPaths.size} selected ${selectedPaths.size === 1 ? "item" : "items"}`}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
{isReadOnly && (
|
||||
<TooltipContent className="text-center">
|
||||
<p>Volume is mounted as read-only.</p>
|
||||
<p>Please remount with read-only disabled to restore files.</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -181,6 +181,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
||||
key={selectedSnapshot?.short_id}
|
||||
snapshot={selectedSnapshot}
|
||||
repositoryName={schedule.repository.name}
|
||||
volume={schedule.volume}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user