From aacc39002c3082329e1abb94742e1af9f0d2a61e Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 26 Oct 2023 13:11:38 -0700 Subject: [PATCH] [docs] Update table selection example to support switching between controlled & uncontrolled logic - requires creating a new prop and semi custom section - + clean up and remove unnecessary table data not relevant to the demo(s) --- ...tsx => in_memory_selection_controlled.tsx} | 78 ++---- .../in_memory/in_memory_selection_section.js | 103 +++---- .../in_memory_selection_uncontrolled.tsx | 259 ++++++++++++++++++ ...selection.tsx => selection_controlled.tsx} | 35 +-- .../tables/selection/selection_section.js | 108 ++++++-- .../selection/selection_uncontrolled.tsx | 234 ++++++++++++++++ 6 files changed, 652 insertions(+), 165 deletions(-) rename src-docs/src/views/tables/in_memory/{in_memory_selection.tsx => in_memory_selection_controlled.tsx} (77%) create mode 100644 src-docs/src/views/tables/in_memory/in_memory_selection_uncontrolled.tsx rename src-docs/src/views/tables/selection/{selection.tsx => selection_controlled.tsx} (86%) create mode 100644 src-docs/src/views/tables/selection/selection_uncontrolled.tsx diff --git a/src-docs/src/views/tables/in_memory/in_memory_selection.tsx b/src-docs/src/views/tables/in_memory/in_memory_selection_controlled.tsx similarity index 77% rename from src-docs/src/views/tables/in_memory/in_memory_selection.tsx rename to src-docs/src/views/tables/in_memory/in_memory_selection_controlled.tsx index 5d01925f869..a6d87de580d 100644 --- a/src-docs/src/views/tables/in_memory/in_memory_selection.tsx +++ b/src-docs/src/views/tables/in_memory/in_memory_selection_controlled.tsx @@ -1,18 +1,15 @@ -import React, { useState, useRef, ReactNode } from 'react'; +import React, { useState, ReactNode } from 'react'; import { faker } from '@faker-js/faker'; -import { formatDate, Random } from '../../../../../src/services'; +import { Random } from '../../../../../src/services'; import { EuiInMemoryTable, EuiBasicTableColumn, EuiTableSelectionType, EuiSearchBarProps, - EuiLink, EuiHealth, EuiButton, EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, EuiSpacer, } from '../../../../../src/components'; @@ -20,8 +17,6 @@ type User = { id: number; firstName: string | null | undefined; lastName: string; - github: string; - dateOfBirth: Date; online: boolean; location: { city: string; @@ -36,8 +31,6 @@ for (let i = 0; i < 20; i++) { id: i + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), - github: faker.internet.userName(), - dateOfBirth: faker.date.past(), online: faker.datatype.boolean(), location: { city: faker.location.city(), @@ -46,17 +39,6 @@ for (let i = 0; i < 20; i++) { }); } -const onlineUsers = userData.filter((user) => user.online); - -const deleteUsersByIds = (...ids: number[]) => { - ids.forEach((id) => { - const index = userData.findIndex((user) => user.id === id); - if (index >= 0) { - userData.splice(index, 1); - } - }); -}; - const columns: Array> = [ { field: 'firstName', @@ -83,23 +65,6 @@ const columns: Array> = [ show: false, }, }, - { - field: 'github', - name: 'Github', - render: (username: User['github']) => ( - - {username} - - ), - }, - { - field: 'dateOfBirth', - name: 'Date of Birth', - dataType: 'date', - render: (dateOfBirth: User['dateOfBirth']) => - formatDate(dateOfBirth, 'dobLong'), - sortable: true, - }, { field: 'location', name: 'Location', @@ -153,7 +118,6 @@ export default () => { const [selection, setSelection] = useState([]); const [error, setError] = useState(); - const tableRef = useRef | null>(null); const loadUsers = () => { setMessage('Loading users...'); @@ -168,6 +132,8 @@ export default () => { }, random.number({ min: 0, max: 3000 })); }; + const onlineUsers = users.filter((user) => user.online); + const loadUsersWithError = () => { setMessage('Loading users...'); setLoading(true); @@ -187,7 +153,23 @@ export default () => { } const onClick = () => { - deleteUsersByIds(...selection.map((user) => user.id)); + const deleteUsersByIds = (users: User[], ids: number[]) => { + const updatedUsers = [...users]; + ids.forEach((id) => { + const index = updatedUsers.findIndex((user) => user.id === id); + if (index >= 0) { + updatedUsers.splice(index, 1); + } + }); + return updatedUsers; + }; + + setUsers((users) => + deleteUsersByIds( + users, + selection.map((user) => user.id) + ) + ); setSelection([]); }; @@ -256,27 +238,19 @@ export default () => { selectableMessage: (selectable) => !selectable ? 'User is currently offline' : '', onSelectionChange: (selection) => setSelection(selection), - initialSelected: onlineUsers, - }; - - const onSelection = () => { - tableRef.current?.setSelection(onlineUsers); + selected: selection, }; return ( <> - - - Select online users - - - + setSelection(onlineUsers)}> + Select online users + - The following example shows how to use EuiInMemoryTable{' '} - along with item selection. It also shows how you can display messages, - errors and show loading indication. You can set items to be selected - initially by passing an array of items as the{' '} - initialSelected value inside{' '} - selection property and passing{' '} - itemId property to enable selection. You can also use - the setSelection method to take complete control over - table selection. This can be useful if you want to handle selection in - table based on user interaction with another part of the UI. -

+ <> +

+ To enable selection, both the itemId and{' '} + selection props must be passed. The following example + shows how to use EuiInMemoryTable with both controlled + and uncontrolled item selection. It also shows how you can display + messages, errors and show loading indication. +

+

+ For uncontrolled usage, where selection changes are determined entirely + by the user, you can set items to be selected initially by passing an + array of items to an array of items to{' '} + selection.initialSelected. You can also use{' '} + selected.onSelectionChange to track or respond to the + items that users select. +

+

+ To completely control table selection, use{' '} + selection.selected instead (which requires passing{' '} + selected.onSelectionChange). This can be useful if + you want to handle selection in table based on user interaction with + another part of the UI. +

+ + ), + children: ( + <> + + } + uncontrolledDemo={} + controlledSource={controlledSource} + uncontrolledSource={uncontrolledSource} + /> + ), - props: { - EuiInMemoryTable, - Criteria, - CriteriaWithPagination, - Pagination, - EuiTableSortingType, - EuiTableSelectionType, - EuiTableFieldDataColumnType, - EuiTableComputedColumnType, - EuiTableActionsColumnType, - DefaultItemAction, - CustomItemAction, - Search, - SearchFilterConfig, - FieldValueOptionType, - FieldValueToggleGroupFilterItemType, - }, - demo: , }; diff --git a/src-docs/src/views/tables/in_memory/in_memory_selection_uncontrolled.tsx b/src-docs/src/views/tables/in_memory/in_memory_selection_uncontrolled.tsx new file mode 100644 index 00000000000..c3c0a1258dc --- /dev/null +++ b/src-docs/src/views/tables/in_memory/in_memory_selection_uncontrolled.tsx @@ -0,0 +1,259 @@ +import React, { useState, ReactNode } from 'react'; +import { faker } from '@faker-js/faker'; +import { Random } from '../../../../../src/services'; + +import { + EuiInMemoryTable, + EuiBasicTableColumn, + EuiTableSelectionType, + EuiSearchBarProps, + EuiHealth, + EuiButton, + EuiEmptyPrompt, +} from '../../../../../src/components'; + +type User = { + id: number; + firstName: string | null | undefined; + lastName: string; + online: boolean; + location: { + city: string; + country: string; + }; +}; + +const userData: User[] = []; + +for (let i = 0; i < 20; i++) { + userData.push({ + id: i + 1, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + online: faker.datatype.boolean(), + location: { + city: faker.location.city(), + country: faker.location.country(), + }, + }); +} + +const onlineUsers = userData.filter((user) => user.online); + +const columns: Array> = [ + { + field: 'firstName', + name: 'First Name', + sortable: true, + truncateText: true, + mobileOptions: { + render: (user: User) => ( + + {user.firstName} {user.lastName} + + ), + header: false, + truncateText: false, + enlarge: true, + width: '100%', + }, + }, + { + field: 'lastName', + name: 'Last Name', + truncateText: true, + mobileOptions: { + show: false, + }, + }, + { + field: 'location', + name: 'Location', + truncateText: true, + textOnly: true, + render: (location: User['location']) => { + return `${location.city}, ${location.country}`; + }, + }, + { + field: 'online', + name: 'Online', + dataType: 'boolean', + render: (online: User['online']) => { + const color = online ? 'success' : 'danger'; + const label = online ? 'Online' : 'Offline'; + return {label}; + }, + sortable: true, + mobileOptions: { + show: false, + }, + }, +]; + +const random = new Random(); + +const noItemsFoundMsg = 'No users match search criteria'; + +export default () => { + const [loading, setLoading] = useState(false); + const [users, setUsers] = useState([]); + const [message, setMessage] = useState( + No users} + titleSize="xs" + body="Looks like you don’t have any users. Let’s create some!" + actions={ + { + loadUsers(); + }} + > + Load Users + + } + /> + ); + + const [selection, setSelection] = useState([]); + const [error, setError] = useState(); + + const loadUsers = () => { + setMessage('Loading users...'); + setLoading(true); + setUsers([]); + setError(undefined); + setTimeout(() => { + setLoading(false); + setMessage(noItemsFoundMsg); + setError(undefined); + setUsers(userData); + }, random.number({ min: 0, max: 3000 })); + }; + + const loadUsersWithError = () => { + setMessage('Loading users...'); + setLoading(true); + setUsers([]); + setError(undefined); + setTimeout(() => { + setLoading(false); + setMessage(noItemsFoundMsg); + setError('ouch!... again... '); + setUsers([]); + }, random.number({ min: 0, max: 3000 })); + }; + + const renderToolsLeft = () => { + if (selection.length === 0) { + return; + } + + const onClick = () => { + const deleteUsersByIds = (users: User[], ids: number[]) => { + const updatedUsers = [...users]; + ids.forEach((id) => { + const index = updatedUsers.findIndex((user) => user.id === id); + if (index >= 0) { + updatedUsers.splice(index, 1); + } + }); + return updatedUsers; + }; + + setUsers((users) => + deleteUsersByIds( + users, + selection.map((user) => user.id) + ) + ); + setSelection([]); + }; + + return ( + + Delete {selection.length} Users + + ); + }; + + const renderToolsRight = () => { + return [ + { + loadUsers(); + }} + isDisabled={loading} + > + Load Users + , + { + loadUsersWithError(); + }} + isDisabled={loading} + > + Load Users (Error) + , + ]; + }; + + const search: EuiSearchBarProps = { + toolsLeft: renderToolsLeft(), + toolsRight: renderToolsRight(), + box: { + incremental: true, + }, + filters: [ + { + type: 'is', + field: 'online', + name: 'Online', + negatedName: 'Offline', + }, + { + type: 'field_value_selection', + field: 'location.country', + name: 'Country', + multiSelect: false, + options: userData.map(({ location: { country } }) => ({ + value: country, + })), + }, + ], + }; + + const pagination = { + initialPageSize: 5, + pageSizeOptions: [3, 5, 8], + }; + + const selectionValue: EuiTableSelectionType = { + selectable: (user) => user.online, + selectableMessage: (selectable) => + !selectable ? 'User is currently offline' : '', + onSelectionChange: (selection) => setSelection(selection), + initialSelected: onlineUsers, + }; + + return ( + + ); +}; diff --git a/src-docs/src/views/tables/selection/selection.tsx b/src-docs/src/views/tables/selection/selection_controlled.tsx similarity index 86% rename from src-docs/src/views/tables/selection/selection.tsx rename to src-docs/src/views/tables/selection/selection_controlled.tsx index fe6484debd6..45173ff008f 100644 --- a/src-docs/src/views/tables/selection/selection.tsx +++ b/src-docs/src/views/tables/selection/selection_controlled.tsx @@ -1,6 +1,6 @@ -import React, { useState, useRef } from 'react'; +import React, { useState } from 'react'; import { faker } from '@faker-js/faker'; -import { formatDate, Comparators } from '../../../../../src/services'; +import { Comparators } from '../../../../../src/services'; import { EuiBasicTable, @@ -8,7 +8,6 @@ import { EuiTableSelectionType, EuiTableSortingType, Criteria, - EuiLink, EuiHealth, EuiButton, EuiFlexGroup, @@ -20,8 +19,6 @@ type User = { id: number; firstName: string | null | undefined; lastName: string; - github: string; - dateOfBirth: Date; online: boolean; location: { city: string; @@ -36,8 +33,6 @@ for (let i = 0; i < 20; i++) { id: i + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), - github: faker.internet.userName(), - dateOfBirth: faker.date.past(), online: faker.datatype.boolean(), location: { city: faker.location.city(), @@ -46,7 +41,7 @@ for (let i = 0; i < 20; i++) { }); } -const onlineUsers = users.filter((user) => user.online); +const getOnlineUsers = () => users.filter((user) => user.online); const deleteUsersByIds = (...ids: number[]) => { ids.forEach((id) => { @@ -83,23 +78,6 @@ const columns: Array> = [ show: false, }, }, - { - field: 'github', - name: 'Github', - render: (username: User['github']) => ( - - {username} - - ), - }, - { - field: 'dateOfBirth', - name: 'Date of Birth', - dataType: 'date', - render: (dateOfBirth: User['dateOfBirth']) => - formatDate(dateOfBirth, 'dobLong'), - sortable: true, - }, { field: 'location', name: 'Location', @@ -133,10 +111,8 @@ export default () => { const onSelectionChange = (selectedItems: User[]) => { setSelectedItems(selectedItems); }; - - const tableRef = useRef(null); const selectOnlineUsers = () => { - tableRef.current?.setSelection(onlineUsers); + setSelectedItems(getOnlineUsers()); }; const selection: EuiTableSelectionType = { @@ -144,7 +120,7 @@ export default () => { selectableMessage: (selectable: boolean) => !selectable ? 'User is currently offline' : '', onSelectionChange, - initialSelected: onlineUsers, + selected: selectedItems, }; const deleteSelectedUsers = () => { @@ -253,7 +229,6 @@ export default () => { { + const [isControlled, setIsControlled] = useState(false); + + return ( + + + setIsControlled(!isControlled)} + /> + + + {isControlled ? controlledDemo : uncontrolledDemo} + + + + + + ); +}; export const section = { title: 'Adding selection to a table', - source: [ - { - type: GuideSectionTypes.TSX, - code: source, - }, - ], text: ( -

- The following example shows how to configure selection via the{' '} - selection - property. You can set items to be selected initially by passing an array - of items as the initialSelected value inside{' '} - selection property. You can also use the{' '} - setSelection method to take complete control over table - selection. This can be useful if you want to handle selection in table - based on user interaction with another part of the UI. -

+ <> +

+ The following example shows how to configure selection via the{' '} + selection property. For uncontrolled usage, where + selection changes are determined entirely by the user, you can set items + to be selected initially by passing an array of items to an array of + items to selection.initialSelected. You can also use{' '} + selected.onSelectionChange to track or respond to the + items that users select. +

+

+ To completely control table selection, use{' '} + selection.selected instead (which requires passing{' '} + selected.onSelectionChange). This can be useful if + you want to handle selection in table based on user interaction with + another part of the UI. +

+ + ), + children: ( + <> + + } + uncontrolledSource={uncontrolledSource} + controlledDemo={} + controlledSource={controlledSource} + /> + ), - components: { EuiBasicTable }, - demo:
, }; diff --git a/src-docs/src/views/tables/selection/selection_uncontrolled.tsx b/src-docs/src/views/tables/selection/selection_uncontrolled.tsx new file mode 100644 index 00000000000..3b0da5e2085 --- /dev/null +++ b/src-docs/src/views/tables/selection/selection_uncontrolled.tsx @@ -0,0 +1,234 @@ +import React, { useState } from 'react'; +import { faker } from '@faker-js/faker'; +import { Comparators } from '../../../../../src/services'; + +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiTableSelectionType, + EuiTableSortingType, + Criteria, + EuiHealth, + EuiButton, + EuiSpacer, +} from '../../../../../src/components'; + +type User = { + id: number; + firstName: string | null | undefined; + lastName: string; + online: boolean; + location: { + city: string; + country: string; + }; +}; + +const users: User[] = []; + +for (let i = 0; i < 20; i++) { + users.push({ + id: i + 1, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + online: faker.datatype.boolean(), + location: { + city: faker.location.city(), + country: faker.location.country(), + }, + }); +} + +const onlineUsers = users.filter((user) => user.online); + +const deleteUsersByIds = (...ids: number[]) => { + ids.forEach((id) => { + const index = users.findIndex((user) => user.id === id); + if (index >= 0) { + users.splice(index, 1); + } + }); +}; + +const columns: Array> = [ + { + field: 'firstName', + name: 'First Name', + sortable: true, + truncateText: true, + mobileOptions: { + render: (user: User) => ( + + {user.firstName} {user.lastName} + + ), + header: false, + truncateText: false, + enlarge: true, + width: '100%', + }, + }, + { + field: 'lastName', + name: 'Last Name', + truncateText: true, + mobileOptions: { + show: false, + }, + }, + { + field: 'location', + name: 'Location', + truncateText: true, + textOnly: true, + render: (location: User['location']) => { + return `${location.city}, ${location.country}`; + }, + }, + { + field: 'online', + name: 'Online', + dataType: 'boolean', + render: (online: User['online']) => { + const color = online ? 'success' : 'danger'; + const label = online ? 'Online' : 'Offline'; + return {label}; + }, + sortable: true, + mobileOptions: { + show: false, + }, + }, +]; + +export default () => { + /** + * Selection + */ + const [selectedItems, setSelectedItems] = useState([]); + const onSelectionChange = (selectedItems: User[]) => { + setSelectedItems(selectedItems); + }; + + const selection: EuiTableSelectionType = { + selectable: (user: User) => user.online, + selectableMessage: (selectable: boolean) => + !selectable ? 'User is currently offline' : '', + onSelectionChange, + initialSelected: onlineUsers, + }; + + const deleteSelectedUsers = () => { + deleteUsersByIds(...selectedItems.map((user: User) => user.id)); + setSelectedItems([]); + }; + + const deleteButton = + selectedItems.length > 0 ? ( + + Delete {selectedItems.length} Users + + ) : null; + + /** + * Pagination & sorting + */ + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(5); + const [sortField, setSortField] = useState('firstName'); + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); + + const onTableChange = ({ page, sort }: Criteria) => { + if (page) { + const { index: pageIndex, size: pageSize } = page; + setPageIndex(pageIndex); + setPageSize(pageSize); + } + if (sort) { + const { field: sortField, direction: sortDirection } = sort; + setSortField(sortField); + setSortDirection(sortDirection); + } + }; + + // Manually handle sorting and pagination of data + const findUsers = ( + users: User[], + pageIndex: number, + pageSize: number, + sortField: keyof User, + sortDirection: 'asc' | 'desc' + ) => { + let items; + + if (sortField) { + items = users + .slice(0) + .sort( + Comparators.property(sortField, Comparators.default(sortDirection)) + ); + } else { + items = users; + } + + let pageOfItems; + + if (!pageIndex && !pageSize) { + pageOfItems = items; + } else { + const startIndex = pageIndex * pageSize; + pageOfItems = items.slice( + startIndex, + Math.min(startIndex + pageSize, users.length) + ); + } + + return { + pageOfItems, + totalItemCount: users.length, + }; + }; + + const { pageOfItems, totalItemCount } = findUsers( + users, + pageIndex, + pageSize, + sortField, + sortDirection + ); + + const pagination = { + pageIndex: pageIndex, + pageSize: pageSize, + totalItemCount: totalItemCount, + pageSizeOptions: [3, 5, 8], + }; + + const sorting: EuiTableSortingType = { + sort: { + field: sortField, + direction: sortDirection, + }, + }; + + return ( + <> + {deleteButton} + + + + + + ); +};