Improve UX for storage settings file size upload limimt input field

This commit is contained in:
Joshen Lim committed 2022-07-06 14:17:38 +08:00
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)
}
-2
View File
@@ -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