From acc07607cefd9cc0e929fc3f9a61ec6078c5550f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Tib=C3=BArcio?= Date: Wed, 13 Nov 2024 14:56:34 -0300 Subject: [PATCH 1/2] chore: better select prop types to match antd exports (#486) Co-authored-by: mparticle-automation Co-authored-by: Daniel Siemens --- .../SelectWithRangePicker.tsx | 11 ++++----- docs/UX Patterns/Table/Filters.stories.tsx | 2 +- package-lock.json | 4 ++-- package.json | 2 +- .../data-entry/DatePicker/DatePicker.tsx | 2 ++ .../data-entry/QueryItem/Qualifier.tsx | 2 ++ src/components/data-entry/Select/Select.tsx | 24 ++++++++++++------- src/components/index.ts | 7 +++++- 8 files changed, 34 insertions(+), 20 deletions(-) diff --git a/docs/Candidate Components/Directory/Date Range Filter/SelectWithRangePicker.tsx b/docs/Candidate Components/Directory/Date Range Filter/SelectWithRangePicker.tsx index 59f5a2873..1b218cfce 100644 --- a/docs/Candidate Components/Directory/Date Range Filter/SelectWithRangePicker.tsx +++ b/docs/Candidate Components/Directory/Date Range Filter/SelectWithRangePicker.tsx @@ -1,11 +1,10 @@ import React from 'react' -import { Select } from 'antd' import { useMemo, useState } from 'react' import { DateRangeString, type IDateRangeStringProps } from './DateRangeString' -import type { RangePickerProps } from 'antd/es/date-picker' -import type { BaseOptionType, DefaultOptionType } from 'antd/es/select' import dayjs from 'dayjs' -import { DatePicker, Divider, Flex, type ISelectProps, Typography } from 'src/components' +import { Select, DatePicker, Divider, Flex, type ISelectProps, Typography } from 'src/components' +import type { IRangePickerProps } from 'src/components/data-entry/DatePicker/DatePicker' +import type { SelectBaseOptionType, SelectDefaultOptionType } from 'src/components/data-entry/Select/Select' export type SelectWithRangePickerValue = ValueType | [string, string] | null @@ -16,7 +15,7 @@ interface SelectWithRangePickerProps 'open' | 'value' | 'dropdownRender' | 'defaultValue' | 'mode' > { value: SelectWithRangePickerValue - rangePickerProps?: Omit + rangePickerProps?: Omit rangePickerLabel?: React.ReactNode formatOptions?: IDateRangeStringProps['formatOptions'] } @@ -25,7 +24,7 @@ const DEFAULT_PICKER_LABEL = Custom date range, - OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType, + OptionType extends SelectBaseOptionType | SelectDefaultOptionType = SelectDefaultOptionType, >({ value, rangePickerProps = {}, diff --git a/docs/UX Patterns/Table/Filters.stories.tsx b/docs/UX Patterns/Table/Filters.stories.tsx index 8b3bac047..138eb3423 100644 --- a/docs/UX Patterns/Table/Filters.stories.tsx +++ b/docs/UX Patterns/Table/Filters.stories.tsx @@ -257,7 +257,7 @@ export const WithComplexFilters: Story = { showHour: true, showMinute: true, showSecond: false, - disabledDate: antdDayJS => { + disabledDate: (antdDayJS: any) => { const fourteenDaysInMs = 14 * 24 * 60 * 60 * 1000 return antdDayJS.isBefore(new Date(Date.now() - fourteenDaysInMs)) }, diff --git a/package-lock.json b/package-lock.json index d7c44e700..b7e8613fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mparticle/aquarium", - "version": "1.34.0", + "version": "1.34.1-chore-better-select-prop-types.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mparticle/aquarium", - "version": "1.34.0", + "version": "1.34.1-chore-better-select-prop-types.2", "license": "Apache-2.0", "dependencies": { "lodash.clonedeep": "4.5.0" diff --git a/package.json b/package.json index 34c879ebd..a944659af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mparticle/aquarium", - "version": "1.34.0", + "version": "1.34.1-chore-better-select-prop-types.2", "description": "mParticle Component Library", "license": "Apache-2.0", "keywords": [ diff --git a/src/components/data-entry/DatePicker/DatePicker.tsx b/src/components/data-entry/DatePicker/DatePicker.tsx index 248410be9..33153ce99 100644 --- a/src/components/data-entry/DatePicker/DatePicker.tsx +++ b/src/components/data-entry/DatePicker/DatePicker.tsx @@ -1,7 +1,9 @@ import { DatePicker as AntDatePicker, type DatePickerProps as AntDatePickerProps } from 'antd' +import type { RangePickerProps as AntRangePickerProps } from 'antd/es/date-picker' import { ConfigProvider } from 'src/components' export interface IDatePickerProps extends AntDatePickerProps {} +export interface IRangePickerProps extends AntRangePickerProps {} export const DatePicker = (props: IDatePickerProps) => { return ( diff --git a/src/components/data-entry/QueryItem/Qualifier.tsx b/src/components/data-entry/QueryItem/Qualifier.tsx index 8730e45bb..b70507f52 100644 --- a/src/components/data-entry/QueryItem/Qualifier.tsx +++ b/src/components/data-entry/QueryItem/Qualifier.tsx @@ -21,6 +21,8 @@ const Qualifier = (props: IQueryItemQualifierProps) => { defaultValue: props.options?.length ? props.options[0].value : undefined, menuItemSelectedIcon: node => node.isSelected ? : null, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error Introduced when we properly typed the Select value and option generics from Aquarium. Need to double check to fix this. onChange: props.onChange, onDropdownVisibleChange: () => { setIsOpen(!isOpen) diff --git a/src/components/data-entry/Select/Select.tsx b/src/components/data-entry/Select/Select.tsx index 7c1e332a4..0bd297307 100644 --- a/src/components/data-entry/Select/Select.tsx +++ b/src/components/data-entry/Select/Select.tsx @@ -1,19 +1,25 @@ import { Select as AntSelect } from 'antd' -import { type SelectProps as AntSelectProps } from 'antd' -import { type BaseOptionType, type DefaultOptionType } from 'antd/es/select' -import { ConfigProvider } from 'src/components' +import type { SelectProps as AntSelectProps } from 'antd' +import type { BaseOptionType as AntBaseOptionType, DefaultOptionType as AntDefaultOptionType } from 'antd/es/select' +import { ConfigProvider, Icon } from 'src/components' -export type { DefaultOptionType } +export type SelectBaseOptionType = AntBaseOptionType +export type SelectDefaultOptionType = AntDefaultOptionType export interface ISelectProps< - ValueType = any, - OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType, -> extends AntSelectProps {} + SelectValueType = unknown, + SelectOptionType extends SelectBaseOptionType | SelectDefaultOptionType = SelectDefaultOptionType, +> extends AntSelectProps {} -export const Select = (props: ISelectProps) => { +export const Select = < + SelectValueType, + SelectOptionType extends SelectBaseOptionType | SelectDefaultOptionType = SelectDefaultOptionType, +>( + props: ISelectProps, +) => { return ( - + } {...props} /> ) } diff --git a/src/components/index.ts b/src/components/index.ts index 6b89922da..d33abfaa8 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -4,7 +4,12 @@ export { Icon, type IIconProps } from './general/Icon/Icon' export { Rate, type IRateProps } from './not-prod-ready/Rate/Rate' export { Form, type IFormProps, type FormInstance } from './data-entry/Form/Form' export { TreeSelect, type ITreeSelectProps } from './data-entry/TreeSelect/TreeSelect' -export { Select, type ISelectProps, type DefaultOptionType } from './data-entry/Select/Select' +export { + Select, + type ISelectProps, + type SelectDefaultOptionType, + type SelectBaseOptionType, +} from './data-entry/Select/Select' export { Mentions, type IMentionsProps } from './not-prod-ready/Mentions/Mentions' export { Radio, type IRadioProps } from './data-entry/Radio/Radio' export { ColorPicker, type IColorPickerProps } from './not-prod-ready/ColorPicker/ColorPicker' From a5afe30e45ad8b6e5dc1fe70317703d2ad124469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Tib=C3=BArcio?= Date: Wed, 13 Nov 2024 15:04:56 -0300 Subject: [PATCH 2/2] docs: complex table filters example (#489) --- .eslintrc.cjs | 1 + .storybook/preview.tsx | 9 +- docs/UX Patterns/Table/Filters.mdx | 14 +- docs/UX Patterns/Table/Filters.stories.tsx | 278 ---------- docs/UX Patterns/Table/Table.mdx | 6 +- docs/UX Patterns/Table/Table.stories.tsx | 496 +++++++++++------- docs/UX Patterns/Table/TableStoryUtils.tsx | 201 +++++++ .../data-display/Collapse/Collapse.tsx | 2 +- 8 files changed, 522 insertions(+), 485 deletions(-) delete mode 100644 docs/UX Patterns/Table/Filters.stories.tsx create mode 100644 docs/UX Patterns/Table/TableStoryUtils.tsx diff --git a/.eslintrc.cjs b/.eslintrc.cjs index ad9515584..b16e7e386 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -124,6 +124,7 @@ module.exports = { 'react/react-in-jsx-scope': 'off', 'import/no-duplicates': 'off', 'react/jsx-boolean-value': 'warn', + '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }], }, globals: { React: true, diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index f82951939..41d49813b 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,9 +1,16 @@ import type { Preview } from '@storybook/react' +// import type { IndexEntry } from '@storybook/types' const preview: Preview = { parameters: { layout: 'centered', options: { + // TODO https://mparticle-eng.atlassian.net/browse/UNI-1214 + // storySort: (a: IndexEntry, b: IndexEntry) => { + // console.log('Ordering stories', { a, b }) + // const order = ['Documentation', 'Cell Types', 'Filters', 'Primary', 'Complex'] + // return order.indexOf(a[1].name) - order.indexOf(b[1].name) + // }, storySort: { order: [ 'About', @@ -38,7 +45,7 @@ const preview: Preview = { ], 'Candidate Components', 'UX Patterns', - ['Table', ['Table', 'Filters']], + ['Table', ['Table', ['Documentation', 'Cell Types', 'Filters', 'Primary', 'Complex']]], 'Contributing', ['Introduction', 'Commits', 'Testing in the platforms', 'Release Process', 'Maintainers'], ], diff --git a/docs/UX Patterns/Table/Filters.mdx b/docs/UX Patterns/Table/Filters.mdx index 7d3eae893..2b997dbe5 100644 --- a/docs/UX Patterns/Table/Filters.mdx +++ b/docs/UX Patterns/Table/Filters.mdx @@ -1,8 +1,8 @@ import { Meta, Story } from '@storybook/blocks' -import * as FiltersStories from './Filters.stories' +import * as TableStories from './Table.stories' - + # Filters @@ -11,24 +11,20 @@ import * as FiltersStories from './Filters.stories' _Note: This section covers filters specifically for tables. For query-related filters, please refer to the [Analytics Filters - Coming Soon](#coming)._ #### Filter Search + Located above the table on the right, the search filter allows users to quickly find specific data within the table by entering keywords. #### **Simple Filters** Simple filters are ideal when there are only a few filter options. These are straightforward and quick to use and appearing as dropdowns above the table. For examples, refer to the [Select Component](https://mparticle.github.io/aquarium/?path=/docs/components-data-entry-select--documentation). -Examples: - - - #### **Filters with Apply Button** Complex filters provide more advanced filtering options, allowing users to apply multiple criteria at once. These filters often include dropdowns, checkboxes, and text fields. Complex filters are particularly useful when multiple filters need to be applied simultaneously or when load times might be a concern. -- daterange -- modal: sorting, filters: one of each input type: checkboxes, input, tree select and placeholder for tags +Example: - + #### **Date Range Filters** diff --git a/docs/UX Patterns/Table/Filters.stories.tsx b/docs/UX Patterns/Table/Filters.stories.tsx deleted file mode 100644 index 138eb3423..000000000 --- a/docs/UX Patterns/Table/Filters.stories.tsx +++ /dev/null @@ -1,278 +0,0 @@ -import type { ReactNode } from 'react' -import { CopyOutlined } from '@ant-design/icons' -import { faker } from '@faker-js/faker' -import type { Meta, StoryObj } from '@storybook/react' -import { - Flex, - Icon, - Input, - Select, - Badge, - type IBadgeProps, - Table, - type TableProps, - Tag, - type ITagProps, - Typography, - Space, - Tooltip, -} from 'src/components' -import { DatePickerWithDisabledYears } from 'src/components/data-entry/DatePicker/DatePicker.stories' -import { SelectWithRangePicker } from 'docs/Candidate Components/Directory/Date Range Filter/SelectWithRangePicker' - -interface DataType { - key: string - name: string - id: string - timestamp: number - output: string - environment: Environment - status: Status - mpId: string -} - -type Environment = 'unknown' | 'development' | 'production' -type Status = 'draft' | 'error' | 'ready' - -const EnvironmentColors: Record = { - production: 'blue', - development: 'purple', - unknown: 'default', -} - -const EnvironmentNames: Record = { - production: 'Prod', - development: 'Dev', - unknown: 'Unknown', -} - -const getTagColorForEnvironment = (env: Environment): ITagProps['color'] => EnvironmentColors[env] - -const getNameForEnvironment = (env: Environment) => EnvironmentNames[env] - -const StatusColors: Record = { - draft: 'cyan', - error: 'red', - ready: 'green', -} - -const StatusNames: Record = { - draft: 'Draft', - error: 'Error', - ready: 'Ready', -} - -const getStatusColor = (status: Status) => StatusColors[status] - -const getStatusName = (status: Status) => StatusNames[status] - -const columns: TableProps['columns'] = [ - { - title: 'Name', - dataIndex: 'name', - key: 'name', - render: (name: string) => { - const path = window.location.pathname.split('/') - path.pop() - const route = `${path.join('/')}/${name}` - - return {name} - }, - }, - { - title: () => ( - - Help lorem ipsum. - - Learn More - - - }> - - ID - - - - ), - dataIndex: 'id', - key: 'id', - }, - { - title: 'Timestamp (UTC)', - dataIndex: 'timestamp', - key: 'timestamp', - render: (timestampInMicroseconds: number): string => { - return new Date(timestampInMicroseconds / (1000 * 1000)).toLocaleString(undefined, { - month: 'short', - day: '2-digit', - year: 'numeric', - hour: 'numeric', - minute: '2-digit', - second: '2-digit', - timeZone: 'UTC', - hour12: false, - }) - }, - }, - { - title: 'mPID', - dataIndex: 'mpId', - key: 'mpId', - render: (mpId: string): ReactNode => { - return }}>{mpId} - }, - }, - { - title: 'Output', - dataIndex: 'output', - key: 'output', - }, - { - title: 'Environment', - key: 'environment', - dataIndex: 'environment', - render: (env: Environment): React.ReactNode => { - return {getNameForEnvironment(env)} - }, - }, - { - title: 'Status', - dataIndex: 'status', - key: 'status', - render: (status: Status): React.ReactNode => , - }, - { - title: 'Actions', - dataIndex: 'actions', - key: 'actions', - render: (): ReactNode => ( - } - placeholder="Search" - style={{ width: '240px' }} - /> - - - columns={columns} dataSource={data} scroll={{ x: 'max-content' }} /> - - ), -} - -const TIME_OPTIONS = [ - { - value: 'last12hours', - label: 'Last 12 hours', - } as const, - { - value: 'last7days', - label: 'Last 7 days', - } as const, - { - value: 'last14days', - label: 'Last 14 days', - } as const, -] - -export const WithComplexFilters: Story = { - name: 'Complex', - render: () => ( - - - - - onUpdateFilters({ time })} - rangePickerProps={{ - showTime: true, - showHour: true, - showMinute: true, - showSecond: false, - disabledDate: (antdDayJS: any) => { - const fourteenDaysInMs = 14 * 24 * 60 * 60 * 1000 - return antdDayJS.isBefore(new Date(Date.now() - fourteenDaysInMs)) - }, - }} - /> - - } - placeholder="Search" - style={{ width: '240px' }} - /> - - - columns={columns} dataSource={data} scroll={{ x: 'max-content' }} /> - - ), -} diff --git a/docs/UX Patterns/Table/Table.mdx b/docs/UX Patterns/Table/Table.mdx index 33dacf04a..9ad581a53 100644 --- a/docs/UX Patterns/Table/Table.mdx +++ b/docs/UX Patterns/Table/Table.mdx @@ -29,7 +29,6 @@ A table row with expand and collapse functionality, allowing users to toggle add -> Insert Example - ### Pagination Use pagination in tables to improve performance and reduce load times by fetching only the data needed for the current page. @@ -40,14 +39,11 @@ Use pagination in tables to improve performance and reduce load times by fetchin Filters help users narrow down large datasets within tables. Learn more about the variety of filter types available in the mParticle table [here.](https://mparticle.github.io/aquarium/?path=/docs/ux-patterns-table-filters--documentation) - ### Table Example
- - - + ### Related Links diff --git a/docs/UX Patterns/Table/Table.stories.tsx b/docs/UX Patterns/Table/Table.stories.tsx index bb0f6eb45..c774dd819 100644 --- a/docs/UX Patterns/Table/Table.stories.tsx +++ b/docs/UX Patterns/Table/Table.stories.tsx @@ -1,200 +1,30 @@ -import type { ReactNode } from 'react' -import { CopyOutlined } from '@ant-design/icons' -import { faker } from '@faker-js/faker' import type { Meta, StoryObj } from '@storybook/react' import { Flex, Icon, Input, - Select, - Badge, - type IBadgeProps, Table, - type TableProps, - Tag, - type ITagProps, - Typography, Space, - Tooltip, + Button, + Checkbox, + Collapse, + ConfigProvider, + Divider, + type ICollapseProps, + Modal, + Select, + Typography, + TreeSelect, + Row, + Col, } from 'src/components' import { DatePickerWithDisabledYears } from 'src/components/data-entry/DatePicker/DatePicker.stories' -import { ColorError, ColorSuccess, ColorTextPlaceholder } from 'src/styles/style' - -interface DataType { - key: string - name: string - id: string - timestamp: number - output: string - environment: Environment - status: Status - mpId: string -} - -type Environment = 'development' | 'production' -type Status = 'draft' | 'error' | 'ready' - -const EnvironmentColors: Record = { - production: 'blue', - development: 'purple', -} - -const EnvironmentNames: Record = { - production: 'Prod', - development: 'Dev', -} - -const getTagColorForEnvironment = (env: Environment): ITagProps['color'] => EnvironmentColors[env] - -const getNameForEnvironment = (env: Environment) => EnvironmentNames[env] - -const StatusColors: Record = { - draft: ColorTextPlaceholder, - error: ColorError, - ready: ColorSuccess, -} - -const StatusNames: Record = { - draft: 'Draft', - error: 'Error', - ready: 'Ready', -} - -const getStatusColor = (status: Status) => StatusColors[status] - -const getStatusName = (status: Status) => StatusNames[status] - -const columns: TableProps['columns'] = [ - { - title: 'Name', - dataIndex: 'name', - key: 'name', - render: (name: string) => { - const path = window.location.pathname.split('/') - path.pop() - const route = `${path.join('/')}/${name}` - - return {name} - }, - }, - { - title: () => ( - - ID - - Help lorem ipsum. - - Learn More - - - }> - - - - ), - dataIndex: 'id', - key: 'id', - }, - { - title: 'Timestamp (UTC)', - dataIndex: 'timestamp', - key: 'timestamp', - render: (timestampInMicroseconds: number): string => { - return new Date(timestampInMicroseconds / (1000 * 1000)).toLocaleString(undefined, { - month: 'short', - day: '2-digit', - year: 'numeric', - hour: 'numeric', - minute: '2-digit', - second: '2-digit', - timeZone: 'UTC', - hour12: false, - }) - }, - }, - { - title: 'mPID', - dataIndex: 'mpId', - key: 'mpId', - render: (mpId: string): ReactNode => { - return }}>{mpId} - }, - }, - { - title: 'Output', - dataIndex: 'output', - key: 'output', - }, - { - title: 'Environment', - key: 'environment', - dataIndex: 'environment', - render: (env: Environment): React.ReactNode => { - return {getNameForEnvironment(env)} - }, - }, - { - title: 'Status', - dataIndex: 'status', - key: 'status', - render: (status: Status): React.ReactNode => , - }, - { - title: 'Actions', - dataIndex: 'actions', - key: 'actions', - render: (): ReactNode => ( - + Recent first + Oldest first + + + + + + + + + + + + + + + + } + placeholder="Search" + style={{ width: '240px' }} + /> + + + + columns={tableColumns} + dataSource={tableData.slice(0, 2)} + scroll={{ x: 'max-content' }} + /> + + + ) + }, +} diff --git a/docs/UX Patterns/Table/TableStoryUtils.tsx b/docs/UX Patterns/Table/TableStoryUtils.tsx new file mode 100644 index 000000000..55600a6e0 --- /dev/null +++ b/docs/UX Patterns/Table/TableStoryUtils.tsx @@ -0,0 +1,201 @@ +import type { ReactNode } from 'react' +import { faker } from '@faker-js/faker' +import { CopyOutlined } from '@ant-design/icons' +import { type TableProps, Typography, Flex, Tooltip, Tag, Badge, Select } from 'src/components' +import { type ITagProps, type IBadgeProps, Icon } from 'src/components' +import { ColorTextPlaceholder, ColorError, ColorSuccess } from 'src/styles/style' + +export interface TableDataType { + key: string + name: string + id: string + timestamp: number + output: string + environment: Environment + status: Status + mpId: string +} + +type Environment = 'development' | 'production' +type Status = 'draft' | 'error' | 'ready' + +const EnvironmentColors: Record = { + production: 'blue', + development: 'purple', +} + +const EnvironmentNames: Record = { + production: 'Prod', + development: 'Dev', +} + +const getTagColorForEnvironment = (env: Environment): ITagProps['color'] => EnvironmentColors[env] + +const getNameForEnvironment = (env: Environment) => EnvironmentNames[env] + +const StatusColors: Record = { + draft: ColorTextPlaceholder, + error: ColorError, + ready: ColorSuccess, +} + +const StatusNames: Record = { + draft: 'Draft', + error: 'Error', + ready: 'Ready', +} + +const getStatusColor = (status: Status) => StatusColors[status] + +const getStatusName = (status: Status) => StatusNames[status] + +export const tableColumns: TableProps['columns'] = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + render: (name: string): ReactNode => { + const path = window.location.pathname.split('/') + path.pop() + const route = `${path.join('/')}/${name}` + + return {name} + }, + }, + { + title: () => ( + + ID + + Help lorem ipsum. + + Learn More + + + }> + + + + ), + dataIndex: 'id', + key: 'id', + }, + { + title: 'Timestamp (UTC)', + dataIndex: 'timestamp', + key: 'timestamp', + render: (timestampInMicroseconds: number): string => { + return new Date(timestampInMicroseconds / (1000 * 1000)).toLocaleString(undefined, { + month: 'short', + day: '2-digit', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + timeZone: 'UTC', + hour12: false, + }) + }, + }, + { + title: 'mPID', + dataIndex: 'mpId', + key: 'mpId', + render: (mpId: string): ReactNode => { + return }}>{mpId} + }, + }, + { + title: 'Output', + dataIndex: 'output', + key: 'output', + }, + { + title: 'Environment', + key: 'environment', + dataIndex: 'environment', + render: (env: Environment): React.ReactNode => { + return {getNameForEnvironment(env)} + }, + }, + { + title: 'Status', + dataIndex: 'status', + key: 'status', + render: (status: Status): React.ReactNode => , + }, + { + title: 'Actions', + dataIndex: 'actions', + key: 'actions', + render: (): ReactNode => ( +