mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
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
This commit is contained in:
parent
0a1628c0cc
commit
21f9918677
@ -133,7 +133,7 @@ export const ManageTable: React.VFC<ManageTableProps> = (
|
||||
if (isLoading) return <IndicatorCard status="info">Loading...</IndicatorCard>;
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-y-auto bg-gray-50">
|
||||
<div className="w-full bg-gray-50">
|
||||
<div className="px-md pt-md mb-xs">
|
||||
<Breadcrumbs dataSourceName={dataSourceName} tableName={tableName} />
|
||||
<TableName
|
||||
|
@ -41,6 +41,7 @@ export function adaptSQLDataType(
|
||||
],
|
||||
number: ['BIGNUMERIC', 'FLOAT64', 'INT64', 'INTERVAL', 'NUMERIC'],
|
||||
json: ['JSON', 'xml'],
|
||||
float: ['FLOAT64'],
|
||||
};
|
||||
|
||||
const [dataType] = getEntries(DataTypeToSQLTypeMap).find(([, value]) =>
|
||||
|
@ -66,6 +66,7 @@ export function adaptSQLDataType(
|
||||
'serial4',
|
||||
],
|
||||
json: ['interval', 'json', 'jsonb', 'xml'],
|
||||
float: ['float8', 'float4'],
|
||||
};
|
||||
|
||||
const [dataType] = getEntries(consoleDataTypeToSQLTypeMap).find(([, value]) =>
|
||||
|
@ -22,6 +22,7 @@ export function adaptSQLDataType(
|
||||
],
|
||||
text: ['text'],
|
||||
json: [],
|
||||
float: ['float'],
|
||||
};
|
||||
|
||||
const [dataType] = getEntries(DataTypeToSQLTypeMap).find(([, value]) =>
|
||||
|
@ -74,6 +74,7 @@ export function adaptSQLDataType(
|
||||
'serial4',
|
||||
],
|
||||
json: ['interval', 'json', 'jsonb', 'xml'],
|
||||
float: ['float8'],
|
||||
};
|
||||
|
||||
const [dataType] = getEntries(consoleDataTypeToSQLTypeMap).find(([, value]) =>
|
||||
|
@ -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?: {
|
||||
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -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 }) => {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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<ColumnRowInputProps> = ({
|
||||
return <DateInput {...props} />;
|
||||
}
|
||||
|
||||
if (dataType === dataSource.columnDataTypes.DATETIME) {
|
||||
return <DateTimeInput {...props} />;
|
||||
}
|
||||
|
||||
if (dataType === dataSource.columnDataTypes.TIME) {
|
||||
return <TimeInput {...props} />;
|
||||
}
|
||||
|
||||
if (
|
||||
dataType === dataSource.columnDataTypes.JSONDTYPE ||
|
||||
dataType === dataSource.columnDataTypes.JSONB
|
||||
|
@ -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<TextInputProps> = ({
|
||||
name,
|
||||
@ -88,6 +88,7 @@ export const DateInput: React.VFC<TextInputProps> = ({
|
||||
<DatePicker
|
||||
inline
|
||||
onSelect={() => setCalendarPickerVisible(false)}
|
||||
onClickOutside={() => setCalendarPickerVisible(false)}
|
||||
onChange={onDateChange}
|
||||
calendarContainer={CustomPickerContainer}
|
||||
/>
|
||||
|
@ -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<typeof DateTimeInput>;
|
||||
|
||||
const Template: ComponentStory<typeof DateTimeInput> = args => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
return <DateTimeInput {...args} inputRef={inputRef} />;
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
@ -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,
|
||||
}) => (
|
||||
<div className="absolute top-10 left-0 z-50 min-w-[300px]">
|
||||
<CalendarContainer className={className}>
|
||||
<div style={{ position: 'relative' }}>{children}</div>
|
||||
</CalendarContainer>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const DateTimeInput: React.VFC<TextInputProps> = ({
|
||||
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 (
|
||||
<div className="w-full relative">
|
||||
<Input
|
||||
className="w-full"
|
||||
name={name}
|
||||
label={name}
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
onChange={onChange as ChangeEventHandler<HTMLInputElement>}
|
||||
onInput={onInput as ChangeEventHandler<HTMLInputElement>}
|
||||
inputProps={{
|
||||
onBlur: onBlur,
|
||||
ref: inputRef,
|
||||
}}
|
||||
rightButton={
|
||||
<Button
|
||||
icon={
|
||||
<FaCalendar
|
||||
className={clsx(
|
||||
isCalendarPickerVisible && 'fill-yellow-600 stroke-yellow-600'
|
||||
)}
|
||||
/>
|
||||
}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
setCalendarPickerVisible(!isCalendarPickerVisible);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{isCalendarPickerVisible && (
|
||||
<DatePicker
|
||||
inline
|
||||
showTimeSelect
|
||||
onClickOutside={() => setCalendarPickerVisible(false)}
|
||||
onChange={onDateTimeChange}
|
||||
calendarContainer={CustomPickerContainer}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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<TextInputProps> = ({
|
||||
name,
|
||||
|
@ -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<typeof TimeInput>;
|
||||
|
||||
const Template: ComponentStory<typeof TimeInput> = args => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
return <TimeInput {...args} inputRef={inputRef} />;
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
@ -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,
|
||||
}) => (
|
||||
<div className="absolute top-10 left-0 z-50">
|
||||
<CalendarContainer className={className}>
|
||||
<div style={{ position: 'relative' }}>{children}</div>
|
||||
</CalendarContainer>
|
||||
</div>
|
||||
);
|
||||
|
||||
const getISOTimePart = (date: Date) => date.toISOString().slice(11, 19);
|
||||
|
||||
export const TimeInput: React.VFC<TextInputProps> = ({
|
||||
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 (
|
||||
<div className="w-full relative">
|
||||
<Input
|
||||
className="w-full"
|
||||
name={name}
|
||||
label={name}
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
onChange={onChange as ChangeEventHandler<HTMLInputElement>}
|
||||
onInput={onInput as ChangeEventHandler<HTMLInputElement>}
|
||||
inputProps={{
|
||||
onBlur: onBlur,
|
||||
ref: inputRef,
|
||||
}}
|
||||
rightButton={
|
||||
<Button
|
||||
icon={
|
||||
<FaClock
|
||||
className={clsx(
|
||||
isCalendarPickerVisible && 'fill-yellow-600 stroke-yellow-600'
|
||||
)}
|
||||
/>
|
||||
}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
setCalendarPickerVisible(!isCalendarPickerVisible);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{isCalendarPickerVisible && (
|
||||
<DatePicker
|
||||
inline
|
||||
showTimeSelect
|
||||
showTimeSelectOnly
|
||||
onClickOutside={() => setCalendarPickerVisible(false)}
|
||||
onChange={date => {
|
||||
if (date) {
|
||||
onTimeChange(date);
|
||||
setCalendarPickerVisible(false);
|
||||
}
|
||||
}}
|
||||
calendarContainer={CustomPickerContainer}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user