mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 11:47:47 +01:00
Add max file size settings for user (#167)
This commit is contained in:
@@ -20,6 +20,7 @@ interface WorkspaceStorageAggregateRow {
|
||||
interface UserStorageRow {
|
||||
id: string;
|
||||
storage_limit: string;
|
||||
max_file_size: string;
|
||||
storage_used: string | null;
|
||||
}
|
||||
|
||||
@@ -79,6 +80,7 @@ export const workspaceStorageGetRoute: FastifyPluginCallbackZod = (
|
||||
SELECT
|
||||
u.id,
|
||||
u.storage_limit,
|
||||
u.max_file_size,
|
||||
COALESCE(c.value, '0') as storage_used
|
||||
FROM users u
|
||||
LEFT JOIN counters c ON c.key = CONCAT(u.id, '.storage.used')
|
||||
@@ -103,7 +105,7 @@ export const workspaceStorageGetRoute: FastifyPluginCallbackZod = (
|
||||
})
|
||||
.map(([subtype, size]) => ({
|
||||
subtype: subtype as FileSubtype,
|
||||
size: size.toString(),
|
||||
storageUsed: size.toString(),
|
||||
}));
|
||||
|
||||
const users = usersWithStorage.rows
|
||||
@@ -119,13 +121,14 @@ export const workspaceStorageGetRoute: FastifyPluginCallbackZod = (
|
||||
})
|
||||
.map((user) => ({
|
||||
id: user.id,
|
||||
used: user.storage_used ?? '0',
|
||||
limit: user.storage_limit,
|
||||
storageUsed: user.storage_used ?? '0',
|
||||
storageLimit: user.storage_limit,
|
||||
maxFileSize: user.max_file_size,
|
||||
}));
|
||||
|
||||
return {
|
||||
limit: workspace.storage_limit,
|
||||
used: totalUsed.toString(),
|
||||
storageLimit: workspace.storage_limit,
|
||||
storageUsed: totalUsed.toString(),
|
||||
subtypes: subtypes,
|
||||
users: users,
|
||||
};
|
||||
|
||||
@@ -55,13 +55,22 @@ export const userStorageUpdateRoute: FastifyPluginCallbackZod = (
|
||||
});
|
||||
}
|
||||
|
||||
const limit = BigInt(input.limit);
|
||||
const storageLimit = BigInt(input.storageLimit);
|
||||
const maxFileSize = BigInt(input.maxFileSize);
|
||||
|
||||
if (maxFileSize > storageLimit) {
|
||||
return reply.code(400).send({
|
||||
code: ApiErrorCode.ValidationError,
|
||||
message: 'Max file size cannot be larger than storage limit.',
|
||||
});
|
||||
}
|
||||
|
||||
const updatedUser = await database
|
||||
.updateTable('users')
|
||||
.returningAll()
|
||||
.set({
|
||||
storage_limit: limit.toString(),
|
||||
storage_limit: storageLimit.toString(),
|
||||
max_file_size: maxFileSize.toString(),
|
||||
updated_at: new Date(),
|
||||
updated_by: user.account_id,
|
||||
})
|
||||
|
||||
@@ -19,7 +19,8 @@ export class UserStorageUpdateMutationHandler
|
||||
|
||||
try {
|
||||
const body: UserStorageUpdateInput = {
|
||||
limit: input.limit,
|
||||
storageLimit: input.storageLimit,
|
||||
maxFileSize: input.maxFileSize,
|
||||
};
|
||||
|
||||
const output = await workspace.account.client
|
||||
|
||||
@@ -38,8 +38,8 @@ export class UserStorageGetQueryHandler
|
||||
return {
|
||||
hasChanges: true,
|
||||
result: {
|
||||
limit: '0',
|
||||
used: '0',
|
||||
storageLimit: '0',
|
||||
storageUsed: '0',
|
||||
subtypes: [],
|
||||
},
|
||||
};
|
||||
@@ -111,7 +111,7 @@ export class UserStorageGetQueryHandler
|
||||
|
||||
const subtypes: {
|
||||
subtype: FileSubtype;
|
||||
size: string;
|
||||
storageUsed: string;
|
||||
}[] = [];
|
||||
let totalUsed = 0n;
|
||||
|
||||
@@ -120,14 +120,14 @@ export class UserStorageGetQueryHandler
|
||||
const sizeString = row.total_size || '0';
|
||||
subtypes.push({
|
||||
subtype,
|
||||
size: sizeString,
|
||||
storageUsed: sizeString,
|
||||
});
|
||||
totalUsed += BigInt(sizeString);
|
||||
}
|
||||
|
||||
return {
|
||||
limit: workspace.storageLimit,
|
||||
used: totalUsed.toString(),
|
||||
storageLimit: workspace.storageLimit,
|
||||
storageUsed: totalUsed.toString(),
|
||||
subtypes,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import { Event } from '@colanode/client/types/events';
|
||||
import { WorkspaceStorageGetOutput } from '@colanode/core';
|
||||
|
||||
const EMPTY_STORAGE_OUTPUT: WorkspaceStorageGetOutput = {
|
||||
limit: '0',
|
||||
used: '0',
|
||||
storageLimit: '0',
|
||||
storageUsed: '0',
|
||||
subtypes: [],
|
||||
users: [],
|
||||
};
|
||||
|
||||
@@ -3,7 +3,8 @@ export type UserStorageUpdateMutationInput = {
|
||||
accountId: string;
|
||||
workspaceId: string;
|
||||
userId: string;
|
||||
limit: string;
|
||||
storageLimit: string;
|
||||
maxFileSize: string;
|
||||
};
|
||||
|
||||
export type UserStorageUpdateMutationOutput = {
|
||||
|
||||
@@ -7,11 +7,11 @@ export type UserStorageGetQueryInput = {
|
||||
};
|
||||
|
||||
export type UserStorageGetQueryOutput = {
|
||||
limit: string;
|
||||
used: string;
|
||||
storageLimit: string;
|
||||
storageUsed: string;
|
||||
subtypes: {
|
||||
subtype: FileSubtype;
|
||||
size: string;
|
||||
storageUsed: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { fileSubtypeSchema } from '@colanode/core/types/files';
|
||||
|
||||
export const workspaceStorageFileSubtypeSchema = z.object({
|
||||
subtype: fileSubtypeSchema,
|
||||
size: z.string(),
|
||||
storageUsed: z.string(),
|
||||
});
|
||||
|
||||
export type WorkspaceStorageFileSubtype = z.infer<
|
||||
@@ -13,15 +13,16 @@ export type WorkspaceStorageFileSubtype = z.infer<
|
||||
|
||||
export const workspaceStorageUserSchema = z.object({
|
||||
id: z.string(),
|
||||
used: z.string(),
|
||||
limit: z.string(),
|
||||
storageUsed: z.string(),
|
||||
storageLimit: z.string(),
|
||||
maxFileSize: z.string(),
|
||||
});
|
||||
|
||||
export type WorkspaceStorageUser = z.infer<typeof workspaceStorageUserSchema>;
|
||||
|
||||
export const workspaceStorageGetOutputSchema = z.object({
|
||||
limit: z.string().nullable().optional(),
|
||||
used: z.string(),
|
||||
storageLimit: z.string().nullable().optional(),
|
||||
storageUsed: z.string(),
|
||||
subtypes: z.array(workspaceStorageFileSubtypeSchema),
|
||||
users: z.array(workspaceStorageUserSchema),
|
||||
});
|
||||
|
||||
@@ -106,7 +106,8 @@ export const userRoleUpdateInputSchema = z.object({
|
||||
export type UserRoleUpdateInput = z.infer<typeof userRoleUpdateInputSchema>;
|
||||
|
||||
export const userStorageUpdateInputSchema = z.object({
|
||||
limit: z.string(),
|
||||
storageLimit: z.string(),
|
||||
maxFileSize: z.string(),
|
||||
});
|
||||
|
||||
export type UserStorageUpdateInput = z.infer<
|
||||
|
||||
@@ -37,31 +37,33 @@ const subtypeMetadata: SubtypeMetadata[] = [
|
||||
|
||||
interface StorageSubtype {
|
||||
subtype: string;
|
||||
size: string;
|
||||
storageUsed: string;
|
||||
}
|
||||
|
||||
interface StorageStatsProps {
|
||||
usedBytes: bigint;
|
||||
limitBytes: bigint | null;
|
||||
storageUsed: string;
|
||||
storageLimit: string | null;
|
||||
subtypes: StorageSubtype[];
|
||||
}
|
||||
|
||||
export const StorageStats = ({
|
||||
usedBytes,
|
||||
limitBytes,
|
||||
storageUsed,
|
||||
storageLimit,
|
||||
subtypes,
|
||||
}: StorageStatsProps) => {
|
||||
const usedPercentage = limitBytes
|
||||
? bigintToPercent(limitBytes, usedBytes)
|
||||
const usedPercentage = storageLimit
|
||||
? bigintToPercent(BigInt(storageLimit), BigInt(storageUsed))
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<span className="text-2xl font-medium">{formatBytes(usedBytes)}</span>
|
||||
<span className="text-2xl font-medium">
|
||||
{formatBytes(BigInt(storageUsed))}
|
||||
</span>
|
||||
<span className="text-xl text-muted-foreground">
|
||||
{' '}
|
||||
of {limitBytes ? formatBytes(limitBytes) : 'Unlimited'}
|
||||
of {storageLimit ? formatBytes(BigInt(storageLimit)) : 'Unlimited'}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
({usedPercentage}%) used
|
||||
@@ -69,12 +71,14 @@ export const StorageStats = ({
|
||||
</div>
|
||||
<div className="w-full h-3 bg-gray-200 rounded-full overflow-hidden flex">
|
||||
{subtypes.map((subtype) => {
|
||||
const size = BigInt(subtype.size);
|
||||
if (size === BigInt(0)) {
|
||||
const storageUsed = BigInt(subtype.storageUsed);
|
||||
if (storageUsed === BigInt(0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const percentage = limitBytes ? bigintToPercent(limitBytes, size) : 0;
|
||||
const percentage = storageLimit
|
||||
? bigintToPercent(BigInt(storageLimit), storageUsed)
|
||||
: 0;
|
||||
const metadata = subtypeMetadata.find(
|
||||
(m) => m.subtype === subtype.subtype
|
||||
);
|
||||
@@ -85,7 +89,7 @@ export const StorageStats = ({
|
||||
|
||||
const name = metadata.name;
|
||||
const color = metadata.color;
|
||||
const title = `${name}: ${formatBytes(BigInt(subtype.size))}`;
|
||||
const title = `${name}: ${formatBytes(BigInt(subtype.storageUsed))}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -106,8 +110,8 @@ export const StorageStats = ({
|
||||
|
||||
<div className="mb-6 space-y-2">
|
||||
{subtypes.map((subtype) => {
|
||||
const size = BigInt(subtype.size);
|
||||
if (size === BigInt(0)) {
|
||||
const storageUsed = BigInt(subtype.storageUsed);
|
||||
if (storageUsed === BigInt(0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -132,7 +136,7 @@ export const StorageStats = ({
|
||||
<span className="text-sm">{name}</span>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatBytes(BigInt(subtype.size))}
|
||||
{formatBytes(BigInt(subtype.storageUsed))}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,12 +13,10 @@ export const UserStorageStats = () => {
|
||||
});
|
||||
|
||||
const data = userStorageGetQuery.data ?? {
|
||||
limit: '0',
|
||||
used: '0',
|
||||
storageLimit: '0',
|
||||
storageUsed: '0',
|
||||
subtypes: [],
|
||||
};
|
||||
const usedBytes = BigInt(data.used);
|
||||
const limitBytes = BigInt(data.limit);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -32,8 +30,8 @@ export const UserStorageStats = () => {
|
||||
</div>
|
||||
) : (
|
||||
<StorageStats
|
||||
usedBytes={usedBytes}
|
||||
limitBytes={limitBytes}
|
||||
storageUsed={data.storageUsed}
|
||||
storageLimit={data.storageLimit}
|
||||
subtypes={data.subtypes}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -26,13 +26,11 @@ export const WorkspaceStorageStats = () => {
|
||||
);
|
||||
|
||||
const data = workspaceStorageGetQuery.data ?? {
|
||||
limit: '0',
|
||||
used: '0',
|
||||
storageLimit: '0',
|
||||
storageUsed: '0',
|
||||
subtypes: [],
|
||||
users: [],
|
||||
};
|
||||
const usedBytes = BigInt(data.used);
|
||||
const limitBytes = data.limit ? BigInt(data.limit) : null;
|
||||
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
@@ -61,8 +59,8 @@ export const WorkspaceStorageStats = () => {
|
||||
.with({ isPending: false, isError: false }, () => (
|
||||
<>
|
||||
<StorageStats
|
||||
usedBytes={usedBytes}
|
||||
limitBytes={limitBytes}
|
||||
storageUsed={data.storageUsed}
|
||||
storageLimit={data.storageLimit ?? null}
|
||||
subtypes={data.subtypes}
|
||||
/>
|
||||
<WorkspaceStorageUserTable
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Settings } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { formatBytes } from '@colanode/core';
|
||||
import { formatBytes, WorkspaceStorageUser } from '@colanode/core';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
import {
|
||||
@@ -17,22 +17,11 @@ import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { bigintToPercent, cn } from '@colanode/ui/lib/utils';
|
||||
|
||||
interface WorkspaceStorageUser {
|
||||
id: string;
|
||||
used: string;
|
||||
limit: string;
|
||||
}
|
||||
|
||||
interface UserStorageProgressBarProps {
|
||||
used: bigint;
|
||||
limit: bigint;
|
||||
}
|
||||
|
||||
const UserStorageProgressBar = ({
|
||||
used,
|
||||
limit,
|
||||
}: UserStorageProgressBarProps) => {
|
||||
const percentage = limit > 0n ? bigintToPercent(limit, used) : 0;
|
||||
storageUsed,
|
||||
storageLimit,
|
||||
}: WorkspaceStorageUser) => {
|
||||
const percentage = bigintToPercent(BigInt(storageLimit), BigInt(storageUsed));
|
||||
|
||||
const getBarColor = () => {
|
||||
if (percentage >= 90) return 'bg-red-500';
|
||||
@@ -43,7 +32,7 @@ const UserStorageProgressBar = ({
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium">{formatBytes(used)}</span>
|
||||
<span className="font-medium">{formatBytes(BigInt(storageUsed))}</span>
|
||||
<span className="text-muted-foreground">
|
||||
({percentage.toFixed(1)}%)
|
||||
</span>
|
||||
@@ -81,8 +70,8 @@ const WorkspaceStorageUserRow = ({
|
||||
const email = userQuery.data?.email ?? '';
|
||||
const avatar = userQuery.data?.avatar ?? null;
|
||||
|
||||
const usedBytes = BigInt(user.used);
|
||||
const limitBytes = BigInt(user.limit);
|
||||
const storageLimitBytes = BigInt(user.storageLimit);
|
||||
const maxFileSizeBytes = user.maxFileSize ? BigInt(user.maxFileSize) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -99,10 +88,17 @@ const WorkspaceStorageUserRow = ({
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span className="text-sm font-medium">{formatBytes(limitBytes)}</span>
|
||||
<span className="text-sm font-medium">
|
||||
{maxFileSizeBytes ? formatBytes(maxFileSizeBytes) : '#'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span className="text-sm font-medium">
|
||||
{formatBytes(storageLimitBytes)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="min-w-[200px] text-center">
|
||||
<UserStorageProgressBar used={usedBytes} limit={limitBytes} />
|
||||
<UserStorageProgressBar {...user} />
|
||||
</TableCell>
|
||||
<TableCell className="w-10 text-right">
|
||||
<Button
|
||||
@@ -117,8 +113,7 @@ const WorkspaceStorageUserRow = ({
|
||||
</TableRow>
|
||||
{openUpdateDialog && (
|
||||
<WorkspaceStorageUserUpdateDialog
|
||||
userId={user.id}
|
||||
limit={user.limit}
|
||||
user={user}
|
||||
open={openUpdateDialog}
|
||||
onOpenChange={setOpenUpdateDialog}
|
||||
onUpdate={() => {
|
||||
@@ -149,6 +144,7 @@ export const WorkspaceStorageUserTable = ({
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead className="text-center">File Size Limit</TableHead>
|
||||
<TableHead className="text-center">Total Storage</TableHead>
|
||||
<TableHead className="text-center">Used Storage</TableHead>
|
||||
<TableHead className="w-10 text-right"></TableHead>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
import { WorkspaceStorageUser } from '@colanode/core';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
import {
|
||||
@@ -75,20 +76,19 @@ const formatBytes = (bytes: string): string => {
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
limit: z.string().min(1, 'Storage limit is required'),
|
||||
storageLimit: z.string().min(1, 'Storage limit is required'),
|
||||
maxFileSize: z.string().min(1, 'Max file size is required'),
|
||||
});
|
||||
|
||||
interface WorkspaceStorageUserUpdateDialogProps {
|
||||
userId: string;
|
||||
limit: string;
|
||||
user: WorkspaceStorageUser;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
export const WorkspaceStorageUserUpdateDialog = ({
|
||||
userId,
|
||||
limit,
|
||||
user,
|
||||
open,
|
||||
onOpenChange,
|
||||
onUpdate,
|
||||
@@ -96,15 +96,21 @@ export const WorkspaceStorageUserUpdateDialog = ({
|
||||
const workspace = useWorkspace();
|
||||
const { mutate, isPending } = useMutation();
|
||||
|
||||
const initialLimit = convertBytesToUnit(limit);
|
||||
const initialStorageLimit = convertBytesToUnit(user.storageLimit);
|
||||
const initialMaxFileSize = convertBytesToUnit(user.maxFileSize);
|
||||
|
||||
const [limitUnit, setLimitUnit] = useState(initialLimit.unit);
|
||||
const [storageLimitUnit, setStorageLimitUnit] = useState(
|
||||
initialStorageLimit.unit
|
||||
);
|
||||
const [maxFileSizeUnit, setMaxFileSizeUnit] = useState(
|
||||
initialMaxFileSize.unit
|
||||
);
|
||||
|
||||
const userQuery = useQuery({
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
userId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const name = userQuery.data?.name ?? 'Unknown';
|
||||
@@ -114,13 +120,15 @@ export const WorkspaceStorageUserUpdateDialog = ({
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
limit: initialLimit.value,
|
||||
storageLimit: initialStorageLimit.value,
|
||||
maxFileSize: initialMaxFileSize.value,
|
||||
},
|
||||
});
|
||||
|
||||
const handleCancel = () => {
|
||||
form.reset();
|
||||
setLimitUnit(initialLimit.unit);
|
||||
setStorageLimitUnit(initialStorageLimit.unit);
|
||||
setMaxFileSizeUnit(initialMaxFileSize.unit);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
@@ -130,16 +138,23 @@ export const WorkspaceStorageUserUpdateDialog = ({
|
||||
}
|
||||
|
||||
const apiValues = {
|
||||
limit: convertUnitToBytes(values.limit, limitUnit),
|
||||
storageLimit: convertUnitToBytes(values.storageLimit, storageLimitUnit),
|
||||
maxFileSize: convertUnitToBytes(values.maxFileSize, maxFileSizeUnit),
|
||||
};
|
||||
|
||||
if (BigInt(apiValues.maxFileSize) > BigInt(apiValues.storageLimit)) {
|
||||
toast.error('Max file size cannot be larger than storage limit');
|
||||
return;
|
||||
}
|
||||
|
||||
mutate({
|
||||
input: {
|
||||
type: 'user.storage.update',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
userId,
|
||||
limit: apiValues.limit,
|
||||
userId: user.id,
|
||||
storageLimit: apiValues.storageLimit,
|
||||
maxFileSize: apiValues.maxFileSize,
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('User storage settings updated');
|
||||
@@ -152,8 +167,10 @@ export const WorkspaceStorageUserUpdateDialog = ({
|
||||
});
|
||||
};
|
||||
|
||||
const unit = UNITS.find((u) => u.value === limitUnit);
|
||||
const unitLabel = unit?.label ?? 'bytes';
|
||||
const storageLimitUnitData = UNITS.find((u) => u.value === storageLimitUnit);
|
||||
const storageLimitUnitLabel = storageLimitUnitData?.label ?? 'bytes';
|
||||
const maxFileSizeUnitData = UNITS.find((u) => u.value === maxFileSizeUnit);
|
||||
const maxFileSizeUnitLabel = maxFileSizeUnitData?.label ?? 'bytes';
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -161,11 +178,11 @@ export const WorkspaceStorageUserUpdateDialog = ({
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update storage settings</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update the storage limit for this user
|
||||
Update the storage limits for this user
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex items-center space-x-3 py-4 border-b">
|
||||
<Avatar id={userId} name={name} avatar={avatar} />
|
||||
<Avatar id={user.id} name={name} avatar={avatar} />
|
||||
<div className="flex-grow min-w-0">
|
||||
<p className="text-sm font-medium leading-none truncate">{name}</p>
|
||||
<p className="text-sm text-muted-foreground truncate">{email}</p>
|
||||
@@ -179,7 +196,7 @@ export const WorkspaceStorageUserUpdateDialog = ({
|
||||
<div className="flex-grow space-y-6 py-2 pb-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="limit"
|
||||
name="storageLimit"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Storage Limit</FormLabel>
|
||||
@@ -190,8 +207,8 @@ export const WorkspaceStorageUserUpdateDialog = ({
|
||||
placeholder="5"
|
||||
{...field}
|
||||
className="flex-1"
|
||||
min="0"
|
||||
step="0.01"
|
||||
min="1"
|
||||
step="1"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -199,7 +216,7 @@ export const WorkspaceStorageUserUpdateDialog = ({
|
||||
variant="outline"
|
||||
className="w-20 justify-between"
|
||||
>
|
||||
{unitLabel}
|
||||
{storageLimitUnitLabel}
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -207,11 +224,11 @@ export const WorkspaceStorageUserUpdateDialog = ({
|
||||
{UNITS.map((unit) => (
|
||||
<DropdownMenuItem
|
||||
key={unit.value}
|
||||
onClick={() => setLimitUnit(unit.value)}
|
||||
onClick={() => setStorageLimitUnit(unit.value)}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<span>{unit.label}</span>
|
||||
{limitUnit === unit.value && (
|
||||
{storageLimitUnit === unit.value && (
|
||||
<Check className="h-4 w-4" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
@@ -223,7 +240,62 @@ export const WorkspaceStorageUserUpdateDialog = ({
|
||||
<div className="text-xs text-muted-foreground">
|
||||
={' '}
|
||||
{formatBytes(
|
||||
convertUnitToBytes(field.value || '0', limitUnit)
|
||||
convertUnitToBytes(field.value || '0', storageLimitUnit)
|
||||
)}{' '}
|
||||
bytes
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="maxFileSize"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Max File Size</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex space-x-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="10"
|
||||
{...field}
|
||||
value={field.value || ''}
|
||||
className="flex-1"
|
||||
min="1"
|
||||
step="1"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-20 justify-between"
|
||||
>
|
||||
{maxFileSizeUnitLabel}
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{UNITS.map((unit) => (
|
||||
<DropdownMenuItem
|
||||
key={unit.value}
|
||||
onClick={() => setMaxFileSizeUnit(unit.value)}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<span>{unit.label}</span>
|
||||
{maxFileSizeUnit === unit.value && (
|
||||
<Check className="h-4 w-4" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</FormControl>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
={' '}
|
||||
{formatBytes(
|
||||
convertUnitToBytes(field.value || '0', maxFileSizeUnit)
|
||||
)}{' '}
|
||||
bytes
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import semver from 'semver';
|
||||
|
||||
export const FeatureVersions = {
|
||||
'workspace.storage.management': '0.2.9',
|
||||
'workspace.storage.management': '0.3.0',
|
||||
} as const;
|
||||
|
||||
export type FeatureKey = keyof typeof FeatureVersions;
|
||||
|
||||
Reference in New Issue
Block a user