From 904c370378586482443cec743ce41184cf7f4b58 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Wed, 28 Jul 2021 10:16:43 -0400 Subject: [PATCH] [App Search] Added a ReorderableTable component (#106306) --- .../app_search/components/library/library.tsx | 103 +++++++++- .../reorderable_table/body_row.test.tsx | 75 +++++++ .../tables/reorderable_table/body_row.tsx | 51 +++++ .../reorderable_table/body_rows.test.tsx | 41 ++++ .../tables/reorderable_table/body_rows.tsx | 17 ++ .../tables/reorderable_table/cell.test.tsx | 30 +++ .../shared/tables/reorderable_table/cell.tsx | 33 +++ .../tables/reorderable_table/constants.ts | 14 ++ .../draggable_body_row.test.tsx | 70 +++++++ .../reorderable_table/draggable_body_row.tsx | 48 +++++ .../draggable_body_rows.test.tsx | 60 ++++++ .../reorderable_table/draggable_body_rows.tsx | 39 ++++ .../reorderable_table/header_row.test.tsx | 38 ++++ .../tables/reorderable_table/header_row.tsx | 41 ++++ .../shared/tables/reorderable_table/index.ts | 8 + .../reorderable_table/reorderable_table.scss | 26 +++ .../reorderable_table.test.tsx | 191 ++++++++++++++++++ .../reorderable_table/reorderable_table.tsx | 106 ++++++++++ .../shared/tables/reorderable_table/types.ts | 16 ++ 19 files changed, 1006 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_row.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_row.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_rows.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_rows.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/cell.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/cell.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/constants.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_row.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_row.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_rows.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_rows.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/header_row.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/header_row.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/types.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx index b9d3dbd9ee41..e9ef3b3cfee2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx @@ -19,12 +19,18 @@ import { EuiDroppable, EuiDraggable, EuiButtonIconColor, + EuiEmptyPrompt, } from '@elastic/eui'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { Schema, SchemaType } from '../../../shared/schema/types'; +import { ReorderableTable } from '../../../shared/tables/reorderable_table'; import { Result } from '../result'; +const NO_ITEMS = ( + No Items} body={

No Items

} /> +); + export const Library: React.FC = () => { const props = { isMetaEngine: false, @@ -249,13 +255,108 @@ export const Library: React.FC = () => { -

With field value type highlights

+ + +

ReorderableTable

+
+ + +
{item.id}
}, + { name: 'Whatever', render: (item) =>
Whatever
}, + ]} + /> + + + +

With reordering disabled

+
+ +
{item.id}
}, + { name: 'Whatever', render: (item) =>
Whatever
}, + ]} + /> + + + +

With reordering enabled, but dragging disabled

+
+ +
{item.id}
}, + { name: 'Whatever', render: (item) =>
Whatever
}, + ]} + /> + + + +

With unreorderable items

+
+ +
{item.id}
}, + { name: 'Whatever', render: (item) =>
Whatever
}, + ]} + /> + + + +

Using the rowProps prop to apply dynamic properties to each row

+
+ + ({ + style: { + backgroundColor: item.id % 2 === 0 ? 'red' : 'green', + }, + })} + noItemsMessage={NO_ITEMS} + items={[{ id: 1 }, { id: 2 }, { id: 3 }]} + columns={[ + { name: 'ID', render: (item) =>
{item.id}
}, + { name: 'Whatever', render: (item) =>
Whatever
}, + ]} + /> + + + +

With no items

