Skip to content

Commit a8b390b

Browse files
[8.19] Share experience Improvements (#222242) (#225321)
# Backport This will backport the following commits from `main` to `8.19`: - [Share experience Improvements (#222242)](#222242) <!--- Backport version: 10.0.1 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Eyo O. Eyo","email":"7893459+eokoneyo@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-06-23T09:37:41Z","message":"Share experience Improvements (#222242)\n\n## Summary\n\nCloses #222093 \n\nChanges\n\n- restrict copy link height\n~- copy link to clipboard for lens without opening flyout, when lens is\nalready saved~\n- display share modal without tab indicator when there's only one share\noption available\n\n\n\n---------\n\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"4bedf051b449f0f72fe3c379c7fcb1c876e641b7","branchLabelMapping":{"^v9.1.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:skip","Team:SharedUX","v9.1.0"],"title":"Share experience Improvements","number":222242,"url":"https://github.com/elastic/kibana/pull/222242","mergeCommit":{"message":"Share experience Improvements (#222242)\n\n## Summary\n\nCloses #222093 \n\nChanges\n\n- restrict copy link height\n~- copy link to clipboard for lens without opening flyout, when lens is\nalready saved~\n- display share modal without tab indicator when there's only one share\noption available\n\n\n\n---------\n\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"4bedf051b449f0f72fe3c379c7fcb1c876e641b7"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/222242","number":222242,"mergeCommit":{"message":"Share experience Improvements (#222242)\n\n## Summary\n\nCloses #222093 \n\nChanges\n\n- restrict copy link height\n~- copy link to clipboard for lens without opening flyout, when lens is\nalready saved~\n- display share modal without tab indicator when there's only one share\noption available\n\n\n\n---------\n\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"4bedf051b449f0f72fe3c379c7fcb1c876e641b7"}}]}] BACKPORT--> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
1 parent b180744 commit a8b390b

11 files changed

Lines changed: 166 additions & 67 deletions

File tree

src/platform/packages/shared/shared-ux/modal/tabbed/src/tabbed_modal.test.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ describe('TabbedModal', () => {
4242

4343
return (
4444
<EuiFieldText
45+
data-test-subj="log-user-input-field"
4546
placeholder="Placeholder text"
4647
value={state.inputText}
4748
onChange={onChange}
@@ -57,7 +58,7 @@ describe('TabbedModal', () => {
5758
},
5859
};
5960

60-
it('renders the modal component', async () => {
61+
it("when a single tab definition is passed it simply renders it's content into the modal component without tabs", async () => {
6162
render(
6263
<TabbedModal
6364
tabs={[tabDefinition]}
@@ -66,6 +67,24 @@ describe('TabbedModal', () => {
6667
/>
6768
);
6869

70+
expect(screen.queryByText(tabDefinition.name)).not.toBeInTheDocument();
71+
72+
expect(screen.getByTestId('log-user-input-field')).toBeInTheDocument();
73+
74+
await userEvent.click(await screen.findByTestId(tabDefinition.modalActionBtn!.dataTestSubj));
75+
76+
expect(mockedHandlerFn).toHaveBeenCalled();
77+
});
78+
79+
it('renders the tabbed modal with tabs for tab definition with length greater than 1', async () => {
80+
render(
81+
<TabbedModal
82+
tabs={[tabDefinition, { ...tabDefinition, id: 'anotherTab', name: 'another tab' }]}
83+
defaultSelectedTabId="logUserInput"
84+
onClose={modalOnCloseHandler}
85+
/>
86+
);
87+
6988
expect(screen.queryByText(tabDefinition.name)).toBeInTheDocument();
7089

7190
await userEvent.click(await screen.findByTestId(tabDefinition.modalActionBtn!.dataTestSubj));

src/platform/packages/shared/shared-ux/modal/tabbed/src/tabbed_modal.tsx

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -127,21 +127,29 @@ const TabbedModalInner: FC<ITabbedModalInner> = ({
127127
);
128128

129129
const renderTabs = useCallback(() => {
130-
return tabs.map((tab, index) => {
131-
return (
132-
<EuiTab
133-
key={index}
134-
onClick={() => onSelectedTabChanged(tab.id)}
135-
isSelected={tab.id === selectedTabId}
136-
disabled={tab.disabled}
137-
prepend={tab.prepend}
138-
append={tab.append}
139-
data-test-subj={tab.id}
140-
>
141-
{tab.name}
142-
</EuiTab>
143-
);
144-
});
130+
if (tabs.length === 1) {
131+
return null;
132+
}
133+
134+
return (
135+
<EuiTabs>
136+
{tabs.map((tab, index) => {
137+
return (
138+
<EuiTab
139+
key={index}
140+
onClick={() => onSelectedTabChanged(tab.id)}
141+
isSelected={tab.id === selectedTabId}
142+
disabled={tab.disabled}
143+
prepend={tab.prepend}
144+
append={tab.append}
145+
data-test-subj={tab.id}
146+
>
147+
{tab.name}
148+
</EuiTab>
149+
);
150+
})}
151+
</EuiTabs>
152+
);
145153
}, [onSelectedTabChanged, selectedTabId, tabs]);
146154

147155
const modalPositionOverrideStyles: React.CSSProperties = {
@@ -170,17 +178,22 @@ const TabbedModalInner: FC<ITabbedModalInner> = ({
170178
</EuiModalHeader>
171179
<EuiModalBody>
172180
<Fragment>
173-
<EuiTabs>{renderTabs()}</EuiTabs>
181+
<Fragment>{renderTabs()}</Fragment>
174182
<EuiSpacer size="m" />
175183
{React.createElement(function RenderSelectedTabContent() {
176184
useLayoutEffect(onTabContentRender, []);
177185
return (
178-
<SelectedTabContent
179-
{...{
180-
state: selectedTabState,
181-
dispatch,
182-
}}
183-
/>
186+
<div
187+
css={{ display: 'contents' }}
188+
data-test-subj={`tabbedModal-${selectedTabId}-content`}
189+
>
190+
<SelectedTabContent
191+
{...{
192+
state: selectedTabState,
193+
dispatch,
194+
}}
195+
/>
196+
</div>
184197
);
185198
})}
186199
</Fragment>

src/platform/plugins/shared/share/public/components/export_popover/export_popover.test.tsx renamed to src/platform/plugins/shared/share/public/components/export_integrations/export_integrations.test.tsx

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import { render, screen } from '@testing-library/react';
1212
import { userEvent } from '@testing-library/user-event';
1313
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
1414
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
15-
import { ExportMenu } from './export_popover';
15+
import { ExportMenu } from './export_integrations';
1616
import type { IShareContext } from '../context';
17+
import type { ExportShareConfig } from '../../types';
1718

1819
const mockShareContext: IShareContext = {
1920
shareMenuItems: [
@@ -51,15 +52,19 @@ const mockShareContext: IShareContext = {
5152
onClose: jest.fn(),
5253
};
5354

54-
function ExportPopoverRender() {
55+
function ExportPopoverRender({
56+
shareContext = mockShareContext,
57+
}: {
58+
shareContext?: IShareContext;
59+
}) {
5560
const [clickTarget, setClickTarget] = React.useState<HTMLElement | null>();
5661

5762
return (
5863
<IntlProvider locale="en">
5964
{Boolean(clickTarget) && (
6065
<ExportMenu
6166
shareContext={{
62-
...mockShareContext,
67+
...shareContext,
6368
anchorElement: clickTarget!,
6469
}}
6570
/>
@@ -69,7 +74,7 @@ function ExportPopoverRender() {
6974
);
7075
}
7176

72-
describe('ExportPopover', () => {
77+
describe('Export Integrations', () => {
7378
it('renders a popover with the list of registered export types', async () => {
7479
const user = userEvent.setup();
7580

@@ -83,4 +88,33 @@ describe('ExportPopover', () => {
8388
expect(screen.getByText(label)).toBeInTheDocument();
8489
});
8590
});
91+
92+
it('will invoke the export integrations generateAssetExport config method if it is the singular export type available', async () => {
93+
const user = userEvent.setup();
94+
95+
const singleExportShareContext: IShareContext = {
96+
...mockShareContext,
97+
shareMenuItems: [
98+
{
99+
shareType: 'integration',
100+
groupId: 'export',
101+
id: 'csv',
102+
config: {
103+
icon: 'empty',
104+
label: 'CSV',
105+
generateAssetExport: jest.fn(() => Promise.resolve()),
106+
},
107+
} as unknown as ExportShareConfig,
108+
],
109+
};
110+
111+
render(<ExportPopoverRender shareContext={singleExportShareContext} />);
112+
113+
await user.click(screen.getByText('click me'));
114+
115+
expect(
116+
(singleExportShareContext.shareMenuItems[0] as ExportShareConfig).config.generateAssetExport
117+
).toHaveBeenCalled();
118+
expect(singleExportShareContext.onClose).toHaveBeenCalled();
119+
});
86120
});

src/platform/plugins/shared/share/public/components/export_popover/export_popover.tsx renamed to src/platform/plugins/shared/share/public/components/export_integrations/export_integrations.tsx

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ function ManagedFlyout({
190190
<EuiCodeBlock
191191
data-test-subj="exportAssetValue"
192192
css={{ overflowWrap: 'break-word' }}
193+
overflowHeight={360}
193194
language={exportIntegration.config.copyAssetURIConfig.contentType}
194195
isCopyable
195196
copyAriaLabel={i18n.translate('share.export.copyPostURLAriaLabel', {
@@ -286,34 +287,65 @@ function ExportMenuPopover({ intl }: ExportMenuProps) {
286287
setIsFlyoutVisible(true);
287288
}, []);
288289

289-
useEffect(() => {
290-
// when there is only one share menu item, and no export derivatives registered,
291-
// we want to open the flyout and not the popover
292-
if (
293-
exportIntegrations.length === 1 &&
294-
exportDerivatives.length === 0 &&
295-
!selectedMenuItemMeta
296-
) {
297-
openFlyout(exportIntegrations[0]);
298-
}
299-
}, [exportIntegrations, exportDerivatives, openFlyout, selectedMenuItemMeta]);
290+
const exportIntegrationInteractionHandler = useCallback(
291+
async (menuItem: ExportShareConfig) => {
292+
if (
293+
!menuItem.config.copyAssetURIConfig &&
294+
!menuItem.config.generateAssetComponent &&
295+
menuItem.config.generateAssetExport
296+
) {
297+
await menuItem.config
298+
.generateAssetExport({
299+
intl,
300+
optimizedForPrinting: false,
301+
})
302+
.finally(() => {
303+
onClose();
304+
});
305+
} else {
306+
openFlyout(menuItem);
307+
}
308+
},
309+
[intl, onClose, openFlyout]
310+
);
311+
312+
const flyoutRef = useRef<HTMLDivElement | null>(null);
313+
314+
const canSkipDisplayingPopover = useMemo<boolean>(() => {
315+
// when there is only one export share menu item, and no export derivatives registered,
316+
// we'd like to skip displaying the popover
317+
return exportIntegrations.length === 1 && !exportDerivatives.length;
318+
}, [exportIntegrations, exportDerivatives]);
300319

301320
const flyoutOnCloseHandler = useCallback(() => {
302-
return exportIntegrations.length === 1 && exportDerivatives.length === 0
303-
? onClose()
304-
: setIsFlyoutVisible(false);
305-
}, [exportDerivatives.length, exportIntegrations.length, onClose]);
321+
setIsFlyoutVisible(false);
322+
if (canSkipDisplayingPopover) {
323+
onClose();
324+
}
325+
}, [onClose, canSkipDisplayingPopover]);
306326

307-
const flyoutRef = useRef<HTMLDivElement | null>(null);
327+
useEffect(() => {
328+
if (canSkipDisplayingPopover && !selectedMenuItemMeta) {
329+
exportIntegrationInteractionHandler(exportIntegrations[0]);
330+
}
331+
}, [
332+
exportIntegrationInteractionHandler,
333+
exportIntegrations,
334+
onClose,
335+
selectedMenuItemMeta,
336+
canSkipDisplayingPopover,
337+
]);
308338

309339
return (
310340
<Fragment>
311341
<EuiWrappingPopover
312-
isOpen={!isFlyoutVisible}
313-
data-test-subj="exportPopover"
342+
isOpen={!isFlyoutVisible && !canSkipDisplayingPopover}
314343
button={anchorElement!}
315344
closePopover={onClose}
316345
panelPaddingSize="s"
346+
panelProps={{
347+
'data-test-subj': 'exportPopoverPanel',
348+
}}
317349
>
318350
<EuiListGroup flush>
319351
{exportIntegrations.map((menuItem) => (
@@ -328,7 +360,7 @@ function ExportMenuPopover({ intl }: ExportMenuProps) {
328360
label={menuItem.config.label}
329361
data-test-subj={`exportMenuItem-${menuItem.config.label}`}
330362
isDisabled={menuItem.config.disabled}
331-
onClick={() => openFlyout(menuItem)}
363+
onClick={exportIntegrationInteractionHandler.bind(null, menuItem)}
332364
/>
333365
</EuiToolTip>
334366
))}
@@ -368,6 +400,7 @@ function ExportMenuPopover({ intl }: ExportMenuProps) {
368400
? selectedMenuItem.config.flyoutSizing || {}
369401
: {})}
370402
>
403+
{/* TODO: remove this global style once https://github.com/elastic/eui/issues/8801 is resolved */}
371404
<Global
372405
// @ts-expect-error -- we pass a z-index specifying important so we override the default z-index, so solve a known bug,
373406
// where when `headerZindexLocation` is set to `above`, the popover panel z-index is not high enough

src/platform/plugins/shared/share/public/components/export_popover/index.ts renamed to src/platform/plugins/shared/share/public/components/export_integrations/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 { ExportMenu } from './export_popover';
10+
export { ExportMenu } from './export_integrations';

src/platform/plugins/shared/share/public/components/share_tabs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const ShareMenuTabs = () => {
3434
tabs.push(linkTab);
3535
}
3636

37-
// Embed is disabled in the serverless offering, hence the need to check that we received it
37+
// Embed is disabled in the serverless offering, hence the need to check if the embed tab should be shown
3838
if (
3939
shareMenuItems.some(({ shareType }) => shareType === 'embed') &&
4040
!objectTypeMeta?.config?.embed?.disabled

src/platform/plugins/shared/share/public/services/share_menu_manager.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { ShowShareMenuOptions } from '../types';
1616
import { ShareRegistry } from './share_menu_registry';
1717
import type { ShareConfigs } from '../types';
1818
import { ShareMenu } from '../components/share_tabs';
19-
import { ExportMenu } from '../components/export_popover';
19+
import { ExportMenu } from '../components/export_integrations';
2020

2121
interface ShareMenuManagerStartDeps {
2222
core: CoreStart;

src/platform/test/functional/page_objects/export_page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class ExportPageObject extends FtrService {
2424
}
2525

2626
async isExportPopoverOpen() {
27-
return await this.testSubjects.exists('exportPopover');
27+
return await this.testSubjects.exists('exportPopoverPanel');
2828
}
2929

3030
async isPopoverItemEnabled(label: string) {

x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
190190

191191
it('should be possible to download a visualization with adhoc dataViews', async () => {
192192
await lens.setCSVDownloadDebugFlag(true);
193-
await lens.openCSVDownloadExport();
193+
await lens.triggerCSVDownloadExport();
194194

195195
const csv = await lens.getCSVContent();
196196
expect(csv).to.be.ok();

x-pack/test/functional/apps/lens/group4/share.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
5656
});
5757

5858
it('should enable both download and URL sharing for valid configuration', async () => {
59-
await lens.clickShareModal();
60-
6159
expect(await lens.isExportActionEnabled()).to.eql(true);
60+
61+
await lens.clickShareButton();
6262
expect(await lens.isShareActionEnabled('link'));
6363
});
6464

@@ -84,7 +84,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
8484

8585
it('should be able to download CSV data of the current visualization', async () => {
8686
await lens.setCSVDownloadDebugFlag(true);
87-
await lens.openCSVDownloadExport();
87+
await lens.triggerCSVDownloadExport();
8888

8989
const csv = await lens.getCSVContent();
9090
expect(csv).to.be.ok();
@@ -106,7 +106,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
106106
field: 'bytes',
107107
});
108108

109-
await lens.openCSVDownloadExport();
109+
await lens.triggerCSVDownloadExport();
110110

111111
const csv = await lens.getCSVContent();
112112
expect(csv).to.be.ok();

0 commit comments

Comments
 (0)