Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SpaceTime] [Saved Search] Inline editing #10

Closed
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function getESQLWithSafeLimit(esql: string, limit: number): string {
return parts
.map((part, i) => {
if (i === index) {
return `${part.trim()} \n| LIMIT ${limit}`;
return `${part.trim()} | limit ${limit}`;
}
return part;
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
* Side Public License, v 1.
*/

import { Reference } from '@kbn/content-management-utils';
import { apiHasParentApi, apiHasUniqueId, PublishingSubject } from '@kbn/presentation-publishing';
import { BehaviorSubject, combineLatest, isObservable, map, Observable, of, switchMap } from 'rxjs';
import { apiCanAddNewPanel, CanAddNewPanel } from './can_add_new_panel';

export interface PanelPackage<SerializedState extends object = object> {
panelType: string;
initialState?: SerializedState;
references?: Reference[];
}

export interface PresentationContainer extends CanAddNewPanel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function syncUnifiedSearchState(
const {
explicitInput: { filters, query },
} = this.getState();
if (this.ignoreUnifiedSearch) return;
OnFiltersChange$.next({
filters: filters ?? [],
query: query ?? queryString.getDefaultQuery(),
Expand All @@ -66,6 +67,7 @@ export function syncUnifiedSearchState(
set: ({ filters: newFilters, query: newQuery }) => {
intermediateFilterState.filters = cleanFiltersForSerialize(newFilters);
intermediateFilterState.query = newQuery;
if (this.ignoreUnifiedSearch) return;
this.dispatch.setFiltersAndQuery(intermediateFilterState);
},
state$: OnFiltersChange$.pipe(distinctUntilChanged()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ export class DashboardContainer
private domNode?: HTMLElement;
private overlayRef?: OverlayRef;
private allDataViews: DataView[] = [];
public dataViews: BehaviorSubject<DataView[] | undefined> = new BehaviorSubject<
DataView[] | undefined
>(undefined);

// performance monitoring
public lastLoadStartTime?: number;
Expand All @@ -172,6 +175,8 @@ export class DashboardContainer
private hadContentfulRender = false;
private scrollPosition?: number;

public ignoreUnifiedSearch: boolean = false;

// cleanup
public stopSyncingWithUnifiedSearch?: () => void;
private cleanupStateTools: () => void;
Expand Down Expand Up @@ -522,6 +527,7 @@ export class DashboardContainer
...panelPackage.initialState,
id: newId,
},
references: panelPackage.references,
};
this.updateInput({ panels: { ...otherPanels, [newId]: newPanel } });
onSuccess(newId, newPanel.explicitInput.title);
Expand Down Expand Up @@ -712,6 +718,7 @@ export class DashboardContainer
public setAllDataViews = (newDataViews: DataView[]) => {
this.allDataViews = newDataViews;
this.onDataViewsUpdate$.next(newDataViews);
this.dataViews.next(newDataViews);
};

public getExpandedPanelId = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,16 @@ export const dashboardContainerReducers = {
) => {
state.componentState.animatePanelTransforms = action.payload;
},

setDisableQueryInput: (state: DashboardReduxState, action: PayloadAction<boolean>) => {
state.componentState.disableQueryInput = action.payload;
},

setDisableAutoRefresh: (state: DashboardReduxState, action: PayloadAction<boolean>) => {
state.componentState.disableAutoRefresh = action.payload;
},

setDisableFilters: (state: DashboardReduxState, action: PayloadAction<boolean>) => {
state.componentState.disableFilters = action.payload;
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export const reducersToIgnore: Array<keyof typeof dashboardContainerReducers> =
'setFullScreenMode',
'setExpandedPanelId',
'setHasUnsavedChanges',
'setDisableQueryInput',
'setDisableAutoRefresh',
'setDisableFilters',
];

/**
Expand Down
4 changes: 4 additions & 0 deletions src/plugins/dashboard/public/dashboard_container/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export interface DashboardPublicState {
scrollToPanelId?: string;
highlightPanelId?: string;
focusedPanelId?: string;

disableQueryInput?: boolean;
disableAutoRefresh?: boolean;
disableFilters?: boolean;
}

export type DashboardLoadType =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ export function InternalDashboardTopNav({
const hasRunMigrations = dashboard.select(
(state) => state.componentState.hasRunClientsideMigrations
);
const disableQueryInput = dashboard.select((state) => state.componentState.disableQueryInput);
const disableAutoRefresh = dashboard.select((state) => state.componentState.disableAutoRefresh);
const disableFilters = dashboard.select((state) => state.componentState.disableFilters);
const hasUnsavedChanges = dashboard.select((state) => state.componentState.hasUnsavedChanges);
const fullScreenMode = dashboard.select((state) => state.componentState.fullScreenMode);
const savedQueryId = dashboard.select((state) => state.componentState.savedQueryId);
Expand All @@ -104,6 +107,8 @@ export function InternalDashboardTopNav({
const query = dashboard.select((state) => state.explicitInput.query);
const title = dashboard.select((state) => state.explicitInput.title);

// const disableQueryBar = dashboard.select((state) => state.componentState.disableQueryBar);

// store data views in state & subscribe to dashboard data view changes.
const [allDataViews, setAllDataViews] = useState<DataView[]>([]);
useEffect(() => {
Expand Down Expand Up @@ -321,6 +326,7 @@ export function InternalDashboardTopNav({
>{`${getDashboardBreadcrumb()} - ${dashboardTitle}`}</h1>
<TopNavMenu
{...visibilityProps}
// isDisabled={dashboard.ignoreUnifiedSearch}
query={query}
badges={badges}
screenTitle={title}
Expand All @@ -335,6 +341,9 @@ export function InternalDashboardTopNav({
? setCustomHeaderActionMenu ?? undefined
: setHeaderActionMenu
}
disableQueryInput={disableQueryInput}
showFilterBar={!disableFilters}
disableAutoRefresh={disableAutoRefresh}
className={fullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined}
config={
visibilityProps.showTopNavMenu
Expand Down
11 changes: 9 additions & 2 deletions src/plugins/discover/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"unifiedDocViewer",
"unifiedSearch",
"unifiedHistogram",
"contentManagement"
"contentManagement",
"presentationUtil"
],
"optionalPlugins": [
"dataVisualizer",
Expand All @@ -42,7 +43,13 @@
"observabilityAIAssistant",
"aiops"
],
"requiredBundles": ["kibanaUtils", "kibanaReact", "unifiedSearch", "savedObjects"],
"requiredBundles": [
"kibanaUtils",
"kibanaReact",
"unifiedSearch",
"savedObjects",
"textBasedLanguages"
],
"extraPublicDirs": ["common"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { SearchSource } from '@kbn/data-plugin/common';
import { DataView } from '@kbn/data-views-plugin/common';
import { DataTableRecord, SEARCH_EMBEDDABLE_TYPE } from '@kbn/discover-utils';
import { embeddableInputToSubject } from '@kbn/embeddable-plugin/public';
import { getESQLWithSafeLimit } from '@kbn/esql-utils';
import { i18n } from '@kbn/i18n';
import { CanAddNewPanel } from '@kbn/presentation-containers';
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { BehaviorSubject } from 'rxjs';
import { DiscoverServices } from '../../build_services';
import {
SearchEmbeddableApi,
SearchEmbeddableRuntimeState,
SearchEmbeddableSerializedState,
} from '../types';

export const ADD_SEARCH_EMBEDDABLE_ACTION_ID = 'create_saved_search_embeddable';

const parentApiIsCompatible = async (parentApi: unknown): Promise<CanAddNewPanel | undefined> => {
const { apiCanAddNewPanel } = await import('@kbn/presentation-containers');
// we cannot have an async type check, so return the casted parentApi rather than a boolean
return apiCanAddNewPanel(parentApi) ? (parentApi as CanAddNewPanel) : undefined;
};

export const registerCreateSavedSearchAction = (discoverServices: DiscoverServices) => {
discoverServices.uiActions.registerAction<EmbeddableApiContext>({
id: ADD_SEARCH_EMBEDDABLE_ACTION_ID,
getIconType: () => 'discoverApp',
isCompatible: async ({ embeddable: parentApi }) => {
return Boolean(await parentApiIsCompatible(parentApi));
},
execute: async ({ embeddable: parentApi }) => {
const canAddNewPanelParent = await parentApiIsCompatible(parentApi);
if (!canAddNewPanelParent) throw new IncompatibleActionError();
const { openSavedSearchEditFlyout } = await import(
'../components/editor/open_saved_search_edit_flyout'
);
try {
const savedSearch = discoverServices.savedSearch.getNew();
const defaultIndexPattern = await discoverServices.data.dataViews.getDefault();
if (defaultIndexPattern) {
const queryString = getESQLWithSafeLimit(
`from ${defaultIndexPattern?.getIndexPattern()}`,
10
);
savedSearch.searchSource.setField('index', defaultIndexPattern);
savedSearch.searchSource.setField('query', { esql: queryString });
}
const { searchSourceJSON, references } = savedSearch.searchSource.serialize();

const embeddable = await canAddNewPanelParent.addNewPanel<SearchEmbeddableSerializedState>({
panelType: SEARCH_EMBEDDABLE_TYPE,
initialState: {
attributes: {
isTextBasedQuery: true,
kibanaSavedObjectMeta: {
searchSourceJSON,
},
references,
},
},
});

// open the flyout if embeddable has been created successfully
if (embeddable) {
await openSavedSearchEditFlyout({
isEditing: false,
services: discoverServices,
api: embeddable as SearchEmbeddableApi,
});
}
} catch {
// swallow the rejection, since this just means the user closed without saving
}
},
getDisplayName: () =>
i18n.translate('discover.embeddable.search.displayName', {
defaultMessage: 'Saved search',
}),
});

discoverServices.uiActions.attachAction('ADD_PANEL_TRIGGER', ADD_SEARCH_EMBEDDABLE_ACTION_ID);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React, { lazy, Suspense } from 'react';

import { EuiLoadingSpinner } from '@elastic/eui';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { apiIsPresentationContainer, tracksOverlays } from '@kbn/presentation-containers';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { toMountPoint } from '@kbn/react-kibana-mount';

import { DiscoverServices } from '../../../build_services';
import { isEsqlMode } from '../../initialize_fetch';
import { SearchEmbeddableApi } from '../../types';

const SavedSearchEditorFlyout = lazy(() => import('./saved_search_edit_flyout'));

export const openSavedSearchEditFlyout = async ({
isEditing,
services,
api,
navigateToEditor,
}: {
isEditing: boolean;
services: DiscoverServices;
api: SearchEmbeddableApi;
navigateToEditor?: () => Promise<void>;
}) => {
const overlayTracker = tracksOverlays(api.parentApi) ? api.parentApi : undefined;
const initialState = api.snapshotRuntimeState();
const isEsql = isEsqlMode(api.savedSearch$.getValue());

return new Promise(async (resolve, reject) => {
try {
const onCancel = async () => {
if (!isEditing && apiIsPresentationContainer(api.parentApi)) {
api.parentApi.removePanel(api.uuid);
} else {
// Reset to initialState
const stateManager = api.getStateManager();
const initialSearchSource = await services.data.search.searchSource.create(
initialState.serializedSearchSource
);
stateManager.searchSource.next(initialSearchSource);
stateManager.columns.next(initialState.columns);
api.setTimeRange(initialState.timeRange);
}
flyoutSession.close();
overlayTracker?.clearOverlays();
};

const onSave = async () => {
flyoutSession.close();
overlayTracker?.clearOverlays();
};

const flyoutSession = services.core.overlays.openFlyout(
toMountPoint(
<KibanaRenderContextProvider {...services}>
<KibanaContextProvider services={services}>
<Suspense fallback={<EuiLoadingSpinner />}>
<SavedSearchEditorFlyout
api={api}
isEsql={isEsql}
onSave={onSave}
onCancel={onCancel}
services={services}
isEditing={isEditing}
stateManager={api.getStateManager()}
navigateToEditor={navigateToEditor}
/>
</Suspense>
</KibanaContextProvider>
</KibanaRenderContextProvider>,
services.core
),
{
ownFocus: true,
size: 's',
type: 'push',
'data-test-subj': 'fieldStatisticsInitializerFlyout',
onClose: onCancel,
paddingSize: 'm',
hideCloseButton: true,
pushMinBreakpoint: 'xs', // TODO: Better handling of overlay mode
className: 'lnsConfigPanel__overlay savedSearchFlyout',
}
);

if (tracksOverlays(api.parentApi)) {
api.parentApi.openOverlay(flyoutSession, { focusedPanelId: api.uuid });
}
} catch (error) {
reject(error);
}
});
};
Loading