diff --git a/web-console/package-lock.json b/web-console/package-lock.json index cac4f505fbc..9fd91b1c45b 100644 --- a/web-console/package-lock.json +++ b/web-console/package-lock.json @@ -991,6 +991,24 @@ } } }, + "@blueprintjs/datetime": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@blueprintjs/datetime/-/datetime-3.14.0.tgz", + "integrity": "sha512-NfCiV19zMPWZ7kcx1A2OympSLy7vp8wFrhMaMAKK6R+ndTUuEDZi3Z5NxoyX2jBJ71F9nhWYFjH1nQVOn/eDaw==", + "requires": { + "@blueprintjs/core": "^3.19.0", + "classnames": "^2.2", + "react-day-picker": "^7.3.2", + "tslib": "~1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" + } + } + }, "@blueprintjs/icons": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.11.0.tgz", @@ -11279,6 +11297,14 @@ "prop-types": "^15.7.2" } }, + "react-day-picker": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-7.4.0.tgz", + "integrity": "sha512-dqfr96EY7mHSpbW23hJI6of2JvxClDfHLNQ7VqctxBvNsJIzEiwh3zS8hEhqNza7xuR0vC4KN517zxndgb3/fw==", + "requires": { + "prop-types": "^15.6.2" + } + }, "react-dom": { "version": "16.11.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.11.0.tgz", diff --git a/web-console/package.json b/web-console/package.json index e54894a82c9..b97c7403844 100644 --- a/web-console/package.json +++ b/web-console/package.json @@ -55,6 +55,7 @@ "dependencies": { "@blueprintjs/core": "^3.19.1", "@blueprintjs/icons": "^3.11.0", + "@blueprintjs/datetime": "^3.11.0", "axios": "^0.19.0", "brace": "^0.11.1", "classnames": "^2.2.6", diff --git a/web-console/src/components/auto-form/auto-form.tsx b/web-console/src/components/auto-form/auto-form.tsx index e4db7df7b29..9adfa4cbd77 100644 --- a/web-console/src/components/auto-form/auto-form.tsx +++ b/web-console/src/components/auto-form/auto-form.tsx @@ -22,6 +22,7 @@ import React from 'react'; import { deepDelete, deepGet, deepSet } from '../../utils/object-change'; import { ArrayInput } from '../array-input/array-input'; import { FormGroupWithInfo } from '../form-group-with-info/form-group-with-info'; +import { IntervalInput } from '../interval-input/interval-input'; import { JsonInput } from '../json-input/json-input'; import { SuggestibleInput, SuggestionGroup } from '../suggestible-input/suggestible-input'; @@ -31,7 +32,15 @@ export interface Field { name: string; label?: string; info?: React.ReactNode; - type: 'number' | 'size-bytes' | 'string' | 'duration' | 'boolean' | 'string-array' | 'json'; + type: + | 'number' + | 'size-bytes' + | 'string' + | 'duration' + | 'boolean' + | 'string-array' + | 'json' + | 'interval'; defaultValue?: any; suggestions?: (string | SuggestionGroup)[]; placeholder?: string; @@ -288,6 +297,21 @@ export class AutoForm> extends React.PureComponent ); } + private renderIntervalInput(field: Field): JSX.Element { + const { model } = this.props; + + const modelValue = deepGet(model as any, field.name); + return ( + { + this.fieldChange(field, v); + }} + placeholder={field.placeholder} + /> + ); + } + renderFieldInput(field: Field) { switch (field.type) { case 'number': @@ -306,6 +330,8 @@ export class AutoForm> extends React.PureComponent return this.renderStringArrayInput(field); case 'json': return this.renderJsonInput(field); + case 'interval': + return this.renderIntervalInput(field); default: throw new Error(`unknown field type '${field.type}'`); } diff --git a/web-console/src/components/interval-input/__snapshots__/interval-input.spec.tsx.snap b/web-console/src/components/interval-input/__snapshots__/interval-input.spec.tsx.snap new file mode 100644 index 00000000000..2ff93507b8a --- /dev/null +++ b/web-console/src/components/interval-input/__snapshots__/interval-input.spec.tsx.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`interval calendar component matches snapshot 1`] = ` +
+ + +
+ + + + + +
+
+
+`; diff --git a/web-console/src/components/interval-input/interval-input.scss b/web-console/src/components/interval-input/interval-input.scss new file mode 100644 index 00000000000..f07522cd181 --- /dev/null +++ b/web-console/src/components/interval-input/interval-input.scss @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.calendar { + .bp3-popover-content { + right: 310px; + } +} diff --git a/web-console/src/components/interval-input/interval-input.spec.tsx b/web-console/src/components/interval-input/interval-input.spec.tsx new file mode 100644 index 00000000000..0027ad14bef --- /dev/null +++ b/web-console/src/components/interval-input/interval-input.spec.tsx @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; + +import { IntervalInput } from './interval-input'; + +describe('interval calendar component', () => { + it('matches snapshot', () => { + const intervalInput = ( + {}} + /> + ); + const { container } = render(intervalInput); + expect(container.firstChild).toMatchSnapshot(); + }); +}); diff --git a/web-console/src/components/interval-input/interval-input.tsx b/web-console/src/components/interval-input/interval-input.tsx new file mode 100644 index 00000000000..3f9fa02f73d --- /dev/null +++ b/web-console/src/components/interval-input/interval-input.tsx @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Button, InputGroup, Popover, Position } from '@blueprintjs/core'; +import { DateRange, DateRangePicker } from '@blueprintjs/datetime'; +import { IconNames } from '@blueprintjs/icons'; +import React from 'react'; + +import './interval-input.scss'; + +const CURRENT_YEAR = new Date().getUTCFullYear(); + +function removeLocalTimezone(localDate: Date): Date { + // Function removes the local timezone of the date and displays it in UTC + return new Date(localDate.getTime() - localDate.getTimezoneOffset() * 60000); +} + +function parseInterval(interval: string): DateRange { + const dates = interval.split('/'); + if (dates.length !== 2) { + return [undefined, undefined]; + } + const startDate = Date.parse(dates[0]) ? new Date(dates[0]) : undefined; + const endDate = Date.parse(dates[1]) ? new Date(dates[1]) : undefined; + // Must check if the start and end dates are within range + return [ + startDate && startDate.getFullYear() < CURRENT_YEAR - 20 ? undefined : startDate, + endDate && endDate.getFullYear() > CURRENT_YEAR ? undefined : endDate, + ]; +} +function stringifyDateRange(localRange: DateRange): string { + // This function takes in the dates selected from datepicker in local time, and displays them in UTC + // Shall Blueprint make any changes to the way dates are selected, this function will have to be reworked + const [localStartDate, localEndDate] = localRange; + return `${ + localStartDate + ? removeLocalTimezone(localStartDate) + .toISOString() + .substring(0, 19) + : '' + }/${ + localEndDate + ? removeLocalTimezone(localEndDate) + .toISOString() + .substring(0, 19) + : '' + }`; +} + +export interface IntervalInputProps { + interval: string; + placeholder: string | undefined; + onValueChange: (interval: string) => void; +} + +export const IntervalInput = React.memo(function IntervalInput(props: IntervalInputProps) { + const { interval, placeholder, onValueChange } = props; + + return ( + + { + onValueChange(stringifyDateRange(selectedRange)); + }} + /> + } + position={Position.BOTTOM_RIGHT} + > +