Skip to content

Commit c348586

Browse files
jughostadavismcpheekibanamachine
authored
[Discover] Persist tabs in local storage and sync selected tab ID with URL (elastic#217706)
- Closes elastic#216549 - Closes elastic#216071 ## Summary This PR allows to restore the following state for the previously opened tabs: - the selected data view - classic or ES|QL mode - query and filters - time range and refresh interval - and other properties of the app state https://github.com/elastic/kibana/blob/bcba741abce257e8b1342d650d54a4813340b40b/src/platform/plugins/shared/discover/public/application/main/state_management/discover_app_state_container.ts#L92 ## Changes - [x] Sync selected tab id to URL => after refresh the initial tab would be the last selected one - [x] Restore tabs after refresh - [x] Restore appState and globalState after reopening closed tabs - [x] Clear tabs if Discover was opened from another Kibana app - [x] Store tabs in LocalStorage - [x] Fix "New" action and clear all tabs - [x] Populate "Recently closed tabs" with data from LocalStorage - [x] If selected tab id changes in URL externally => update the state - [x] Reset the stored state when userId or space Id changes - [x] Fix all tests ### Testing - Test that the existing functionality is not affected - Enable tabs feature in https://github.com/elastic/kibana/blob/bcba741abce257e8b1342d650d54a4813340b40b/src/platform/plugins/shared/discover/public/constants.ts#L15 and test that tabs are being persisted and can be restored manually too. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Davis McPhee <davismcphee@hotmail.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
1 parent 9d94d8f commit c348586

38 files changed

Lines changed: 1655 additions & 229 deletions

examples/unified_tabs_examples/public/example_app.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import type { AppMountParameters } from '@kbn/core-application-browser';
2424
import type { DataView } from '@kbn/data-views-plugin/public';
2525
import type { DataViewField } from '@kbn/data-views-plugin/public';
2626
import type { DataViewPickerProps } from '@kbn/unified-search-plugin/public';
27-
import { type TabItem, UnifiedTabs, useNewTabProps } from '@kbn/unified-tabs';
27+
import { UnifiedTabs, useNewTabProps, type UnifiedTabsProps } from '@kbn/unified-tabs';
2828
import { type TabPreviewData, TabStatus } from '@kbn/unified-tabs';
2929
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
3030
import { FieldListSidebar, FieldListSidebarProps } from './field_list_sidebar';
@@ -67,9 +67,13 @@ export const UnifiedTabsExampleApp: React.FC<UnifiedTabsExampleAppProps> = ({
6767
const [dataView, setDataView] = useState<DataView | null>();
6868
const [selectedFieldNames, setSelectedFieldNames] = useState<string[]>([]);
6969
const { getNewTabDefaultProps } = useNewTabProps({ numberOfInitialItems: 0 });
70-
const [initialItems] = useState<TabItem[]>(() =>
71-
Array.from({ length: 7 }, () => getNewTabDefaultProps())
72-
);
70+
const [{ managedItems, managedSelectedItemId }, setState] = useState<{
71+
managedItems: UnifiedTabsProps['items'];
72+
managedSelectedItemId: UnifiedTabsProps['selectedItemId'];
73+
}>(() => ({
74+
managedItems: Array.from({ length: 7 }, () => getNewTabDefaultProps()),
75+
managedSelectedItemId: undefined,
76+
}));
7377

7478
const onAddFieldToWorkspace = useCallback(
7579
(field: DataViewField) => {
@@ -121,13 +125,20 @@ export const UnifiedTabsExampleApp: React.FC<UnifiedTabsExampleAppProps> = ({
121125
{dataView ? (
122126
<div className="eui-fullHeight">
123127
<UnifiedTabs
124-
initialItems={initialItems}
128+
items={managedItems}
129+
selectedItemId={managedSelectedItemId}
130+
recentlyClosedItems={[]}
125131
maxItemsCount={25}
126132
services={services}
127-
onChanged={() => {}}
133+
onChanged={(updatedState) =>
134+
setState({
135+
managedItems: updatedState.items,
136+
managedSelectedItemId: updatedState.selectedItem?.id,
137+
})
138+
}
128139
createItem={getNewTabDefaultProps}
129-
getPreviewData={
130-
() => TAB_CONTENT_MOCK[Math.floor(Math.random() * TAB_CONTENT_MOCK.length)] // TODO change mock to real data when ready
140+
getPreviewData={() =>
141+
TAB_CONTENT_MOCK[Math.floor(Math.random() * TAB_CONTENT_MOCK.length)]
131142
}
132143
renderContent={({ label }) => {
133144
return (

src/platform/packages/shared/kbn-unified-tabs/src/components/__stories__/tabs.stories.tsx

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
import React from 'react';
10+
import React, { useState } from 'react';
1111
import type { Meta, StoryFn, StoryObj } from '@storybook/react';
1212
import { action } from '@storybook/addon-actions';
1313
import { TabbedContent, type TabbedContentProps } from '../tabbed_content';
@@ -27,16 +27,33 @@ export default {
2727

2828
const TabbedContentTemplate: StoryFn<TabbedContentProps> = (args) => {
2929
const { getNewTabDefaultProps } = useNewTabProps({
30-
numberOfInitialItems: args.initialItems.length,
30+
numberOfInitialItems: args.items.length,
3131
});
3232

33+
const [{ managedItems, managedSelectedItemId }, setState] = useState<{
34+
managedItems: TabbedContentProps['items'];
35+
managedSelectedItemId: TabbedContentProps['selectedItemId'];
36+
}>(() => ({
37+
managedItems: args.items,
38+
managedSelectedItemId: args.selectedItemId,
39+
}));
40+
3341
return (
3442
<TabbedContent
3543
{...args}
44+
items={managedItems}
45+
selectedItemId={managedSelectedItemId}
46+
recentlyClosedItems={[]}
3647
createItem={getNewTabDefaultProps}
3748
getPreviewData={getPreviewDataMock}
3849
services={servicesMock}
39-
onChanged={action('onClosed')}
50+
onChanged={(updatedState) => {
51+
action('onChanged')(updatedState);
52+
setState({
53+
managedItems: updatedState.items,
54+
managedSelectedItemId: updatedState.selectedItem?.id,
55+
});
56+
}}
4057
renderContent={(item) => (
4158
<div style={{ paddingTop: '16px' }}>Content for tab: {item.label}</div>
4259
)}
@@ -48,7 +65,7 @@ export const Default: StoryObj<TabbedContentProps> = {
4865
render: TabbedContentTemplate,
4966

5067
args: {
51-
initialItems: [
68+
items: [
5269
{
5370
id: '1',
5471
label: 'Tab 1',
@@ -61,7 +78,7 @@ export const WithMultipleTabs: StoryObj<TabbedContentProps> = {
6178
render: TabbedContentTemplate,
6279

6380
args: {
64-
initialItems: [
81+
items: [
6582
{
6683
id: '1',
6784
label: 'Tab 1',
@@ -75,6 +92,6 @@ export const WithMultipleTabs: StoryObj<TabbedContentProps> = {
7592
label: 'Tab 3',
7693
},
7794
],
78-
initialSelectedItemId: '3',
95+
selectedItemId: '3',
7996
},
8097
};

src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.test.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
import React from 'react';
10+
import React, { useState } from 'react';
1111
import { render, screen, waitFor } from '@testing-library/react';
1212
import { userEvent } from '@testing-library/user-event';
1313
import { TabbedContent, type TabbedContentProps } from './tabbed_content';
@@ -26,15 +26,33 @@ describe('TabbedContent', () => {
2626
initialItems,
2727
initialSelectedItemId,
2828
onChanged,
29-
}: Pick<TabbedContentProps, 'initialItems' | 'initialSelectedItemId' | 'onChanged'>) => {
29+
}: {
30+
initialItems: TabbedContentProps['items'];
31+
initialSelectedItemId?: TabbedContentProps['selectedItemId'];
32+
onChanged: TabbedContentProps['onChanged'];
33+
}) => {
34+
const [{ managedItems, managedSelectedItemId }, setState] = useState<{
35+
managedItems: TabbedContentProps['items'];
36+
managedSelectedItemId: TabbedContentProps['selectedItemId'];
37+
}>(() => ({
38+
managedItems: initialItems,
39+
managedSelectedItemId: initialSelectedItemId,
40+
}));
3041
return (
3142
<TabbedContent
32-
initialItems={initialItems}
33-
initialSelectedItemId={initialSelectedItemId}
43+
items={managedItems}
44+
selectedItemId={managedSelectedItemId}
45+
recentlyClosedItems={[]}
3446
createItem={() => NEW_TAB}
3547
getPreviewData={getPreviewDataMock}
3648
services={servicesMock}
37-
onChanged={onChanged}
49+
onChanged={(updatedState) => {
50+
onChanged(updatedState);
51+
setState({
52+
managedItems: updatedState.items,
53+
managedSelectedItemId: updatedState.selectedItem?.id,
54+
});
55+
}}
3856
renderContent={(item) => (
3957
<div style={{ paddingTop: '16px' }}>Content for tab: {item.label}</div>
4058
)}

src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,18 @@ import {
1818
addTab,
1919
closeTab,
2020
selectTab,
21+
selectRecentlyClosedTab,
2122
insertTabAfter,
2223
replaceTabWith,
2324
closeOtherTabs,
2425
closeTabsToTheRight,
2526
} from '../../utils/manage_tabs';
2627
import type { TabItem, TabsServices, TabPreviewData } from '../../types';
2728

28-
// TODO replace with real data when ready
29-
const RECENTLY_CLOSED_TABS_MOCK = [
30-
{
31-
label: 'Session 4',
32-
id: '4',
33-
},
34-
{
35-
label: 'Session 5',
36-
id: '5',
37-
},
38-
{
39-
label: 'Session 6',
40-
id: '6',
41-
},
42-
];
43-
4429
export interface TabbedContentProps extends Pick<TabsBarProps, 'maxItemsCount'> {
45-
initialItems: TabItem[];
46-
initialSelectedItemId?: string;
30+
items: TabItem[];
31+
selectedItemId?: string;
32+
recentlyClosedItems: TabItem[];
4733
'data-test-subj'?: string;
4834
services: TabsServices;
4935
renderContent: (selectedItem: TabItem) => React.ReactNode;
@@ -58,8 +44,9 @@ export interface TabbedContentState {
5844
}
5945

6046
export const TabbedContent: React.FC<TabbedContentProps> = ({
61-
initialItems,
62-
initialSelectedItemId,
47+
items: managedItems,
48+
selectedItemId: managedSelectedItemId,
49+
recentlyClosedItems,
6350
maxItemsCount,
6451
services,
6552
renderContent,
@@ -69,14 +56,10 @@ export const TabbedContent: React.FC<TabbedContentProps> = ({
6956
}) => {
7057
const tabsBarApi = useRef<TabsBarApi | null>(null);
7158
const [tabContentId] = useState(() => htmlIdGenerator()());
72-
const [state, _setState] = useState<TabbedContentState>(() => {
73-
return {
74-
items: initialItems,
75-
selectedItem:
76-
(initialSelectedItemId && initialItems.find((item) => item.id === initialSelectedItemId)) ||
77-
initialItems[0],
78-
};
79-
});
59+
const state = useMemo(
60+
() => prepareStateFromProps(managedItems, managedSelectedItemId),
61+
[managedItems, managedSelectedItemId]
62+
);
8063
const { items, selectedItem } = state;
8164
const stateRef = React.useRef<TabbedContentState>();
8265
stateRef.current = state;
@@ -88,10 +71,9 @@ export const TabbedContent: React.FC<TabbedContentProps> = ({
8871
}
8972

9073
const nextState = getNextState(stateRef.current);
91-
_setState(nextState);
9274
onChanged(nextState);
9375
},
94-
[_setState, onChanged]
76+
[onChanged]
9577
);
9678

9779
const onLabelEdited = useCallback(
@@ -110,6 +92,13 @@ export const TabbedContent: React.FC<TabbedContentProps> = ({
11092
[changeState]
11193
);
11294

95+
const onSelectRecentlyClosed = useCallback(
96+
async (item: TabItem) => {
97+
changeState((prevState) => selectRecentlyClosedTab(prevState, item));
98+
},
99+
[changeState]
100+
);
101+
113102
const onClose = useCallback(
114103
async (item: TabItem) => {
115104
changeState((prevState) => {
@@ -194,14 +183,15 @@ export const TabbedContent: React.FC<TabbedContentProps> = ({
194183
ref={tabsBarApi}
195184
items={items}
196185
selectedItem={selectedItem}
197-
recentlyClosedItems={RECENTLY_CLOSED_TABS_MOCK}
186+
recentlyClosedItems={recentlyClosedItems}
198187
maxItemsCount={maxItemsCount}
199188
tabContentId={tabContentId}
200189
getTabMenuItems={getTabMenuItems}
201190
services={services}
202191
onAdd={onAdd}
203192
onLabelEdited={onLabelEdited}
204193
onSelect={onSelect}
194+
onSelectRecentlyClosed={onSelectRecentlyClosed}
205195
onReorder={onReorder}
206196
onClose={onClose}
207197
getPreviewData={getPreviewData}
@@ -220,3 +210,11 @@ export const TabbedContent: React.FC<TabbedContentProps> = ({
220210
</EuiFlexGroup>
221211
);
222212
};
213+
214+
function prepareStateFromProps(items: TabItem[], selectedItemId?: string): TabbedContentState {
215+
const selectedItem = selectedItemId && items.find((item) => item.id === selectedItemId);
216+
return {
217+
items,
218+
selectedItem: selectedItem || items[0],
219+
};
220+
}

src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ describe('TabsBar', () => {
2929
it('renders tabs bar', async () => {
3030
const onAdd = jest.fn();
3131
const onSelect = jest.fn();
32+
const onSelectRecentlyClosed = jest.fn();
3233
const onLabelEdited = jest.fn();
3334
const onClose = jest.fn();
3435
const onReorder = jest.fn();
@@ -53,6 +54,7 @@ describe('TabsBar', () => {
5354
onAdd={onAdd}
5455
onLabelEdited={onLabelEdited}
5556
onSelect={onSelect}
57+
onSelectRecentlyClosed={onSelectRecentlyClosed}
5658
onClose={onClose}
5759
onReorder={onReorder}
5860
getPreviewData={getPreviewData}

src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import type { TabItem, TabsServices } from '../../types';
3636
import { getTabIdAttribute } from '../../utils/get_tab_attributes';
3737
import { useResponsiveTabs } from '../../hooks/use_responsive_tabs';
3838
import { TabsBarWithBackground } from '../tabs_visual_glue_to_header/tabs_bar_with_background';
39-
import { TabsBarMenu } from '../tabs_bar_menu';
39+
import { TabsBarMenu, type TabsBarMenuProps } from '../tabs_bar_menu';
4040

4141
const DROPPABLE_ID = 'unifiedTabsOrder';
4242

@@ -60,6 +60,7 @@ export type TabsBarProps = Pick<
6060
maxItemsCount?: number;
6161
services: TabsServices;
6262
onAdd: () => Promise<void>;
63+
onSelectRecentlyClosed: TabsBarMenuProps['onSelectRecentlyClosed'];
6364
onReorder: (items: TabItem[]) => void;
6465
};
6566

@@ -80,6 +81,7 @@ export const TabsBar = forwardRef<TabsBarApi, TabsBarProps>(
8081
onAdd,
8182
onLabelEdited,
8283
onSelect,
84+
onSelectRecentlyClosed,
8385
onReorder,
8486
onClose,
8587
getPreviewData,
@@ -278,10 +280,11 @@ export const TabsBar = forwardRef<TabsBarApi, TabsBarProps>(
278280
</EuiFlexItem>
279281
<EuiFlexItem grow={false}>
280282
<TabsBarMenu
281-
openedItems={items}
283+
items={items}
282284
selectedItem={selectedItem}
283-
onSelectOpenedTab={onSelect}
284285
recentlyClosedItems={recentlyClosedItems}
286+
onSelect={onSelect}
287+
onSelectRecentlyClosed={onSelectRecentlyClosed}
285288
/>
286289
</EuiFlexItem>
287290
</EuiFlexGroup>

src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar_menu/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
export { TabsBarMenu } from './tabs_bar_menu';
10+
export { TabsBarMenu, type TabsBarMenuProps } from './tabs_bar_menu';

0 commit comments

Comments
 (0)