[Lens] Edit data view in flyout (#142362)

* add explore matching index

* adjust type

* move things around

* fix types

* fix tests

* fix imports

* fix limit

* do not clean datasource on adding ad hoc data view

* manage data view in flyout

* fix phrase

* make sure all changes are propagated correctly

* fix test

* Update src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx

Co-authored-by: Anton Dosov <dosantappdev@gmail.com>

* only show for persisted data views

Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
Co-authored-by: Anton Dosov <dosantappdev@gmail.com>
This commit is contained in:
Joe Reuter 2022-10-05 10:06:12 +02:00 committed by GitHub
parent 205a2b81dd
commit b270921241
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 130 additions and 35 deletions

View file

@ -7,7 +7,14 @@
*/
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiLoadingSpinner } from '@elastic/eui';
import {
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiLoadingSpinner,
EuiLink,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import memoizeOne from 'memoize-one';
import {
@ -67,6 +74,7 @@ export interface Props {
defaultTypeIsRollup?: boolean;
requireTimestampField?: boolean;
editData?: DataView;
showManagementLink?: boolean;
allowAdHoc: boolean;
}
@ -85,9 +93,10 @@ const IndexPatternEditorFlyoutContentComponent = ({
requireTimestampField = false,
editData,
allowAdHoc,
showManagementLink,
}: Props) => {
const {
services: { http, dataViews, uiSettings, overlays },
services: { application, http, dataViews, uiSettings, overlays },
} = useKibana<DataViewEditorContext>();
const { form } = useForm<IndexPatternConfig, FormInternal>({
@ -376,6 +385,17 @@ const IndexPatternEditorFlyoutContentComponent = ({
<EuiTitle data-test-subj="flyoutTitle">
<h2>{editData ? editorTitleEditMode : editorTitle}</h2>
</EuiTitle>
{showManagementLink && editData && editData.id && (
<EuiLink
href={application.getUrlForApp('management', {
path: `/kibana/dataViews/dataView/${editData.id}`,
})}
>
{i18n.translate('indexPatternEditor.goToManagementPage', {
defaultMessage: 'View on data view management page',
})}
</EuiLink>
)}
<Form form={form} className="indexPatternEditor__form">
<UseField path="isAdHoc" />
{indexPatternTypeSelect}
@ -425,6 +445,7 @@ const IndexPatternEditorFlyoutContentComponent = ({
}}
submitDisabled={form.isSubmitted && !form.isValid}
isEdit={!!editData}
isPersisted={Boolean(editData && editData.isPersisted())}
allowAdHoc={allowAdHoc}
/>
</FlyoutPanels.Item>

View file

@ -20,6 +20,7 @@ const IndexPatternFlyoutContentContainer = ({
requireTimestampField = false,
editData,
allowAdHocDataView,
showManagementLink,
}: DataViewEditorProps) => {
const {
services: { dataViews, notifications },
@ -68,6 +69,7 @@ const IndexPatternFlyoutContentContainer = ({
defaultTypeIsRollup={defaultTypeIsRollup}
requireTimestampField={requireTimestampField}
editData={editData}
showManagementLink={showManagementLink}
allowAdHoc={allowAdHocDataView || false}
/>
);

View file

@ -22,6 +22,7 @@ interface FooterProps {
onSubmit: (isAdHoc?: boolean) => void;
submitDisabled: boolean;
isEdit: boolean;
isPersisted: boolean;
allowAdHoc: boolean;
}
@ -37,11 +38,25 @@ const editButtonLabel = i18n.translate('indexPatternEditor.editor.flyoutEditButt
defaultMessage: 'Save',
});
const editUnpersistedButtonLabel = i18n.translate(
'indexPatternEditor.editor.flyoutEditUnpersistedButtonLabel',
{
defaultMessage: 'Continue to use without saving',
}
);
const exploreButtonLabel = i18n.translate('indexPatternEditor.editor.flyoutExploreButtonLabel', {
defaultMessage: 'Use without saving',
});
export const Footer = ({ onCancel, onSubmit, submitDisabled, isEdit, allowAdHoc }: FooterProps) => {
export const Footer = ({
onCancel,
onSubmit,
submitDisabled,
isEdit,
allowAdHoc,
isPersisted,
}: FooterProps) => {
const submitPersisted = () => {
onSubmit(false);
};
@ -89,7 +104,11 @@ export const Footer = ({ onCancel, onSubmit, submitDisabled, isEdit, allowAdHoc
fill
disabled={submitDisabled}
>
{isEdit ? editButtonLabel : saveButtonLabel}
{isEdit
? isPersisted
? editButtonLabel
: editUnpersistedButtonLabel
: saveButtonLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -35,6 +35,7 @@ export const getEditorOpener =
notifications,
application,
dataViews,
overlays,
searchClient,
});
@ -46,6 +47,7 @@ export const getEditorOpener =
defaultTypeIsRollup = false,
requireTimestampField = false,
allowAdHocDataView = false,
editData,
}: DataViewEditorProps): CloseEditor => {
const closeEditor = () => {
if (overlayRef) {
@ -72,9 +74,11 @@ export const getEditorOpener =
closeEditor();
onCancel();
}}
editData={editData}
defaultTypeIsRollup={defaultTypeIsRollup}
requireTimestampField={requireTimestampField}
allowAdHocDataView={allowAdHocDataView}
showManagementLink={Boolean(editData && editData.isPersisted())}
/>
</I18nProvider>
</KibanaReactContextProvider>,

View file

@ -21,7 +21,7 @@ export class DataViewEditorPlugin
}
public start(core: CoreStart, plugins: StartPlugins) {
const { application, uiSettings, docLinks, http, notifications } = core;
const { application, uiSettings, docLinks, http, notifications, overlays } = core;
const { data, dataViews } = plugins;
return {
@ -48,6 +48,7 @@ export class DataViewEditorPlugin
http,
notifications,
application,
overlays,
dataViews,
searchClient: data.search.search,
}}

View file

@ -13,6 +13,7 @@ import {
NotificationsStart,
DocLinksStart,
HttpSetup,
OverlayStart,
} from '@kbn/core/public';
import { EuiComboBoxOptionOption } from '@elastic/eui';
@ -31,6 +32,7 @@ export interface DataViewEditorContext {
http: HttpSetup;
notifications: NotificationsStart;
application: ApplicationStart;
overlays: OverlayStart;
dataViews: DataViewsPublicPluginStart;
searchClient: DataPublicPluginStart['search']['search'];
}
@ -62,6 +64,11 @@ export interface DataViewEditorProps {
* if set to true user is presented with an option to create ad-hoc dataview without a saved object.
*/
allowAdHocDataView?: boolean;
/**
* if set to true a link to the management page is shown
*/
showManagementLink?: boolean;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface

View file

@ -72,6 +72,7 @@ export function ChangeDataView({
onTextLangQuerySubmit,
textBasedLanguage,
isDisabled,
onEditDataView,
onCreateDefaultAdHocDataView,
}: DataViewPickerPropsExtended) {
const { euiTheme } = useEuiTheme();
@ -88,7 +89,7 @@ export function ChangeDataView({
const [selectedDataViewId, setSelectedDataViewId] = useState(currentDataViewId);
const kibana = useKibana<IUnifiedSearchPluginServices>();
const { application, data, storage } = kibana.services;
const { application, data, storage, dataViews, dataViewEditor } = kibana.services;
const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth });
const [isTextLangTransitionModalDismissed, setIsTextLangTransitionModalDismissed] = useState(() =>
Boolean(storage.get(TEXT_LANG_TRANSITION_MODAL_KEY))
@ -193,11 +194,21 @@ export function ChangeDataView({
key="manage"
icon="indexSettings"
data-test-subj="indexPattern-manage-field"
onClick={() => {
onClick={async () => {
if (onEditDataView) {
const dataView = await dataViews.get(currentDataViewId!);
dataViewEditor.openEditor({
editData: dataView,
onSave: (updatedDataView) => {
onEditDataView(updatedDataView);
},
});
} else {
application.navigateToApp('management', {
path: `/kibana/indexPatterns/patterns/${currentDataViewId}`,
});
}
setPopoverIsOpen(false);
application.navigateToApp('management', {
path: `/kibana/indexPatterns/patterns/${currentDataViewId}`,
});
}}
>
{i18n.translate('unifiedSearch.query.queryBar.indexPattern.manageFieldButton', {

View file

@ -41,6 +41,11 @@ export interface DataViewPickerProps {
* Callback that is called when the user changes the currently selected dataview.
*/
onChangeDataView: (newId: string) => void;
/**
* Callback that is called when the user edits the current data view via flyout.
* The first parameter is the updated data view stub without fetched fields
*/
onEditDataView?: (updatedDataViewStub: DataView) => void;
/**
* The id of the selected dataview.
*/
@ -98,6 +103,7 @@ export const DataViewPicker = ({
currentDataViewId,
adHocDataViews,
onChangeDataView,
onEditDataView,
onAddField,
onDataViewCreated,
trigger,
@ -114,6 +120,7 @@ export const DataViewPicker = ({
isMissingCurrent={isMissingCurrent}
currentDataViewId={currentDataViewId}
onChangeDataView={onChangeDataView}
onEditDataView={onEditDataView}
onAddField={onAddField}
onDataViewCreated={onDataViewCreated}
onCreateDefaultAdHocDataView={onCreateDefaultAdHocDataView}

View file

@ -7,6 +7,7 @@
*/
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
@ -87,5 +88,6 @@ export interface IUnifiedSearchPluginServices extends Partial<CoreStart> {
docLinks: DocLinksStart;
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
dataViewEditor: DataViewEditorStart;
usageCollection?: UsageCollectionStart;
}

View file

@ -259,7 +259,6 @@ export const LensTopNavMenu = ({
[dispatch]
);
const [indexPatterns, setIndexPatterns] = useState<DataView[]>([]);
const [dataViewsList, setDataViewsList] = useState<DataView[]>([]);
const [currentIndexPattern, setCurrentIndexPattern] = useState<DataView>();
const [isOnTextBasedMode, setIsOnTextBasedMode] = useState(false);
const [rejectedIndexPatterns, setRejectedIndexPatterns] = useState<string[]>([]);
@ -357,27 +356,18 @@ export const LensTopNavMenu = ({
]);
useEffect(() => {
if (activeDatasourceId && datasourceStates[activeDatasourceId].state) {
const dataViewId = datasourceMap[activeDatasourceId].getUsedDataView(
datasourceStates[activeDatasourceId].state
);
const dataView = dataViewsList.find((pattern) => pattern.id === dataViewId);
setCurrentIndexPattern(dataView ?? indexPatterns[0]);
}
}, [activeDatasourceId, datasourceMap, datasourceStates, indexPatterns, dataViewsList]);
useEffect(() => {
const fetchDataViews = async () => {
const totalDataViewsList = [];
const dataViewsIds = await data.dataViews.getIds();
for (let i = 0; i < dataViewsIds.length; i++) {
const d = await data.dataViews.get(dataViewsIds[i]);
totalDataViewsList.push(d);
const setCurrentPattern = async () => {
if (activeDatasourceId && datasourceStates[activeDatasourceId].state) {
const dataViewId = datasourceMap[activeDatasourceId].getUsedDataView(
datasourceStates[activeDatasourceId].state
);
const dataView = await data.dataViews.get(dataViewId);
setCurrentIndexPattern(dataView ?? indexPatterns[0]);
}
setDataViewsList(totalDataViewsList);
};
fetchDataViews();
}, [data]);
setCurrentPattern();
}, [activeDatasourceId, datasourceMap, datasourceStates, indexPatterns, data.dataViews]);
useEffect(() => {
if (typeof query === 'object' && query !== null && isOfAggregateQueryType(query)) {
@ -847,10 +837,8 @@ export const LensTopNavMenu = ({
onDataViewCreated: createNewDataView,
onCreateDefaultAdHocDataView,
adHocDataViews: indexPatterns.filter((pattern) => !pattern.isPersisted()),
onChangeDataView: (newIndexPatternId: string) => {
const currentDataView = dataViewsList.find(
(indexPattern) => indexPattern.id === newIndexPatternId
);
onChangeDataView: async (newIndexPatternId: string) => {
const currentDataView = await data.dataViews.get(newIndexPatternId);
setCurrentIndexPattern(currentDataView);
dispatchChangeIndexPattern(newIndexPatternId);
if (isOnTextBasedMode) {
@ -864,6 +852,39 @@ export const LensTopNavMenu = ({
setIsOnTextBasedMode(false);
}
},
onEditDataView: async (updatedDataViewStub) => {
if (!currentIndexPattern) return;
if (currentIndexPattern.isPersisted()) {
// clear instance cache and fetch again to make sure fields are up to date (in case pattern changed)
dataViewsService.clearInstanceCache(currentIndexPattern.id);
const updatedCurrentIndexPattern = await dataViewsService.get(currentIndexPattern.id!);
// if the data view was persisted, reload it from cache
const updatedCache = {
...dataViews.indexPatterns,
};
delete updatedCache[currentIndexPattern.id!];
const newIndexPatterns = await indexPatternService.ensureIndexPattern({
id: updatedCurrentIndexPattern.id!,
cache: updatedCache,
});
dispatch(
changeIndexPattern({
dataViews: { indexPatterns: newIndexPatterns },
indexPatternId: updatedCurrentIndexPattern.id!,
})
);
// Renew session id to make sure the request is done again
dispatchSetState({
searchSessionId: data.search.session.start(),
resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter),
});
// update list of index patterns to pick up mutations in the changed data view
setCurrentIndexPattern(updatedCurrentIndexPattern);
} else {
// if it was an ad-hoc data view, we need to switch to a new data view anyway
indexPatternService.replaceDataViewId(updatedDataViewStub);
}
},
textBasedLanguages: supportedTextBasedLanguages as DataViewPickerProps['textBasedLanguages'],
};

View file

@ -62,7 +62,7 @@ export function createMockDatasource(id: string): DatasourceMock {
isTimeBased: jest.fn(),
isValidColumn: jest.fn(),
isEqual: jest.fn(),
getUsedDataView: jest.fn(),
getUsedDataView: jest.fn((state, layer) => 'mockip'),
getUsedDataViews: jest.fn(),
onRefreshIndexPattern: jest.fn(),
};