diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/table_header/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/index.ts
new file mode 100644
index 0000000000000..34ce070fcde46
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { TableHeader } from './table_header';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx
new file mode 100644
index 0000000000000..70e2ac7ac6f0d
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { EuiTableHeader, EuiTableHeaderCell } from '@elastic/eui';
+
+import { TableHeader } from './table_header';
+
+const headerItems = ['foo', 'bar', 'baz'];
+
+describe('TableHeader', () => {
+ it('renders', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiTableHeader)).toHaveLength(1);
+ expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(3);
+ });
+
+ it('renders extra cell', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiTableHeader)).toHaveLength(1);
+ expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(4);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.tsx
new file mode 100644
index 0000000000000..e7f9617fdcd91
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.tsx
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { EuiTableHeader, EuiTableHeaderCell } from '@elastic/eui';
+
+interface ITableHeaderProps {
+ headerItems: string[];
+ extraCell?: boolean;
+}
+
+export const TableHeader: React.FC = ({ headerItems, extraCell }) => (
+
+ {headerItems.map((item, i) => (
+ {item}
+ ))}
+ {extraCell && }
+
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/index.ts
new file mode 100644
index 0000000000000..d3ee618e92b5b
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { truncate, truncateBeginning } from './truncate';
+export { TruncatedContent } from './truncated_content';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx
new file mode 100644
index 0000000000000..aa8427cd822be
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { TruncatedContent } from './';
+
+const content = 'foobarbaz';
+
+describe('TruncatedContent', () => {
+ it('renders with no truncation', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find('span.truncated-content')).toHaveLength(0);
+ expect(wrapper.text()).toEqual('foo');
+ });
+
+ it('renders with truncation at the end', () => {
+ const wrapper = shallow();
+ const element = wrapper.find('span.truncated-content');
+
+ expect(element).toHaveLength(1);
+ expect(element.prop('title')).toEqual(content);
+ expect(wrapper.text()).toEqual('foob…');
+ expect(wrapper.find('span.truncated-content__tooltip')).toHaveLength(0);
+ });
+
+ it('renders with truncation at the beginning', () => {
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper.find('span.truncated-content')).toHaveLength(1);
+ expect(wrapper.text()).toEqual('…rbaz');
+ });
+
+ it('renders with inline tooltip', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find('span.truncated-content').prop('title')).toEqual('');
+ expect(wrapper.find('span.truncated-content__tooltip')).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.ts b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.ts
new file mode 100644
index 0000000000000..36094e3abe258
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export function truncate(text: string, length: number) {
+ return `${text.substring(0, length)}…`;
+}
+
+export function truncateBeginning(text: string, length: number) {
+ return `…${text.substring(text.length - length)}`;
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.scss b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.scss
new file mode 100644
index 0000000000000..701834acfed9d
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.scss
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+.truncated-content {
+ position: relative;
+ z-index: 2;
+ display: inline-block;
+ white-space: nowrap;
+
+ &__tooltip {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ left: -3px;
+ margin-top: -1px;
+ background: $euiColorEmptyShade;
+ border-radius: 2px;
+ width: calc(100% + 4px);
+ height: calc(100% + 4px);
+ padding: 0 2px;
+ display: none;
+ align-items: center;
+ box-shadow: 0 1px 3px rgba(black, 0.1);
+ border: 1px solid $euiBorderColor;
+ width: auto;
+ white-space: nowrap;
+
+ .truncated-content:hover & {
+ display: flex;
+ }
+ }
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.tsx
new file mode 100644
index 0000000000000..7785f75b71d34
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { truncate, truncateBeginning } from './';
+
+import './truncated_content.scss';
+
+interface ITruncatedContentProps {
+ content: string;
+ length: number;
+ beginning?: boolean;
+ tooltipType?: 'inline' | 'title';
+}
+
+export const TruncatedContent: React.FC = ({
+ content,
+ length,
+ beginning = false,
+ tooltipType = 'inline',
+}) => {
+ if (content.length <= length) return <>{content}>;
+
+ const inline = tooltipType === 'inline';
+ return (
+
+ {beginning ? truncateBeginning(content, length) : truncate(content, length)}
+ {inline && {content}}
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts
new file mode 100644
index 0000000000000..05c60ebced088
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { useDidUpdateEffect } from './use_did_update_effect';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx
new file mode 100644
index 0000000000000..e3d2ffb44f01e
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState } from 'react';
+import { mount } from 'enzyme';
+
+import { EuiLink } from '@elastic/eui';
+
+import { useDidUpdateEffect } from './use_did_update_effect';
+
+const fn = jest.fn();
+
+const TestHook = ({ value }: { value: number }) => {
+ const [inputValue, setValue] = useState(value);
+ useDidUpdateEffect(fn, [inputValue]);
+ return setValue(2)} />;
+};
+
+const wrapper = mount();
+
+describe('useDidUpdateEffect', () => {
+ it('should not fire function when value unchanged', () => {
+ expect(fn).not.toHaveBeenCalled();
+ });
+
+ it('should fire function when value changed', () => {
+ wrapper.find(EuiLink).simulate('click');
+ expect(fn).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx
new file mode 100644
index 0000000000000..4c3e10fc84b84
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/*
+ * Sometimes we don't want to fire the initial useEffect call.
+ * This custom Hook only fires after the intial render has completed.
+ */
+import { useEffect, useRef, DependencyList } from 'react';
+
+export const useDidUpdateEffect = (fn: Function, inputs: DependencyList) => {
+ const didMountRef = useRef(false);
+
+ useEffect(() => {
+ if (didMountRef.current) {
+ fn();
+ } else {
+ didMountRef.current = true;
+ }
+ }, inputs);
+};