+
+ +
{item.id}
}, + { name: 'Whatever', render: (item) =>
Whatever
}, + ]} + /> + + + + diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_row.test.tsx new file mode 100644 index 000000000000..4484fd89ed22 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_row.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { BodyRow } from './body_row'; +import { Cell } from './cell'; + +interface Foo { + id: number; +} + +describe('BodyRow', () => { + const columns = [ + { + name: 'ID', + flexBasis: 'foo', + flexGrow: 0, + alignItems: 'bar', + render: (item: Foo) => item.id, + }, + { + name: 'Whatever', + render: () => 'Whatever', + }, + ]; + + const item = { id: 1 }; + + it('renders a table row from the provided item and columns', () => { + const wrapper = shallow(); + + const cells = wrapper.find(Cell); + expect(cells.length).toBe(2); + + expect(cells.at(0).props()).toEqual({ + alignItems: 'bar', + children: 1, + flexBasis: 'foo', + flexGrow: 0, + }); + + expect(cells.at(1).props()).toEqual({ + children: 'Whatever', + }); + }); + + it('will accept additional properties to apply to this row', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="row"]').hasClass('some_class_name')).toBe(true); + }); + + it('will render an additional cell in the first column if one is provided', () => { + const wrapper = shallow( + Left Action} /> + ); + const cells = wrapper.find(Cell); + expect(cells.length).toBe(3); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_row.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_row.tsx new file mode 100644 index 000000000000..474d49f5eef0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_row.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { Cell } from './cell'; +import { DRAGGABLE_UX_STYLE } from './constants'; +import { Column } from './types'; + +export interface BodyRowProps { + columns: Array>; + item: Item; + additionalProps?: object; + // Cell to put in first column before other columns + leftAction?: React.ReactNode; +} + +export const BodyRow = ({ + columns, + item, + additionalProps, + leftAction, +}: BodyRowProps) => { + return ( +
+ + + + {!!leftAction && {leftAction}} + {columns.map((column, columnIndex) => ( + + {column.render(item)} + + ))} + + + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_rows.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_rows.test.tsx new file mode 100644 index 000000000000..6a8f59b7a4d0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_rows.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { BodyRows } from './body_rows'; + +const Row = ({ item, itemIndex }: { item: Item; itemIndex: number }) => ( +
+ {item} {itemIndex} +
+); + +describe('BodyRows', () => { + it('renders a row for each provided item', () => { + const wrapper = shallow( + } + /> + ); + const rows = wrapper.find(Row); + expect(rows.length).toBe(2); + + expect(rows.at(0).props()).toEqual({ + itemIndex: 0, + item: { id: 1 }, + }); + + expect(rows.at(1).props()).toEqual({ + itemIndex: 1, + item: { id: 2 }, + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_rows.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_rows.tsx new file mode 100644 index 000000000000..9f2a89e51f15 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_rows.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +export interface BodyRowsProps { + items: Item[]; + renderItem: (item: Item, itemIndex: number) => React.ReactNode; +} + +export const BodyRows = ({ items, renderItem }: BodyRowsProps) => ( + <>{items.map(renderItem)} +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/cell.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/cell.test.tsx new file mode 100644 index 000000000000..c3f74ec18b6c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/cell.test.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { Cell } from './cell'; + +describe('Cell', () => { + it('renders a table cell with the provided content and styles', () => { + const wrapper = shallow( + + Content + + ); + expect(wrapper.props()).toEqual({ + style: { + flexBasis: 'foo', + flexGrow: 0, + alignItems: 'bar', + }, + children: 'Content', + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/cell.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/cell.tsx new file mode 100644 index 000000000000..64f4ce1c718c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/cell.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiFlexItem } from '@elastic/eui'; + +import { DraggableUXStyles } from './types'; + +type CellProps = DraggableUXStyles; + +export const Cell = ({ + children, + alignItems, + flexBasis, + flexGrow, +}: CellProps & { children?: React.ReactNode }) => { + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/constants.ts new file mode 100644 index 000000000000..7dae50b1ff7f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/constants.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DraggableUXStyles } from './types'; + +export const DRAGGABLE_UX_STYLE: DraggableUXStyles = { + flexBasis: '16px', + flexGrow: 0, + alignItems: 'center', +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_row.test.tsx new file mode 100644 index 000000000000..b7f9b38f802f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_row.test.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiDraggable, EuiIcon } from '@elastic/eui'; + +import { BodyRow } from './body_row'; +import { DraggableBodyRow } from './draggable_body_row'; + +interface Foo { + id: number; +} + +describe('DraggableBodyRow', () => { + const columns = [ + { + name: 'ID', + flexBasis: 'foo', + flexGrow: 0, + alignItems: 'bar', + render: (item: Foo) => item.id, + }, + { + name: 'Whatever', + render: () => 'Whatever', + }, + ]; + const item = { id: 1 }; + const additionalProps = {}; + + it('wraps a BodyRow with an EuiDraggable and injects a drag handle as the first cell', () => { + const wrapper = shallow( + + ); + + const euiDraggable = wrapper.find(EuiDraggable); + expect(euiDraggable.exists()).toBe(true); + // It adds an index and unique draggable id from the provided rowIndex + expect(euiDraggable.prop('index')).toEqual(1); + expect(euiDraggable.prop('draggableId')).toEqual('draggable_row_1'); + + const bodyRow = wrapper.find(BodyRow); + expect(bodyRow.exists()).toEqual(true); + expect(bodyRow.props()).toEqual(expect.objectContaining({ columns, item, additionalProps })); + const leftAction = shallow(
{bodyRow.prop('leftAction')}
); + expect(leftAction.find(EuiIcon).exists()).toBe(true); + }); + + it('will accept a parameter that disables dragging', () => { + const wrapper = shallow( + + ); + + const euiDraggable = wrapper.find(EuiDraggable); + expect(euiDraggable.prop('isDragDisabled')).toBe(true); + expect(euiDraggable.prop('disableInteractiveElementBlocking')).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_row.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_row.tsx new file mode 100644 index 000000000000..191843a2e6e7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_row.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiDraggable, EuiIcon } from '@elastic/eui'; + +import { BodyRow } from './body_row'; +import { Column } from './types'; + +export interface DraggableBodyRowProps { + columns: Array>; + item: Item; + rowIndex: number; + additionalProps?: object; + disableDragging?: boolean; +} + +export const DraggableBodyRow = ({ + columns, + item, + rowIndex, + additionalProps, + disableDragging = false, +}: DraggableBodyRowProps) => { + const draggableId = `draggable_row_${rowIndex}`; + + return ( + + : <>} + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_rows.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_rows.test.tsx new file mode 100644 index 000000000000..10fc09ea7422 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_rows.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiDragDropContext } from '@elastic/eui'; + +import { BodyRows } from './body_rows'; +import { DraggableBodyRows } from './draggable_body_rows'; + +describe('DraggableBodyRows', () => { + const items = [{ id: 1 }, { id: 2 }]; + const onReorder = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('wraps BodyRows in a EuiDragDropContext', () => { + const renderItem = jest.fn(); + const wrapper = shallow( + + ); + + expect(wrapper.find(EuiDragDropContext).exists()).toBe(true); + + const bodyRows = wrapper.find(BodyRows); + expect(bodyRows.props()).toEqual({ + items, + renderItem, + }); + }); + + it('will call the provided onReorder function whenever items are reordered', () => { + const wrapper = shallow( + + ); + wrapper.find(EuiDragDropContext).simulate('dragEnd', { + source: { index: 1 }, + destination: { index: 0 }, + }); + expect(onReorder).toHaveBeenCalledWith([{ id: 2 }, { id: 1 }], items); + }); + + it('will not call the provided onReorder function if there are not a source AND destination provided', () => { + const wrapper = shallow( + + ); + wrapper.find(EuiDragDropContext).simulate('dragEnd', { + source: { index: 1 }, + }); + expect(onReorder).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_rows.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_rows.tsx new file mode 100644 index 000000000000..e531dc451197 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_rows.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiDragDropContext, euiDragDropReorder, EuiDroppable } from '@elastic/eui'; + +import { BodyRows } from './body_rows'; + +interface DraggableBodyRowsProps { + items: Item[]; + onReorder: (reorderedItems: Item[], items: Item[]) => void; + renderItem: (item: Item, itemIndex: number) => React.ReactNode; +} + +export const DraggableBodyRows = ({ + items, + onReorder, + renderItem, +}: DraggableBodyRowsProps) => { + return ( + { + if (source && destination) { + const reorderedItems = euiDragDropReorder(items, source.index, destination?.index); + onReorder(reorderedItems, items); + } + }} + > + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/header_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/header_row.test.tsx new file mode 100644 index 000000000000..3bcf2e56f7c9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/header_row.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { Cell } from './cell'; +import { HeaderRow } from './header_row'; + +interface Foo { + id: number; +} + +describe('HeaderRow', () => { + const columns = [ + { name: 'ID', render: (item: Foo) =>
{item.id}
}, + { name: 'Whatever', render: () => 'Whatever' }, + ]; + + it('renders a table header row from the provided column names', () => { + const wrapper = shallow(); + const cells = wrapper.find(Cell); + expect(cells.length).toBe(2); + expect(cells.at(0).children().dive().text()).toEqual('ID'); + expect(cells.at(1).children().dive().text()).toEqual('Whatever'); + }); + + it('will render an additional cell in the first column if one is provided', () => { + const wrapper = shallow(Left Action} />); + const cells = wrapper.find(Cell); + expect(cells.length).toBe(3); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/header_row.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/header_row.tsx new file mode 100644 index 000000000000..d1f152c45f47 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/header_row.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + +import { Cell } from './cell'; +import { DRAGGABLE_UX_STYLE } from './constants'; +import { Column } from './types'; + +interface HeaderRowProps { + columns: Array>; + // Cell to put in first column before other columns + leftAction?: React.ReactNode; +} + +export const HeaderRow = ({ columns, leftAction }: HeaderRowProps) => { + return ( +
+ + + + {!!leftAction && {leftAction}} + {columns.map((column, columnIndex) => ( + + + {column.name} + + + ))} + + + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/index.ts new file mode 100644 index 000000000000..aab36fb2e001 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ReorderableTable } from './reorderable_table'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.scss b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.scss new file mode 100644 index 000000000000..d2ea90bbbfec --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.scss @@ -0,0 +1,26 @@ +.reorderableTable { + + &NoItems { + border-top: $euiBorderThin; + background-color: $euiColorEmptyShade; + } + + &Row { + border-top: $euiBorderThin; + background-color: $euiColorEmptyShade; + + > .euiFlexGroup--directionRow.euiFlexGroup--gutterLarge { + margin: 0; + } + } + + &Header { + > .euiFlexGroup--directionRow.euiFlexGroup--gutterLarge { + margin: -12px 0; + } + } + + .euiDraggable .euiDraggable__item.euiDraggable__item--isDisabled { + cursor: unset; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.test.tsx new file mode 100644 index 000000000000..78bbe2d3fc4e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.test.tsx @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { BodyRow } from './body_row'; +import { BodyRows } from './body_rows'; +import { DraggableBodyRow } from './draggable_body_row'; +import { DraggableBodyRows } from './draggable_body_rows'; +import { HeaderRow } from './header_row'; +import { ReorderableTable } from './reorderable_table'; +import { Column } from './types'; + +interface Foo { + id: number; +} + +describe('ReorderableTable', () => { + const items: Foo[] = [{ id: 1 }, { id: 2 }]; + const columns: Array> = []; + + describe('when the table is reorderable', () => { + it('renders with a header that has an additional column injected as the first column, which is empty', () => { + const wrapper = shallow( + No Items

} items={items} columns={columns} /> + ); + const header = wrapper.find(HeaderRow); + expect(header.exists()).toEqual(true); + expect(header.prop('columns')).toEqual(columns); + expect(header.prop('leftAction')).not.toBeUndefined(); + }); + + it('renders draggable rows inside of the reorderable table', () => { + const wrapper = shallow( + No Items

} items={items} columns={columns} /> + ); + const bodyRows = wrapper.find(DraggableBodyRows); + expect(bodyRows.exists()).toBe(true); + expect(bodyRows.prop('items')).toEqual(items); + + const renderedRow = bodyRows.renderProp('renderItem')({ id: 1 }, 0); + expect(renderedRow.type()).toEqual(DraggableBodyRow); + expect(renderedRow.props()).toEqual({ + columns, + item: { id: 1 }, + additionalProps: {}, + disableDragging: false, + rowIndex: 0, + }); + }); + + it('can append additional properties to each row, which can be dynamically calculated from the item in that row', () => { + const wrapper = shallow( + No Items

} + items={items} + columns={columns} + rowProps={(item) => ({ + className: `someClassName_${item.id}`, + })} + /> + ); + const renderedRow = wrapper.find(DraggableBodyRows).renderProp('renderItem')({ id: 1 }, 0); + expect(renderedRow.prop('additionalProps')).toEqual({ + className: 'someClassName_1', + }); + }); + + it('will disableDragging on individual rows if disableDragging is enabled', () => { + const wrapper = shallow( + No Items

} + items={items} + columns={columns} + disableDragging + /> + ); + const renderedRow = wrapper.find(DraggableBodyRows).renderProp('renderItem')({ id: 1 }, 0); + expect(renderedRow.prop('disableDragging')).toEqual(true); + }); + + it('will accept a callback which will be triggered every time a row is reordered', () => { + const onReorder = jest.fn(); + const wrapper = shallow( + No Items

} + items={items} + columns={columns} + onReorder={onReorder} + /> + ); + expect(wrapper.find(DraggableBodyRows).prop('onReorder')).toEqual(onReorder); + }); + + it('will provide a default callback for reordered if none is provided, which does nothing', () => { + const wrapper = shallow( + No Items

} items={items} columns={columns} /> + ); + const onReorder = wrapper.find(DraggableBodyRows).prop('onReorder'); + expect(onReorder([], [])).toBeUndefined(); + }); + + it('will render items that cant be reordered', () => { + const unreorderableItems = [{ id: 3 }]; + const wrapper = shallow( + No Items

} + items={items} + unreorderableItems={unreorderableItems} + columns={columns} + /> + ); + const bodyRows = wrapper.find(BodyRows); + expect(bodyRows.exists()).toBe(true); + expect(bodyRows.prop('items')).toEqual(unreorderableItems); + + const renderedRow = bodyRows.renderProp('renderItem')({ id: 1 }, 0); + expect(renderedRow.type()).toEqual(BodyRow); + expect(renderedRow.props()).toEqual({ + columns, + item: { id: 1 }, + additionalProps: {}, + leftAction: expect.anything(), + }); + }); + }); + + describe('when reorderable is turned off on the table', () => { + it('renders a table with a header and non-reorderable rows', () => { + const wrapper = shallow( + No Items

} + items={items} + columns={columns} + disableReordering + /> + ); + const bodyRows = wrapper.find(BodyRows); + expect(bodyRows.exists()).toBe(true); + expect(bodyRows.prop('items')).toEqual(items); + + const renderedRow = bodyRows.renderProp('renderItem')({ id: 1 }, 0); + expect(renderedRow.type()).toEqual(BodyRow); + expect(renderedRow.props()).toEqual({ + columns, + item: { id: 1 }, + additionalProps: {}, + }); + }); + + it('can append additional properties to each row, which can be dynamically calculated from the item in that row', () => { + const wrapper = shallow( + No Items

} + items={items} + columns={columns} + rowProps={(item) => ({ + className: `someClassName_${item.id}`, + })} + disableReordering + /> + ); + const renderedRow = wrapper.find(BodyRows).renderProp('renderItem')({ id: 1 }, 0); + expect(renderedRow.prop('additionalProps')).toEqual({ + className: 'someClassName_1', + }); + }); + }); + + it('appends an additional className if specified', () => { + const wrapper = shallow( + No Items

} items={[]} columns={[]} className="foo" /> + ); + expect(wrapper.hasClass('foo')).toBe(true); + }); + + it('will show a no items message when there are no items', () => { + const wrapper = shallow( + No Items

} items={[]} columns={columns} /> + ); + expect(wrapper.find('[data-test-subj="NoItems"]').exists()).toBe(true); + expect(wrapper.find(BodyRows).exists()).toBe(false); + expect(wrapper.find(DraggableBodyRows).exists()).toBe(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.tsx new file mode 100644 index 000000000000..f43b940fbf3e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import classNames from 'classnames'; + +import './reorderable_table.scss'; + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { BodyRow } from './body_row'; +import { BodyRows } from './body_rows'; +import { DraggableBodyRow } from './draggable_body_row'; +import { DraggableBodyRows } from './draggable_body_rows'; +import { HeaderRow } from './header_row'; +import { Column } from './types'; + +interface ReorderableTableProps { + columns: Array>; + items: Item[]; + noItemsMessage: React.ReactNode; + unreorderableItems?: Item[]; + className?: string; + disableDragging?: boolean; + disableReordering?: boolean; + onReorder?: (items: Item[], oldItems: Item[]) => void; + rowProps?: (item: Item) => object; +} + +export const ReorderableTable = ({ + columns, + items, + noItemsMessage, + unreorderableItems = [], + className = '', + disableDragging = false, + disableReordering = false, + onReorder = () => undefined, + rowProps = () => ({}), +}: ReorderableTableProps) => { + return ( +
+ : undefined} /> + + {items.length === 0 && ( + + + {noItemsMessage} + + + )} + + {items.length > 0 && !disableReordering && ( + <> + ( + + )} + onReorder={onReorder} + /> + {unreorderableItems.length > 0 && ( + ( + } + /> + )} + /> + )} + + )} + + {items.length > 0 && disableReordering && ( + ( + + )} + /> + )} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/types.ts new file mode 100644 index 000000000000..77c1495977d2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/types.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface DraggableUXStyles { + alignItems?: string; + flexBasis?: string; + flexGrow?: number; +} +export interface Column extends DraggableUXStyles { + name: string; + render: (item: Item) => React.ReactNode; +}