mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
205a2b81dd
commit
b270921241
11 changed files with 130 additions and 35 deletions
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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,
|
||||
}}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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'],
|
||||
};
|
||||
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue