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;
+}