From 21f99186770b7772ebb68ac909649fa2b1a13007 Mon Sep 17 00:00:00 2001 From: Luca Restagno <59067245+lucarestagno@users.noreply.github.com> Date: Tue, 21 Mar 2023 12:16:57 +0100 Subject: [PATCH] console: add support for specific data types (datetime / time) for gdc PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8393 GitOrigin-RevId: c1eeb1fb6a5cb825853ec29d7ec1f8b0b702ad07 --- .../features/Data/ManageTable/ManageTable.tsx | 2 +- .../bigquery/introspection/utils.ts | 5 +- .../features/DataSource/cockroach/utils.ts | 1 + .../lib/features/DataSource/mssql/utils.ts | 1 + .../lib/features/DataSource/postgres/utils.ts | 1 + .../src/lib/features/DataSource/types.ts | 2 +- .../InsertRow/InsertRowForm.stories.tsx | 36 +++++- .../InsertRow/InsertRowForm.utils.test.ts | 1 + .../features/InsertRow/InsertRowForm.utils.ts | 4 +- .../InsertRow/components/ColumnRowInput.tsx | 10 ++ .../InsertRow/components/DateInput.tsx | 5 +- .../components/DateTimeInput.stories.tsx | 75 +++++++++++++ .../InsertRow/components/DateTimeInput.tsx | 98 ++++++++++++++++ .../InsertRow/components/TextInput.tsx | 2 +- .../components/TimeInput.stories.tsx | 59 ++++++++++ .../InsertRow/components/TimeInput.tsx | 106 ++++++++++++++++++ .../InsertRow/components/date-input.css | 13 +++ .../src/lib/new-components/Form/Input.tsx | 2 +- 18 files changed, 413 insertions(+), 10 deletions(-) create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/DateTimeInput.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/DateTimeInput.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/TimeInput.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/TimeInput.tsx diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageTable/ManageTable.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageTable/ManageTable.tsx index 429a7bd2a4c..e7590b779b6 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageTable/ManageTable.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageTable/ManageTable.tsx @@ -133,7 +133,7 @@ export const ManageTable: React.VFC = ( if (isLoading) return Loading...; return ( -
+
{ - /* + /* The `slice(1)` on the result is done because the first item of the result is always the columns names from the SQL output. - It is not required for the final result and should be avoided + It is not required for the final result and should be avoided */ const adaptedResponse = runSqlResponse.result ?.slice(1) @@ -41,6 +41,7 @@ export function adaptSQLDataType( ], number: ['BIGNUMERIC', 'FLOAT64', 'INT64', 'INTERVAL', 'NUMERIC'], json: ['JSON', 'xml'], + float: ['FLOAT64'], }; const [dataType] = getEntries(DataTypeToSQLTypeMap).find(([, value]) => diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/cockroach/utils.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/cockroach/utils.ts index 801271812a1..55949fd198c 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/cockroach/utils.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/cockroach/utils.ts @@ -66,6 +66,7 @@ export function adaptSQLDataType( 'serial4', ], json: ['interval', 'json', 'jsonb', 'xml'], + float: ['float8', 'float4'], }; const [dataType] = getEntries(consoleDataTypeToSQLTypeMap).find(([, value]) => diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mssql/utils.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mssql/utils.ts index 9399364f2a4..9d826262d1f 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mssql/utils.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mssql/utils.ts @@ -22,6 +22,7 @@ export function adaptSQLDataType( ], text: ['text'], json: [], + float: ['float'], }; const [dataType] = getEntries(DataTypeToSQLTypeMap).find(([, value]) => diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/utils.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/utils.ts index d9712917a08..7811fc12551 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/utils.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/utils.ts @@ -74,6 +74,7 @@ export function adaptSQLDataType( 'serial4', ], json: ['interval', 'json', 'jsonb', 'xml'], + float: ['float8'], }; const [dataType] = getEntries(consoleDataTypeToSQLTypeMap).find(([, value]) => diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts index f95f341fa19..546a3570d11 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts @@ -66,7 +66,7 @@ export type TableColumn = { /** * console data type: the dataType property is group into one of these types and console uses this internally */ - consoleDataType: 'string' | 'text' | 'json' | 'number' | 'boolean'; + consoleDataType: 'string' | 'text' | 'json' | 'number' | 'boolean' | 'float'; nullable?: boolean; isPrimaryKey?: boolean; graphQLProperties?: { diff --git a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/InsertRowForm.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/InsertRowForm.stories.tsx index 2ca34f9d63c..98dd463df2e 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/InsertRowForm.stories.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/InsertRowForm.stories.tsx @@ -27,6 +27,9 @@ const columns: InsertRowFormProps['columns'] = [ }, isPrimaryKey: true, placeholder: 'bigint', + insertable: true, + description: '', + nullable: true, }, { name: 'name', @@ -36,6 +39,9 @@ const columns: InsertRowFormProps['columns'] = [ comment: '', }, placeholder: 'text', + insertable: true, + description: '', + nullable: true, }, { name: 'json', @@ -46,6 +52,8 @@ const columns: InsertRowFormProps['columns'] = [ }, nullable: true, placeholder: '{"name":"john"}', + insertable: true, + description: '', }, { name: 'date', @@ -55,7 +63,33 @@ const columns: InsertRowFormProps['columns'] = [ comment: '', }, nullable: true, - placeholder: '2023-02-01', + insertable: true, + description: '', + placeholder: '2023-01-01', + }, + { + name: 'datetime', + dataType: 'datetime', + consoleDataType: 'text', + config: { + comment: '', + }, + nullable: true, + insertable: true, + description: '', + placeholder: '2020-01-14T16:30:00+01:00', + }, + { + name: 'time', + dataType: 'time with time zone', + consoleDataType: 'text', + config: { + comment: '', + }, + nullable: true, + insertable: true, + description: '', + placeholder: '11:00:00+01:00', }, ]; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/InsertRowForm.utils.test.ts b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/InsertRowForm.utils.test.ts index 025cd0e8f05..824ded42e31 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/InsertRowForm.utils.test.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/InsertRowForm.utils.test.ts @@ -10,6 +10,7 @@ describe('convertTableValue', () => { ${'false'} | ${'boolean'} | ${false} ${'true'} | ${'bool'} | ${true} ${'false'} | ${'bool'} | ${false} + ${'123'} | ${'float'} | ${123} `( 'given $dataType it returns $expected', ({ columnValue, dataType, expected }) => { diff --git a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/InsertRowForm.utils.ts b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/InsertRowForm.utils.ts index d113d034188..6765a55d607 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/InsertRowForm.utils.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/InsertRowForm.utils.ts @@ -1,11 +1,13 @@ import { TableColumn } from '../DataSource'; +const numericDataTypes = ['number', 'float', 'integer']; + export const convertTableValue = ( value: unknown, dataType: TableColumn['dataType'] | undefined ): string | number | boolean | unknown => { if (typeof value === 'string') { - if (dataType === 'number') { + if (dataType && numericDataTypes.includes(dataType)) { return parseFloat(value); } diff --git a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/ColumnRowInput.tsx b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/ColumnRowInput.tsx index 548ba6e1639..c3232ec106d 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/ColumnRowInput.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/ColumnRowInput.tsx @@ -1,12 +1,14 @@ import { dataSource } from '../../../dataSources'; import { BooleanInput } from './BooleanInput'; import { DateInput } from './DateInput'; +import { DateTimeInput } from './DateTimeInput'; import { ExpandableTextInput, ExpandableTextInputProps, } from './ExpandableTextInput'; import { JsonInput } from './JsonInput'; import { TextInput } from './TextInput'; +import { TimeInput } from './TimeInput'; type ColumnRowInputProps = ExpandableTextInputProps & { dataType: string; @@ -28,6 +30,14 @@ export const ColumnRowInput: React.VFC = ({ return ; } + if (dataType === dataSource.columnDataTypes.DATETIME) { + return ; + } + + if (dataType === dataSource.columnDataTypes.TIME) { + return ; + } + if ( dataType === dataSource.columnDataTypes.JSONDTYPE || dataType === dataSource.columnDataTypes.JSONB diff --git a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/DateInput.tsx b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/DateInput.tsx index 5ec67e86885..d7699188c13 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/DateInput.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/DateInput.tsx @@ -3,12 +3,12 @@ import { Input } from '../../../new-components/Form/Input'; import { ChangeEventHandler, useState } from 'react'; import { FaCalendar } from 'react-icons/fa'; import DatePicker, { CalendarContainer } from 'react-datepicker'; +import { format } from 'date-fns'; +import clsx from 'clsx'; import { CustomEventHandler, TextInputProps } from './TextInput'; import 'react-datepicker/dist/react-datepicker.css'; import './date-input.css'; -import { format } from 'date-fns'; -import clsx from 'clsx'; export const DateInput: React.VFC = ({ name, @@ -88,6 +88,7 @@ export const DateInput: React.VFC = ({ setCalendarPickerVisible(false)} + onClickOutside={() => setCalendarPickerVisible(false)} onChange={onDateChange} calendarContainer={CustomPickerContainer} /> diff --git a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/DateTimeInput.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/DateTimeInput.stories.tsx new file mode 100644 index 00000000000..256159d3424 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/DateTimeInput.stories.tsx @@ -0,0 +1,75 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { handlers } from '../../../mocks/metadata.mock'; +import { userEvent, within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; +import { DateTimeInput } from './DateTimeInput'; +import { useRef } from 'react'; +import { format } from 'date-fns'; + +export default { + title: 'Data/Insert Row/components/DateTimeInput', + component: DateTimeInput, + parameters: { + msw: handlers(), + mockdate: new Date('2020-01-14T15:47:18.502Z'), + }, + argTypes: { + onChange: { action: true }, + onInput: { action: true }, + }, +} as ComponentMeta; + +const Template: ComponentStory = args => { + const inputRef = useRef(null); + return ; +}; + +export const Base = Template.bind({}); +Base.args = { + name: 'date', + placeholder: 'date...', +}; + +export const Disabled = Template.bind({}); +Disabled.args = { + ...Base.args, + disabled: true, +}; + +Base.play = async ({ args, canvasElement }) => { + const canvas = within(canvasElement); + + userEvent.type( + await canvas.findByPlaceholderText('date...'), + '2020-01-14T12:00:00.000Z' + ); + + expect(args.onChange).toHaveBeenCalled(); + expect(args.onInput).toHaveBeenCalled(); + + const baseDate = new Date(); + + expect( + await canvas.findByDisplayValue('2020-01-14T12:00:00.000Z') + ).toBeInTheDocument(); + + userEvent.click(await canvas.findByRole('button')); + + const baseDateLabel = `Choose ${format(baseDate, 'EEEE, LLLL do, u')}`; + + userEvent.click((await canvas.findAllByLabelText(baseDateLabel))[0]); + userEvent.click(await canvas.findByText('4:30 PM')); + + expect(await canvas.findByLabelText('date')).toHaveDisplayValue( + '2020-01-14T16:30:00.502Z' + ); + + expect(args.onChange).toHaveBeenCalled(); + expect(args.onInput).toHaveBeenCalled(); + + // click outside the calendar picker + userEvent.click(await canvas.findByLabelText('date')); + + expect(await canvas.queryByText('January 2020')).not.toBeInTheDocument(); + expect(await canvas.queryByText('Time')).not.toBeInTheDocument(); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/DateTimeInput.tsx b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/DateTimeInput.tsx new file mode 100644 index 00000000000..dc2ccf86983 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/DateTimeInput.tsx @@ -0,0 +1,98 @@ +import { Button } from '../../../new-components/Button'; +import { Input } from '../../../new-components/Form/Input'; +import { ChangeEventHandler, useState } from 'react'; +import { FaCalendar } from 'react-icons/fa'; +import DatePicker, { CalendarContainer } from 'react-datepicker'; +import clsx from 'clsx'; +import { sub } from 'date-fns'; +import { CustomEventHandler, TextInputProps } from './TextInput'; + +import 'react-datepicker/dist/react-datepicker.css'; +import './date-input.css'; + +const CustomPickerContainer: React.FC<{ className: string }> = ({ + className, + children, +}) => ( +
+ +
{children}
+
+
+); + +export const DateTimeInput: React.VFC = ({ + name, + disabled, + placeholder, + inputRef, + onChange, + onInput, + onBlur, +}) => { + const [isCalendarPickerVisible, setCalendarPickerVisible] = useState(false); + + const onDateTimeChange = (date: Date) => { + const timeZoneOffsetMinutes = date.getTimezoneOffset(); + const utcDate = sub(date, { minutes: timeZoneOffsetMinutes }); + const dateString = utcDate.toISOString(); + + if (inputRef && 'current' in inputRef && inputRef.current) { + inputRef.current.value = dateString; + } + if (onChange) { + const changeCb = onChange as CustomEventHandler; + changeCb({ target: { value: dateString } }); + } + if (onInput) { + const inputCb = onInput as CustomEventHandler; + inputCb({ target: { value: dateString } }); + } + }; + + return ( +
+ } + onInput={onInput as ChangeEventHandler} + inputProps={{ + onBlur: onBlur, + ref: inputRef, + }} + rightButton={ +
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/TextInput.tsx b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/TextInput.tsx index 7d7f3b8e322..1f795fb5249 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/TextInput.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/TextInput.tsx @@ -21,7 +21,7 @@ export type TextInputProps = Omit< }; export const baseInputTw = - 'block w-full h-input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-yellow-200 focus-visible:border-yellow-400 placeholder:text-slate-300 placeholder:italic'; + 'block w-full h-input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-yellow-200 focus-visible:border-yellow-400 placeholder:text-slate-400'; export const TextInput: React.VFC = ({ name, diff --git a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/TimeInput.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/TimeInput.stories.tsx new file mode 100644 index 00000000000..f00ef3d3fcf --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/TimeInput.stories.tsx @@ -0,0 +1,59 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { handlers } from '../../../mocks/metadata.mock'; +import { userEvent, within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; +import { TimeInput } from './TimeInput'; +import { useRef } from 'react'; + +export default { + title: 'Data/Insert Row/components/TimeInput', + component: TimeInput, + parameters: { + msw: handlers(), + mockdate: new Date('2020-01-14T15:47:18.502Z'), + }, + argTypes: { + onChange: { action: true }, + onInput: { action: true }, + }, +} as ComponentMeta; + +const Template: ComponentStory = args => { + const inputRef = useRef(null); + return ; +}; + +export const Base = Template.bind({}); +Base.args = { + name: 'date', + placeholder: 'date...', +}; + +export const Disabled = Template.bind({}); +Disabled.args = { + ...Base.args, + disabled: true, +}; + +Base.play = async ({ args, canvasElement }) => { + const canvas = within(canvasElement); + + userEvent.type(await canvas.findByPlaceholderText('date...'), '15:30:00'); + + expect(args.onChange).toHaveBeenCalled(); + expect(args.onInput).toHaveBeenCalled(); + + expect(await canvas.findByDisplayValue('15:30:00')).toBeInTheDocument(); + + userEvent.click(await canvas.findByRole('button')); + + userEvent.click(await canvas.findByText('4:30 PM')); + + expect(await canvas.findByLabelText('date')).toHaveDisplayValue('16:30:00'); + + expect(args.onChange).toHaveBeenCalled(); + expect(args.onInput).toHaveBeenCalled(); + + // the picker is automatically hidden on selection + expect(await canvas.queryByText('Time')).not.toBeInTheDocument(); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/TimeInput.tsx b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/TimeInput.tsx new file mode 100644 index 00000000000..9316bb4905c --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/TimeInput.tsx @@ -0,0 +1,106 @@ +import { Button } from '../../../new-components/Button'; +import { Input } from '../../../new-components/Form/Input'; +import { ChangeEventHandler, useState } from 'react'; +import { FaClock } from 'react-icons/fa'; +import DatePicker, { CalendarContainer } from 'react-datepicker'; +import { sub } from 'date-fns'; +import clsx from 'clsx'; +import { CustomEventHandler, TextInputProps } from './TextInput'; + +import 'react-datepicker/dist/react-datepicker.css'; +import './date-input.css'; + +const CustomPickerContainer: React.FC<{ className: string }> = ({ + className, + children, +}) => ( +
+ +
{children}
+
+
+); + +const getISOTimePart = (date: Date) => date.toISOString().slice(11, 19); + +export const TimeInput: React.VFC = ({ + name, + disabled, + placeholder, + inputRef, + onChange, + onInput, + onBlur, +}) => { + const [isCalendarPickerVisible, setCalendarPickerVisible] = useState(false); + + const onTimeChange = (date: Date) => { + const timeZoneOffsetMinutes = date.getTimezoneOffset(); + const utcDate = sub(date, { minutes: timeZoneOffsetMinutes }); + const timeString = getISOTimePart(utcDate); + + if (inputRef && 'current' in inputRef && inputRef.current) { + inputRef.current.value = timeString; + } + if (onChange) { + const changeCb = onChange as CustomEventHandler; + changeCb({ target: { value: timeString } }); + } + if (onInput) { + const inputCb = onInput as CustomEventHandler; + inputCb({ target: { value: timeString } }); + } + }; + + return ( +
+ } + onInput={onInput as ChangeEventHandler} + inputProps={{ + onBlur: onBlur, + ref: inputRef, + }} + rightButton={ +
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/date-input.css b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/date-input.css index 8fb68bd142a..cdad1f3c388 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/date-input.css +++ b/frontend/libs/console/legacy-ce/src/lib/features/InsertRow/components/date-input.css @@ -1,3 +1,7 @@ +.react-datepicker { + overflow: hidden; +} + .react-datepicker-popper[data-placement^='bottom'] .react-datepicker__triangle::before, .react-datepicker-popper[data-placement^='top'] @@ -106,6 +110,15 @@ padding: 7px 10px; } +.react-datepicker__time, +.react-datepicker__time ul { + height: 173px !important; +} + +.react-datepicker__time-box { + height: 173px; +} + .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box diff --git a/frontend/libs/console/legacy-ce/src/lib/new-components/Form/Input.tsx b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/Input.tsx index 63a0c8763c5..fc431b7a901 100644 --- a/frontend/libs/console/legacy-ce/src/lib/new-components/Form/Input.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/Input.tsx @@ -152,7 +152,7 @@ export const Input = ({ aria-label={label} data-test={dataTest} className={clsx( - 'block w-full h-input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-yellow-200 focus-visible:border-yellow-400 placeholder:text-muted', + 'block w-full h-input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-yellow-200 focus-visible:border-yellow-400 placeholder:text-slate-400', prependLabel !== '' ? 'rounded-l-none' : '', appendLabel !== '' ? 'rounded-r-none' : '', maybeError