From 82453ba70d7b59a92e3656fd3c556b055b9d3e59 Mon Sep 17 00:00:00 2001 From: Radoslaw Szwajkowski Date: Mon, 2 Oct 2023 17:30:48 +0200 Subject: [PATCH] Add Date filter Allow to filter events at given date. Key points: 1. multiple dates can be choosen 2. ISO date format used (YYYY-MM-DD) regardless of the locale 3. date comparison is done in UTC zone 4. Migration started column (timestamp) added to Plans table Reference-Url: https://github.com/oVirt/ovirt-web-ui/blob/ea9a10e75e7dc965ddc6a1a7f2d36f6301117bbc/src/components/Toolbar/DatePickerFilter.js Signed-off-by: Radoslaw Szwajkowski --- .../src/components/Filter/DateFilter.tsx | 74 +++++++++++++++++++ .../common/src/components/Filter/index.ts | 1 + .../src/components/FilterGroup/matchers.ts | 11 ++- packages/common/src/utils/dates.ts | 12 +++ packages/common/src/utils/index.ts | 1 + .../en/plugin__forklift-console-plugin.json | 1 + .../src/modules/Plans/PlanRow.tsx | 7 +- .../src/modules/Plans/PlansPage.tsx | 10 +++ .../__snapshots__/PlanRow.test.tsx.snap | 62 ++++++++++++++++ .../src/modules/Plans/data.ts | 5 +- 10 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 packages/common/src/components/Filter/DateFilter.tsx create mode 100644 packages/common/src/utils/dates.ts diff --git a/packages/common/src/components/Filter/DateFilter.tsx b/packages/common/src/components/Filter/DateFilter.tsx new file mode 100644 index 000000000..9e4c4ff23 --- /dev/null +++ b/packages/common/src/components/Filter/DateFilter.tsx @@ -0,0 +1,74 @@ +import React, { FormEvent, useState } from 'react'; +import { DateTime } from 'luxon'; + +import { DatePicker, InputGroup, ToolbarFilter } from '@patternfly/react-core'; + +import { FilterTypeProps } from './types'; + +/** + * This Filter type enables selecting a single date (a day). + * + * **FilterTypeProps are interpreted as follows**:
+ * 1) selectedFilters - dates in YYYY-MM-DD format (ISO date format).
+ * 2) onFilterUpdate - accepts the list of dates.
+ * + * [ + * View component source on GitHub](https://github.com/kubev2v/forklift-console-plugin/blob/main/packages/common/src/components/Filter/DateFilter.tsx) + */ +export const DateFilter = ({ + selectedFilters = [], + onFilterUpdate, + title, + filterId, + placeholderLabel, + showFilter = true, +}: FilterTypeProps) => { + const validFilters = + selectedFilters + ?.map((str) => DateTime.fromISO(str)) + ?.filter((dt: DateTime) => dt.isValid) + ?.map((dt: DateTime) => dt.toISODate()) ?? []; + + // internal state - stored as ISO date string (no time) + const [date, setDate] = useState(DateTime.now().toISODate()); + + const clearSingleDate = (option) => { + onFilterUpdate([...validFilters.filter((d) => d !== option)]); + }; + + const onDateChange = (even: FormEvent, value: string) => { + // require full format "YYYY-MM-DD" although partial date is also accepted + // i.e. YYYY-MM gets parsed as YYYY-MM-01 and results in auto completing the date + // unfortunately due to auto-complete user cannot delete the date char after char + if (value?.length === 10 && DateTime.fromISO(value).isValid) { + const targetDate = DateTime.fromISO(value).toISODate(); + setDate(targetDate); + onFilterUpdate([...validFilters.filter((d) => d !== targetDate), targetDate]); + } + }; + + return ( + clearSingleDate(option)} + deleteChipGroup={() => onFilterUpdate([])} + categoryName={title} + showToolbarItem={showFilter} + > + + DateTime.fromJSDate(date).toISODate()} + dateParse={(str) => DateTime.fromISO(str).toJSDate()} + onChange={onDateChange} + aria-label={title} + placeholder={placeholderLabel} + invalidFormatText={placeholderLabel} + // default value ("parent") creates collision with sticky table header + appendTo={document.body} + /> + + + ); +}; diff --git a/packages/common/src/components/Filter/index.ts b/packages/common/src/components/Filter/index.ts index 334f6f06f..78ffce26c 100644 --- a/packages/common/src/components/Filter/index.ts +++ b/packages/common/src/components/Filter/index.ts @@ -1,4 +1,5 @@ // @index(['./*', /__/g], f => `export * from '${f.path}';`) +export * from './DateFilter'; export * from './EnumFilter'; export * from './FreetextFilter'; export * from './GroupedEnumFilter'; diff --git a/packages/common/src/components/FilterGroup/matchers.ts b/packages/common/src/components/FilterGroup/matchers.ts index 8ed9ad518..1a162aea8 100644 --- a/packages/common/src/components/FilterGroup/matchers.ts +++ b/packages/common/src/components/FilterGroup/matchers.ts @@ -1,7 +1,8 @@ import jsonpath from 'jsonpath'; +import { DateTime } from 'luxon'; import { ResourceField } from '../../utils'; -import { EnumFilter, FreetextFilter, GroupedEnumFilter, SwitchFilter } from '../Filter'; +import { DateFilter, EnumFilter, FreetextFilter, GroupedEnumFilter, SwitchFilter } from '../Filter'; import { FilterRenderer, ValueMatcher } from './types'; @@ -96,6 +97,12 @@ const groupedEnumMatcher = { matchValue: enumMatcher.matchValue, }; +const dateMatcher = { + filterType: 'date', + matchValue: (value: string) => (filter: string) => + DateTime.fromISO(value).toUTC().hasSame(DateTime.fromISO(filter).toUTC(), 'day'), +}; + const sliderMatcher = { filterType: 'slider', matchValue: (value: string) => (filter: string) => Boolean(value).toString() === filter || !value, @@ -106,9 +113,11 @@ export const defaultValueMatchers: ValueMatcher[] = [ enumMatcher, groupedEnumMatcher, sliderMatcher, + dateMatcher, ]; export const defaultSupportedFilters: Record = { + date: DateFilter, enum: EnumFilter, freetext: FreetextFilter, groupedEnum: GroupedEnumFilter, diff --git a/packages/common/src/utils/dates.ts b/packages/common/src/utils/dates.ts new file mode 100644 index 000000000..290365528 --- /dev/null +++ b/packages/common/src/utils/dates.ts @@ -0,0 +1,12 @@ +import { DateTime } from 'luxon'; + +/** + * Converts a given ISO date string in a known format and timezone to a UTC ISO string. + * + * @param {string} isoDateString - The ISO date string in a known format and timezone. + * @returns {string} The equivalent UTC ISO string if date is valid or undefined otherwise. + */ +export function convertToUTC(isoDateString: string): string | undefined { + const date = DateTime.fromISO(isoDateString); + return date.isValid ? date.toUTC().toISO() : undefined; +} diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index 4d2fcb217..848231a39 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -1,5 +1,6 @@ // @index(['./*', /__/g], f => `export * from '${f.path}';`) export * from './constants'; +export * from './dates'; export * from './localCompare'; export * from './localStorage'; export * from './types'; diff --git a/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json b/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json index 32d0ba2d5..6a39ec364 100644 --- a/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json +++ b/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json @@ -178,6 +178,7 @@ "Migration networks maps are used to map network interfaces between source and target workloads.": "Migration networks maps are used to map network interfaces between source and target workloads.", "Migration plans are used to plan migration or virtualization workloads from source providers to target providers.": "Migration plans are used to plan migration or virtualization workloads from source providers to target providers.", "Migration plans are used to plan migration or virtualization workloads from source providers to target providers. At least one source and one target provider must be available in order to create a migration plan, <2>Learn more.": "Migration plans are used to plan migration or virtualization workloads from source providers to target providers. At least one source and one target provider must be available in order to create a migration plan, <2>Learn more.", + "Migration started": "Migration started", "Migration storage maps are used to map storage interfaces between source and target workloads, at least one source and one target provider must be available in order to create a migration plan, <2>Learn more.": "Migration storage maps are used to map storage interfaces between source and target workloads, at least one source and one target provider must be available in order to create a migration plan, <2>Learn more.", "Migration storage maps are used to map storage interfaces between source and target workloads.": "Migration storage maps are used to map storage interfaces between source and target workloads.", "Migration Toolkit for Virtualization": "Migration Toolkit for Virtualization", diff --git a/packages/forklift-console-plugin/src/modules/Plans/PlanRow.tsx b/packages/forklift-console-plugin/src/modules/Plans/PlanRow.tsx index a1954e244..36f5efcb9 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/PlanRow.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/PlanRow.tsx @@ -16,7 +16,11 @@ import { MigrateOrCutoverButton } from '@kubev2v/legacy/Plans/components/Migrate import { PlanNameNavLink as Link } from '@kubev2v/legacy/Plans/components/PlanStatusNavLink'; import { ScheduledCutoverTime } from '@kubev2v/legacy/Plans/components/ScheduledCutoverTime'; import { StatusIcon } from '@migtools/lib-ui'; -import { K8sGroupVersionKind, ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; +import { + K8sGroupVersionKind, + ResourceLink, + Timestamp, +} from '@openshift-console/dynamic-plugin-sdk'; import { Flex, FlexItem, @@ -192,6 +196,7 @@ const cellCreator: Record JSX.Element> = { {value} ), + [C.MIGRATION_STARTED]: ({ value }: CellProps) => , }; const PlanRow = ({ diff --git a/packages/forklift-console-plugin/src/modules/Plans/PlansPage.tsx b/packages/forklift-console-plugin/src/modules/Plans/PlansPage.tsx index ce30e9865..e43e60837 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/PlansPage.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/PlansPage.tsx @@ -43,6 +43,16 @@ export const fieldsMetadataFactory: ResourceFieldFactory = (t) => [ }, sortable: true, }, + { + resourceFieldId: C.MIGRATION_STARTED, + label: t('Migration started'), + isVisible: true, + filter: { + type: 'date', + placeholderLabel: 'YYYY-MM-DD', + }, + sortable: true, + }, { resourceFieldId: C.SOURCE, label: t('Source provider'), diff --git a/packages/forklift-console-plugin/src/modules/Plans/__tests__/__snapshots__/PlanRow.test.tsx.snap b/packages/forklift-console-plugin/src/modules/Plans/__tests__/__snapshots__/PlanRow.test.tsx.snap index 7be477bc7..f4bad7dcc 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/__tests__/__snapshots__/PlanRow.test.tsx.snap +++ b/packages/forklift-console-plugin/src/modules/Plans/__tests__/__snapshots__/PlanRow.test.tsx.snap @@ -43,6 +43,12 @@ exports[`Plan rows plantest-01 1`] = ` name: openshift-migration, gvk: ~~, ns: undefined + + 2020-10-10T14:04:10Z + + + + 2020-10-10T14:04:10Z + + + 2020-10-10T14:04:10Z + + + 2020-10-10T14:04:10Z + + + + 2020-10-10T14:04:10Z + + + 2020-10-10T14:04:10Z + + + 2020-10-10T14:04:10Z + + + 2020-10-10T14:04:10Z + + + 2020-10-10T14:04:10Z + condition.type === 'Canceled'), ).length || 0, - migrationCompleted: migration?.completed, - migrationStarted: migration?.started, + migrationCompleted: convertToUTC(migration?.completed), + migrationStarted: convertToUTC(migration?.started), latestMigration, object: plan, }),