diff --git a/app/components/form/fields/DateTimeRangePicker.spec.tsx b/app/components/form/fields/DateTimeRangePicker.spec.tsx new file mode 100644 index 0000000000..dd418db2de --- /dev/null +++ b/app/components/form/fields/DateTimeRangePicker.spec.tsx @@ -0,0 +1,148 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { subDays, subHours } from 'date-fns' +import { vi } from 'vitest' + +import { clickByRole } from 'app/test/unit' + +import type { RangeKey } from './DateTimeRangePicker' +import { DateTimeRangePicker, dateForInput } from './DateTimeRangePicker' + +const now = new Date(2020, 1, 1) + +function renderLastDay() { + const onChange = vi.fn() + render( + + ) + return { onChange } +} + +describe('useDateTimeRangePicker', () => { + it.each([ + ['lastHour', subHours(now, 1)], + ['last3Hours', subHours(now, 3)], + ['lastDay', subDays(now, 1)], + ['lastWeek', subDays(now, 7)], + ['last30Days', subDays(now, 30)], + ])('sets initial start and end', (preset, start) => { + const onChange = vi.fn() + render( + + ) + + expect(screen.getByLabelText('Start time')).toHaveValue(dateForInput(start)) + expect(screen.getByLabelText('End time')).toHaveValue(dateForInput(now)) + }) +}) + +it.each([ + ['Last hour', subHours(now, 1)], + ['Last 3 hours', subHours(now, 3)], + // ['Last day', subDays(now, 1)], // skip because we're starting on it + ['Last week', subDays(now, 7)], + ['Last 30 days', subDays(now, 30)], +])('choosing a preset sets the times', (option, start) => { + vi.useFakeTimers() + vi.setSystemTime(now) + + const { onChange } = renderLastDay() + + clickByRole('button', 'Choose a time range') + clickByRole('option', option) + + expect(onChange).toBeCalledWith(start, now) + + vi.useRealTimers() +}) + +describe('custom mode', () => { + it('enables datetime inputs', () => { + const { onChange } = renderLastDay() + + expect(screen.getByLabelText('Start time')).toBeDisabled() + + clickByRole('button', 'Choose a time range') + clickByRole('option', 'Custom...') + + expect(onChange).not.toBeCalled() + 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 { onChange } = renderLastDay() + + expect(screen.getByLabelText('Start time')).toHaveValue(dateForInput(subDays(now, 1))) + expect(screen.getByLabelText('End time')).toHaveValue(dateForInput(now)) + + 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' } }) + + const endInput = screen.getByLabelText('End time') + fireEvent.change(endInput, { target: { value: '2020-01-17T00:00' } }) + + // changing the input value without clicking Load doesn't do anything + expect(onChange).not.toBeCalled() + + // clicking Load calls onChange + clickByRole('button', 'Load') + expect(onChange).toBeCalledWith(new Date(2020, 0, 15), new Date(2020, 0, 17)) + }) + + it('clicking reset after changing inputs resets inputs', async () => { + const { onChange } = renderLastDay() + + expect(screen.getByLabelText('Start time')).toHaveValue(dateForInput(subDays(now, 1))) + expect(screen.getByLabelText('End time')).toHaveValue(dateForInput(now)) + + clickByRole('button', 'Choose a time range') + clickByRole('option', 'Custom...') + + 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(endInput).toHaveValue('2020-01-17T00:00') + + // clicking reset resets the inputs + clickByRole('button', 'Reset') + expect(startInput).toHaveValue('2020-01-31T00:00') + expect(endInput).toHaveValue('2020-02-01T00:00') + + // onChange is never called + expect(onChange).not.toBeCalled() + }) + + it('shows error for invalid range', () => { + renderLastDay() + + clickByRole('button', 'Choose a time range') + clickByRole('option', 'Custom...') + + 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' } }) + + 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 new file mode 100644 index 0000000000..c3ecfb4a2a --- /dev/null +++ b/app/components/form/fields/DateTimeRangePicker.tsx @@ -0,0 +1,180 @@ +import { format, subDays, subHours } from 'date-fns' +import { useMemo, useState } from 'react' + +import { Listbox, useInterval } from '@oxide/ui' +import { Button, TextInput } from '@oxide/ui' + +export const dateForInput = (d: Date) => format(d, "yyyy-MM-dd'T'HH:mm") + +const rangePresets = [ + { label: 'Last hour', value: 'lastHour' as const }, + { label: 'Last 3 hours', value: 'last3Hours' as const }, + { label: 'Last day', value: 'lastDay' as const }, + { label: 'Last week', value: 'lastWeek' as const }, + { label: 'Last 30 days', value: 'last30Days' as const }, + { label: 'Custom...', value: 'custom' as const }, +] + +// custom doesn't have an associated range +type RangeKeyAll = typeof rangePresets[number]['value'] +export type RangeKey = Exclude + +// Record ensures we have an entry for every preset +const computeStart: Record Date> = { + lastHour: (now) => subHours(now, 1), + last3Hours: (now) => subHours(now, 3), + lastDay: (now) => subDays(now, 1), + lastWeek: (now) => subDays(now, 7), + last30Days: (now) => subDays(now, 30), +} + +// Limitations: +// - list of presets is hard-coded +// - initial preset can't be "custom" + +type Props = { + initialPreset: RangeKey + /** + * if set and range is a relative preset, update the range to have `endTime` + * of now every X ms + */ + slideInterval?: number + startTime: Date + endTime: Date + onChange: (startTime: Date, endTime: Date) => void +} + +export function useDateTimeRangePickerState(initialPreset: RangeKey) { + // default endTime is now, i.e., mount time + const now = useMemo(() => new Date(), []) + + const [startTime, setStartTime] = useState(computeStart[initialPreset](now)) + const [endTime, setEndTime] = useState(now) + + function onChange(newStart: Date, newEnd: Date) { + setStartTime(newStart) + setEndTime(newEnd) + } + + 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. + */ +export function DateTimeRangePicker({ + initialPreset, + slideInterval, + startTime, + endTime, + onChange, +}: Props) { + const [preset, setPreset] = useState(initialPreset) + + // needs a separate pair of values because they can be edited without + // submitting and updating the graphs + const [startTimeInput, setStartTimeInput] = useState(startTime) + 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 + + const enableInputs = preset === 'custom' + + /** Set the input values and call the passed-on onChange with the new times */ + function setTimesForPreset(newPreset: RangeKey) { + const now = new Date() + const newStartTime = computeStart[newPreset](now) + onChange(newStartTime, now) + setStartTimeInput(newStartTime) + setEndTimeInput(now) + } + + useInterval( + () => { + if (preset !== 'custom') setTimesForPreset(preset) + }, + slideInterval && preset !== 'custom' ? slideInterval : null + ) + + return ( +
+ { + if (item) { + setPreset(item.value as RangeKeyAll) + if (item.value !== 'custom') setTimesForPreset(item.value as RangeKey) + } + }} + /> + + {/* 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))} + /> +
+ {error &&
{error}
} +
+ {/* TODO: fix goofy ass button text. use icons? tooltips to explain? lord */} + {enableInputs && ( + + )} + {enableInputs && ( + + )} + + ) +} 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/components/form/fields/useDateTimeRangePicker.tsx b/app/components/form/fields/useDateTimeRangePicker.tsx deleted file mode 100644 index aec2324c9c..0000000000 --- a/app/components/form/fields/useDateTimeRangePicker.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import * as Yup from 'yup' -import { format, subDays, subHours } from 'date-fns' -import { Form, Formik } from 'formik' -import { useMemo, useRef, useState } from 'react' - -import { useInterval } from '@oxide/ui' -import { Button } from '@oxide/ui' - -import { ListboxField } from './ListboxField' -import { TextField } from './TextField' - -const dateForInput = (d: Date) => format(d, "yyyy-MM-dd'T'HH:mm") - -const rangePresets = [ - { label: 'Last hour', value: 'lastHour' as const }, - { label: 'Last 3 hours', value: 'last3Hours' as const }, - { label: 'Last day', value: 'lastDay' as const }, - { label: 'Last week', value: 'lastWeek' as const }, - { label: 'Last 30 days', value: 'last30Days' as const }, - { label: 'Custom...', value: 'custom' as const }, -] - -// custom doesn't have an associated range -type RangeKeyAll = typeof rangePresets[number]['value'] -export type RangeKey = Exclude - -// Record ensures we have an entry for every preset -const computeStart: Record Date> = { - lastHour: (now) => subHours(now, 1), - last3Hours: (now) => subHours(now, 3), - lastDay: (now) => subDays(now, 1), - lastWeek: (now) => subDays(now, 7), - 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 Args = { - initialPreset: RangeKey - /** - * if set and range is a relative preset, update the range to have `endTime` - * of now every X ms - */ - slideInterval?: number -} - -/** - * Exposes `startTime` and `endTime` plus the whole set of picker UI controls as - * a JSX element to render. - */ -export function useDateTimeRangePicker({ initialPreset, slideInterval }: Args) { - // default endTime is now, i.e., mount time - const now = useMemo(() => new Date(), []) - - const [startTime, setStartTime] = useState(computeStart[initialPreset](now)) - const [endTime, setEndTime] = useState(now) - - // only exists to make current preset value available to window slider - const presetRef = useRef(initialPreset) - - useInterval( - () => { - if (presetRef.current !== 'custom') { - const now = new Date() - setStartTime(computeStart[presetRef.current](now)) - setEndTime(now) - } - }, - slideInterval && presetRef.current !== '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 && ( - - )} - - ) - }} -
- ) - - return { startTime, endTime, dateTimeRangePicker } -} diff --git a/app/pages/SiloUtilizationPage.tsx b/app/pages/SiloUtilizationPage.tsx index 57bbf213f9..fd28206efc 100644 --- a/app/pages/SiloUtilizationPage.tsx +++ b/app/pages/SiloUtilizationPage.tsx @@ -5,7 +5,7 @@ import { Divider, Listbox, PageHeader, PageTitle, Snapshots24Icon } from '@oxide import { bytesToGiB } from '@oxide/util' import { SystemMetric } from 'app/components/SystemMetric' -import { useDateTimeRangePicker } from 'app/components/form' +import { DateTimeRangePicker, useDateTimeRangePickerState } from 'app/components/form' const DEFAULT_SILO_ID = '001de000-5110-4000-8000-000000000000' const ALL_PROJECTS = '|ALL_PROJECTS|' @@ -32,10 +32,12 @@ export function SiloUtilizationPage() { { enabled: !!orgName } ) - const { startTime, endTime, dateTimeRangePicker } = useDateTimeRangePicker({ - initialPreset: 'lastHour', - slideInterval: 5000, - }) + const initialPreset = 'lastHour' + const { + startTime, + endTime, + onChange: onTimeChange, + } = useDateTimeRangePickerState(initialPreset) const orgItems = useMemo(() => { const items = orgs?.items.map(toListboxItem) || [] @@ -68,7 +70,7 @@ export function SiloUtilizationPage() { - {dateTimeRangePicker} + {/* TODO: this divider is supposed to go all the way across */} diff --git a/app/pages/project/instances/instance/tabs/MetricsTab.tsx b/app/pages/project/instances/instance/tabs/MetricsTab.tsx index 65989c95a5..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,9 +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', - }) + + 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) @@ -88,27 +92,25 @@ 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} - /> -
- {dateTimeRangePicker} + { + if (item) { + setDiskName(item.value) + } + }} + defaultValue={diskName} + /> + {/* TODO: separate "Reads" from "(count)" so we can diff --git a/app/pages/system/CapacityUtilizationPage.tsx b/app/pages/system/CapacityUtilizationPage.tsx index f728c16a5a..c8396a6c53 100644 --- a/app/pages/system/CapacityUtilizationPage.tsx +++ b/app/pages/system/CapacityUtilizationPage.tsx @@ -5,7 +5,7 @@ import { Divider, Listbox, PageHeader, PageTitle, Snapshots24Icon } from '@oxide import { bytesToGiB } from '@oxide/util' import { SystemMetric } from 'app/components/SystemMetric' -import { useDateTimeRangePicker } from 'app/components/form' +import { DateTimeRangePicker, useDateTimeRangePickerState } from 'app/components/form' const FLEET_ID = '001de000-1334-4000-8000-000000000000' const DEFAULT_SILO_ID = '001de000-5110-4000-8000-000000000000' @@ -19,10 +19,12 @@ export function CapacityUtilizationPage() { const [siloId, setSiloId] = useState(FLEET_ID) const { data: silos } = useApiQuery('siloList', {}) - const { startTime, endTime, dateTimeRangePicker } = useDateTimeRangePicker({ - initialPreset: 'lastHour', - slideInterval: 5000, - }) + const initialPreset = 'lastHour' + const { + startTime, + endTime, + onChange: onTimeChange, + } = useDateTimeRangePickerState(initialPreset) const siloItems = useMemo(() => { const items = silos?.items.map((silo) => ({ label: silo.name, value: silo.id })) || [] @@ -64,7 +66,13 @@ export function CapacityUtilizationPage() { {/* TODO: need a button to clear the silo */} - {dateTimeRangePicker} + {/* TODO: this divider is supposed to go all the way across */} 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 = ({