mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
Improve UX for storage settings file size upload limimt input field
This commit is contained in:
1 parent
376b1d0b8b
commit
c00c8fd90d
4 files changed
+192
-60
No files matched your search
@@ -0,0 +1,8 @@
|
||||
export const STORAGE_FILE_SIZE_LIMIT_MAX_BYTES = 5 * 1024 * 1024 * 1024 // 5 GB
|
||||
|
||||
export enum StorageSizeUnits {
|
||||
BYTES = 'bytes',
|
||||
KB = 'KB',
|
||||
MB = 'MB',
|
||||
GB = 'GB',
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
import useSWR from 'swr'
|
||||
import { FC, useState } from 'react'
|
||||
import { AutoField } from 'uniforms-bootstrap4'
|
||||
import { Typography } from '@supabase/ui'
|
||||
import { Button, Form, Input, Listbox } from '@supabase/ui'
|
||||
|
||||
import { API_URL, STORAGE_FILE_SIZE_LIMIT_MAX_BYTES } from 'lib/constants'
|
||||
import { patch, get } from 'lib/common/fetch'
|
||||
import { useStore } from 'hooks'
|
||||
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
|
||||
import { patch, get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import { convertFromBytes, convertToBytes } from './StorageSettings.utils'
|
||||
import { StorageSizeUnits, STORAGE_FILE_SIZE_LIMIT_MAX_BYTES } from './StorageSettings.constants'
|
||||
|
||||
const StorageSettings: FC<any> = ({ projectRef }) => {
|
||||
const { data, error } = useSWR(`${API_URL}/projects/${projectRef}/config?app=storage`, get)
|
||||
|
||||
if (error) {
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="p-6 mx-auto sm:w-full md:w-3/4 text-center">
|
||||
<Typography.Title level={3}>Error loading storage settings</Typography.Title>
|
||||
<div className="mx-auto p-6 text-center sm:w-full md:w-3/4">
|
||||
<p className="text-sm">Error loading storage settings</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="p-6 mx-auto sm:w-full md:w-3/4 text-center">
|
||||
<Typography.Title level={3}>Loading...</Typography.Title>
|
||||
<div className="mx-auto p-6 text-center sm:w-full md:w-3/4">
|
||||
<p className="text-sm">Loading...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -32,64 +32,171 @@ const StorageSettings: FC<any> = ({ projectRef }) => {
|
||||
}
|
||||
|
||||
const StorageConfig = ({ config, projectRef }: any) => {
|
||||
const { fileSizeLimit, isFreeTier } = config
|
||||
const { value, unit } = convertFromBytes(fileSizeLimit)
|
||||
|
||||
const { ui } = useStore()
|
||||
const [selectedUnit, setSelectedUnit] = useState(unit)
|
||||
let initialValues = { fileSizeLimit: value, unformattedFileSizeLimit: fileSizeLimit }
|
||||
|
||||
const { isFreeTier } = config
|
||||
const [updates, setUpdates] = useState(config)
|
||||
const formattedMaxSizeBytes = `${new Intl.NumberFormat('en-US').format(
|
||||
STORAGE_FILE_SIZE_LIMIT_MAX_BYTES
|
||||
)} bytes`
|
||||
|
||||
const updateConfig = async (updatedConfig: any) => {
|
||||
try {
|
||||
const response = await patch(
|
||||
`${API_URL}/projects/${projectRef}/config?app=storage`,
|
||||
updatedConfig
|
||||
)
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
const onValidate = (values: any) => {
|
||||
const errors = {} as any
|
||||
if (values.fileSizeLimit > STORAGE_FILE_SIZE_LIMIT_MAX_BYTES) {
|
||||
errors['fileSizeLimit'] = `Must be a number up to ${new Intl.NumberFormat('en-US').format(
|
||||
STORAGE_FILE_SIZE_LIMIT_MAX_BYTES
|
||||
)} bytes.`
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
const onSubmit = async (values: any) => {
|
||||
const payload = { fileSizeLimit: convertToBytes(values.fileSizeLimit, selectedUnit) }
|
||||
const errors = onValidate(payload)
|
||||
|
||||
if (errors.fileSizeLimit) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Upload file size limit must be up to 5GB (${formattedMaxSizeBytes})`,
|
||||
})
|
||||
} else {
|
||||
const res = await patch(`${API_URL}/projects/${projectRef}/config?app=storage`, payload)
|
||||
if (res?.error) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to update storage settings: ${res.error.message}`,
|
||||
})
|
||||
} else {
|
||||
ui.setNotification({ category: 'success', message: 'Settings saved' })
|
||||
const updatedValue = convertFromBytes(res.fileSizeLimit)
|
||||
initialValues = {
|
||||
fileSizeLimit: updatedValue.value,
|
||||
unformattedFileSizeLimit: res.fileSizeLimit,
|
||||
}
|
||||
ui.setNotification({ category: 'success', message: 'Sucessfully updated settings' })
|
||||
}
|
||||
} catch (error: any) {
|
||||
ui.setNotification({ category: 'error', message: `Update config failed: ${error.message}` })
|
||||
}
|
||||
}
|
||||
|
||||
// [Joshen] To be refactored using FormContainer, FormPanel, FormContent etc once
|
||||
// Jonny's auth config refactor PR goes in
|
||||
return (
|
||||
<div className="flex flex-col w-full min-h-full">
|
||||
<SchemaFormPanel
|
||||
title="Settings"
|
||||
schema={{
|
||||
properties: {
|
||||
fileSizeLimit: {
|
||||
help: 'The maximum size in bytes of a file that can be uploaded, up to 5 GB.',
|
||||
maximum: STORAGE_FILE_SIZE_LIMIT_MAX_BYTES,
|
||||
minimum: 0,
|
||||
title: 'Upload file size limit',
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
required: ['fileSizeLimit'],
|
||||
type: 'object',
|
||||
<div className="mx-auto w-[56rem] max-w-4xl px-5 pt-12 pb-20">
|
||||
<Form validateOnBlur initialValues={initialValues} validate={onValidate} onSubmit={onSubmit}>
|
||||
{({
|
||||
values,
|
||||
isSubmitting,
|
||||
handleReset,
|
||||
}: {
|
||||
values: any
|
||||
isSubmitting: boolean
|
||||
handleReset: () => void
|
||||
}) => {
|
||||
const hasChanges =
|
||||
initialValues.unformattedFileSizeLimit !==
|
||||
convertToBytes(values.fileSizeLimit, selectedUnit)
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<h3 className="text-scale-1200 mb-2 text-xl">Storage settings</h3>
|
||||
<div className="text-scale-900 text-sm">
|
||||
Configure your project's storage settings
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-20">
|
||||
<div
|
||||
className={[
|
||||
'bg-scale-100 dark:bg-scale-300',
|
||||
'border-scale-400 overflow-hidden',
|
||||
'rounded-md border shadow',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="divide-scale-400 flex flex-col gap-0 divide-y">
|
||||
<div className="block grid grid-cols-12 gap-6 px-8 py-8 lg:gap-12">
|
||||
<div className="relative col-span-12 flex flex-col gap-6 lg:col-span-4">
|
||||
<p className="text-sm">Upload file size limit</p>
|
||||
</div>
|
||||
<div className="relative col-span-12 flex flex-col gap-x-6 gap-y-2 lg:col-span-8">
|
||||
<div className="col-span-12 grid grid-cols-12 gap-2">
|
||||
<div className="col-span-8">
|
||||
<Input
|
||||
id="fileSizeLimit"
|
||||
name="fileSizeLimit"
|
||||
type="number"
|
||||
disabled={isFreeTier}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
<Listbox
|
||||
disabled={isFreeTier}
|
||||
value={selectedUnit}
|
||||
onChange={setSelectedUnit}
|
||||
>
|
||||
{Object.values(StorageSizeUnits).map((unit: string) => (
|
||||
<Listbox.Option
|
||||
key={unit}
|
||||
disabled={isFreeTier}
|
||||
label={unit}
|
||||
value={unit}
|
||||
>
|
||||
<div>{unit}</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
{selectedUnit !== StorageSizeUnits.BYTES &&
|
||||
`Equivalent to ${convertToBytes(
|
||||
values.fileSizeLimit,
|
||||
selectedUnit
|
||||
).toLocaleString()} bytes. `}
|
||||
Maximum size in bytes of a file that can be uploaded is 5 GB (
|
||||
{formattedMaxSizeBytes}).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isFreeTier && (
|
||||
<div className="px-6 pb-6">
|
||||
<UpgradeToPro
|
||||
primaryText="Free Plan has a fixed upload file size limit of 50 MB."
|
||||
projectRef={projectRef}
|
||||
secondaryText="Please upgrade to Pro plan for a configurable upload file size limit of up to 5 GB."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-scale-400 border-t" />
|
||||
<div className="flex justify-between py-4 px-8">
|
||||
<div className="flex w-full items-center justify-end gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="default"
|
||||
htmlType="reset"
|
||||
onClick={() => handleReset()}
|
||||
disabled={!hasChanges && hasChanges !== undefined}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={!hasChanges && hasChanges !== undefined}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
model={updates}
|
||||
onSubmit={(model: any) => updateConfig(model)}
|
||||
onReset={() => setUpdates(config)}
|
||||
>
|
||||
<AutoField
|
||||
disabled={isFreeTier}
|
||||
name="fileSizeLimit"
|
||||
showInlineError
|
||||
errorMessage={`Must be a number up to ${new Intl.NumberFormat('en-US').format(
|
||||
STORAGE_FILE_SIZE_LIMIT_MAX_BYTES
|
||||
)} bytes.`}
|
||||
/>
|
||||
{isFreeTier && (
|
||||
<UpgradeToPro
|
||||
primaryText="Free Plan has a fixed upload file size limit of 50 MB."
|
||||
projectRef={projectRef}
|
||||
secondaryText="Please upgrade to Pro plan for a configurable upload file size limit of up to 5 GB."
|
||||
/>
|
||||
)}
|
||||
</SchemaFormPanel>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { StorageSizeUnits } from './StorageSettings.constants'
|
||||
|
||||
const k = 1024
|
||||
|
||||
// [JOSHEN TODO] Write tests for this
|
||||
export const convertFromBytes = (bytes: number) => {
|
||||
// Up to GB since that's our storage upload limit
|
||||
if (bytes === 0) return { value: 0, unit: StorageSizeUnits.BYTES }
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
const unit = Object.values(StorageSizeUnits)[i]
|
||||
const value = bytes / Math.pow(k, i)
|
||||
return { value, unit }
|
||||
}
|
||||
|
||||
export const convertToBytes = (size: number, unit: StorageSizeUnits) => {
|
||||
const i = Object.values(StorageSizeUnits).indexOf(unit)
|
||||
return size * Math.pow(k, i)
|
||||
}
|
||||
@@ -140,6 +140,4 @@ export const PASSWORD_STRENGTH_PERCENTAGE = {
|
||||
|
||||
export const DEFAULT_FREE_PROJECTS_LIMIT = 2
|
||||
|
||||
export const STORAGE_FILE_SIZE_LIMIT_MAX_BYTES = 5 * 1024 * 1024 * 1024 // 5 GB
|
||||
|
||||
export const DEFAULT_PROJECT_API_SERVICE_ID = 1
|
||||
Reference in new issue
Block a user