From 17736e6459314956052236619c2455b5bae0b028 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Wed, 12 Oct 2022 14:11:34 -0500 Subject: [PATCH 1/5] date picker uses local state instead of formik --- .../form/fields/useDateTimeRangePicker.tsx | 226 ++++++++---------- .../instances/instance/tabs/MetricsTab.tsx | 33 +-- libs/ui/lib/listbox/Listbox.tsx | 2 +- 3 files changed, 118 insertions(+), 143 deletions(-) diff --git a/app/components/form/fields/useDateTimeRangePicker.tsx b/app/components/form/fields/useDateTimeRangePicker.tsx index aec2324c9c..71b2f7b24b 100644 --- a/app/components/form/fields/useDateTimeRangePicker.tsx +++ b/app/components/form/fields/useDateTimeRangePicker.tsx @@ -1,13 +1,9 @@ -import * as Yup from 'yup' +// import * as Yup from 'yup' import { format, subDays, subHours } from 'date-fns' -import { Form, Formik } from 'formik' -import { useMemo, useRef, useState } from 'react' +import { useMemo, useState } from 'react' -import { useInterval } from '@oxide/ui' -import { Button } from '@oxide/ui' - -import { ListboxField } from './ListboxField' -import { TextField } from './TextField' +import { Listbox, useInterval } from '@oxide/ui' +import { Button, TextInput } from '@oxide/ui' const dateForInput = (d: Date) => format(d, "yyyy-MM-dd'T'HH:mm") @@ -33,14 +29,14 @@ const computeStart: Record Date> = { last30Days: (now) => subDays(now, 30), } -const rangeKeys = rangePresets.map((item) => item.value) +// const rangeKeys = rangePresets.map((item) => item.value) /** Validate that they're Dates and end is after start */ -const dateRangeSchema = Yup.object({ - preset: Yup.string().oneOf(rangeKeys), - startTime: Yup.date(), - endTime: Yup.date().min(Yup.ref('startTime'), 'End time must be later than start time'), -}) +// const dateRangeSchema = Yup.object({ +// preset: Yup.string().oneOf(rangeKeys), +// startTime: Yup.date(), +// endTime: Yup.date().min(Yup.ref('startTime'), 'End time must be later than start time'), +// }) // Limitations: // - list of presets is hard-coded @@ -61,129 +57,115 @@ type Args = { * a JSX element to render. */ export function useDateTimeRangePicker({ initialPreset, slideInterval }: Args) { + const [preset, setPreset] = useState(initialPreset) + // default endTime is now, i.e., mount time const now = useMemo(() => new Date(), []) - const [startTime, setStartTime] = useState(computeStart[initialPreset](now)) + const initialStartTime = computeStart[initialPreset](now) + const [startTime, setStartTime] = useState(initialStartTime) const [endTime, setEndTime] = useState(now) - // only exists to make current preset value available to window slider - const presetRef = useRef(initialPreset) + // needs a separate pair of values because they can be edited without + // submitting and updating the graphs + const [startTimeInput, setStartTimeInput] = useState(initialStartTime) + const [endTimeInput, setEndTimeInput] = useState(now) + + // TODO: validate inputs on change and display error someplace + + const customInputsDirty = startTime !== startTimeInput || endTime !== endTimeInput + + const enableInputs = preset === 'custom' useInterval( () => { - if (presetRef.current !== 'custom') { + if (preset !== 'custom') { const now = new Date() - setStartTime(computeStart[presetRef.current](now)) + const newStartTime = computeStart[preset](now) + setStartTime(newStartTime) setEndTime(now) + setStartTimeInput(newStartTime) + setEndTimeInput(now) } }, - slideInterval && presetRef.current !== 'custom' ? slideInterval : null + slideInterval && preset !== 'custom' ? slideInterval : null ) - // We're using Formik to manage the state of the inputs, but this is not - // strictly necessary. It's convenient while we're using `TextField` with - // `type=datetime-local` because we get validationSchema and error display for - // free. Once we use a React date picker library, we can make the inputs - // controlled and manage everything through regular state. I think that will - // be a little cleaner. const dateTimeRangePicker = ( - { - setStartTime(new Date(startTime)) - setEndTime(new Date(endTime)) - }} - validationSchema={dateRangeSchema} - > - {({ values, setFieldValue, submitForm }) => { - // whether the time fields have been changed from what is displayed - const customInputsDirty = - values.startTime !== dateForInput(startTime) || - values.endTime !== dateForInput(endTime) - - // on presets, inputs visible (showing current range) but disabled - const enableInputs = values.preset === 'custom' - - function setRangeValues(startTime: Date, endTime: Date) { - setFieldValue('startTime', dateForInput(startTime), true) - setFieldValue('endTime', dateForInput(endTime), true) - } - - return ( -
- { - if (item) { - // only done to make the value available to the range window slider interval - presetRef.current = item.value as RangeKeyAll - - if (item.value !== 'custom') { - const now = new Date() - const newStartTime = computeStart[item.value as RangeKey](now) - setRangeValues(newStartTime, now) - // goofy, but I like the idea of going through the submit - // pathway instead of duplicating the setStates - submitForm() - // TODO: if input is invalid while on custom, e.g., - // because end is before start, changing to a preset does - // not clear the error. changing a second time does - } - } - }} - required - /> - - {/* TODO: real React date picker lib instead of native for consistent styling across browsers */} - {/* TODO: the field labels look pretty stupid in this context, fix that. probably leave them - there for a11y purposes but hide them for sighted users */} - - - {/* mt-6 is a hack to fake alignment with the inputs. this will change so it doesn't matter */} - {/* TODO: fix goofy ass button text. use icons? tooltips to explain? lord */} - {enableInputs && ( - - )} - {enableInputs && ( - - )} - - ) - }} -
+
+ { + if (item) { + // only done to make the value available to the range window slider interval + setPreset(item.value as RangeKeyAll) + + if (item.value !== 'custom') { + const now = new Date() + const newStartTime = computeStart[item.value as RangeKey](now) + setStartTime(newStartTime) + setEndTime(now) + setStartTimeInput(newStartTime) + setEndTimeInput(now) + } + } + }} + /> + + {/* TODO: real React date picker lib instead of native for consistent styling across browsers */} + setStartTimeInput(e.target.valueAsDate!)} + /> + setEndTimeInput(e.target.valueAsDate!)} + /> + {/* TODO: fix goofy ass button text. use icons? tooltips to explain? lord */} + {enableInputs && ( + + )} + {enableInputs && ( + + )} + ) return { startTime, endTime, dateTimeRangePicker } diff --git a/app/pages/project/instances/instance/tabs/MetricsTab.tsx b/app/pages/project/instances/instance/tabs/MetricsTab.tsx index 65989c95a5..7c20c41b80 100644 --- a/app/pages/project/instances/instance/tabs/MetricsTab.tsx +++ b/app/pages/project/instances/instance/tabs/MetricsTab.tsx @@ -73,6 +73,7 @@ function DiskMetrics({ disks }: { disks: Disk[] }) { const { orgName, projectName } = useRequiredParams('orgName', 'projectName') const { startTime, endTime, dateTimeRangePicker } = useDateTimeRangePicker({ initialPreset: 'lastDay', + slideInterval: 5000, }) invariant(disks.length > 0, 'DiskMetrics should not be rendered with zero disks') @@ -88,26 +89,18 @@ function DiskMetrics({ disks }: { disks: Disk[] }) { {/* TODO: using a Formik field here feels like overkill, but we've built ListboxField to require that, i.e., there's no way to get the nice worked-out layout from ListboxField without using Formik. Something to think about. */} -
-
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} - -
- { - if (item) { - setDiskName(item.value) - } - }} - defaultValue={diskName} - /> -
+ { + if (item) { + setDiskName(item.value) + } + }} + defaultValue={diskName} + /> {dateTimeRangePicker} diff --git a/libs/ui/lib/listbox/Listbox.tsx b/libs/ui/lib/listbox/Listbox.tsx index d5431097c8..bf3e25fe83 100644 --- a/libs/ui/lib/listbox/Listbox.tsx +++ b/libs/ui/lib/listbox/Listbox.tsx @@ -39,7 +39,7 @@ export const Listbox: FC = ({ )} ) - - return { startTime, endTime, dateTimeRangePicker } } diff --git a/app/components/form/fields/index.ts b/app/components/form/fields/index.ts index bad242422b..f72d75da79 100644 --- a/app/components/form/fields/index.ts +++ b/app/components/form/fields/index.ts @@ -8,4 +8,4 @@ export * from './NetworkInterfaceField' export * from './RadioField' export * from './TagsField' export * from './TextField' -export * from './useDateTimeRangePicker' +export * from './DateTimeRangePicker' diff --git a/app/components/form/fields/useDateTimeRangePicker.spec.tsx b/app/components/form/fields/useDateTimeRangePicker.spec.tsx deleted file mode 100644 index 91692468a8..0000000000 --- a/app/components/form/fields/useDateTimeRangePicker.spec.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { fireEvent, render, screen } from '@testing-library/react' -import { renderHook } from '@testing-library/react-hooks' -import { subDays, subHours } from 'date-fns' -import { vi } from 'vitest' - -import { clickByRole } from 'app/test/unit' - -import type { RangeKey } from './useDateTimeRangePicker' -import { useDateTimeRangePicker } from './useDateTimeRangePicker' - -const date = new Date(2020, 1, 1) - -describe('useDateTimeRangePicker', () => { - beforeAll(() => { - vi.useFakeTimers() - vi.setSystemTime(date) - - return () => vi.useRealTimers() - }) - - it.each([ - ['lastHour', subHours(date, 1)], - ['last3Hours', subHours(date, 3)], - ['lastDay', subDays(date, 1)], - ['lastWeek', subDays(date, 7)], - ['last30Days', subDays(date, 30)], - ])('sets initial start and end', (preset, start) => { - const { result } = renderHook(() => - useDateTimeRangePicker({ initialPreset: preset as RangeKey }) - ) - expect(result.current.startTime).toEqual(start) - expect(result.current.endTime).toEqual(date) - }) - - it.each([ - ['Last hour', subHours(date, 1)], - ['Last 3 hours', subHours(date, 3)], - // ['Last day', subDays(date, 1)], // skip because we're starting on it - ['Last week', subDays(date, 7)], - ['Last 30 days', subDays(date, 30)], - ])('choosing a preset sets the times', async (option, start) => { - const { result, waitForNextUpdate } = renderHook(() => - useDateTimeRangePicker({ initialPreset: 'lastDay' }) - ) - render(result.current.dateTimeRangePicker) - - clickByRole('button', 'Choose a time range') - clickByRole('option', option) - - await waitForNextUpdate() - - expect(result.current.startTime).toEqual(start) - expect(result.current.endTime).toEqual(date) - }) - - describe('custom mode', () => { - it('enables datetime inputs', () => { - const { result } = renderHook(() => - useDateTimeRangePicker({ initialPreset: 'last3Hours' }) - ) - - render(result.current.dateTimeRangePicker) - - expect(screen.getByLabelText('Start time')).toBeDisabled() - - clickByRole('button', 'Choose a time range') - clickByRole('option', 'Custom...') - - expect(screen.getByLabelText('Start time')).toBeEnabled() - expect(screen.getByRole('button', { name: 'Reset' })).toBeDisabled() - expect(screen.getByRole('button', { name: 'Load' })).toBeDisabled() - }) - - it('clicking load after changing date changes range', async () => { - const { result, waitForNextUpdate } = renderHook(() => - useDateTimeRangePicker({ initialPreset: 'last3Hours' }) - ) - expect(result.current.startTime).toEqual(subHours(date, 3)) - expect(result.current.endTime).toEqual(date) - - render(result.current.dateTimeRangePicker) - clickByRole('button', 'Choose a time range') - clickByRole('option', 'Custom...') - - const startInput = screen.getByLabelText('Start time') - const endInput = screen.getByLabelText('End time') - - // change input values. figuring out how to actually interact with the - // input through clicks and typing is too complicated - fireEvent.change(startInput, { target: { value: '2020-01-15T00:00' } }) - fireEvent.change(endInput, { target: { value: '2020-01-17T00:00' } }) - - // changing the input value without clicking load doesn't do anything - expect(result.current.startTime).toEqual(subHours(date, 3)) - expect(result.current.endTime).toEqual(date) - - // clicking loading changes startTime - clickByRole('button', 'Load') - await waitForNextUpdate() - expect(result.current.startTime).toEqual(new Date(2020, 0, 15)) - expect(result.current.endTime).toEqual(new Date(2020, 0, 17)) - }) - - it('clicking reset after changing inputs resets inputs', async () => { - const { result } = renderHook(() => - useDateTimeRangePicker({ initialPreset: 'last3Hours' }) - ) - - render(result.current.dateTimeRangePicker) - clickByRole('button', 'Choose a time range') - clickByRole('option', 'Custom...') - - const startInput = screen.getByLabelText('Start time') - const endInput = screen.getByLabelText('End time') - - expect(startInput).toHaveValue('2020-01-31T21:00') - expect(endInput).toHaveValue('2020-02-01T00:00') - - // change input values. figuring out how to actually interact with the - // input through clicks and typing is too complicated - fireEvent.change(startInput, { target: { value: '2020-01-15T00:00' } }) - fireEvent.change(endInput, { target: { value: '2020-01-17T00:00' } }) - - expect(startInput).toHaveValue('2020-01-15T00:00') - expect(endInput).toHaveValue('2020-01-17T00:00') - - // clicking reset resets the inputs - clickByRole('button', 'Reset') - expect(startInput).toHaveValue('2020-01-31T21:00') - expect(endInput).toHaveValue('2020-02-01T00:00') - }) - - it('shows error for invalid range', async () => { - const { result } = renderHook(() => - useDateTimeRangePicker({ initialPreset: 'last3Hours' }) - ) - - render(result.current.dateTimeRangePicker) - clickByRole('button', 'Choose a time range') - clickByRole('option', 'Custom...') - - const startInput = screen.getByLabelText('Start time') - - expect(startInput).toHaveValue('2020-01-31T21:00') - - // start date is after end - fireEvent.change(startInput, { target: { value: '2020-02-03T00:00' } }) - - await screen.findByText('End time must be later than start time') - }) - }) -}) diff --git a/app/pages/project/instances/instance/tabs/MetricsTab.tsx b/app/pages/project/instances/instance/tabs/MetricsTab.tsx index 7c20c41b80..0cb8abb481 100644 --- a/app/pages/project/instances/instance/tabs/MetricsTab.tsx +++ b/app/pages/project/instances/instance/tabs/MetricsTab.tsx @@ -6,7 +6,7 @@ import { useApiQuery } from '@oxide/api' import { Listbox, Spinner } from '@oxide/ui' import { TimeSeriesAreaChart } from 'app/components/TimeSeriesChart' -import { useDateTimeRangePicker } from 'app/components/form' +import { DateTimeRangePicker, useDateTimeRangePickerState } from 'app/components/form' import { useRequiredParams } from 'app/hooks' type DiskMetricParams = { @@ -71,10 +71,13 @@ function DiskMetric({ // which means we can easily set the default selected disk to the first one function DiskMetrics({ disks }: { disks: Disk[] }) { const { orgName, projectName } = useRequiredParams('orgName', 'projectName') - const { startTime, endTime, dateTimeRangePicker } = useDateTimeRangePicker({ - initialPreset: 'lastDay', - slideInterval: 5000, - }) + + const initialPreset = 'lastDay' + const { + startTime, + endTime, + onChange: onTimeChange, + } = useDateTimeRangePickerState(initialPreset) invariant(disks.length > 0, 'DiskMetrics should not be rendered with zero disks') const [diskName, setDiskName] = useState(disks[0].name) @@ -101,7 +104,13 @@ function DiskMetrics({ disks }: { disks: Disk[] }) { }} defaultValue={diskName} /> - {dateTimeRangePicker} + {/* TODO: separate "Reads" from "(count)" so we can diff --git a/package.json b/package.json index be1bb24140..86168a9f1b 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "type-fest": "^2.17.0", "typescript": "4.8.4", "vite": "^3.1.0", - "vitest": "^0.23.4", + "vitest": "^0.24.1", "whatwg-fetch": "^3.6.2" }, "resolutions": { diff --git a/yarn.lock b/yarn.lock index e0cac66049..86cc466c1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1829,11 +1829,21 @@ resolved "https://registry.yarnpkg.com/@cush/relative/-/relative-1.0.0.tgz#8cd1769bf9bde3bb27dac356b1bc94af40f6cc16" integrity sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA== +"@esbuild/android-arm@0.15.10": + version "0.15.10" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.10.tgz#a5f9432eb221afc243c321058ef25fe899886892" + integrity sha512-FNONeQPy/ox+5NBkcSbYJxoXj9GWu8gVGJTVmUyoOCKQFDTrHVKgNSzChdNt0I8Aj/iKcsDf2r9BFwv+FSNUXg== + "@esbuild/linux-loong64@0.14.54": version "0.14.54" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028" integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw== +"@esbuild/linux-loong64@0.15.10": + version "0.15.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.10.tgz#78a42897c2cf8db9fd5f1811f7590393b77774c7" + integrity sha512-w0Ou3Z83LOYEkwaui2M8VwIp+nLi/NA60lBLMvaJ+vXVMcsARYdEzLNE7RSm4+lSg4zq4d7fAVuzk7PNQ5JFgg== + "@esbuild/linux-loong64@0.15.7": version "0.15.7" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.7.tgz#1ec4af4a16c554cbd402cc557ccdd874e3f7be53" @@ -4637,6 +4647,11 @@ esbuild-android-64@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be" integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ== +esbuild-android-64@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.10.tgz#8a59a84acbf2eca96996cadc35642cf055c494f0" + integrity sha512-UI7krF8OYO1N7JYTgLT9ML5j4+45ra3amLZKx7LO3lmLt1Ibn8t3aZbX5Pu4BjWiqDuJ3m/hsvhPhK/5Y/YpnA== + esbuild-android-64@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.7.tgz#a521604d8c4c6befc7affedc897df8ccde189bea" @@ -4652,6 +4667,11 @@ esbuild-android-arm64@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771" integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg== +esbuild-android-arm64@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.10.tgz#f453851dc1d8c5409a38cf7613a33852faf4915d" + integrity sha512-EOt55D6xBk5O05AK8brXUbZmoFj4chM8u3riGflLa6ziEoVvNjRdD7Cnp82NHQGfSHgYR06XsPI8/sMuA/cUwg== + esbuild-android-arm64@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.7.tgz#307b81f1088bf1e81dfe5f3d1d63a2d2a2e3e68e" @@ -4667,6 +4687,11 @@ esbuild-darwin-64@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25" integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug== +esbuild-darwin-64@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.10.tgz#778bd29c8186ff47b176c8af58c08cf0fb8e6b86" + integrity sha512-hbDJugTicqIm+WKZgp208d7FcXcaK8j2c0l+fqSJ3d2AzQAfjEYDRM3Z2oMeqSJ9uFxyj/muSACLdix7oTstRA== + esbuild-darwin-64@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.7.tgz#270117b0c4ec6bcbc5cf3a297a7d11954f007e11" @@ -4682,6 +4707,11 @@ esbuild-darwin-arm64@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73" integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw== +esbuild-darwin-arm64@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.10.tgz#b30bbefb46dc3c5d4708b0435e52f6456578d6df" + integrity sha512-M1t5+Kj4IgSbYmunf2BB6EKLkWUq+XlqaFRiGOk8bmBapu9bCDrxjf4kUnWn59Dka3I27EiuHBKd1rSO4osLFQ== + esbuild-darwin-arm64@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.7.tgz#97851eacd11dacb7719713602e3319e16202fc77" @@ -4697,6 +4727,11 @@ esbuild-freebsd-64@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d" integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg== +esbuild-freebsd-64@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.10.tgz#ab301c5f6ded5110dbdd611140bef1a7c2e99236" + integrity sha512-KMBFMa7C8oc97nqDdoZwtDBX7gfpolkk6Bcmj6YFMrtCMVgoU/x2DI1p74DmYl7CSS6Ppa3xgemrLrr5IjIn0w== + esbuild-freebsd-64@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.7.tgz#1de15ffaf5ae916aa925800aa6d02579960dd8c4" @@ -4712,6 +4747,11 @@ esbuild-freebsd-arm64@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48" integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q== +esbuild-freebsd-arm64@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.10.tgz#a5b09b867a6ff49110f52343b6f12265db63d43f" + integrity sha512-m2KNbuCX13yQqLlbSojFMHpewbn8wW5uDS6DxRpmaZKzyq8Dbsku6hHvh2U+BcLwWY4mpgXzFUoENEf7IcioGg== + esbuild-freebsd-arm64@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.7.tgz#0f160dbf5c9a31a1d8dd87acbbcb1a04b7031594" @@ -4727,6 +4767,11 @@ esbuild-linux-32@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5" integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw== +esbuild-linux-32@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.10.tgz#5282fe9915641caf9c8070e4ba2c3e16d358f837" + integrity sha512-guXrwSYFAvNkuQ39FNeV4sNkNms1bLlA5vF1H0cazZBOLdLFIny6BhT+TUbK/hdByMQhtWQ5jI9VAmPKbVPu1w== + esbuild-linux-32@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.7.tgz#422eb853370a5e40bdce8b39525380de11ccadec" @@ -4742,6 +4787,11 @@ esbuild-linux-64@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652" integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg== +esbuild-linux-64@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.10.tgz#f3726e85a00149580cb19f8abfabcbb96f5d52bb" + integrity sha512-jd8XfaSJeucMpD63YNMO1JCrdJhckHWcMv6O233bL4l6ogQKQOxBYSRP/XLWP+6kVTu0obXovuckJDcA0DKtQA== + esbuild-linux-64@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.7.tgz#f89c468453bb3194b14f19dc32e0b99612e81d2b" @@ -4757,6 +4807,11 @@ esbuild-linux-arm64@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b" integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig== +esbuild-linux-arm64@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.10.tgz#2f0056e9d5286edb0185b56655caa8c574d8dbe7" + integrity sha512-GByBi4fgkvZFTHFDYNftu1DQ1GzR23jws0oWyCfhnI7eMOe+wgwWrc78dbNk709Ivdr/evefm2PJiUBMiusS1A== + esbuild-linux-arm64@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.7.tgz#68a79d6eb5e032efb9168a0f340ccfd33d6350a1" @@ -4772,6 +4827,11 @@ esbuild-linux-arm@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59" integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw== +esbuild-linux-arm@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.10.tgz#40a9270da3c8ffa32cf72e24a79883e323dff08d" + integrity sha512-6N8vThLL/Lysy9y4Ex8XoLQAlbZKUyExCWyayGi2KgTBelKpPgj6RZnUaKri0dHNPGgReJriKVU6+KDGQwn10A== + esbuild-linux-arm@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.7.tgz#2b7c784d0b3339878013dfa82bf5eaf82c7ce7d3" @@ -4787,6 +4847,11 @@ esbuild-linux-mips64le@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34" integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw== +esbuild-linux-mips64le@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.10.tgz#90ce1c4ee0202edb4ac69807dea77f7e5804abc4" + integrity sha512-BxP+LbaGVGIdQNJUNF7qpYjEGWb0YyHVSKqYKrn+pTwH/SiHUxFyJYSP3pqkku61olQiSBnSmWZ+YUpj78Tw7Q== + esbuild-linux-mips64le@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.7.tgz#bb8330a50b14aa84673816cb63cc6c8b9beb62cc" @@ -4802,6 +4867,11 @@ esbuild-linux-ppc64le@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e" integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ== +esbuild-linux-ppc64le@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.10.tgz#782837ae7bd5b279178106c9dd801755a21fabdf" + integrity sha512-LoSQCd6498PmninNgqd/BR7z3Bsk/mabImBWuQ4wQgmQEeanzWd5BQU2aNi9mBURCLgyheuZS6Xhrw5luw3OkQ== + esbuild-linux-ppc64le@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.7.tgz#52544e7fa992811eb996674090d0bc41f067a14b" @@ -4817,6 +4887,11 @@ esbuild-linux-riscv64@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8" integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg== +esbuild-linux-riscv64@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.10.tgz#d7420d806ece5174f24f4634303146f915ab4207" + integrity sha512-Lrl9Cr2YROvPV4wmZ1/g48httE8z/5SCiXIyebiB5N8VT7pX3t6meI7TQVHw/wQpqP/AF4SksDuFImPTM7Z32Q== + esbuild-linux-riscv64@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.7.tgz#a43ae60697992b957e454cbb622f7ee5297e8159" @@ -4832,6 +4907,11 @@ esbuild-linux-s390x@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6" integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA== +esbuild-linux-s390x@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.10.tgz#21fdf0cb3494a7fb520a71934e4dffce67fe47be" + integrity sha512-ReP+6q3eLVVP2lpRrvl5EodKX7EZ1bS1/z5j6hsluAlZP5aHhk6ghT6Cq3IANvvDdscMMCB4QEbI+AjtvoOFpA== + esbuild-linux-s390x@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.7.tgz#8c76a125dd10a84c166294d77416caaf5e1c7b64" @@ -4847,6 +4927,11 @@ esbuild-netbsd-64@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81" integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w== +esbuild-netbsd-64@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.10.tgz#6c06b3107e3df53de381e6299184d4597db0440f" + integrity sha512-iGDYtJCMCqldMskQ4eIV+QSS/CuT7xyy9i2/FjpKvxAuCzrESZXiA1L64YNj6/afuzfBe9i8m/uDkFHy257hTw== + esbuild-netbsd-64@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.7.tgz#19b2e75449d7d9c32b5d8a222bac2f1e0c3b08fd" @@ -4862,6 +4947,11 @@ esbuild-openbsd-64@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b" integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw== +esbuild-openbsd-64@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.10.tgz#4daef5f5d8e74bbda53b65160029445d582570cf" + integrity sha512-ftMMIwHWrnrYnvuJQRJs/Smlcb28F9ICGde/P3FUTCgDDM0N7WA0o9uOR38f5Xe2/OhNCgkjNeb7QeaE3cyWkQ== + esbuild-openbsd-64@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.7.tgz#1357b2bf72fd037d9150e751420a1fe4c8618ad7" @@ -4882,6 +4972,11 @@ esbuild-sunos-64@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da" integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw== +esbuild-sunos-64@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.10.tgz#5fe7bef267a02f322fd249a8214d0274937388a7" + integrity sha512-mf7hBL9Uo2gcy2r3rUFMjVpTaGpFJJE5QTDDqUFf1632FxteYANffDZmKbqX0PfeQ2XjUDE604IcE7OJeoHiyg== + esbuild-sunos-64@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.7.tgz#87ab2c604592a9c3c763e72969da0d72bcde91d2" @@ -4897,6 +4992,11 @@ esbuild-windows-32@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31" integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w== +esbuild-windows-32@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.10.tgz#48e3dde25ab0135579a288b30ab6ddef6d1f0b28" + integrity sha512-ttFVo+Cg8b5+qHmZHbEc8Vl17kCleHhLzgT8X04y8zudEApo0PxPg9Mz8Z2cKH1bCYlve1XL8LkyXGFjtUYeGg== + esbuild-windows-32@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.7.tgz#c81e688c0457665a8d463a669e5bf60870323e99" @@ -4912,6 +5012,11 @@ esbuild-windows-64@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4" integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ== +esbuild-windows-64@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.10.tgz#387a9515bef3fee502d277a5d0a2db49a4ecda05" + integrity sha512-2H0gdsyHi5x+8lbng3hLbxDWR7mKHWh5BXZGKVG830KUmXOOWFE2YKJ4tHRkejRduOGDrBvHBriYsGtmTv3ntA== + esbuild-windows-64@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.7.tgz#2421d1ae34b0561a9d6767346b381961266c4eff" @@ -4927,6 +5032,11 @@ esbuild-windows-arm64@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982" integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg== +esbuild-windows-arm64@0.15.10: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.10.tgz#5a6fcf2fa49e895949bf5495cf088ab1b43ae879" + integrity sha512-S+th4F+F8VLsHLR0zrUcG+Et4hx0RKgK1eyHc08kztmLOES8BWwMiaGdoW9hiXuzznXQ0I/Fg904MNbr11Nktw== + esbuild-windows-arm64@0.15.7: version "0.15.7" resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.7.tgz#7d5e9e060a7b454cb2f57f84a3f3c23c8f30b7d2" @@ -5012,6 +5122,34 @@ esbuild@^0.15.6: esbuild-windows-64 "0.15.7" esbuild-windows-arm64 "0.15.7" +esbuild@^0.15.9: + version "0.15.10" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.10.tgz#85c2f8446e9b1fe04fae68daceacba033eedbd42" + integrity sha512-N7wBhfJ/E5fzn/SpNgX+oW2RLRjwaL8Y0ezqNqhjD6w0H2p0rDuEz2FKZqpqLnO8DCaWumKe8dsC/ljvVSSxng== + optionalDependencies: + "@esbuild/android-arm" "0.15.10" + "@esbuild/linux-loong64" "0.15.10" + esbuild-android-64 "0.15.10" + esbuild-android-arm64 "0.15.10" + esbuild-darwin-64 "0.15.10" + esbuild-darwin-arm64 "0.15.10" + esbuild-freebsd-64 "0.15.10" + esbuild-freebsd-arm64 "0.15.10" + esbuild-linux-32 "0.15.10" + esbuild-linux-64 "0.15.10" + esbuild-linux-arm "0.15.10" + esbuild-linux-arm64 "0.15.10" + esbuild-linux-mips64le "0.15.10" + esbuild-linux-ppc64le "0.15.10" + esbuild-linux-riscv64 "0.15.10" + esbuild-linux-s390x "0.15.10" + esbuild-netbsd-64 "0.15.10" + esbuild-openbsd-64 "0.15.10" + esbuild-sunos-64 "0.15.10" + esbuild-windows-32 "0.15.10" + esbuild-windows-64 "0.15.10" + esbuild-windows-arm64 "0.15.10" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -8685,13 +8823,6 @@ rimraf@~2.6.2: optionalDependencies: fsevents "~2.3.2" -rollup@^2.75.6: - version "2.79.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.0.tgz#9177992c9f09eb58c5e56cbfa641607a12b57ce2" - integrity sha512-x4KsrCgwQ7ZJPcFA/SUu6QVcYlO7uRLfLAy0DSA4NS2eG8japdbpM50ToH7z4iObodRYOJ0soneF0iaQRJ6zhA== - optionalDependencies: - fsevents "~2.3.2" - rollup@~2.78.0: version "2.78.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.78.1.tgz#52fe3934d9c83cb4f7c4cb5fb75d88591be8648f" @@ -9250,7 +9381,7 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strip-literal@^0.4.1: +strip-literal@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-0.4.2.tgz#4f9fa6c38bb157b924e9ace7155ebf8a2342cbcf" integrity sha512-pv48ybn4iE1O9RLgCAN0iU4Xv7RlBTiit6DKmMiErbs9x1wH6vXBs45tWc0H5wUIF6TLTrKweqkmYF/iraQKNw== @@ -9402,10 +9533,10 @@ tiny-warning@^1.0.2, tiny-warning@^1.0.3: resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== -tinybench@^2.1.5: - version "2.1.5" - resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.1.5.tgz#6864341415ff0f912ed160cfd90b7f833ece674c" - integrity sha512-ak+PZZEuH3mw6CCFOgf5S90YH0MARnZNhxjhjguAmoJimEMAJuNip/rJRd6/wyylHItomVpKTzZk9zrhTrQCoQ== +tinybench@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.3.0.tgz#febb2e697c735c0cdb8eb1e43cb1d2fa1821f983" + integrity sha512-zs1gMVBwyyG2QbVchYIbnabRhMOCGvrwZz/q+SV+LIMa9q5YDQZi2kkI6ZRqV2Bz7ba1uvrc7ieUoE4KWnGeKg== tinycolor2@^1.4.1: version "1.4.2" @@ -9901,15 +10032,15 @@ vite-tsconfig-paths@^3.5.0: recrawl-sync "^2.0.3" tsconfig-paths "^4.0.0" -"vite@^2.9.12 || ^3.0.0-0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.2.tgz#2a7b4642c53ae066cf724e7e581d6c1fd24e2c32" - integrity sha512-TAqydxW/w0U5AoL5AsD9DApTvGb2iNbGs3sN4u2VdT1GFkQVUfgUldt+t08TZgi23uIauh1TUOQJALduo9GXqw== +vite@^3.0.0: + version "3.1.8" + resolved "https://registry.yarnpkg.com/vite/-/vite-3.1.8.tgz#fa29144167d19b773baffd65b3972ea4c12359c9" + integrity sha512-m7jJe3nufUbuOfotkntGFupinL/fmuTNuQmiVE7cH2IZMuf4UbfbGYMUT3jVWgGYuRVLY9j8NnrRqgw5rr5QTg== dependencies: - esbuild "^0.14.47" - postcss "^8.4.14" + esbuild "^0.15.9" + postcss "^8.4.16" resolve "^1.22.1" - rollup "^2.75.6" + rollup "~2.78.0" optionalDependencies: fsevents "~2.3.2" @@ -9937,10 +10068,10 @@ vite@^3.1.0: optionalDependencies: fsevents "~2.3.2" -vitest@^0.23.4: - version "0.23.4" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.23.4.tgz#7ebea620f203f4df09a27ca17819dc9da61f88ef" - integrity sha512-iukBNWqQAv8EKDBUNntspLp9SfpaVFbmzmM0sNcnTxASQZMzRw3PsM6DMlsHiI+I6GeO5/sYDg3ecpC+SNFLrQ== +vitest@^0.24.1: + version "0.24.1" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.24.1.tgz#4b4ceb3d9c6710e8fb7a3b936b740f9c9f9cde10" + integrity sha512-NKkK1xnDIOOr42pKBfGQQl6b6IWdFVBpG6ZS1T+nUlJuqcOiZ7lxjVwHy9wrtTYpJ0BWww9y6bSGYXubD29Nag== dependencies: "@types/chai" "^4.3.3" "@types/chai-subset" "^1.3.3" @@ -9948,11 +10079,11 @@ vitest@^0.23.4: chai "^4.3.6" debug "^4.3.4" local-pkg "^0.4.2" - strip-literal "^0.4.1" - tinybench "^2.1.5" + strip-literal "^0.4.2" + tinybench "^2.3.0" tinypool "^0.3.0" tinyspy "^1.0.2" - vite "^2.9.12 || ^3.0.0-0" + vite "^3.0.0" w3c-hr-time@^1.0.2: version "1.0.2" From 197c0ca61b633d2de6b898e03ce6bc5171c39c38 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 13 Oct 2022 16:09:01 -0500 Subject: [PATCH 3/5] get validation working and fix the test for that --- .../form/fields/DateTimeRangePicker.spec.tsx | 38 ++++----- .../form/fields/DateTimeRangePicker.tsx | 77 ++++++++++--------- 2 files changed, 57 insertions(+), 58 deletions(-) diff --git a/app/components/form/fields/DateTimeRangePicker.spec.tsx b/app/components/form/fields/DateTimeRangePicker.spec.tsx index ab2ae38e7c..dd418db2de 100644 --- a/app/components/form/fields/DateTimeRangePicker.spec.tsx +++ b/app/components/form/fields/DateTimeRangePicker.spec.tsx @@ -80,7 +80,7 @@ describe('custom mode', () => { expect(screen.getByRole('button', { name: 'Load' })).toBeDisabled() }) - it.only('clicking load after changing date changes range', async () => { + it('clicking load after changing date changes range', async () => { const { onChange } = renderLastDay() expect(screen.getByLabelText('Start time')).toHaveValue(dateForInput(subDays(now, 1))) @@ -106,7 +106,7 @@ describe('custom mode', () => { }) it('clicking reset after changing inputs resets inputs', async () => { - renderLastDay() + const { onChange } = renderLastDay() expect(screen.getByLabelText('Start time')).toHaveValue(dateForInput(subDays(now, 1))) expect(screen.getByLabelText('End time')).toHaveValue(dateForInput(now)) @@ -114,39 +114,35 @@ describe('custom mode', () => { clickByRole('button', 'Choose a time range') clickByRole('option', 'Custom...') - // change input values. figuring out how to actually interact with the - // input through clicks and typing is too complicated const startInput = screen.getByLabelText('Start time') fireEvent.change(startInput, { target: { value: '2020-01-15T00:00' } }) + expect(startInput).toHaveValue('2020-01-15T00:00') const endInput = screen.getByLabelText('End time') fireEvent.change(endInput, { target: { value: '2020-01-17T00:00' } }) - expect(startInput).toHaveValue('2020-01-15T00:00') expect(endInput).toHaveValue('2020-01-17T00:00') // clicking reset resets the inputs clickByRole('button', 'Reset') - expect(startInput).toHaveValue('2020-01-31T21:00') + expect(startInput).toHaveValue('2020-01-31T00:00') expect(endInput).toHaveValue('2020-02-01T00:00') - }) - // it('shows error for invalid range', async () => { - // const { result } = renderHook(() => - // useDateTimeRangePicker({ initialPreset: 'last3Hours' }) - // ) + // onChange is never called + expect(onChange).not.toBeCalled() + }) - // render(result.current.dateTimeRangePicker) - // clickByRole('button', 'Choose a time range') - // clickByRole('option', 'Custom...') + it('shows error for invalid range', () => { + renderLastDay() - // const startInput = screen.getByLabelText('Start time') + clickByRole('button', 'Choose a time range') + clickByRole('option', 'Custom...') - // expect(startInput).toHaveValue('2020-01-31T21:00') + const startInput = screen.getByLabelText('Start time') + expect(startInput).toHaveValue('2020-01-31T00:00') - // // start date is after end - // fireEvent.change(startInput, { target: { value: '2020-02-03T00:00' } }) + // start date is after end + fireEvent.change(startInput, { target: { value: '2020-02-03T00:00' } }) - // await screen.findByText('End time must be later than start time') - // }) - // }) + screen.getByText('Start time must be earlier than end time') + }) }) diff --git a/app/components/form/fields/DateTimeRangePicker.tsx b/app/components/form/fields/DateTimeRangePicker.tsx index 1d913c6b52..c3ecfb4a2a 100644 --- a/app/components/form/fields/DateTimeRangePicker.tsx +++ b/app/components/form/fields/DateTimeRangePicker.tsx @@ -1,4 +1,3 @@ -// import * as Yup from 'yup' import { format, subDays, subHours } from 'date-fns' import { useMemo, useState } from 'react' @@ -29,18 +28,8 @@ const computeStart: Record Date> = { last30Days: (now) => subDays(now, 30), } -// const rangeKeys = rangePresets.map((item) => item.value) - -/** Validate that they're Dates and end is after start */ -// const dateRangeSchema = Yup.object({ -// preset: Yup.string().oneOf(rangeKeys), -// startTime: Yup.date(), -// endTime: Yup.date().min(Yup.ref('startTime'), 'End time must be later than start time'), -// }) - // Limitations: // - list of presets is hard-coded -// - no onChange, no way to control any inputs beyond initial preset // - initial preset can't be "custom" type Props = { @@ -70,6 +59,14 @@ export function useDateTimeRangePickerState(initialPreset: RangeKey) { return { startTime, endTime, onChange } } +function validateRange(startTime: Date, endTime: Date): string | null { + if (startTime >= endTime) { + return 'Start time must be earlier than end time' + } + + return null +} + /** * Exposes `startTime` and `endTime` plus the whole set of picker UI controls as * a JSX element to render. @@ -89,6 +86,7 @@ export function DateTimeRangePicker({ const [endTimeInput, setEndTimeInput] = useState(endTime) // TODO: validate inputs on change and display error someplace + const error = validateRange(startTimeInput, endTimeInput) const customInputsDirty = startTime !== startTimeInput || endTime !== endTimeInput @@ -111,7 +109,7 @@ export function DateTimeRangePicker({ ) return ( -
+ {/* TODO: real React date picker lib instead of native for consistent styling across browsers */} - setStartTimeInput(new Date(e.target.value))} - /> - setEndTimeInput(new Date(e.target.value))} - /> +
+
+ setStartTimeInput(new Date(e.target.value))} + /> + setEndTimeInput(new Date(e.target.value))} + /> +
+ {error &&
{error}
} +
{/* TODO: fix goofy ass button text. use icons? tooltips to explain? lord */} {enableInputs && (