[Text based] Enables save Lens chart to dashboard from Discover (#159190)

## Summary

Adds a save to dashboard functionality in the Lens charts created in
Discover by text based languages.

<img width="1738" alt="image"
src="c4b5f459-1124-4800-954f-298332601eaf">


<img width="832" alt="image"
src="ae742d0a-5911-4622-8387-60db87daffcc">

We allow only saving by value panels and not by reference because we are
going to remove this functionality in the next minor (create Lens text
based languages SOs).

### Checklist

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] [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
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
This commit is contained in:
Stratoula Kalafateli 2023-06-09 11:17:07 +03:00 committed by GitHub
parent af3f13eaf9
commit 5c7753fc26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 134 additions and 14 deletions

View file

@ -46,4 +46,9 @@ export const unifiedHistogramServicesMock = {
clear: jest.fn(),
},
expressions: expressionsPluginMock.createStartContract(),
capabilities: {
dashboard: {
showWriteControls: true,
},
},
} as unknown as UnifiedHistogramServices;

View file

@ -9,6 +9,7 @@
import React, { ReactElement } from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import type { Capabilities } from '@kbn/core/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { Suggestion } from '@kbn/lens-plugin/public';
import type { UnifiedHistogramFetchStatus } from '../types';
@ -41,6 +42,7 @@ async function mountComponent({
currentSuggestion,
allSuggestions,
isPlainRecord,
hasDashboardPermissions,
}: {
noChart?: boolean;
noHits?: boolean;
@ -51,11 +53,21 @@ async function mountComponent({
currentSuggestion?: Suggestion;
allSuggestions?: Suggestion[];
isPlainRecord?: boolean;
hasDashboardPermissions?: boolean;
} = {}) {
(searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation(
jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: noHits ? 0 : 2 } } }))
);
const services = {
...unifiedHistogramServicesMock,
capabilities: {
dashboard: {
showWriteControls: hasDashboardPermissions ?? true,
},
} as unknown as Capabilities,
};
const props = {
dataView,
query: {
@ -64,7 +76,7 @@ async function mountComponent({
},
filters: [],
timeRange: { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' },
services: unifiedHistogramServicesMock,
services,
hits: noHits
? undefined
: {
@ -221,6 +233,27 @@ describe('Chart', () => {
expect(component.find(SuggestionSelector).exists()).toBeTruthy();
});
it('should render the save button when chart is visible and suggestions exist', async () => {
const component = await mountComponent({
currentSuggestion: currentSuggestionMock,
allSuggestions: allSuggestionsMock,
});
expect(
component.find('[data-test-subj="unifiedHistogramSaveVisualization"]').exists()
).toBeTruthy();
});
it('should not render the save button when the dashboard save by value permissions are false', async () => {
const component = await mountComponent({
currentSuggestion: currentSuggestionMock,
allSuggestions: allSuggestionsMock,
hasDashboardPermissions: false,
});
expect(
component.find('[data-test-subj="unifiedHistogramSaveVisualization"]').exists()
).toBeFalsy();
});
it('should not render the Lens SuggestionsSelector when chart is hidden', async () => {
const component = await mountComponent({
chartHidden: true,

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { ReactElement, useMemo } from 'react';
import { ReactElement, useMemo, useState } from 'react';
import React, { memo } from 'react';
import {
EuiButtonIcon,
@ -111,6 +111,7 @@ export function Chart({
onFilter,
onBrushEnd,
}: ChartProps) {
const [isSaveModalVisible, setIsSaveModalVisible] = useState(false);
const {
showChartOptionsPopover,
chartRef,
@ -221,6 +222,9 @@ export function Chart({
lensAttributes: lensAttributesContext.attributes,
isPlainRecord,
});
const LensSaveModalComponent = services.lens.SaveModalComponent;
const canSaveVisualization =
chartVisible && currentSuggestion && services.capabilities.dashboard?.showWriteControls;
return (
<EuiFlexGroup
@ -271,6 +275,27 @@ export function Chart({
/>
</EuiFlexItem>
)}
{canSaveVisualization && (
<>
<EuiFlexItem grow={false} css={chartToolButtonCss}>
<EuiToolTip
content={i18n.translate('unifiedHistogram.saveVisualizationButton', {
defaultMessage: 'Save visualization',
})}
>
<EuiButtonIcon
size="xs"
iconType="save"
onClick={() => setIsSaveModalVisible(true)}
data-test-subj="unifiedHistogramSaveVisualization"
aria-label={i18n.translate('unifiedHistogram.saveVisualizationButton', {
defaultMessage: 'Save visualization',
})}
/>
</EuiToolTip>
</EuiFlexItem>
</>
)}
{onEditVisualization && (
<EuiFlexItem grow={false} css={chartToolButtonCss}>
<EuiToolTip
@ -354,6 +379,14 @@ export function Chart({
{appendHistogram}
</EuiFlexItem>
)}
{canSaveVisualization && isSaveModalVisible && lensAttributesContext.attributes && (
<LensSaveModalComponent
initialInput={lensAttributesContext.attributes as unknown as LensEmbeddableInput}
onSave={() => {}}
onClose={() => setIsSaveModalVisible(false)}
isSaveable={false}
/>
)}
</EuiFlexGroup>
);
}

View file

@ -739,4 +739,18 @@ describe('getLensAttributes', () => {
}
`);
});
it('should return suggestion title if no title is given', () => {
expect(
getLensAttributes({
title: undefined,
filters,
query,
dataView,
timeInterval,
breakdownField: undefined,
suggestion: currentSuggestionMock,
}).attributes.title
).toBe(currentSuggestionMock.title);
});
});

View file

@ -184,6 +184,7 @@ export const getLensAttributes = ({
const attributes = {
title:
title ??
suggestion?.title ??
i18n.translate('unifiedHistogram.lensTitle', {
defaultMessage: 'Edit visualization',
}),

View file

@ -7,7 +7,7 @@
*/
import type { Theme } from '@kbn/charts-plugin/public/plugin';
import type { IUiSettingsClient } from '@kbn/core/public';
import type { IUiSettingsClient, Capabilities } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { LensPublicStart } from '@kbn/lens-plugin/public';
@ -42,6 +42,7 @@ export interface UnifiedHistogramServices {
lens: LensPublicStart;
storage: Storage;
expressions: ExpressionsStart;
capabilities: Capabilities;
}
/**

View file

@ -184,5 +184,21 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
return dimensions.length === 2 && (await dimensions[1].getVisibleText()) === 'average';
});
});
it('should save correctly chart to dashboard', async () => {
await PageObjects.discover.selectTextBaseLang('SQL');
await PageObjects.header.waitUntilLoadingHasFinished();
await monacoEditor.setCodeEditorValue(
'SELECT extension, AVG("bytes") as average FROM "logstash*" GROUP BY extension'
);
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.click('TextBasedLangEditor-expand');
await testSubjects.click('unifiedHistogramSaveVisualization');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.lens.saveModal('TextBasedChart', false, false, false, 'new');
await testSubjects.existOrFail('embeddablePanelHeading-TextBasedChart');
});
});
}

View file

@ -709,6 +709,30 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
await testSubjects.missingOrFail(`lns-fieldOption-${field}`);
}
},
async saveModal(
title: string,
saveAsNew?: boolean,
redirectToOrigin?: boolean,
saveToLibrary?: boolean,
addToDashboard?: 'new' | 'existing' | null,
dashboardId?: string
) {
await PageObjects.timeToVisualize.setSaveModalValues(title, {
saveAsNew,
redirectToOrigin,
addToDashboard: addToDashboard ? addToDashboard : null,
dashboardId,
saveToLibrary,
});
await testSubjects.click('confirmSaveSavedObjectButton');
await retry.waitForWithTimeout('Save modal to disappear', 1000, () =>
testSubjects
.missingOrFail('confirmSaveSavedObjectButton')
.then(() => true)
.catch(() => false)
);
},
/**
* Save the current Lens visualization.
@ -724,20 +748,13 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.click('lnsApp_saveButton');
await PageObjects.timeToVisualize.setSaveModalValues(title, {
await this.saveModal(
title,
saveAsNew,
redirectToOrigin,
addToDashboard: addToDashboard ? addToDashboard : null,
dashboardId,
saveToLibrary,
});
await testSubjects.click('confirmSaveSavedObjectButton');
await retry.waitForWithTimeout('Save modal to disappear', 1000, () =>
testSubjects
.missingOrFail('confirmSaveSavedObjectButton')
.then(() => true)
.catch(() => false)
addToDashboard,
dashboardId
);
},