mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Textbased languages] Render Lens suggestions in Discover (#151581)
## Summary Part of https://github.com/elastic/kibana/issues/147472 This PR adds the unified histogram on the text based mode in Discover and depicts Lens suggestions to the users. The users can navigate from it to Lens for further exploration and adding the viz into a dashboard.  Some notes: - Lens now exposes a new api to fetch the suggestions from a text based query. Later it can also be used to return suggestions for the dataview mode (the other part of the aforementioned issue) - Lens visualizations have been updated to return the correct previewIcon (the majority of them were using the empty icon and not the icon that represents them). This icon appears on the visualization selection dropdown - When Lens suggests a table, then the chart is not displayed (Discover already offers a table view) - The legacy metric is excluded from the suggestions as it is not compatible with the text based languages. - The text based languages are treated a bit differently on the unified histogram as they do not work with search sessions. - This feature is on technical preview, so we can iterate more on that on later esql milestones. This is the first iteration required for the milestone 1. - The ESQL search strategy has a default size of 1000 https://github.com/elastic/kibana/blob/main/src/plugins/data/common/search/expressions/essql.ts#L113 which means that this is the max rows that we retrieve. I am keeping the default as is irrelevant from the PR. In ESQL this limitation doesn't exist so I think we are fine. **Flaky test runner**: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1972 ### Checklist - [x] 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) - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [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 - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Davis McPhee <davis.mcphee@elastic.co>
This commit is contained in:
parent
a5bf13f453
commit
dec52ef09d
62 changed files with 1864 additions and 227 deletions
|
@ -109,6 +109,7 @@ export const buildDataViewMock = ({
|
|||
getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })),
|
||||
isTimeNanosBased: () => false,
|
||||
isPersisted: () => true,
|
||||
toSpec: () => ({}),
|
||||
getTimeField: () => {
|
||||
return dataViewFields.find((field) => field.name === timeFieldName);
|
||||
},
|
||||
|
|
|
@ -57,6 +57,14 @@ export function createDiscoverServicesMock(): DiscoverServices {
|
|||
},
|
||||
}));
|
||||
dataPlugin.dataViews = createDiscoverDataViewsMock();
|
||||
expressionsPlugin.run = jest.fn(() =>
|
||||
of({
|
||||
partial: false,
|
||||
result: {
|
||||
rows: [],
|
||||
},
|
||||
})
|
||||
) as unknown as typeof expressionsPlugin.run;
|
||||
|
||||
return {
|
||||
core: coreMock.createStart(),
|
||||
|
@ -152,7 +160,14 @@ export function createDiscoverServicesMock(): DiscoverServices {
|
|||
savedObjectsTagging: {},
|
||||
dataViews: dataPlugin.dataViews,
|
||||
timefilter: dataPlugin.query.timefilter.timefilter,
|
||||
lens: { EmbeddableComponent: jest.fn(() => null) },
|
||||
lens: {
|
||||
EmbeddableComponent: jest.fn(() => null),
|
||||
stateHelperApi: jest.fn(() => {
|
||||
return {
|
||||
suggestions: jest.fn(),
|
||||
};
|
||||
}),
|
||||
},
|
||||
locator: {
|
||||
useUrl: jest.fn(() => ''),
|
||||
navigate: jest.fn(),
|
||||
|
|
|
@ -46,6 +46,7 @@ export const DiscoverHistogramLayout = ({
|
|||
inspectorAdapters,
|
||||
savedSearchFetch$: stateContainer.dataState.fetch$,
|
||||
searchSessionId,
|
||||
isPlainRecord,
|
||||
...commonProps,
|
||||
});
|
||||
|
||||
|
|
|
@ -163,20 +163,6 @@ describe('Discover component', () => {
|
|||
).not.toBeNull();
|
||||
}, 10000);
|
||||
|
||||
test('sql query displays no chart toggle', async () => {
|
||||
const container = document.createElement('div');
|
||||
await mountComponent(
|
||||
dataViewWithTimefieldMock,
|
||||
false,
|
||||
{ attachTo: container },
|
||||
{ sql: 'SELECT * FROM test' },
|
||||
true
|
||||
);
|
||||
expect(
|
||||
container.querySelector('[data-test-subj="unifiedHistogramChartOptionsToggle"]')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('the saved search title h1 gains focus on navigate', async () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
|
|
@ -72,12 +72,12 @@ export const DiscoverMainContent = ({
|
|||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
{!isPlainRecord && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
{!isPlainRecord && (
|
||||
<DocumentViewModeToggle viewMode={viewMode} setDiscoverViewMode={setDiscoverViewMode} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{dataState.error && (
|
||||
<ErrorCallout
|
||||
title={i18n.translate('discover.documentsErrorTitle', {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import React, { ReactElement } from 'react';
|
||||
import { AggregateQuery, Query } from '@kbn/es-query';
|
||||
import { buildDataTableRecord } from '../../../../utils/build_data_record';
|
||||
import { esHits } from '../../../../__mocks__/es_hits';
|
||||
import { act, renderHook, WrapperComponent } from '@testing-library/react-hooks';
|
||||
|
@ -38,11 +39,11 @@ import { checkHitCount, sendErrorTo } from '../../hooks/use_saved_search_message
|
|||
import type { InspectorAdapters } from '../../hooks/use_inspector';
|
||||
|
||||
const mockData = dataPluginMock.createStartContract();
|
||||
const mockQueryState = {
|
||||
let mockQueryState = {
|
||||
query: {
|
||||
query: 'query',
|
||||
language: 'kuery',
|
||||
},
|
||||
} as Query | AggregateQuery,
|
||||
filters: [],
|
||||
time: {
|
||||
from: 'now-15m',
|
||||
|
@ -112,6 +113,11 @@ describe('useDiscoverHistogram', () => {
|
|||
foundDocuments: true,
|
||||
}) as DataMain$,
|
||||
savedSearchFetch$ = new Subject() as DataFetch$,
|
||||
documents$ = new BehaviorSubject({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
result: esHits.map((esHit) => buildDataTableRecord(esHit, dataViewWithTimefieldMock)),
|
||||
}) as DataDocuments$,
|
||||
isPlainRecord = false,
|
||||
}: {
|
||||
stateContainer?: DiscoverStateContainer;
|
||||
searchSessionId?: string;
|
||||
|
@ -119,12 +125,9 @@ describe('useDiscoverHistogram', () => {
|
|||
totalHits$?: DataTotalHits$;
|
||||
main$?: DataMain$;
|
||||
savedSearchFetch$?: DataFetch$;
|
||||
documents$?: DataDocuments$;
|
||||
isPlainRecord?: boolean;
|
||||
} = {}) => {
|
||||
const documents$ = new BehaviorSubject({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
result: esHits.map((esHit) => buildDataTableRecord(esHit, dataViewWithTimefieldMock)),
|
||||
}) as DataDocuments$;
|
||||
|
||||
const availableFields$ = new BehaviorSubject({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
fields: [] as string[],
|
||||
|
@ -144,6 +147,7 @@ describe('useDiscoverHistogram', () => {
|
|||
dataView: dataViewWithTimefieldMock,
|
||||
inspectorAdapters,
|
||||
searchSessionId,
|
||||
isPlainRecord,
|
||||
};
|
||||
|
||||
const Wrapper: WrapperComponent<UseDiscoverHistogramProps> = ({ children }) => (
|
||||
|
@ -186,6 +190,7 @@ describe('useDiscoverHistogram', () => {
|
|||
'timeRange',
|
||||
'chartHidden',
|
||||
'timeInterval',
|
||||
'columns',
|
||||
'breakdownField',
|
||||
'searchSessionId',
|
||||
'totalHitsStatus',
|
||||
|
@ -196,6 +201,9 @@ describe('useDiscoverHistogram', () => {
|
|||
});
|
||||
|
||||
describe('state', () => {
|
||||
beforeEach(() => {
|
||||
mockCheckHitCount.mockClear();
|
||||
});
|
||||
it('should subscribe to state changes', async () => {
|
||||
const { hook } = await renderUseDiscoverHistogram();
|
||||
const api = createMockUnifiedHistogramApi({ initialized: true });
|
||||
|
@ -203,7 +211,7 @@ describe('useDiscoverHistogram', () => {
|
|||
act(() => {
|
||||
hook.result.current.setUnifiedHistogramApi(api);
|
||||
});
|
||||
expect(api.state$.subscribe).toHaveBeenCalledTimes(2);
|
||||
expect(api.state$.subscribe).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it('should sync Unified Histogram state with the state container', async () => {
|
||||
|
@ -217,6 +225,7 @@ describe('useDiscoverHistogram', () => {
|
|||
breakdownField: 'test',
|
||||
totalHitsStatus: UnifiedHistogramFetchStatus.loading,
|
||||
totalHitsResult: undefined,
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
} as unknown as UnifiedHistogramState;
|
||||
const api = createMockUnifiedHistogramApi({ initialized: true });
|
||||
api.state$ = new BehaviorSubject({ ...state, lensRequestAdapter });
|
||||
|
@ -241,6 +250,7 @@ describe('useDiscoverHistogram', () => {
|
|||
breakdownField: containerState.breakdownField,
|
||||
totalHitsStatus: UnifiedHistogramFetchStatus.loading,
|
||||
totalHitsResult: undefined,
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
} as unknown as UnifiedHistogramState;
|
||||
const api = createMockUnifiedHistogramApi({ initialized: true });
|
||||
api.state$ = new BehaviorSubject(state);
|
||||
|
@ -303,6 +313,7 @@ describe('useDiscoverHistogram', () => {
|
|||
breakdownField: containerState.breakdownField,
|
||||
totalHitsStatus: UnifiedHistogramFetchStatus.loading,
|
||||
totalHitsResult: undefined,
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
} as unknown as UnifiedHistogramState;
|
||||
const api = createMockUnifiedHistogramApi({ initialized: true });
|
||||
let params: Partial<UnifiedHistogramState> = {};
|
||||
|
@ -347,7 +358,6 @@ describe('useDiscoverHistogram', () => {
|
|||
});
|
||||
|
||||
it('should update total hits when the total hits state changes', async () => {
|
||||
mockCheckHitCount.mockClear();
|
||||
const totalHits$ = new BehaviorSubject({
|
||||
fetchStatus: FetchStatus.LOADING,
|
||||
result: undefined,
|
||||
|
@ -366,6 +376,7 @@ describe('useDiscoverHistogram', () => {
|
|||
breakdownField: containerState.breakdownField,
|
||||
totalHitsStatus: UnifiedHistogramFetchStatus.loading,
|
||||
totalHitsResult: undefined,
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
} as unknown as UnifiedHistogramState;
|
||||
const api = createMockUnifiedHistogramApi({ initialized: true });
|
||||
api.state$ = new BehaviorSubject({
|
||||
|
@ -384,7 +395,19 @@ describe('useDiscoverHistogram', () => {
|
|||
});
|
||||
|
||||
it('should not update total hits when the total hits state changes to an error', async () => {
|
||||
mockCheckHitCount.mockClear();
|
||||
mockQueryState = {
|
||||
query: {
|
||||
query: 'query',
|
||||
language: 'kuery',
|
||||
} as Query | AggregateQuery,
|
||||
filters: [],
|
||||
time: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
},
|
||||
};
|
||||
|
||||
mockData.query.getState = () => mockQueryState;
|
||||
const totalHits$ = new BehaviorSubject({
|
||||
fetchStatus: FetchStatus.UNINITIALIZED,
|
||||
result: undefined,
|
||||
|
@ -399,6 +422,7 @@ describe('useDiscoverHistogram', () => {
|
|||
breakdownField: containerState.breakdownField,
|
||||
totalHitsStatus: UnifiedHistogramFetchStatus.loading,
|
||||
totalHitsResult: undefined,
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
} as unknown as UnifiedHistogramState;
|
||||
const api = createMockUnifiedHistogramApi({ initialized: true });
|
||||
api.state$ = new BehaviorSubject({
|
||||
|
|
|
@ -15,17 +15,24 @@ import {
|
|||
UnifiedHistogramState,
|
||||
} from '@kbn/unified-histogram-plugin/public';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { distinctUntilChanged, map, Observable } from 'rxjs';
|
||||
import { useCallback, useEffect, useRef, useMemo, useState } from 'react';
|
||||
import { distinctUntilChanged, filter, map, Observable, skip } from 'rxjs';
|
||||
import type { Suggestion } from '@kbn/lens-plugin/public';
|
||||
import useLatest from 'react-use/lib/useLatest';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
import { getUiActions } from '../../../../kibana_services';
|
||||
import { FetchStatus } from '../../../types';
|
||||
import { useDataState } from '../../hooks/use_data_state';
|
||||
import type { InspectorAdapters } from '../../hooks/use_inspector';
|
||||
import type { DataFetch$, SavedSearchData } from '../../services/discover_data_state_container';
|
||||
import type {
|
||||
DataDocuments$,
|
||||
DataFetch$,
|
||||
SavedSearchData,
|
||||
} from '../../services/discover_data_state_container';
|
||||
import { checkHitCount, sendErrorTo } from '../../hooks/use_saved_search_messages';
|
||||
import { useAppStateSelector } from '../../services/discover_app_state_container';
|
||||
import type { DiscoverStateContainer } from '../../services/discover_state';
|
||||
import { addLog } from '../../../../utils/add_log';
|
||||
|
||||
export interface UseDiscoverHistogramProps {
|
||||
stateContainer: DiscoverStateContainer;
|
||||
|
@ -34,6 +41,7 @@ export interface UseDiscoverHistogramProps {
|
|||
inspectorAdapters: InspectorAdapters;
|
||||
savedSearchFetch$: DataFetch$;
|
||||
searchSessionId: string | undefined;
|
||||
isPlainRecord: boolean;
|
||||
}
|
||||
|
||||
export const useDiscoverHistogram = ({
|
||||
|
@ -43,6 +51,7 @@ export const useDiscoverHistogram = ({
|
|||
inspectorAdapters,
|
||||
savedSearchFetch$,
|
||||
searchSessionId,
|
||||
isPlainRecord,
|
||||
}: UseDiscoverHistogramProps) => {
|
||||
const services = useDiscoverServices();
|
||||
const timefilter = services.data.query.timefilter.timefilter;
|
||||
|
@ -66,6 +75,7 @@ export const useDiscoverHistogram = ({
|
|||
hideChart: chartHidden,
|
||||
interval: timeInterval,
|
||||
breakdownField,
|
||||
columns,
|
||||
} = stateContainer.appState.getState();
|
||||
|
||||
const { fetchStatus: totalHitsStatus, result: totalHitsResult } =
|
||||
|
@ -85,6 +95,7 @@ export const useDiscoverHistogram = ({
|
|||
timeRange,
|
||||
chartHidden,
|
||||
timeInterval,
|
||||
columns,
|
||||
breakdownField,
|
||||
searchSessionId,
|
||||
totalHitsStatus: totalHitsStatus.toString() as UnifiedHistogramFetchStatus,
|
||||
|
@ -134,18 +145,8 @@ export const useDiscoverHistogram = ({
|
|||
/**
|
||||
* Update Unified Histgoram request params
|
||||
*/
|
||||
|
||||
const {
|
||||
query,
|
||||
filters,
|
||||
fromDate: from,
|
||||
toDate: to,
|
||||
} = useQuerySubscriber({ data: services.data });
|
||||
|
||||
const timeRange = useMemo(
|
||||
() => (from && to ? { from, to } : timefilter.getTimeDefaults()),
|
||||
[timefilter, from, to]
|
||||
);
|
||||
const { query, filters } = useQuerySubscriber({ data: services.data });
|
||||
const timeRange = timefilter.getAbsoluteTime();
|
||||
|
||||
useEffect(() => {
|
||||
unifiedHistogram?.setRequestParams({
|
||||
|
@ -214,6 +215,25 @@ export const useDiscoverHistogram = ({
|
|||
unifiedHistogram?.setBreakdownField(breakdownField);
|
||||
}, [breakdownField, unifiedHistogram]);
|
||||
|
||||
/**
|
||||
* Columns
|
||||
*/
|
||||
|
||||
// Update the columns only when documents are fetched so the Lens suggestions
|
||||
// don't constantly change when the user modifies the table columns
|
||||
useEffect(() => {
|
||||
const subscription = createDocumentsFetchedObservable(
|
||||
stateContainer.dataState.data$.documents$
|
||||
).subscribe(({ textBasedQueryColumns }) => {
|
||||
const columns = textBasedQueryColumns?.map(({ name }) => name) ?? [];
|
||||
unifiedHistogram?.setColumns(columns);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [stateContainer.appState, stateContainer.dataState.data$.documents$, unifiedHistogram]);
|
||||
|
||||
/**
|
||||
* Total hits
|
||||
*/
|
||||
|
@ -260,41 +280,81 @@ export const useDiscoverHistogram = ({
|
|||
}, [
|
||||
savedSearchData$.main$,
|
||||
savedSearchData$.totalHits$,
|
||||
services.data,
|
||||
setTotalHitsError,
|
||||
unifiedHistogram,
|
||||
unifiedHistogram?.state$,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Data fetching
|
||||
*/
|
||||
|
||||
const skipRefetch = useRef<boolean>();
|
||||
const skipDiscoverRefetch = useRef<boolean>();
|
||||
const skipLensSuggestionRefetch = useRef<boolean>();
|
||||
const usingLensSuggestion = useLatest(isPlainRecord && !hideChart);
|
||||
|
||||
// Skip refetching when showing the chart since Lens will
|
||||
// automatically fetch when the chart is shown
|
||||
useEffect(() => {
|
||||
if (skipRefetch.current === undefined) {
|
||||
skipRefetch.current = false;
|
||||
if (skipDiscoverRefetch.current === undefined) {
|
||||
skipDiscoverRefetch.current = false;
|
||||
} else {
|
||||
skipRefetch.current = !hideChart;
|
||||
skipDiscoverRefetch.current = !hideChart;
|
||||
}
|
||||
}, [hideChart]);
|
||||
|
||||
// Trigger a unified histogram refetch when savedSearchFetch$ is triggered
|
||||
useEffect(() => {
|
||||
const subscription = savedSearchFetch$.subscribe(() => {
|
||||
if (!skipRefetch.current) {
|
||||
if (!skipDiscoverRefetch.current) {
|
||||
addLog('Unified Histogram - Discover refetch');
|
||||
unifiedHistogram?.refetch();
|
||||
}
|
||||
|
||||
skipRefetch.current = false;
|
||||
skipDiscoverRefetch.current = false;
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [savedSearchFetch$, unifiedHistogram]);
|
||||
}, [savedSearchFetch$, unifiedHistogram, usingLensSuggestion]);
|
||||
|
||||
// Reload the chart when the current suggestion changes
|
||||
const [currentSuggestion, setCurrentSuggestion] = useState<Suggestion>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!skipLensSuggestionRefetch.current && currentSuggestion && usingLensSuggestion.current) {
|
||||
addLog('Unified Histogram - Lens suggestion refetch');
|
||||
unifiedHistogram?.refetch();
|
||||
}
|
||||
|
||||
skipLensSuggestionRefetch.current = false;
|
||||
}, [currentSuggestion, unifiedHistogram, usingLensSuggestion]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = createCurrentSuggestionObservable(unifiedHistogram?.state$)?.subscribe(
|
||||
setCurrentSuggestion
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscription?.unsubscribe();
|
||||
};
|
||||
}, [unifiedHistogram]);
|
||||
|
||||
// When the data view or query changes, which will trigger a current suggestion change,
|
||||
// skip the next refetch since we want to wait for the columns to update first, which
|
||||
// doesn't happen until after the documents are fetched
|
||||
useEffect(() => {
|
||||
const subscription = createSkipFetchObservable(unifiedHistogram?.state$)?.subscribe(() => {
|
||||
if (usingLensSuggestion.current) {
|
||||
skipLensSuggestionRefetch.current = true;
|
||||
skipDiscoverRefetch.current = true;
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription?.unsubscribe();
|
||||
};
|
||||
}, [unifiedHistogram?.state$, usingLensSuggestion]);
|
||||
|
||||
return { hideChart, setUnifiedHistogramApi };
|
||||
};
|
||||
|
@ -316,9 +376,31 @@ const createStateSyncObservable = (state$?: Observable<UnifiedHistogramState>) =
|
|||
);
|
||||
};
|
||||
|
||||
const createDocumentsFetchedObservable = (documents$: DataDocuments$) => {
|
||||
return documents$.pipe(
|
||||
distinctUntilChanged((prev, curr) => prev.fetchStatus === curr.fetchStatus),
|
||||
filter(({ fetchStatus }) => fetchStatus === FetchStatus.COMPLETE)
|
||||
);
|
||||
};
|
||||
|
||||
const createTotalHitsObservable = (state$?: Observable<UnifiedHistogramState>) => {
|
||||
return state$?.pipe(
|
||||
map((state) => ({ status: state.totalHitsStatus, result: state.totalHitsResult })),
|
||||
distinctUntilChanged((prev, curr) => prev.status === curr.status && prev.result === curr.result)
|
||||
);
|
||||
};
|
||||
|
||||
const createCurrentSuggestionObservable = (state$?: Observable<UnifiedHistogramState>) => {
|
||||
return state$?.pipe(
|
||||
map((state) => state.currentSuggestion),
|
||||
distinctUntilChanged(isEqual)
|
||||
);
|
||||
};
|
||||
|
||||
const createSkipFetchObservable = (state$?: Observable<UnifiedHistogramState>) => {
|
||||
return state$?.pipe(
|
||||
map((state) => [state.dataView.id, state.query]),
|
||||
distinctUntilChanged(isEqual),
|
||||
skip(1)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -262,12 +262,4 @@ describe('discover sidebar', function () {
|
|||
const createDataViewButton = findTestSubject(compWithPickerInViewerMode, 'dataview-create-new');
|
||||
expect(createDataViewButton.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should render the Visualize in Lens button in text based languages mode', async () => {
|
||||
const compInViewerMode = await mountComponent(getCompProps(), {
|
||||
query: { sql: 'SELECT * FROM test' },
|
||||
});
|
||||
const visualizeField = findTestSubject(compInViewerMode, 'textBased-visualize');
|
||||
expect(visualizeField.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiPageSideBar_Deprecated as EuiPageSideBar,
|
||||
} from '@elastic/eui';
|
||||
import { isOfAggregateQueryType } from '@kbn/es-query';
|
||||
import { DataViewPicker } from '@kbn/unified-search-plugin/public';
|
||||
import { type DataViewField, getFieldSubtypeMulti } from '@kbn/data-views-plugin/public';
|
||||
import {
|
||||
|
@ -25,14 +24,13 @@ import {
|
|||
FieldListGroupedProps,
|
||||
FieldsGroupNames,
|
||||
GroupedFieldsParams,
|
||||
triggerVisualizeActionsTextBasedLanguages,
|
||||
useGroupedFields,
|
||||
} from '@kbn/unified-field-list-plugin/public';
|
||||
import { VIEW_MODE } from '../../../../../common/constants';
|
||||
import { useAppStateSelector } from '../../services/discover_app_state_container';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
import { DiscoverField } from './discover_field';
|
||||
import { FIELDS_LIMIT_SETTING, PLUGIN_ID } from '../../../../../common';
|
||||
import { FIELDS_LIMIT_SETTING } from '../../../../../common';
|
||||
import {
|
||||
getSelectedFields,
|
||||
shouldShowField,
|
||||
|
@ -40,7 +38,6 @@ import {
|
|||
INITIAL_SELECTED_FIELDS_RESULT,
|
||||
} from './lib/group_fields';
|
||||
import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive';
|
||||
import { getUiActions } from '../../../../kibana_services';
|
||||
import { getRawRecordType } from '../../utils/get_raw_record_type';
|
||||
import { RecordRawType } from '../../services/discover_data_state_container';
|
||||
|
||||
|
@ -127,7 +124,6 @@ export function DiscoverSidebarComponent({
|
|||
const isPlainRecord = useAppStateSelector(
|
||||
(state) => getRawRecordType(state.query) === RecordRawType.PLAIN
|
||||
);
|
||||
const query = useAppStateSelector((state) => state.query);
|
||||
|
||||
const showFieldStats = useMemo(() => viewMode === VIEW_MODE.DOCUMENT_LEVEL, [viewMode]);
|
||||
const [selectedFieldsState, setSelectedFieldsState] = useState<SelectedFieldsResult>(
|
||||
|
@ -194,17 +190,6 @@ export function DiscoverSidebarComponent({
|
|||
]
|
||||
);
|
||||
|
||||
const visualizeAggregateQuery = useCallback(() => {
|
||||
const aggregateQuery = query && isOfAggregateQueryType(query) ? query : undefined;
|
||||
triggerVisualizeActionsTextBasedLanguages(
|
||||
getUiActions(),
|
||||
columns,
|
||||
PLUGIN_ID,
|
||||
selectedDataView,
|
||||
aggregateQuery
|
||||
);
|
||||
}, [columns, selectedDataView, query]);
|
||||
|
||||
const popularFieldsLimit = useMemo(() => uiSettings.get(FIELDS_LIMIT_SETTING), [uiSettings]);
|
||||
const onSupportedFieldFilter: GroupedFieldsParams<DataViewField>['onSupportedFieldFilter'] =
|
||||
useCallback(
|
||||
|
@ -342,20 +327,6 @@ export function DiscoverSidebarComponent({
|
|||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{isPlainRecord && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
iconType="lensApp"
|
||||
data-test-subj="textBased-visualize"
|
||||
onClick={visualizeAggregateQuery}
|
||||
size="s"
|
||||
>
|
||||
{i18n.translate('discover.textBasedLanguages.visualize.label', {
|
||||
defaultMessage: 'Visualize in Lens',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</FieldList>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -15,7 +15,11 @@ import {
|
|||
import { RecordRawType } from '../services/discover_data_state_container';
|
||||
|
||||
export function getRawRecordType(query?: Query | AggregateQuery) {
|
||||
if (query && isOfAggregateQueryType(query) && getAggregateQueryMode(query) === 'sql') {
|
||||
if (
|
||||
query &&
|
||||
isOfAggregateQueryType(query) &&
|
||||
(getAggregateQueryMode(query) === 'sql' || getAggregateQueryMode(query) === 'esql')
|
||||
) {
|
||||
return RecordRawType.PLAIN;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,11 +7,6 @@
|
|||
"id": "unifiedHistogram",
|
||||
"server": false,
|
||||
"browser": true,
|
||||
"requiredBundles": [
|
||||
"data",
|
||||
"dataViews",
|
||||
"embeddable",
|
||||
"inspector"
|
||||
]
|
||||
"requiredBundles": ["data", "dataViews", "embeddable", "inspector", "expressions"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
|
||||
import { EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks';
|
||||
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
|
||||
import type { UnifiedHistogramServices } from '../types';
|
||||
import { allSuggestionsMock } from './suggestions';
|
||||
|
||||
const dataPlugin = dataPluginMock.createStartContract();
|
||||
dataPlugin.query.filterManager.getFilters = jest.fn(() => []);
|
||||
|
@ -28,11 +30,20 @@ export const unifiedHistogramServicesMock = {
|
|||
useChartsTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme),
|
||||
useChartsBaseTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme),
|
||||
},
|
||||
lens: { EmbeddableComponent: jest.fn(() => null), navigateToPrefilledEditor: jest.fn() },
|
||||
lens: {
|
||||
EmbeddableComponent: jest.fn(() => null),
|
||||
navigateToPrefilledEditor: jest.fn(),
|
||||
stateHelperApi: jest.fn(() => {
|
||||
return {
|
||||
suggestions: jest.fn(() => allSuggestionsMock),
|
||||
};
|
||||
}),
|
||||
},
|
||||
storage: {
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
},
|
||||
expressions: expressionsPluginMock.createStartContract(),
|
||||
} as unknown as UnifiedHistogramServices;
|
||||
|
|
292
src/plugins/unified_histogram/public/__mocks__/suggestions.ts
Normal file
292
src/plugins/unified_histogram/public/__mocks__/suggestions.ts
Normal file
|
@ -0,0 +1,292 @@
|
|||
/*
|
||||
* 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 type { Suggestion } from '@kbn/lens-plugin/public';
|
||||
|
||||
export const currentSuggestionMock = {
|
||||
title: 'Heat map',
|
||||
hide: false,
|
||||
score: 0.6,
|
||||
previewIcon: 'heatmap',
|
||||
visualizationId: 'lnsHeatmap',
|
||||
visualizationState: {
|
||||
shape: 'heatmap',
|
||||
layerId: '46aa21fa-b747-4543-bf90-0b40007c546d',
|
||||
layerType: 'data',
|
||||
legend: {
|
||||
isVisible: true,
|
||||
position: 'right',
|
||||
type: 'heatmap_legend',
|
||||
},
|
||||
gridConfig: {
|
||||
type: 'heatmap_grid',
|
||||
isCellLabelVisible: false,
|
||||
isYAxisLabelVisible: true,
|
||||
isXAxisLabelVisible: true,
|
||||
isYAxisTitleVisible: false,
|
||||
isXAxisTitleVisible: false,
|
||||
},
|
||||
valueAccessor: '5b9b8b76-0836-4a12-b9c0-980c9900502f',
|
||||
xAccessor: '81e332d6-ee37-42a8-a646-cea4fc75d2d3',
|
||||
},
|
||||
keptLayerIds: ['46aa21fa-b747-4543-bf90-0b40007c546d'],
|
||||
datasourceState: {
|
||||
layers: {
|
||||
'46aa21fa-b747-4543-bf90-0b40007c546d': {
|
||||
index: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
|
||||
query: {
|
||||
sql: 'SELECT Dest, AvgTicketPrice FROM "kibana_sample_data_flights"',
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
columnId: '81e332d6-ee37-42a8-a646-cea4fc75d2d3',
|
||||
fieldName: 'Dest',
|
||||
meta: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
columnId: '5b9b8b76-0836-4a12-b9c0-980c9900502f',
|
||||
fieldName: 'AvgTicketPrice',
|
||||
meta: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
],
|
||||
allColumns: [
|
||||
{
|
||||
columnId: '81e332d6-ee37-42a8-a646-cea4fc75d2d3',
|
||||
fieldName: 'Dest',
|
||||
meta: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
columnId: '5b9b8b76-0836-4a12-b9c0-980c9900502f',
|
||||
fieldName: 'AvgTicketPrice',
|
||||
meta: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
],
|
||||
timeField: 'timestamp',
|
||||
},
|
||||
},
|
||||
fieldList: [],
|
||||
indexPatternRefs: [],
|
||||
initialContext: {
|
||||
dataViewSpec: {
|
||||
id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
|
||||
version: 'WzM1ODA3LDFd',
|
||||
title: 'kibana_sample_data_flights',
|
||||
timeFieldName: 'timestamp',
|
||||
sourceFilters: [],
|
||||
fields: {
|
||||
AvgTicketPrice: {
|
||||
count: 0,
|
||||
name: 'AvgTicketPrice',
|
||||
type: 'number',
|
||||
esTypes: ['float'],
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
format: {
|
||||
id: 'number',
|
||||
params: {
|
||||
pattern: '$0,0.[00]',
|
||||
},
|
||||
},
|
||||
shortDotsEnable: false,
|
||||
isMapped: true,
|
||||
},
|
||||
Dest: {
|
||||
count: 0,
|
||||
name: 'Dest',
|
||||
type: 'string',
|
||||
esTypes: ['keyword'],
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
format: {
|
||||
id: 'string',
|
||||
},
|
||||
shortDotsEnable: false,
|
||||
isMapped: true,
|
||||
},
|
||||
timestamp: {
|
||||
count: 0,
|
||||
name: 'timestamp',
|
||||
type: 'date',
|
||||
esTypes: ['date'],
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
format: {
|
||||
id: 'date',
|
||||
},
|
||||
shortDotsEnable: false,
|
||||
isMapped: true,
|
||||
},
|
||||
},
|
||||
allowNoIndex: false,
|
||||
name: 'Kibana Sample Data Flights',
|
||||
},
|
||||
fieldName: '',
|
||||
contextualFields: ['Dest', 'AvgTicketPrice'],
|
||||
query: {
|
||||
sql: 'SELECT Dest, AvgTicketPrice FROM "kibana_sample_data_flights"',
|
||||
},
|
||||
},
|
||||
},
|
||||
datasourceId: 'textBased',
|
||||
columns: 2,
|
||||
changeType: 'initial',
|
||||
} as Suggestion;
|
||||
|
||||
export const allSuggestionsMock = [
|
||||
currentSuggestionMock,
|
||||
{
|
||||
title: 'Donut',
|
||||
score: 0.46,
|
||||
visualizationId: 'lnsPie',
|
||||
previewIcon: 'pie',
|
||||
visualizationState: {
|
||||
shape: 'donut',
|
||||
layers: [
|
||||
{
|
||||
layerId: '2513a3d4-ad9d-48ea-bd58-8b6419ab97e6',
|
||||
primaryGroups: ['923f0681-3fe1-4987-aa27-d9c91fb95fa6'],
|
||||
metrics: ['b5f41c04-4bca-4abe-ae5c-b1d4d6fb00e0'],
|
||||
numberDisplay: 'percent',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'default',
|
||||
nestedLegend: false,
|
||||
layerType: 'data',
|
||||
},
|
||||
],
|
||||
},
|
||||
keptLayerIds: ['2513a3d4-ad9d-48ea-bd58-8b6419ab97e6'],
|
||||
datasourceState: {
|
||||
layers: {
|
||||
'2513a3d4-ad9d-48ea-bd58-8b6419ab97e6': {
|
||||
index: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
|
||||
query: {
|
||||
sql: 'SELECT Dest, AvgTicketPrice FROM "kibana_sample_data_flights"',
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
columnId: '923f0681-3fe1-4987-aa27-d9c91fb95fa6',
|
||||
fieldName: 'Dest',
|
||||
meta: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
columnId: 'b5f41c04-4bca-4abe-ae5c-b1d4d6fb00e0',
|
||||
fieldName: 'AvgTicketPrice',
|
||||
meta: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
],
|
||||
allColumns: [
|
||||
{
|
||||
columnId: '923f0681-3fe1-4987-aa27-d9c91fb95fa6',
|
||||
fieldName: 'Dest',
|
||||
meta: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
columnId: 'b5f41c04-4bca-4abe-ae5c-b1d4d6fb00e0',
|
||||
fieldName: 'AvgTicketPrice',
|
||||
meta: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
],
|
||||
timeField: 'timestamp',
|
||||
},
|
||||
},
|
||||
fieldList: [],
|
||||
indexPatternRefs: [],
|
||||
initialContext: {
|
||||
dataViewSpec: {
|
||||
id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
|
||||
version: 'WzM1ODA3LDFd',
|
||||
title: 'kibana_sample_data_flights',
|
||||
timeFieldName: 'timestamp',
|
||||
sourceFilters: [],
|
||||
fields: {
|
||||
AvgTicketPrice: {
|
||||
count: 0,
|
||||
name: 'AvgTicketPrice',
|
||||
type: 'number',
|
||||
esTypes: ['float'],
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
format: {
|
||||
id: 'number',
|
||||
params: {
|
||||
pattern: '$0,0.[00]',
|
||||
},
|
||||
},
|
||||
shortDotsEnable: false,
|
||||
isMapped: true,
|
||||
},
|
||||
Dest: {
|
||||
count: 0,
|
||||
name: 'Dest',
|
||||
type: 'string',
|
||||
esTypes: ['keyword'],
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
format: {
|
||||
id: 'string',
|
||||
},
|
||||
shortDotsEnable: false,
|
||||
isMapped: true,
|
||||
},
|
||||
timestamp: {
|
||||
count: 0,
|
||||
name: 'timestamp',
|
||||
type: 'date',
|
||||
esTypes: ['date'],
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
format: {
|
||||
id: 'date',
|
||||
},
|
||||
shortDotsEnable: false,
|
||||
isMapped: true,
|
||||
},
|
||||
},
|
||||
typeMeta: {},
|
||||
allowNoIndex: false,
|
||||
name: 'Kibana Sample Data Flights',
|
||||
},
|
||||
fieldName: '',
|
||||
contextualFields: ['Dest', 'AvgTicketPrice'],
|
||||
query: {
|
||||
sql: 'SELECT Dest, AvgTicketPrice FROM "kibana_sample_data_flights"',
|
||||
},
|
||||
},
|
||||
},
|
||||
datasourceId: 'textBased',
|
||||
columns: 2,
|
||||
changeType: 'unchanged',
|
||||
} as Suggestion,
|
||||
];
|
|
@ -10,6 +10,7 @@ import React, { ReactElement } from 'react';
|
|||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { Suggestion } from '@kbn/lens-plugin/public';
|
||||
import type { UnifiedHistogramFetchStatus } from '../types';
|
||||
import { Chart } from './chart';
|
||||
import type { ReactWrapper } from 'enzyme';
|
||||
|
@ -20,6 +21,9 @@ import { HitsCounter } from '../hits_counter';
|
|||
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield';
|
||||
import { dataViewMock } from '../__mocks__/data_view';
|
||||
import { BreakdownFieldSelector } from './breakdown_field_selector';
|
||||
import { SuggestionSelector } from './suggestion_selector';
|
||||
|
||||
import { currentSuggestionMock, allSuggestionsMock } from '../__mocks__/suggestions';
|
||||
|
||||
let mockUseEditVisualization: jest.Mock | undefined = jest.fn();
|
||||
|
||||
|
@ -34,6 +38,8 @@ async function mountComponent({
|
|||
chartHidden = false,
|
||||
appendHistogram,
|
||||
dataView = dataViewWithTimefieldMock,
|
||||
currentSuggestion,
|
||||
allSuggestions,
|
||||
}: {
|
||||
noChart?: boolean;
|
||||
noHits?: boolean;
|
||||
|
@ -41,6 +47,8 @@ async function mountComponent({
|
|||
chartHidden?: boolean;
|
||||
appendHistogram?: ReactElement;
|
||||
dataView?: DataView;
|
||||
currentSuggestion?: Suggestion;
|
||||
allSuggestions?: Suggestion[];
|
||||
} = {}) {
|
||||
(searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation(
|
||||
jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: noHits ? 0 : 2 } } }))
|
||||
|
@ -74,6 +82,8 @@ async function mountComponent({
|
|||
},
|
||||
},
|
||||
breakdown: noBreakdown ? undefined : { field: undefined },
|
||||
currentSuggestion,
|
||||
allSuggestions,
|
||||
appendHistogram,
|
||||
onResetChartHeight: jest.fn(),
|
||||
onChartHiddenChange: jest.fn(),
|
||||
|
@ -191,4 +201,26 @@ describe('Chart', () => {
|
|||
const component = await mountComponent({ noBreakdown: true });
|
||||
expect(component.find(BreakdownFieldSelector).exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should render the Lens SuggestionsSelector when chart is visible and suggestions exist', async () => {
|
||||
const component = await mountComponent({
|
||||
currentSuggestion: currentSuggestionMock,
|
||||
allSuggestions: allSuggestionsMock,
|
||||
});
|
||||
expect(component.find(SuggestionSelector).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not render the Lens SuggestionsSelector when chart is hidden', async () => {
|
||||
const component = await mountComponent({
|
||||
chartHidden: true,
|
||||
currentSuggestion: currentSuggestionMock,
|
||||
allSuggestions: allSuggestionsMock,
|
||||
});
|
||||
expect(component.find(SuggestionSelector).exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not render the Lens SuggestionsSelector when chart is visible and suggestions are undefined', async () => {
|
||||
const component = await mountComponent({ currentSuggestion: currentSuggestionMock });
|
||||
expect(component.find(SuggestionSelector).exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { Suggestion } from '@kbn/lens-plugin/public';
|
||||
import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public';
|
||||
import type { LensEmbeddableInput } from '@kbn/lens-plugin/public';
|
||||
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
|
@ -36,6 +37,7 @@ import type {
|
|||
UnifiedHistogramInputMessage,
|
||||
} from '../types';
|
||||
import { BreakdownFieldSelector } from './breakdown_field_selector';
|
||||
import { SuggestionSelector } from './suggestion_selector';
|
||||
import { useTotalHits } from './hooks/use_total_hits';
|
||||
import { useRequestParams } from './hooks/use_request_params';
|
||||
import { useChartStyles } from './hooks/use_chart_styles';
|
||||
|
@ -50,6 +52,9 @@ export interface ChartProps {
|
|||
dataView: DataView;
|
||||
query?: Query | AggregateQuery;
|
||||
filters?: Filter[];
|
||||
isPlainRecord?: boolean;
|
||||
currentSuggestion?: Suggestion;
|
||||
allSuggestions?: Suggestion[];
|
||||
timeRange?: TimeRange;
|
||||
request?: UnifiedHistogramRequestContext;
|
||||
hits?: UnifiedHistogramHitsContext;
|
||||
|
@ -66,6 +71,7 @@ export interface ChartProps {
|
|||
onChartHiddenChange?: (chartHidden: boolean) => void;
|
||||
onTimeIntervalChange?: (timeInterval: string) => void;
|
||||
onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void;
|
||||
onSuggestionChange?: (suggestion: Suggestion | undefined) => void;
|
||||
onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void;
|
||||
onChartLoad?: (event: UnifiedHistogramChartLoadEvent) => void;
|
||||
onFilter?: LensEmbeddableInput['onFilter'];
|
||||
|
@ -85,6 +91,9 @@ export function Chart({
|
|||
hits,
|
||||
chart,
|
||||
breakdown,
|
||||
currentSuggestion,
|
||||
allSuggestions,
|
||||
isPlainRecord,
|
||||
appendHitsCounter,
|
||||
appendHistogram,
|
||||
disableAutoFetching,
|
||||
|
@ -95,6 +104,7 @@ export function Chart({
|
|||
onResetChartHeight,
|
||||
onChartHiddenChange,
|
||||
onTimeIntervalChange,
|
||||
onSuggestionChange,
|
||||
onBreakdownFieldChange,
|
||||
onTotalHitsChange,
|
||||
onChartLoad,
|
||||
|
@ -118,6 +128,7 @@ export function Chart({
|
|||
onTimeIntervalChange,
|
||||
closePopover: closeChartOptions,
|
||||
onResetChartHeight,
|
||||
isPlainRecord,
|
||||
});
|
||||
|
||||
const chartVisible = !!(
|
||||
|
@ -150,6 +161,7 @@ export function Chart({
|
|||
filters,
|
||||
query,
|
||||
relativeTimeRange,
|
||||
currentSuggestion,
|
||||
disableAutoFetching,
|
||||
input$,
|
||||
beforeRefetch: updateTimeRange,
|
||||
|
@ -166,6 +178,7 @@ export function Chart({
|
|||
getTimeRange,
|
||||
refetch$,
|
||||
onTotalHitsChange,
|
||||
isPlainRecord,
|
||||
});
|
||||
|
||||
const {
|
||||
|
@ -188,8 +201,17 @@ export function Chart({
|
|||
dataView,
|
||||
timeInterval: chart?.timeInterval,
|
||||
breakdownField: breakdown?.field,
|
||||
suggestion: currentSuggestion,
|
||||
}),
|
||||
[breakdown?.field, chart?.timeInterval, chart?.title, dataView, filters, query]
|
||||
[
|
||||
breakdown?.field,
|
||||
chart?.timeInterval,
|
||||
chart?.title,
|
||||
currentSuggestion,
|
||||
dataView,
|
||||
filters,
|
||||
query,
|
||||
]
|
||||
);
|
||||
|
||||
const getRelativeTimeRange = useMemo(
|
||||
|
@ -244,6 +266,15 @@ export function Chart({
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{chartVisible && currentSuggestion && allSuggestions && allSuggestions?.length > 1 && (
|
||||
<EuiFlexItem css={breakdownFieldSelectorItemCss}>
|
||||
<SuggestionSelector
|
||||
suggestions={allSuggestions}
|
||||
activeSuggestion={currentSuggestion}
|
||||
onSuggestionChange={onSuggestionChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{onEditVisualization && (
|
||||
<EuiFlexItem grow={false} css={chartToolButtonCss}>
|
||||
<EuiToolTip
|
||||
|
@ -315,6 +346,7 @@ export function Chart({
|
|||
getTimeRange={getTimeRange}
|
||||
refetch$={refetch$}
|
||||
lensAttributes={lensAttributes}
|
||||
isPlainRecord={isPlainRecord}
|
||||
disableTriggers={disableTriggers}
|
||||
disabledActions={disabledActions}
|
||||
onTotalHitsChange={onTotalHitsChange}
|
||||
|
|
|
@ -35,6 +35,7 @@ const getMockLensAttributes = () =>
|
|||
dataView: dataViewWithTimefieldMock,
|
||||
timeInterval: 'auto',
|
||||
breakdownField: dataViewWithTimefieldMock.getFieldByName('extension'),
|
||||
suggestion: undefined,
|
||||
});
|
||||
|
||||
function mountComponent() {
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { useEuiTheme, useResizeObserver } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common';
|
||||
import type { IKibanaSearchResponse } from '@kbn/data-plugin/public';
|
||||
|
@ -38,6 +38,7 @@ export interface HistogramProps {
|
|||
request?: UnifiedHistogramRequestContext;
|
||||
hits?: UnifiedHistogramHitsContext;
|
||||
chart: UnifiedHistogramChartContext;
|
||||
isPlainRecord?: boolean;
|
||||
getTimeRange: () => TimeRange;
|
||||
refetch$: Observable<UnifiedHistogramInputMessage>;
|
||||
lensAttributes: TypedLensByValueInput['attributes'];
|
||||
|
@ -55,6 +56,7 @@ export function Histogram({
|
|||
request,
|
||||
hits,
|
||||
chart: { timeInterval },
|
||||
isPlainRecord,
|
||||
getTimeRange,
|
||||
refetch$,
|
||||
lensAttributes: attributes,
|
||||
|
@ -66,12 +68,24 @@ export function Histogram({
|
|||
onBrushEnd,
|
||||
}: HistogramProps) {
|
||||
const [bucketInterval, setBucketInterval] = useState<UnifiedHistogramBucketInterval>();
|
||||
const [chartSize, setChartSize] = useState('100%');
|
||||
const { timeRangeText, timeRangeDisplay } = useTimeRange({
|
||||
uiSettings,
|
||||
bucketInterval,
|
||||
timeRange: getTimeRange(),
|
||||
timeInterval,
|
||||
isPlainRecord,
|
||||
});
|
||||
const chartRef = useRef<HTMLDivElement | null>(null);
|
||||
const { height: containerHeight, width: containerWidth } = useResizeObserver(chartRef.current);
|
||||
useEffect(() => {
|
||||
if (attributes.visualizationType === 'lnsMetric') {
|
||||
const size = containerHeight < containerWidth ? containerHeight : containerWidth;
|
||||
setChartSize(`${size}px`);
|
||||
} else {
|
||||
setChartSize('100%');
|
||||
}
|
||||
}, [attributes, containerHeight, containerWidth]);
|
||||
|
||||
const onLoad = useStableCallback(
|
||||
(isLoading: boolean, adapters: Partial<DefaultInspectorAdapters> | undefined) => {
|
||||
|
@ -91,7 +105,10 @@ export function Histogram({
|
|||
return;
|
||||
}
|
||||
|
||||
const totalHits = adapters?.tables?.tables?.unifiedHistogram?.meta?.statistics?.totalCount;
|
||||
const adapterTables = adapters?.tables?.tables;
|
||||
const totalHits = isPlainRecord
|
||||
? Object.values(adapterTables ?? {})?.[0]?.rows?.length
|
||||
: adapterTables?.unifiedHistogram?.meta?.statistics?.totalCount;
|
||||
|
||||
onTotalHitsChange?.(
|
||||
isLoading ? UnifiedHistogramFetchStatus.loading : UnifiedHistogramFetchStatus.complete,
|
||||
|
@ -129,6 +146,13 @@ export function Histogram({
|
|||
|
||||
& > div {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& .lnsExpressionRenderer {
|
||||
width: ${chartSize};
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
& .echLegend .echLegendList {
|
||||
|
@ -145,7 +169,12 @@ export function Histogram({
|
|||
|
||||
return (
|
||||
<>
|
||||
<div data-test-subj="unifiedHistogramChart" data-time-range={timeRangeText} css={chartCss}>
|
||||
<div
|
||||
data-test-subj="unifiedHistogramChart"
|
||||
data-time-range={timeRangeText}
|
||||
css={chartCss}
|
||||
ref={chartRef}
|
||||
>
|
||||
<lens.EmbeddableComponent
|
||||
{...lensProps}
|
||||
disableTriggers={disableTriggers}
|
||||
|
|
|
@ -85,4 +85,24 @@ describe('test useChartPanels', () => {
|
|||
(resetChartHeightButton.onClick as Function)();
|
||||
expect(onResetChartHeight).toBeCalled();
|
||||
});
|
||||
test('useChartsPanel when isPlainRecord', async () => {
|
||||
const { result } = renderHook(() => {
|
||||
return useChartPanels({
|
||||
toggleHideChart: jest.fn(),
|
||||
onTimeIntervalChange: jest.fn(),
|
||||
closePopover: jest.fn(),
|
||||
onResetChartHeight: jest.fn(),
|
||||
isPlainRecord: true,
|
||||
chart: {
|
||||
hidden: true,
|
||||
timeInterval: 'auto',
|
||||
},
|
||||
});
|
||||
});
|
||||
const panels: EuiContextMenuPanelDescriptor[] = result.current;
|
||||
const panel0: EuiContextMenuPanelDescriptor = result.current[0];
|
||||
expect(panels.length).toBe(1);
|
||||
expect(panel0!.items).toHaveLength(1);
|
||||
expect(panel0!.items![0].icon).toBe('eye');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,12 +20,14 @@ export function useChartPanels({
|
|||
onTimeIntervalChange,
|
||||
closePopover,
|
||||
onResetChartHeight,
|
||||
isPlainRecord,
|
||||
}: {
|
||||
chart?: UnifiedHistogramChartContext;
|
||||
toggleHideChart: () => void;
|
||||
onTimeIntervalChange?: (timeInterval: string) => void;
|
||||
closePopover: () => void;
|
||||
onResetChartHeight?: () => void;
|
||||
isPlainRecord?: boolean;
|
||||
}) {
|
||||
if (!chart) {
|
||||
return [];
|
||||
|
@ -71,16 +73,18 @@ export function useChartPanels({
|
|||
});
|
||||
}
|
||||
|
||||
mainPanelItems.push({
|
||||
name: i18n.translate('unifiedHistogram.timeIntervalWithValue', {
|
||||
defaultMessage: 'Time interval: {timeInterval}',
|
||||
values: {
|
||||
timeInterval: intervalDisplay,
|
||||
},
|
||||
}),
|
||||
panel: 1,
|
||||
'data-test-subj': 'unifiedHistogramTimeIntervalPanel',
|
||||
});
|
||||
if (!isPlainRecord) {
|
||||
mainPanelItems.push({
|
||||
name: i18n.translate('unifiedHistogram.timeIntervalWithValue', {
|
||||
defaultMessage: 'Time interval: {timeInterval}',
|
||||
values: {
|
||||
timeInterval: intervalDisplay,
|
||||
},
|
||||
}),
|
||||
panel: 1,
|
||||
'data-test-subj': 'unifiedHistogramTimeIntervalPanel',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const panels: EuiContextMenuPanelDescriptor[] = [
|
||||
|
@ -92,7 +96,7 @@ export function useChartPanels({
|
|||
items: mainPanelItems,
|
||||
},
|
||||
];
|
||||
if (!chart.hidden) {
|
||||
if (!chart.hidden && !isPlainRecord) {
|
||||
panels.push({
|
||||
id: 1,
|
||||
initialFocusedItemIndex: selectedOptionIdx > -1 ? selectedOptionIdx : 0,
|
||||
|
|
|
@ -11,6 +11,7 @@ import { act } from 'react-test-renderer';
|
|||
import { Subject } from 'rxjs';
|
||||
import type { UnifiedHistogramInputMessage } from '../../types';
|
||||
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
|
||||
import { currentSuggestionMock } from '../../__mocks__/suggestions';
|
||||
import { getLensAttributes } from '../utils/get_lens_attributes';
|
||||
import { getLensProps, useLensProps } from './use_lens_props';
|
||||
|
||||
|
@ -29,6 +30,45 @@ describe('useLensProps', () => {
|
|||
dataView: dataViewWithTimefieldMock,
|
||||
timeInterval: 'auto',
|
||||
breakdownField: dataViewWithTimefieldMock.getFieldByName('extension'),
|
||||
suggestion: undefined,
|
||||
});
|
||||
const lensProps = renderHook(() => {
|
||||
return useLensProps({
|
||||
request: {
|
||||
searchSessionId: 'id',
|
||||
adapter: undefined,
|
||||
},
|
||||
getTimeRange,
|
||||
refetch$,
|
||||
attributes,
|
||||
onLoad,
|
||||
});
|
||||
});
|
||||
expect(lensProps.result.current).toEqual(
|
||||
getLensProps({
|
||||
searchSessionId: 'id',
|
||||
getTimeRange,
|
||||
attributes,
|
||||
onLoad,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return lens props for text based languages', () => {
|
||||
const getTimeRange = jest.fn();
|
||||
const refetch$ = new Subject<UnifiedHistogramInputMessage>();
|
||||
const onLoad = jest.fn();
|
||||
const attributes = getLensAttributes({
|
||||
title: 'test',
|
||||
filters: [],
|
||||
query: {
|
||||
language: 'kuery',
|
||||
query: '',
|
||||
},
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
timeInterval: 'auto',
|
||||
breakdownField: dataViewWithTimefieldMock.getFieldByName('extension'),
|
||||
suggestion: currentSuggestionMock,
|
||||
});
|
||||
const lensProps = renderHook(() => {
|
||||
return useLensProps({
|
||||
|
@ -73,6 +113,7 @@ describe('useLensProps', () => {
|
|||
dataView: dataViewWithTimefieldMock,
|
||||
timeInterval: 'auto',
|
||||
breakdownField: dataViewWithTimefieldMock.getFieldByName('extension'),
|
||||
suggestion: undefined,
|
||||
}),
|
||||
onLoad,
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import type { Suggestion } from '@kbn/lens-plugin/public';
|
||||
import { cloneDeep, isEqual } from 'lodash';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { filter, share, tap } from 'rxjs';
|
||||
|
@ -29,6 +30,7 @@ export const useRefetch = ({
|
|||
filters,
|
||||
query,
|
||||
relativeTimeRange,
|
||||
currentSuggestion,
|
||||
disableAutoFetching,
|
||||
input$,
|
||||
beforeRefetch,
|
||||
|
@ -42,6 +44,7 @@ export const useRefetch = ({
|
|||
filters: Filter[];
|
||||
query: Query | AggregateQuery;
|
||||
relativeTimeRange: TimeRange;
|
||||
currentSuggestion?: Suggestion;
|
||||
disableAutoFetching?: boolean;
|
||||
input$: UnifiedHistogramInput$;
|
||||
beforeRefetch: () => void;
|
||||
|
@ -67,6 +70,7 @@ export const useRefetch = ({
|
|||
filters,
|
||||
query,
|
||||
relativeTimeRange,
|
||||
currentSuggestion,
|
||||
});
|
||||
|
||||
if (!isEqual(refetchDeps.current, newRefetchDeps)) {
|
||||
|
@ -80,6 +84,7 @@ export const useRefetch = ({
|
|||
breakdown,
|
||||
chart,
|
||||
chartVisible,
|
||||
currentSuggestion,
|
||||
dataView,
|
||||
disableAutoFetching,
|
||||
filters,
|
||||
|
@ -111,6 +116,7 @@ const getRefetchDeps = ({
|
|||
filters,
|
||||
query,
|
||||
relativeTimeRange,
|
||||
currentSuggestion,
|
||||
}: {
|
||||
dataView: DataView;
|
||||
request: UnifiedHistogramRequestContext | undefined;
|
||||
|
@ -121,6 +127,7 @@ const getRefetchDeps = ({
|
|||
filters: Filter[];
|
||||
query: Query | AggregateQuery;
|
||||
relativeTimeRange: TimeRange;
|
||||
currentSuggestion?: Suggestion;
|
||||
}) =>
|
||||
cloneDeep([
|
||||
dataView.id,
|
||||
|
@ -133,4 +140,5 @@ const getRefetchDeps = ({
|
|||
filters,
|
||||
query,
|
||||
relativeTimeRange,
|
||||
currentSuggestion?.visualizationId,
|
||||
]);
|
||||
|
|
|
@ -236,4 +236,35 @@ describe('useTimeRange', () => {
|
|||
</EuiFlexGroup>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should render time range display and no interval for text based languages', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTimeRange({
|
||||
uiSettings,
|
||||
bucketInterval,
|
||||
timeRange,
|
||||
timeInterval,
|
||||
isPlainRecord: true,
|
||||
})
|
||||
);
|
||||
expect(result.current.timeRangeDisplay).toMatchInlineSnapshot(`
|
||||
<EuiText
|
||||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1vgo99t",
|
||||
"next": undefined,
|
||||
"styles": "
|
||||
padding: 0 8px 0 8px;
|
||||
",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
size="xs"
|
||||
textAlign="center"
|
||||
>
|
||||
2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z
|
||||
</EuiText>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,11 +20,13 @@ export const useTimeRange = ({
|
|||
bucketInterval,
|
||||
timeRange: { from, to },
|
||||
timeInterval,
|
||||
isPlainRecord,
|
||||
}: {
|
||||
uiSettings: IUiSettingsClient;
|
||||
bucketInterval?: UnifiedHistogramBucketInterval;
|
||||
timeRange: TimeRange;
|
||||
timeInterval?: string;
|
||||
isPlainRecord?: boolean;
|
||||
}) => {
|
||||
const dateFormat = useMemo(() => uiSettings.get('dateFormat'), [uiSettings]);
|
||||
|
||||
|
@ -47,26 +49,28 @@ export const useTimeRange = ({
|
|||
to: dateMath.parse(to, { roundUp: true }),
|
||||
};
|
||||
|
||||
const intervalText = i18n.translate('unifiedHistogram.histogramTimeRangeIntervalDescription', {
|
||||
defaultMessage: '(interval: {value})',
|
||||
values: {
|
||||
value: `${
|
||||
timeInterval === 'auto'
|
||||
? `${i18n.translate('unifiedHistogram.histogramTimeRangeIntervalAuto', {
|
||||
defaultMessage: 'Auto',
|
||||
})} - `
|
||||
: ''
|
||||
}${
|
||||
bucketInterval?.description ??
|
||||
i18n.translate('unifiedHistogram.histogramTimeRangeIntervalLoading', {
|
||||
defaultMessage: 'Loading',
|
||||
})
|
||||
}`,
|
||||
},
|
||||
});
|
||||
const intervalText = Boolean(isPlainRecord)
|
||||
? ''
|
||||
: i18n.translate('unifiedHistogram.histogramTimeRangeIntervalDescription', {
|
||||
defaultMessage: '(interval: {value})',
|
||||
values: {
|
||||
value: `${
|
||||
timeInterval === 'auto'
|
||||
? `${i18n.translate('unifiedHistogram.histogramTimeRangeIntervalAuto', {
|
||||
defaultMessage: 'Auto',
|
||||
})} - `
|
||||
: ''
|
||||
}${
|
||||
bucketInterval?.description ??
|
||||
i18n.translate('unifiedHistogram.histogramTimeRangeIntervalLoading', {
|
||||
defaultMessage: 'Loading',
|
||||
})
|
||||
}`,
|
||||
},
|
||||
});
|
||||
|
||||
return `${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${intervalText}`;
|
||||
}, [bucketInterval, from, timeInterval, to, toMoment]);
|
||||
}, [bucketInterval?.description, from, isPlainRecord, timeInterval, to, toMoment]);
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const timeRangeCss = css`
|
||||
|
|
|
@ -18,6 +18,7 @@ import { of, Subject, throwError } from 'rxjs';
|
|||
import { waitFor } from '@testing-library/dom';
|
||||
import { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||
import { DataViewType, SearchSourceSearchOptions } from '@kbn/data-plugin/common';
|
||||
import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks';
|
||||
|
||||
jest.mock('react-use/lib/useDebounce', () => {
|
||||
return jest.fn((...args) => {
|
||||
|
@ -29,7 +30,20 @@ describe('useTotalHits', () => {
|
|||
const timeRange = { from: 'now-15m', to: 'now' };
|
||||
const refetch$: UnifiedHistogramInput$ = new Subject();
|
||||
const getDeps = () => ({
|
||||
services: { data: dataPluginMock.createStartContract() } as any,
|
||||
services: {
|
||||
data: dataPluginMock.createStartContract(),
|
||||
expressions: {
|
||||
...expressionsPluginMock.createStartContract(),
|
||||
run: jest.fn(() =>
|
||||
of({
|
||||
partial: false,
|
||||
result: {
|
||||
rows: [{}, {}, {}],
|
||||
},
|
||||
})
|
||||
),
|
||||
},
|
||||
} as any,
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
request: undefined,
|
||||
hits: {
|
||||
|
@ -103,6 +117,22 @@ describe('useTotalHits', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should fetch total hits if isPlainRecord is true', async () => {
|
||||
const onTotalHitsChange = jest.fn();
|
||||
const deps = {
|
||||
...getDeps(),
|
||||
isPlainRecord: true,
|
||||
onTotalHitsChange,
|
||||
query: { sql: 'select * from test' },
|
||||
};
|
||||
renderHook(() => useTotalHits(deps));
|
||||
expect(onTotalHitsChange).toBeCalledTimes(1);
|
||||
await waitFor(() => {
|
||||
expect(deps.services.expressions.run).toBeCalledTimes(1);
|
||||
expect(onTotalHitsChange).toBeCalledWith(UnifiedHistogramFetchStatus.complete, 3);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fetch total hits if chartVisible is true', async () => {
|
||||
const onTotalHitsChange = jest.fn();
|
||||
const fetchSpy = jest.spyOn(searchSourceInstanceMock, 'fetch$').mockClear();
|
||||
|
|
|
@ -6,13 +6,15 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { textBasedQueryStateToAstWithValidation } from '@kbn/data-plugin/common';
|
||||
import { isCompleteResponse } from '@kbn/data-plugin/public';
|
||||
import { DataView, DataViewType } from '@kbn/data-views-plugin/public';
|
||||
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import { Datatable, isExpressionValueError } from '@kbn/expressions-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { MutableRefObject, useEffect, useRef } from 'react';
|
||||
import useEffectOnce from 'react-use/lib/useEffectOnce';
|
||||
import { catchError, filter, lastValueFrom, map, Observable, of } from 'rxjs';
|
||||
import { catchError, filter, lastValueFrom, map, Observable, of, pluck } from 'rxjs';
|
||||
import {
|
||||
UnifiedHistogramFetchStatus,
|
||||
UnifiedHistogramHitsContext,
|
||||
|
@ -33,6 +35,7 @@ export const useTotalHits = ({
|
|||
getTimeRange,
|
||||
refetch$,
|
||||
onTotalHitsChange,
|
||||
isPlainRecord,
|
||||
}: {
|
||||
services: UnifiedHistogramServices;
|
||||
dataView: DataView;
|
||||
|
@ -44,6 +47,7 @@ export const useTotalHits = ({
|
|||
getTimeRange: () => TimeRange;
|
||||
refetch$: Observable<UnifiedHistogramInputMessage>;
|
||||
onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void;
|
||||
isPlainRecord?: boolean;
|
||||
}) => {
|
||||
const abortController = useRef<AbortController>();
|
||||
const fetch = useStableCallback(() => {
|
||||
|
@ -58,6 +62,7 @@ export const useTotalHits = ({
|
|||
query,
|
||||
timeRange: getTimeRange(),
|
||||
onTotalHitsChange,
|
||||
isPlainRecord,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -70,16 +75,17 @@ export const useTotalHits = ({
|
|||
};
|
||||
|
||||
const fetchTotalHits = async ({
|
||||
services: { data },
|
||||
services,
|
||||
abortController,
|
||||
dataView,
|
||||
request,
|
||||
hits,
|
||||
chartVisible,
|
||||
filters: originalFilters,
|
||||
filters,
|
||||
query,
|
||||
timeRange,
|
||||
onTotalHitsChange,
|
||||
isPlainRecord,
|
||||
}: {
|
||||
services: UnifiedHistogramServices;
|
||||
abortController: MutableRefObject<AbortController | undefined>;
|
||||
|
@ -91,6 +97,7 @@ const fetchTotalHits = async ({
|
|||
query: Query | AggregateQuery;
|
||||
timeRange: TimeRange;
|
||||
onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void;
|
||||
isPlainRecord?: boolean;
|
||||
}) => {
|
||||
abortController.current?.abort();
|
||||
abortController.current = undefined;
|
||||
|
@ -103,6 +110,53 @@ const fetchTotalHits = async ({
|
|||
|
||||
onTotalHitsChange?.(UnifiedHistogramFetchStatus.loading, hits.total);
|
||||
|
||||
const newAbortController = new AbortController();
|
||||
|
||||
abortController.current = newAbortController;
|
||||
|
||||
const response = isPlainRecord
|
||||
? await fetchTotalHitsTextBased({
|
||||
services,
|
||||
abortController: newAbortController,
|
||||
dataView,
|
||||
request,
|
||||
query,
|
||||
timeRange,
|
||||
})
|
||||
: await fetchTotalHitsSearchSource({
|
||||
services,
|
||||
abortController: newAbortController,
|
||||
dataView,
|
||||
request,
|
||||
filters,
|
||||
query,
|
||||
timeRange,
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
onTotalHitsChange?.(response.resultStatus, response.result);
|
||||
};
|
||||
|
||||
const fetchTotalHitsSearchSource = async ({
|
||||
services: { data },
|
||||
abortController,
|
||||
dataView,
|
||||
request,
|
||||
filters: originalFilters,
|
||||
query,
|
||||
timeRange,
|
||||
}: {
|
||||
services: UnifiedHistogramServices;
|
||||
abortController: AbortController;
|
||||
dataView: DataView;
|
||||
request: UnifiedHistogramRequestContext | undefined;
|
||||
filters: Filter[];
|
||||
query: Query | AggregateQuery;
|
||||
timeRange: TimeRange;
|
||||
}) => {
|
||||
const searchSource = data.search.searchSource.createEmpty();
|
||||
|
||||
searchSource
|
||||
|
@ -131,8 +185,6 @@ const fetchTotalHits = async ({
|
|||
|
||||
searchSource.setField('filter', filters);
|
||||
|
||||
abortController.current = new AbortController();
|
||||
|
||||
// Let the consumer inspect the request if they want to track it
|
||||
const inspector = request?.adapter
|
||||
? {
|
||||
|
@ -150,7 +202,7 @@ const fetchTotalHits = async ({
|
|||
.fetch$({
|
||||
inspector,
|
||||
sessionId: request?.searchSessionId,
|
||||
abortSignal: abortController.current.signal,
|
||||
abortSignal: abortController.signal,
|
||||
executionContext: {
|
||||
description: 'fetch total hits',
|
||||
},
|
||||
|
@ -168,5 +220,63 @@ const fetchTotalHits = async ({
|
|||
? UnifiedHistogramFetchStatus.error
|
||||
: UnifiedHistogramFetchStatus.complete;
|
||||
|
||||
onTotalHitsChange?.(resultStatus, result);
|
||||
return { resultStatus, result };
|
||||
};
|
||||
|
||||
const fetchTotalHitsTextBased = async ({
|
||||
services: { expressions },
|
||||
abortController,
|
||||
dataView,
|
||||
request,
|
||||
query,
|
||||
timeRange,
|
||||
}: {
|
||||
services: UnifiedHistogramServices;
|
||||
abortController: AbortController;
|
||||
dataView: DataView;
|
||||
request: UnifiedHistogramRequestContext | undefined;
|
||||
query: Query | AggregateQuery;
|
||||
timeRange: TimeRange;
|
||||
}) => {
|
||||
const ast = await textBasedQueryStateToAstWithValidation({
|
||||
query,
|
||||
time: timeRange,
|
||||
dataView,
|
||||
});
|
||||
|
||||
if (abortController.signal.aborted) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!ast) {
|
||||
return {
|
||||
resultStatus: UnifiedHistogramFetchStatus.error,
|
||||
result: new Error('Invalid text based query'),
|
||||
};
|
||||
}
|
||||
|
||||
const result = await lastValueFrom(
|
||||
expressions
|
||||
.run<null, Datatable>(ast, null, {
|
||||
inspectorAdapters: { requests: request?.adapter },
|
||||
searchSessionId: request?.searchSessionId,
|
||||
executionContext: {
|
||||
description: 'fetch total hits',
|
||||
},
|
||||
})
|
||||
.pipe(pluck('result'))
|
||||
);
|
||||
|
||||
if (abortController.signal.aborted) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isExpressionValueError(result)) {
|
||||
return {
|
||||
resultStatus: UnifiedHistogramFetchStatus.error,
|
||||
result: new Error(result.error.message),
|
||||
};
|
||||
}
|
||||
|
||||
return { resultStatus: UnifiedHistogramFetchStatus.complete, result: result.rows.length };
|
||||
};
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { EuiComboBox } from '@elastic/eui';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import React from 'react';
|
||||
import { currentSuggestionMock, allSuggestionsMock } from '../__mocks__/suggestions';
|
||||
import { SuggestionSelector } from './suggestion_selector';
|
||||
|
||||
describe('SuggestionSelector', () => {
|
||||
it('should pass the suggestions charts titles to the EuiComboBox', () => {
|
||||
const onSuggestionChange = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<SuggestionSelector
|
||||
suggestions={allSuggestionsMock}
|
||||
activeSuggestion={currentSuggestionMock}
|
||||
onSuggestionChange={onSuggestionChange}
|
||||
/>
|
||||
);
|
||||
const comboBox = wrapper.find(EuiComboBox);
|
||||
expect(comboBox.prop('options')).toEqual(
|
||||
allSuggestionsMock.map((sug) => {
|
||||
return {
|
||||
label: sug.title,
|
||||
value: sug.title,
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass the current suggestion as selectedProps to the EuiComboBox', () => {
|
||||
const onSuggestionChange = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<SuggestionSelector
|
||||
suggestions={allSuggestionsMock}
|
||||
activeSuggestion={currentSuggestionMock}
|
||||
onSuggestionChange={onSuggestionChange}
|
||||
/>
|
||||
);
|
||||
const comboBox = wrapper.find(EuiComboBox);
|
||||
expect(comboBox.prop('selectedOptions')).toEqual([
|
||||
{ label: currentSuggestionMock.title, value: currentSuggestionMock.title },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call onSuggestionChange when the user selects another suggestion', () => {
|
||||
const onSuggestionChange = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<SuggestionSelector
|
||||
suggestions={allSuggestionsMock}
|
||||
activeSuggestion={currentSuggestionMock}
|
||||
onSuggestionChange={onSuggestionChange}
|
||||
/>
|
||||
);
|
||||
const comboBox = wrapper.find(EuiComboBox);
|
||||
const selectedSuggestion = allSuggestionsMock.find((sug) => sug.title === 'Donut')!;
|
||||
comboBox.prop('onChange')!([
|
||||
{ label: selectedSuggestion.title, value: selectedSuggestion.title },
|
||||
]);
|
||||
expect(onSuggestionChange).toHaveBeenCalledWith(selectedSuggestion);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiComboBox,
|
||||
EuiFlexItem,
|
||||
EuiToolTip,
|
||||
useEuiTheme,
|
||||
EuiFlexGroup,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import type { Suggestion } from '@kbn/lens-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
export interface SuggestionSelectorProps {
|
||||
suggestions: Suggestion[];
|
||||
activeSuggestion?: Suggestion;
|
||||
onSuggestionChange?: (sug: Suggestion | undefined) => void;
|
||||
}
|
||||
|
||||
export const SuggestionSelector = ({
|
||||
suggestions,
|
||||
activeSuggestion,
|
||||
onSuggestionChange,
|
||||
}: SuggestionSelectorProps) => {
|
||||
const suggestionOptions = suggestions.map((sug) => {
|
||||
return {
|
||||
label: sug.title,
|
||||
value: sug.title,
|
||||
};
|
||||
});
|
||||
|
||||
const selectedSuggestion = activeSuggestion
|
||||
? [
|
||||
{
|
||||
label: activeSuggestion.title,
|
||||
value: activeSuggestion.title,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const onSelectionChange = useCallback(
|
||||
(newOptions) => {
|
||||
const suggestion = newOptions.length
|
||||
? suggestions.find((current) => current.title === newOptions[0].value)
|
||||
: activeSuggestion;
|
||||
|
||||
onSuggestionChange?.(suggestion);
|
||||
},
|
||||
[activeSuggestion, onSuggestionChange, suggestions]
|
||||
);
|
||||
|
||||
const [suggestionsPopoverDisabled, setSuggestionaPopoverDisabled] = useState(false);
|
||||
const disableFieldPopover = useCallback(() => setSuggestionaPopoverDisabled(true), []);
|
||||
const enableFieldPopover = useCallback(
|
||||
() => setTimeout(() => setSuggestionaPopoverDisabled(false)),
|
||||
[]
|
||||
);
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const suggestionComboCss = css`
|
||||
width: 100%;
|
||||
max-width: ${euiTheme.base * 22}px;
|
||||
`;
|
||||
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={suggestionsPopoverDisabled ? undefined : activeSuggestion?.title}
|
||||
anchorProps={{ css: suggestionComboCss }}
|
||||
>
|
||||
<EuiComboBox
|
||||
data-test-subj="unifiedHistogramSuggestionSelector"
|
||||
prepend={i18n.translate('unifiedHistogram.suggestionSelectorLabel', {
|
||||
defaultMessage: 'Visualization',
|
||||
})}
|
||||
placeholder={i18n.translate('unifiedHistogram.suggestionSelectorPlaceholder', {
|
||||
defaultMessage: 'Select visualization',
|
||||
})}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={suggestionOptions}
|
||||
selectedOptions={selectedSuggestion}
|
||||
onChange={onSelectionChange}
|
||||
compressed
|
||||
fullWidth={true}
|
||||
isClearable={false}
|
||||
onFocus={disableFieldPopover}
|
||||
onBlur={enableFieldPopover}
|
||||
renderOption={(option) => {
|
||||
const suggestion = suggestions.find((s) => {
|
||||
return s.title === option.label;
|
||||
});
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={null}>
|
||||
<EuiIcon type={suggestion?.previewIcon ?? 'empty'} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{option.label}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
|
@ -10,6 +10,7 @@ import { getLensAttributes } from './get_lens_attributes';
|
|||
import { AggregateQuery, Filter, FilterStateStore, Query } from '@kbn/es-query';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
|
||||
import { currentSuggestionMock } from '../../__mocks__/suggestions';
|
||||
|
||||
describe('getLensAttributes', () => {
|
||||
const dataView: DataView = dataViewWithTimefieldMock;
|
||||
|
@ -45,7 +46,15 @@ describe('getLensAttributes', () => {
|
|||
it('should return correct attributes', () => {
|
||||
const breakdownField: DataViewField | undefined = undefined;
|
||||
expect(
|
||||
getLensAttributes({ title: 'test', filters, query, dataView, timeInterval, breakdownField })
|
||||
getLensAttributes({
|
||||
title: 'test',
|
||||
filters,
|
||||
query,
|
||||
dataView,
|
||||
timeInterval,
|
||||
breakdownField,
|
||||
suggestion: undefined,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"references": Array [
|
||||
|
@ -185,7 +194,15 @@ describe('getLensAttributes', () => {
|
|||
(f) => f.name === 'extension'
|
||||
);
|
||||
expect(
|
||||
getLensAttributes({ title: 'test', filters, query, dataView, timeInterval, breakdownField })
|
||||
getLensAttributes({
|
||||
title: 'test',
|
||||
filters,
|
||||
query,
|
||||
dataView,
|
||||
timeInterval,
|
||||
breakdownField,
|
||||
suggestion: undefined,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"references": Array [
|
||||
|
@ -343,7 +360,15 @@ describe('getLensAttributes', () => {
|
|||
(f) => f.name === 'scripted'
|
||||
);
|
||||
expect(
|
||||
getLensAttributes({ title: 'test', filters, query, dataView, timeInterval, breakdownField })
|
||||
getLensAttributes({
|
||||
title: 'test',
|
||||
filters,
|
||||
query,
|
||||
dataView,
|
||||
timeInterval,
|
||||
breakdownField,
|
||||
suggestion: undefined,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"references": Array [
|
||||
|
@ -477,4 +502,209 @@ describe('getLensAttributes', () => {
|
|||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should return correct attributes for text based languages', () => {
|
||||
expect(
|
||||
getLensAttributes({
|
||||
title: 'test',
|
||||
filters,
|
||||
query,
|
||||
dataView,
|
||||
timeInterval,
|
||||
breakdownField: undefined,
|
||||
suggestion: currentSuggestionMock,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "index-pattern-with-timefield-id",
|
||||
"name": "indexpattern-datasource-current-indexpattern",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"id": "index-pattern-with-timefield-id",
|
||||
"name": "indexpattern-datasource-layer-unifiedHistogram",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"state": Object {
|
||||
"datasourceStates": Object {
|
||||
"textBased": Object {
|
||||
"fieldList": Array [],
|
||||
"indexPatternRefs": Array [],
|
||||
"initialContext": Object {
|
||||
"contextualFields": Array [
|
||||
"Dest",
|
||||
"AvgTicketPrice",
|
||||
],
|
||||
"dataViewSpec": Object {
|
||||
"allowNoIndex": false,
|
||||
"fields": Object {
|
||||
"AvgTicketPrice": Object {
|
||||
"aggregatable": true,
|
||||
"count": 0,
|
||||
"esTypes": Array [
|
||||
"float",
|
||||
],
|
||||
"format": Object {
|
||||
"id": "number",
|
||||
"params": Object {
|
||||
"pattern": "$0,0.[00]",
|
||||
},
|
||||
},
|
||||
"isMapped": true,
|
||||
"name": "AvgTicketPrice",
|
||||
"readFromDocValues": true,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"shortDotsEnable": false,
|
||||
"type": "number",
|
||||
},
|
||||
"Dest": Object {
|
||||
"aggregatable": true,
|
||||
"count": 0,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"format": Object {
|
||||
"id": "string",
|
||||
},
|
||||
"isMapped": true,
|
||||
"name": "Dest",
|
||||
"readFromDocValues": true,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"shortDotsEnable": false,
|
||||
"type": "string",
|
||||
},
|
||||
"timestamp": Object {
|
||||
"aggregatable": true,
|
||||
"count": 0,
|
||||
"esTypes": Array [
|
||||
"date",
|
||||
],
|
||||
"format": Object {
|
||||
"id": "date",
|
||||
},
|
||||
"isMapped": true,
|
||||
"name": "timestamp",
|
||||
"readFromDocValues": true,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"shortDotsEnable": false,
|
||||
"type": "date",
|
||||
},
|
||||
},
|
||||
"id": "d3d7af60-4c81-11e8-b3d7-01146121b73d",
|
||||
"name": "Kibana Sample Data Flights",
|
||||
"sourceFilters": Array [],
|
||||
"timeFieldName": "timestamp",
|
||||
"title": "kibana_sample_data_flights",
|
||||
"version": "WzM1ODA3LDFd",
|
||||
},
|
||||
"fieldName": "",
|
||||
"query": Object {
|
||||
"sql": "SELECT Dest, AvgTicketPrice FROM \\"kibana_sample_data_flights\\"",
|
||||
},
|
||||
},
|
||||
"layers": Object {
|
||||
"46aa21fa-b747-4543-bf90-0b40007c546d": Object {
|
||||
"allColumns": Array [
|
||||
Object {
|
||||
"columnId": "81e332d6-ee37-42a8-a646-cea4fc75d2d3",
|
||||
"fieldName": "Dest",
|
||||
"meta": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"columnId": "5b9b8b76-0836-4a12-b9c0-980c9900502f",
|
||||
"fieldName": "AvgTicketPrice",
|
||||
"meta": Object {
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
],
|
||||
"columns": Array [
|
||||
Object {
|
||||
"columnId": "81e332d6-ee37-42a8-a646-cea4fc75d2d3",
|
||||
"fieldName": "Dest",
|
||||
"meta": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"columnId": "5b9b8b76-0836-4a12-b9c0-980c9900502f",
|
||||
"fieldName": "AvgTicketPrice",
|
||||
"meta": Object {
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
],
|
||||
"index": "d3d7af60-4c81-11e8-b3d7-01146121b73d",
|
||||
"query": Object {
|
||||
"sql": "SELECT Dest, AvgTicketPrice FROM \\"kibana_sample_data_flights\\"",
|
||||
},
|
||||
"timeField": "timestamp",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"filters": Array [
|
||||
Object {
|
||||
"$state": Object {
|
||||
"store": "appState",
|
||||
},
|
||||
"meta": Object {
|
||||
"alias": null,
|
||||
"disabled": false,
|
||||
"index": "index-pattern-with-timefield-id",
|
||||
"key": "extension",
|
||||
"negate": false,
|
||||
"params": Object {
|
||||
"query": "js",
|
||||
},
|
||||
"type": "phrase",
|
||||
},
|
||||
"query": Object {
|
||||
"match": Object {
|
||||
"extension": Object {
|
||||
"query": "js",
|
||||
"type": "phrase",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"query": Object {
|
||||
"language": "kuery",
|
||||
"query": "extension : css",
|
||||
},
|
||||
"visualization": Object {
|
||||
"gridConfig": Object {
|
||||
"isCellLabelVisible": false,
|
||||
"isXAxisLabelVisible": true,
|
||||
"isXAxisTitleVisible": false,
|
||||
"isYAxisLabelVisible": true,
|
||||
"isYAxisTitleVisible": false,
|
||||
"type": "heatmap_grid",
|
||||
},
|
||||
"layerId": "46aa21fa-b747-4543-bf90-0b40007c546d",
|
||||
"layerType": "data",
|
||||
"legend": Object {
|
||||
"isVisible": true,
|
||||
"position": "right",
|
||||
"type": "heatmap_legend",
|
||||
},
|
||||
"shape": "heatmap",
|
||||
"valueAccessor": "5b9b8b76-0836-4a12-b9c0-980c9900502f",
|
||||
"xAccessor": "81e332d6-ee37-42a8-a646-cea4fc75d2d3",
|
||||
},
|
||||
},
|
||||
"title": "test",
|
||||
"visualizationType": "lnsHeatmap",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
|||
GenericIndexPatternColumn,
|
||||
TermsIndexPatternColumn,
|
||||
TypedLensByValueInput,
|
||||
Suggestion,
|
||||
} from '@kbn/lens-plugin/public';
|
||||
import { fieldSupportsBreakdown } from './field_supports_breakdown';
|
||||
|
||||
|
@ -25,6 +26,7 @@ export const getLensAttributes = ({
|
|||
dataView,
|
||||
timeInterval,
|
||||
breakdownField,
|
||||
suggestion,
|
||||
}: {
|
||||
title?: string;
|
||||
filters: Filter[];
|
||||
|
@ -32,6 +34,7 @@ export const getLensAttributes = ({
|
|||
dataView: DataView;
|
||||
timeInterval: string | undefined;
|
||||
breakdownField: DataViewField | undefined;
|
||||
suggestion: Suggestion | undefined;
|
||||
}) => {
|
||||
const showBreakdown = breakdownField && fieldSupportsBreakdown(breakdownField);
|
||||
|
||||
|
@ -103,35 +106,27 @@ export const getLensAttributes = ({
|
|||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title:
|
||||
title ??
|
||||
i18n.translate('unifiedHistogram.lensTitle', {
|
||||
defaultMessage: 'Edit visualization',
|
||||
}),
|
||||
references: [
|
||||
{
|
||||
id: dataView.id ?? '',
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: dataView.id ?? '',
|
||||
name: 'indexpattern-datasource-layer-unifiedHistogram',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
state: {
|
||||
datasourceStates: {
|
||||
formBased: {
|
||||
layers: {
|
||||
unifiedHistogram: { columnOrder, columns },
|
||||
const suggestionDatasourceState = Object.assign({}, suggestion?.datasourceState);
|
||||
const suggestionVisualizationState = Object.assign({}, suggestion?.visualizationState);
|
||||
const datasourceStates =
|
||||
suggestion && suggestion.datasourceState
|
||||
? {
|
||||
[suggestion.datasourceId!]: {
|
||||
...suggestionDatasourceState,
|
||||
},
|
||||
},
|
||||
},
|
||||
filters,
|
||||
query: 'language' in query ? query : { language: 'kuery', query: '' },
|
||||
visualization: {
|
||||
}
|
||||
: {
|
||||
formBased: {
|
||||
layers: {
|
||||
unifiedHistogram: { columnOrder, columns },
|
||||
},
|
||||
},
|
||||
};
|
||||
const visualization = suggestion
|
||||
? {
|
||||
...suggestionVisualizationState,
|
||||
}
|
||||
: {
|
||||
layers: [
|
||||
{
|
||||
accessors: ['count_column'],
|
||||
|
@ -173,8 +168,32 @@ export const getLensAttributes = ({
|
|||
yLeft: true,
|
||||
yRight: false,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
title:
|
||||
title ??
|
||||
i18n.translate('unifiedHistogram.lensTitle', {
|
||||
defaultMessage: 'Edit visualization',
|
||||
}),
|
||||
references: [
|
||||
{
|
||||
id: dataView.id ?? '',
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: dataView.id ?? '',
|
||||
name: 'indexpattern-datasource-layer-unifiedHistogram',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
state: {
|
||||
datasourceStates,
|
||||
filters,
|
||||
query,
|
||||
visualization,
|
||||
},
|
||||
visualizationType: 'lnsXY',
|
||||
visualizationType: suggestion ? suggestion.visualizationId : 'lnsXY',
|
||||
} as TypedLensByValueInput['attributes'];
|
||||
};
|
||||
|
|
|
@ -31,6 +31,8 @@ describe('UnifiedHistogramContainer', () => {
|
|||
topPanelHeight: 100,
|
||||
totalHitsStatus: UnifiedHistogramFetchStatus.uninitialized,
|
||||
totalHitsResult: undefined,
|
||||
columns: [],
|
||||
currentSuggestion: undefined,
|
||||
};
|
||||
|
||||
it('should set ref', () => {
|
||||
|
@ -61,7 +63,7 @@ describe('UnifiedHistogramContainer', () => {
|
|||
const component = mountWithIntl(
|
||||
<UnifiedHistogramContainer ref={setApi} resizeRef={{ current: null }} />
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
|
||||
expect(component.update().isEmptyRender()).toBe(false);
|
||||
});
|
||||
|
||||
|
@ -80,6 +82,7 @@ describe('UnifiedHistogramContainer', () => {
|
|||
});
|
||||
}
|
||||
});
|
||||
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
|
||||
expect(api?.initialized).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import React, { forwardRef, useImperativeHandle, useMemo, useState } from 'react';
|
||||
import { Subject } from 'rxjs';
|
||||
import { pick } from 'lodash';
|
||||
import { LensSuggestionsApi } from '@kbn/lens-plugin/public';
|
||||
import { UnifiedHistogramLayout, UnifiedHistogramLayoutProps } from '../layout';
|
||||
import type { UnifiedHistogramInputMessage } from '../types';
|
||||
import {
|
||||
|
@ -19,6 +20,8 @@ import {
|
|||
import { useStateProps } from './hooks/use_state_props';
|
||||
import { useStateSelector } from './utils/use_state_selector';
|
||||
import {
|
||||
columnsSelector,
|
||||
currentSuggestionSelector,
|
||||
dataViewSelector,
|
||||
filtersSelector,
|
||||
querySelector,
|
||||
|
@ -81,6 +84,7 @@ export type UnifiedHistogramInitializedApi = {
|
|||
| 'setChartHidden'
|
||||
| 'setTopPanelHeight'
|
||||
| 'setBreakdownField'
|
||||
| 'setColumns'
|
||||
| 'setTimeInterval'
|
||||
| 'setRequestParams'
|
||||
| 'setTotalHits'
|
||||
|
@ -98,6 +102,7 @@ export const UnifiedHistogramContainer = forwardRef<
|
|||
const [initialized, setInitialized] = useState(false);
|
||||
const [layoutProps, setLayoutProps] = useState<LayoutProps>();
|
||||
const [stateService, setStateService] = useState<UnifiedHistogramStateService>();
|
||||
const [lensSuggestionsApi, setLensSuggestionsApi] = useState<LensSuggestionsApi>();
|
||||
const [input$] = useState(() => new Subject<UnifiedHistogramInputMessage>());
|
||||
const api = useMemo<UnifiedHistogramApi>(
|
||||
() => ({
|
||||
|
@ -111,6 +116,12 @@ export const UnifiedHistogramContainer = forwardRef<
|
|||
getRelativeTimeRange,
|
||||
} = options;
|
||||
|
||||
// API helpers are loaded async from Lens
|
||||
(async () => {
|
||||
const apiHelper = await services.lens.stateHelperApi();
|
||||
setLensSuggestionsApi(() => apiHelper.suggestions);
|
||||
})();
|
||||
|
||||
setLayoutProps({
|
||||
services,
|
||||
disableAutoFetching,
|
||||
|
@ -130,6 +141,7 @@ export const UnifiedHistogramContainer = forwardRef<
|
|||
'setChartHidden',
|
||||
'setTopPanelHeight',
|
||||
'setBreakdownField',
|
||||
'setColumns',
|
||||
'setTimeInterval',
|
||||
'setRequestParams',
|
||||
'setTotalHits'
|
||||
|
@ -146,10 +158,12 @@ export const UnifiedHistogramContainer = forwardRef<
|
|||
const query = useStateSelector(stateService?.state$, querySelector);
|
||||
const filters = useStateSelector(stateService?.state$, filtersSelector);
|
||||
const timeRange = useStateSelector(stateService?.state$, timeRangeSelector);
|
||||
const columns = useStateSelector(stateService?.state$, columnsSelector);
|
||||
const currentSuggestion = useStateSelector(stateService?.state$, currentSuggestionSelector);
|
||||
const topPanelHeight = useStateSelector(stateService?.state$, topPanelHeightSelector);
|
||||
|
||||
// Don't render anything until the container is initialized
|
||||
if (!layoutProps || !dataView) {
|
||||
if (!layoutProps || !dataView || !lensSuggestionsApi) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -162,8 +176,11 @@ export const UnifiedHistogramContainer = forwardRef<
|
|||
query={query}
|
||||
filters={filters}
|
||||
timeRange={timeRange}
|
||||
columns={columns}
|
||||
currentSuggestion={currentSuggestion}
|
||||
topPanelHeight={topPanelHeight}
|
||||
input$={input$}
|
||||
lensSuggestionsApi={lensSuggestionsApi}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -8,11 +8,13 @@
|
|||
|
||||
import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/common';
|
||||
import { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||
import { Suggestion } from '@kbn/lens-plugin/public';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { act } from 'react-test-renderer';
|
||||
import { UnifiedHistogramFetchStatus } from '../../types';
|
||||
import { dataViewMock } from '../../__mocks__/data_view';
|
||||
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
|
||||
import { currentSuggestionMock } from '../../__mocks__/suggestions';
|
||||
import { unifiedHistogramServicesMock } from '../../__mocks__/services';
|
||||
import {
|
||||
createStateService,
|
||||
|
@ -36,6 +38,8 @@ describe('useStateProps', () => {
|
|||
topPanelHeight: 100,
|
||||
totalHitsStatus: UnifiedHistogramFetchStatus.uninitialized,
|
||||
totalHitsResult: undefined,
|
||||
columns: [],
|
||||
currentSuggestion: undefined,
|
||||
};
|
||||
|
||||
const getStateService = (options: Omit<UnifiedHistogramStateOptions, 'services'>) => {
|
||||
|
@ -50,6 +54,7 @@ describe('useStateProps', () => {
|
|||
jest.spyOn(stateService, 'setRequestParams');
|
||||
jest.spyOn(stateService, 'setLensRequestAdapter');
|
||||
jest.spyOn(stateService, 'setTotalHits');
|
||||
jest.spyOn(stateService, 'setCurrentSuggestion');
|
||||
return stateService;
|
||||
};
|
||||
|
||||
|
@ -76,9 +81,11 @@ describe('useStateProps', () => {
|
|||
"status": "uninitialized",
|
||||
"total": undefined,
|
||||
},
|
||||
"isPlainRecord": false,
|
||||
"onBreakdownFieldChange": [Function],
|
||||
"onChartHiddenChange": [Function],
|
||||
"onChartLoad": [Function],
|
||||
"onSuggestionChange": [Function],
|
||||
"onTimeIntervalChange": [Function],
|
||||
"onTopPanelHeightChange": [Function],
|
||||
"onTotalHitsChange": [Function],
|
||||
|
@ -104,11 +111,19 @@ describe('useStateProps', () => {
|
|||
expect(result.current).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"breakdown": undefined,
|
||||
"chart": undefined,
|
||||
"hits": undefined,
|
||||
"chart": Object {
|
||||
"hidden": false,
|
||||
"timeInterval": "auto",
|
||||
},
|
||||
"hits": Object {
|
||||
"status": "uninitialized",
|
||||
"total": undefined,
|
||||
},
|
||||
"isPlainRecord": true,
|
||||
"onBreakdownFieldChange": [Function],
|
||||
"onChartHiddenChange": [Function],
|
||||
"onChartLoad": [Function],
|
||||
"onSuggestionChange": [Function],
|
||||
"onTimeIntervalChange": [Function],
|
||||
"onTopPanelHeightChange": [Function],
|
||||
"onTotalHitsChange": [Function],
|
||||
|
@ -126,6 +141,20 @@ describe('useStateProps', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('should return the correct props when a text based language is used', () => {
|
||||
const stateService = getStateService({
|
||||
initialState: {
|
||||
...initialState,
|
||||
query: { sql: 'SELECT * FROM index' },
|
||||
currentSuggestion: currentSuggestionMock,
|
||||
},
|
||||
});
|
||||
const { result } = renderHook(() => useStateProps(stateService));
|
||||
expect(result.current.chart).toStrictEqual({ hidden: false, timeInterval: 'auto' });
|
||||
expect(result.current.breakdown).toBe(undefined);
|
||||
expect(result.current.isPlainRecord).toBe(true);
|
||||
});
|
||||
|
||||
it('should return the correct props when a rollup data view is used', () => {
|
||||
const stateService = getStateService({
|
||||
initialState: {
|
||||
|
@ -145,9 +174,11 @@ describe('useStateProps', () => {
|
|||
"status": "uninitialized",
|
||||
"total": undefined,
|
||||
},
|
||||
"isPlainRecord": false,
|
||||
"onBreakdownFieldChange": [Function],
|
||||
"onChartHiddenChange": [Function],
|
||||
"onChartLoad": [Function],
|
||||
"onSuggestionChange": [Function],
|
||||
"onTimeIntervalChange": [Function],
|
||||
"onTopPanelHeightChange": [Function],
|
||||
"onTotalHitsChange": [Function],
|
||||
|
@ -178,9 +209,11 @@ describe('useStateProps', () => {
|
|||
"status": "uninitialized",
|
||||
"total": undefined,
|
||||
},
|
||||
"isPlainRecord": false,
|
||||
"onBreakdownFieldChange": [Function],
|
||||
"onChartHiddenChange": [Function],
|
||||
"onChartLoad": [Function],
|
||||
"onSuggestionChange": [Function],
|
||||
"onTimeIntervalChange": [Function],
|
||||
"onTopPanelHeightChange": [Function],
|
||||
"onTotalHitsChange": [Function],
|
||||
|
@ -208,6 +241,7 @@ describe('useStateProps', () => {
|
|||
onChartHiddenChange,
|
||||
onChartLoad,
|
||||
onBreakdownFieldChange,
|
||||
onSuggestionChange,
|
||||
} = result.current;
|
||||
act(() => {
|
||||
onTopPanelHeightChange(200);
|
||||
|
@ -237,6 +271,11 @@ describe('useStateProps', () => {
|
|||
onBreakdownFieldChange({ name: 'field' } as DataViewField);
|
||||
});
|
||||
expect(stateService.setBreakdownField).toHaveBeenLastCalledWith('field');
|
||||
|
||||
act(() => {
|
||||
onSuggestionChange({ title: 'Stacked Bar' } as Suggestion);
|
||||
});
|
||||
expect(stateService.setCurrentSuggestion).toHaveBeenLastCalledWith({ title: 'Stacked Bar' });
|
||||
});
|
||||
|
||||
it('should clear lensRequestAdapter when chart is hidden', () => {
|
||||
|
|
|
@ -40,7 +40,11 @@ export const useStateProps = (stateService: UnifiedHistogramStateService | undef
|
|||
*/
|
||||
|
||||
const isPlainRecord = useMemo(() => {
|
||||
return query && isOfAggregateQueryType(query) && getAggregateQueryMode(query) === 'sql';
|
||||
return (
|
||||
query &&
|
||||
isOfAggregateQueryType(query) &&
|
||||
['sql', 'esql'].some((mode) => mode === getAggregateQueryMode(query))
|
||||
);
|
||||
}, [query]);
|
||||
|
||||
const isTimeBased = useMemo(() => {
|
||||
|
@ -48,7 +52,7 @@ export const useStateProps = (stateService: UnifiedHistogramStateService | undef
|
|||
}, [dataView]);
|
||||
|
||||
const hits = useMemo(() => {
|
||||
if (isPlainRecord || totalHitsResult instanceof Error) {
|
||||
if (totalHitsResult instanceof Error) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
@ -56,10 +60,10 @@ export const useStateProps = (stateService: UnifiedHistogramStateService | undef
|
|||
status: totalHitsStatus,
|
||||
total: totalHitsResult,
|
||||
};
|
||||
}, [isPlainRecord, totalHitsResult, totalHitsStatus]);
|
||||
}, [totalHitsResult, totalHitsStatus]);
|
||||
|
||||
const chart = useMemo(() => {
|
||||
if (isPlainRecord || !isTimeBased) {
|
||||
if (!isTimeBased && !isPlainRecord) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
@ -136,6 +140,13 @@ export const useStateProps = (stateService: UnifiedHistogramStateService | undef
|
|||
[stateService]
|
||||
);
|
||||
|
||||
const onSuggestionChange = useCallback(
|
||||
(suggestion) => {
|
||||
stateService?.setCurrentSuggestion(suggestion);
|
||||
},
|
||||
[stateService]
|
||||
);
|
||||
|
||||
/**
|
||||
* Effects
|
||||
*/
|
||||
|
@ -152,11 +163,13 @@ export const useStateProps = (stateService: UnifiedHistogramStateService | undef
|
|||
chart,
|
||||
breakdown,
|
||||
request,
|
||||
isPlainRecord,
|
||||
onTopPanelHeightChange,
|
||||
onTimeIntervalChange,
|
||||
onTotalHitsChange,
|
||||
onChartHiddenChange,
|
||||
onChartLoad,
|
||||
onBreakdownFieldChange,
|
||||
onSuggestionChange,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -59,6 +59,8 @@ describe('UnifiedHistogramStateService', () => {
|
|||
topPanelHeight: 100,
|
||||
totalHitsStatus: UnifiedHistogramFetchStatus.uninitialized,
|
||||
totalHitsResult: undefined,
|
||||
columns: [],
|
||||
currentSuggestion: undefined,
|
||||
};
|
||||
|
||||
it('should initialize state with default values', () => {
|
||||
|
@ -84,6 +86,9 @@ describe('UnifiedHistogramStateService', () => {
|
|||
topPanelHeight: undefined,
|
||||
totalHitsResult: undefined,
|
||||
totalHitsStatus: UnifiedHistogramFetchStatus.uninitialized,
|
||||
columns: [],
|
||||
currentSuggestion: undefined,
|
||||
allSuggestions: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import type { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||
import type { Suggestion } from '@kbn/lens-plugin/public';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { UnifiedHistogramFetchStatus } from '../..';
|
||||
import type { UnifiedHistogramServices } from '../../types';
|
||||
|
@ -29,6 +30,14 @@ export interface UnifiedHistogramState {
|
|||
* The current field used for the breakdown
|
||||
*/
|
||||
breakdownField: string | undefined;
|
||||
/**
|
||||
* The current selected columns
|
||||
*/
|
||||
columns: string[] | undefined;
|
||||
/**
|
||||
* The current Lens suggestion
|
||||
*/
|
||||
currentSuggestion: Suggestion | undefined;
|
||||
/**
|
||||
* Whether or not the chart is hidden
|
||||
*/
|
||||
|
@ -109,6 +118,14 @@ export interface UnifiedHistogramStateService {
|
|||
* Sets the current chart hidden state
|
||||
*/
|
||||
setChartHidden: (chartHidden: boolean) => void;
|
||||
/**
|
||||
* Sets current Lens suggestion
|
||||
*/
|
||||
setCurrentSuggestion: (suggestion: Suggestion | undefined) => void;
|
||||
/**
|
||||
* Sets columns
|
||||
*/
|
||||
setColumns: (columns: string[] | undefined) => void;
|
||||
/**
|
||||
* Sets the current top panel height
|
||||
*/
|
||||
|
@ -163,7 +180,9 @@ export const createStateService = (
|
|||
const state$ = new BehaviorSubject({
|
||||
breakdownField: initialBreakdownField,
|
||||
chartHidden: initialChartHidden,
|
||||
columns: [],
|
||||
filters: [],
|
||||
currentSuggestion: undefined,
|
||||
lensRequestAdapter: undefined,
|
||||
query: services.data.query.queryString.getDefaultQuery(),
|
||||
requestAdapter: undefined,
|
||||
|
@ -210,6 +229,14 @@ export const createStateService = (
|
|||
updateState({ breakdownField });
|
||||
},
|
||||
|
||||
setCurrentSuggestion: (suggestion: Suggestion | undefined) => {
|
||||
updateState({ currentSuggestion: suggestion });
|
||||
},
|
||||
|
||||
setColumns: (columns: string[] | undefined) => {
|
||||
updateState({ columns });
|
||||
},
|
||||
|
||||
setTimeInterval: (timeInterval: string) => {
|
||||
updateState({ timeInterval });
|
||||
},
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import type { UnifiedHistogramState } from '../services/state_service';
|
||||
|
||||
export const breakdownFieldSelector = (state: UnifiedHistogramState) => state.breakdownField;
|
||||
export const columnsSelector = (state: UnifiedHistogramState) => state.columns;
|
||||
export const chartHiddenSelector = (state: UnifiedHistogramState) => state.chartHidden;
|
||||
export const dataViewSelector = (state: UnifiedHistogramState) => state.dataView;
|
||||
export const filtersSelector = (state: UnifiedHistogramState) => state.filters;
|
||||
|
@ -20,3 +21,4 @@ export const timeRangeSelector = (state: UnifiedHistogramState) => state.timeRan
|
|||
export const topPanelHeightSelector = (state: UnifiedHistogramState) => state.topPanelHeight;
|
||||
export const totalHitsResultSelector = (state: UnifiedHistogramState) => state.totalHitsResult;
|
||||
export const totalHitsStatusSelector = (state: UnifiedHistogramState) => state.totalHitsStatus;
|
||||
export const currentSuggestionSelector = (state: UnifiedHistogramState) => state.currentSuggestion;
|
||||
|
|
|
@ -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 { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { AggregateQuery, isOfAggregateQueryType, Query } from '@kbn/es-query';
|
||||
import { LensSuggestionsApi, Suggestion } from '@kbn/lens-plugin/public';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
export const useLensSuggestions = ({
|
||||
dataView,
|
||||
query,
|
||||
originalSuggestion,
|
||||
isPlainRecord,
|
||||
columns,
|
||||
lensSuggestionsApi,
|
||||
onSuggestionChange,
|
||||
}: {
|
||||
dataView: DataView;
|
||||
query?: Query | AggregateQuery;
|
||||
originalSuggestion?: Suggestion;
|
||||
isPlainRecord?: boolean;
|
||||
columns?: string[];
|
||||
lensSuggestionsApi: LensSuggestionsApi;
|
||||
onSuggestionChange?: (suggestion: Suggestion | undefined) => void;
|
||||
}) => {
|
||||
const suggestions = useMemo(() => {
|
||||
const context = {
|
||||
dataViewSpec: dataView?.toSpec(),
|
||||
fieldName: '',
|
||||
contextualFields: columns,
|
||||
query: query && isOfAggregateQueryType(query) ? query : undefined,
|
||||
};
|
||||
const lensSuggestions = isPlainRecord ? lensSuggestionsApi(context, dataView) : undefined;
|
||||
const firstSuggestion = lensSuggestions?.length ? lensSuggestions[0] : undefined;
|
||||
const restSuggestions = lensSuggestions?.filter((sug) => {
|
||||
return !sug.hide && sug.visualizationId !== 'lnsLegacyMetric';
|
||||
});
|
||||
const firstSuggestionExists = restSuggestions?.find(
|
||||
(sug) => sug.title === firstSuggestion?.title
|
||||
);
|
||||
if (firstSuggestion && !firstSuggestionExists) {
|
||||
restSuggestions?.push(firstSuggestion);
|
||||
}
|
||||
return { firstSuggestion, restSuggestions };
|
||||
}, [columns, dataView, isPlainRecord, lensSuggestionsApi, query]);
|
||||
|
||||
const [allSuggestions, setAllSuggestions] = useState(suggestions.restSuggestions);
|
||||
const currentSuggestion = originalSuggestion ?? suggestions.firstSuggestion;
|
||||
const suggestionDeps = useRef(getSuggestionDeps({ dataView, query, columns }));
|
||||
|
||||
useEffect(() => {
|
||||
const newSuggestionsDeps = getSuggestionDeps({ dataView, query, columns });
|
||||
|
||||
if (!isEqual(suggestionDeps.current, newSuggestionsDeps)) {
|
||||
setAllSuggestions(suggestions.restSuggestions);
|
||||
onSuggestionChange?.(suggestions.firstSuggestion);
|
||||
|
||||
suggestionDeps.current = newSuggestionsDeps;
|
||||
}
|
||||
}, [
|
||||
columns,
|
||||
dataView,
|
||||
onSuggestionChange,
|
||||
query,
|
||||
suggestions.firstSuggestion,
|
||||
suggestions.restSuggestions,
|
||||
]);
|
||||
|
||||
return {
|
||||
allSuggestions,
|
||||
currentSuggestion,
|
||||
suggestionUnsupported:
|
||||
isPlainRecord &&
|
||||
(!currentSuggestion || currentSuggestion?.visualizationId === 'lnsDatatable'),
|
||||
};
|
||||
};
|
||||
|
||||
const getSuggestionDeps = ({
|
||||
dataView,
|
||||
query,
|
||||
columns,
|
||||
}: {
|
||||
dataView: DataView;
|
||||
query?: Query | AggregateQuery;
|
||||
columns?: string[];
|
||||
}) => [dataView.id, columns, query];
|
|
@ -76,6 +76,7 @@ describe('Layout', () => {
|
|||
from: '2020-05-14T11:05:13.590',
|
||||
to: '2020-05-14T11:20:13.590',
|
||||
}}
|
||||
lensSuggestionsApi={jest.fn()}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
*/
|
||||
|
||||
import { EuiSpacer, useEuiTheme, useIsWithinBreakpoints } from '@elastic/eui';
|
||||
import type { PropsWithChildren, ReactElement, RefObject } from 'react';
|
||||
import { PropsWithChildren, ReactElement, RefObject } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
|
||||
import { css } from '@emotion/css';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import type { LensEmbeddableInput } from '@kbn/lens-plugin/public';
|
||||
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import type { LensEmbeddableInput, LensSuggestionsApi, Suggestion } from '@kbn/lens-plugin/public';
|
||||
import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import { Chart } from '../chart';
|
||||
import { Panels, PANELS_MODE } from '../panels';
|
||||
import type {
|
||||
|
@ -26,6 +26,7 @@ import type {
|
|||
UnifiedHistogramChartLoadEvent,
|
||||
UnifiedHistogramInput$,
|
||||
} from '../types';
|
||||
import { useLensSuggestions } from './hooks/use_lens_suggestions';
|
||||
|
||||
export interface UnifiedHistogramLayoutProps extends PropsWithChildren<unknown> {
|
||||
/**
|
||||
|
@ -48,10 +49,22 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren<unknown>
|
|||
* The current filters
|
||||
*/
|
||||
filters?: Filter[];
|
||||
/**
|
||||
* The current Lens suggestion
|
||||
*/
|
||||
currentSuggestion?: Suggestion;
|
||||
/**
|
||||
* Flag that indicates that a text based language is used
|
||||
*/
|
||||
isPlainRecord?: boolean;
|
||||
/**
|
||||
* The current time range
|
||||
*/
|
||||
timeRange?: TimeRange;
|
||||
/**
|
||||
* The current columns
|
||||
*/
|
||||
columns?: string[];
|
||||
/**
|
||||
* Context object for requests made by Unified Histogram components -- optional
|
||||
*/
|
||||
|
@ -96,6 +109,10 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren<unknown>
|
|||
* Input observable
|
||||
*/
|
||||
input$?: UnifiedHistogramInput$;
|
||||
/**
|
||||
* The Lens suggestions API
|
||||
*/
|
||||
lensSuggestionsApi: LensSuggestionsApi;
|
||||
/**
|
||||
* Callback to get the relative time range, useful when passing an absolute time range (e.g. for edit visualization button)
|
||||
*/
|
||||
|
@ -116,6 +133,10 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren<unknown>
|
|||
* Callback to update the breakdown field -- should set {@link UnifiedHistogramBreakdownContext.field} to breakdownField
|
||||
*/
|
||||
onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void;
|
||||
/**
|
||||
* Callback to update the suggested chart
|
||||
*/
|
||||
onSuggestionChange?: (suggestion: Suggestion | undefined) => void;
|
||||
/**
|
||||
* Callback to update the total hits -- should set {@link UnifiedHistogramHitsContext.status} to status
|
||||
* and {@link UnifiedHistogramHitsContext.total} to result
|
||||
|
@ -141,10 +162,13 @@ export const UnifiedHistogramLayout = ({
|
|||
dataView,
|
||||
query,
|
||||
filters,
|
||||
currentSuggestion: originalSuggestion,
|
||||
isPlainRecord,
|
||||
timeRange,
|
||||
columns,
|
||||
request,
|
||||
hits,
|
||||
chart,
|
||||
chart: originalChart,
|
||||
breakdown,
|
||||
resizeRef,
|
||||
topPanelHeight,
|
||||
|
@ -152,18 +176,32 @@ export const UnifiedHistogramLayout = ({
|
|||
disableAutoFetching,
|
||||
disableTriggers,
|
||||
disabledActions,
|
||||
lensSuggestionsApi,
|
||||
input$,
|
||||
getRelativeTimeRange,
|
||||
onTopPanelHeightChange,
|
||||
onChartHiddenChange,
|
||||
onTimeIntervalChange,
|
||||
onBreakdownFieldChange,
|
||||
onSuggestionChange,
|
||||
onTotalHitsChange,
|
||||
onChartLoad,
|
||||
onFilter,
|
||||
onBrushEnd,
|
||||
children,
|
||||
}: UnifiedHistogramLayoutProps) => {
|
||||
const { allSuggestions, currentSuggestion, suggestionUnsupported } = useLensSuggestions({
|
||||
dataView,
|
||||
query,
|
||||
originalSuggestion,
|
||||
isPlainRecord,
|
||||
columns,
|
||||
lensSuggestionsApi,
|
||||
onSuggestionChange,
|
||||
});
|
||||
|
||||
const chart = suggestionUnsupported ? undefined : originalChart;
|
||||
|
||||
const topPanelNode = useMemo(
|
||||
() => createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }),
|
||||
[]
|
||||
|
@ -214,6 +252,9 @@ export const UnifiedHistogramLayout = ({
|
|||
timeRange={timeRange}
|
||||
request={request}
|
||||
hits={hits}
|
||||
currentSuggestion={currentSuggestion}
|
||||
allSuggestions={allSuggestions}
|
||||
isPlainRecord={isPlainRecord}
|
||||
chart={chart}
|
||||
breakdown={breakdown}
|
||||
appendHitsCounter={appendHitsCounter}
|
||||
|
@ -227,6 +268,7 @@ export const UnifiedHistogramLayout = ({
|
|||
onChartHiddenChange={onChartHiddenChange}
|
||||
onTimeIntervalChange={onTimeIntervalChange}
|
||||
onBreakdownFieldChange={onBreakdownFieldChange}
|
||||
onSuggestionChange={onSuggestionChange}
|
||||
onTotalHitsChange={onTotalHitsChange}
|
||||
onChartLoad={onChartLoad}
|
||||
onFilter={onFilter}
|
||||
|
|
|
@ -24,6 +24,7 @@ export const createMockUnifiedHistogramApi = (
|
|||
setChartHidden: jest.fn(),
|
||||
setTopPanelHeight: jest.fn(),
|
||||
setBreakdownField: jest.fn(),
|
||||
setColumns: jest.fn(),
|
||||
setTimeInterval: jest.fn(),
|
||||
setRequestParams: jest.fn(),
|
||||
setTotalHits: jest.fn(),
|
||||
|
|
|
@ -17,6 +17,7 @@ import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common';
|
|||
import type { Subject } from 'rxjs';
|
||||
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
|
||||
|
||||
/**
|
||||
* The fetch status of a Unified Histogram request
|
||||
|
@ -40,6 +41,7 @@ export interface UnifiedHistogramServices {
|
|||
fieldFormats: FieldFormatsStart;
|
||||
lens: LensPublicStart;
|
||||
storage: Storage;
|
||||
expressions: ExpressionsStart;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -66,8 +66,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await testSubjects.exists('showQueryBarMenu')).to.be(false);
|
||||
expect(await testSubjects.exists('addFilter')).to.be(false);
|
||||
expect(await testSubjects.exists('dscViewModeDocumentButton')).to.be(false);
|
||||
// here Lens suggests a table so the chart is not rendered
|
||||
expect(await testSubjects.exists('unifiedHistogramChart')).to.be(false);
|
||||
expect(await testSubjects.exists('unifiedHistogramQueryHits')).to.be(false);
|
||||
expect(await testSubjects.exists('unifiedHistogramQueryHits')).to.be(true);
|
||||
expect(await testSubjects.exists('discoverAlertsButton')).to.be(false);
|
||||
expect(await testSubjects.exists('shareTopNavButton')).to.be(false);
|
||||
expect(await testSubjects.exists('docTableExpandToggleColumn')).to.be(false);
|
||||
|
@ -87,7 +88,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await monacoEditor.setCodeEditorValue(testQuery);
|
||||
await testSubjects.click('querySubmitButton');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
// here Lens suggests a heatmap so it is rendered
|
||||
expect(await testSubjects.exists('unifiedHistogramChart')).to.be(true);
|
||||
expect(await testSubjects.exists('heatmapChart')).to.be(true);
|
||||
const cell = await dataGrid.getCellElement(0, 3);
|
||||
expect(await cell.getVisibleText()).to.be('2269');
|
||||
});
|
||||
|
|
|
@ -210,6 +210,10 @@ export class DiscoverPageObject extends FtrService {
|
|||
await this.comboBox.set('unifiedHistogramBreakdownFieldSelector', field);
|
||||
}
|
||||
|
||||
public async chooseLensChart(chart: string) {
|
||||
await this.comboBox.set('unifiedHistogramSuggestionSelector', chart);
|
||||
}
|
||||
|
||||
public async getHistogramLegendList() {
|
||||
const unifiedHistogram = await this.testSubjects.find('unifiedHistogramChart');
|
||||
const list = await unifiedHistogram.findAllByClassName('echLegendItem__label');
|
||||
|
|
|
@ -93,7 +93,7 @@ export const getRotatingNumberVisualization = ({
|
|||
{
|
||||
previewIcon: 'refresh',
|
||||
score: 0.5,
|
||||
title: `Rotating ${table.label}` || 'Rotating number',
|
||||
title: table.label ? `Rotating ${table.label}` : 'Rotating number',
|
||||
state: {
|
||||
layerId: table.layerId,
|
||||
color: state?.color || DEFAULT_COLOR,
|
||||
|
|
|
@ -32,6 +32,7 @@ export * from './visualizations/gauge';
|
|||
export * from './datasources/form_based/form_based';
|
||||
export { getTextBasedDatasource } from './datasources/text_based/text_based_languages';
|
||||
export { createFormulaPublicApi } from './datasources/form_based/operations/definitions/formula/formula_public_api';
|
||||
export * from './lens_suggestions_api';
|
||||
|
||||
export * from './datasources/text_based';
|
||||
export * from './datasources/form_based';
|
||||
|
|
|
@ -441,7 +441,7 @@ describe('Textbased Data Source', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
index: 'foo',
|
||||
index: '1',
|
||||
query: {
|
||||
sql: 'SELECT * FROM "foo"',
|
||||
},
|
||||
|
|
|
@ -116,7 +116,7 @@ export function getTextBasedDatasource({
|
|||
};
|
||||
});
|
||||
|
||||
const index = context.dataViewSpec.title;
|
||||
const index = context.dataViewSpec.id ?? context.dataViewSpec.title;
|
||||
const query = context.query;
|
||||
const updatedState = {
|
||||
...state,
|
||||
|
@ -127,6 +127,7 @@ export function getTextBasedDatasource({
|
|||
query,
|
||||
columns: newColumns ?? [],
|
||||
allColumns: newColumns ?? [],
|
||||
timeField: context.dataViewSpec.timeFieldName,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -30,6 +30,7 @@ export type {
|
|||
TableSuggestion,
|
||||
Visualization,
|
||||
VisualizationSuggestion,
|
||||
Suggestion,
|
||||
} from './types';
|
||||
export type {
|
||||
LegacyMetricState as MetricState,
|
||||
|
@ -109,6 +110,6 @@ export type { ChartInfo } from './chart_info_api';
|
|||
export { layerTypes } from '../common/layer_types';
|
||||
export { LENS_EMBEDDABLE_TYPE } from '../common/constants';
|
||||
|
||||
export type { LensPublicStart, LensPublicSetup } from './plugin';
|
||||
export type { LensPublicStart, LensPublicSetup, LensSuggestionsApi } from './plugin';
|
||||
|
||||
export const plugin = () => new LensPlugin();
|
||||
|
|
105
x-pack/plugins/lens/public/lens_suggestions_api.test.ts
Normal file
105
x-pack/plugins/lens/public/lens_suggestions_api.test.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
// import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { createMockVisualization, DatasourceMock, createMockDatasource } from './mocks';
|
||||
import { DatasourceSuggestion } from './types';
|
||||
import { suggestionsApi } from './lens_suggestions_api';
|
||||
|
||||
const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSuggestion => ({
|
||||
state,
|
||||
table: {
|
||||
columns: [],
|
||||
isMultiRow: false,
|
||||
layerId,
|
||||
changeType: 'unchanged',
|
||||
},
|
||||
keptLayerIds: [layerId],
|
||||
});
|
||||
|
||||
describe('suggestionsApi', () => {
|
||||
let datasourceMap: Record<string, DatasourceMock>;
|
||||
const mockVis = createMockVisualization();
|
||||
beforeEach(() => {
|
||||
datasourceMap = {
|
||||
textBased: createMockDatasource('textBased'),
|
||||
};
|
||||
});
|
||||
test('call the getDatasourceSuggestionsForVisualizeField for the text based query', async () => {
|
||||
const dataView = { id: 'index1' } as unknown as DataView;
|
||||
const visualizationMap = {
|
||||
testVis: {
|
||||
...mockVis,
|
||||
},
|
||||
};
|
||||
datasourceMap.textBased.getDatasourceSuggestionsForVisualizeField.mockReturnValue([
|
||||
generateSuggestion(),
|
||||
]);
|
||||
const context = {
|
||||
dataViewSpec: {
|
||||
id: 'index1',
|
||||
title: 'index1',
|
||||
name: 'DataView',
|
||||
},
|
||||
fieldName: '',
|
||||
contextualFields: ['field1', 'field2'],
|
||||
query: {
|
||||
sql: 'SELECT field1, field2 FROM "index1"',
|
||||
},
|
||||
};
|
||||
const suggestions = suggestionsApi({ context, dataView, datasourceMap, visualizationMap });
|
||||
expect(datasourceMap.textBased.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalledWith(
|
||||
{ layers: {}, fieldList: [], indexPatternRefs: [], initialContext: context },
|
||||
'index1',
|
||||
'',
|
||||
{ index1: { id: 'index1' } }
|
||||
);
|
||||
expect(datasourceMap.textBased.getDatasourceSuggestionsFromCurrentState).not.toHaveBeenCalled();
|
||||
expect(suggestions?.length).toEqual(0);
|
||||
});
|
||||
|
||||
test('returns the visualizations suggestions', async () => {
|
||||
const dataView = { id: 'index1' } as unknown as DataView;
|
||||
const visualizationMap = {
|
||||
testVis: {
|
||||
...mockVis,
|
||||
getSuggestions: () => [
|
||||
{
|
||||
score: 0.2,
|
||||
title: 'Test',
|
||||
state: {},
|
||||
previewIcon: 'empty',
|
||||
},
|
||||
{
|
||||
score: 0.8,
|
||||
title: 'Test2',
|
||||
state: {},
|
||||
previewIcon: 'empty',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
datasourceMap.textBased.getDatasourceSuggestionsForVisualizeField.mockReturnValue([
|
||||
generateSuggestion(),
|
||||
]);
|
||||
const context = {
|
||||
dataViewSpec: {
|
||||
id: 'index1',
|
||||
title: 'index1',
|
||||
name: 'DataView',
|
||||
},
|
||||
fieldName: '',
|
||||
contextualFields: ['field1', 'field2'],
|
||||
query: {
|
||||
sql: 'SELECT field1, field2 FROM "index1"',
|
||||
},
|
||||
};
|
||||
const suggestions = suggestionsApi({ context, dataView, datasourceMap, visualizationMap });
|
||||
expect(datasourceMap.textBased.getDatasourceSuggestionsFromCurrentState).toHaveBeenCalled();
|
||||
expect(suggestions?.length).toEqual(1);
|
||||
});
|
||||
});
|
82
x-pack/plugins/lens/public/lens_suggestions_api.ts
Normal file
82
x-pack/plugins/lens/public/lens_suggestions_api.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { getSuggestions } from './editor_frame_service/editor_frame/suggestion_helpers';
|
||||
import type { DatasourceMap, VisualizationMap, VisualizeEditorContext } from './types';
|
||||
import type { DataViewsState } from './state_management';
|
||||
|
||||
interface SuggestionsApi {
|
||||
context: VisualizeFieldContext | VisualizeEditorContext;
|
||||
dataView: DataView;
|
||||
visualizationMap?: VisualizationMap;
|
||||
datasourceMap?: DatasourceMap;
|
||||
}
|
||||
|
||||
export const suggestionsApi = ({
|
||||
context,
|
||||
dataView,
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
}: SuggestionsApi) => {
|
||||
if (!datasourceMap || !visualizationMap || !dataView.id) return undefined;
|
||||
const datasourceStates = {
|
||||
formBased: {
|
||||
isLoading: false,
|
||||
state: {
|
||||
layers: {},
|
||||
},
|
||||
},
|
||||
textBased: {
|
||||
isLoading: false,
|
||||
state: {
|
||||
layers: {},
|
||||
fieldList: [],
|
||||
indexPatternRefs: [],
|
||||
initialContext: context,
|
||||
},
|
||||
},
|
||||
};
|
||||
const currentDataViewId = dataView.id;
|
||||
const dataViews = {
|
||||
indexPatterns: {
|
||||
[currentDataViewId]: dataView,
|
||||
},
|
||||
indexPatternRefs: [],
|
||||
} as unknown as DataViewsState;
|
||||
|
||||
const initialVisualization = visualizationMap?.[Object.keys(visualizationMap)[0]] || null;
|
||||
|
||||
// find the active visualizations from the context
|
||||
const suggestions = getSuggestions({
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
visualizationMap,
|
||||
activeVisualization: initialVisualization,
|
||||
visualizationState: undefined,
|
||||
visualizeTriggerFieldContext: context,
|
||||
dataViews,
|
||||
});
|
||||
if (!suggestions.length) return [];
|
||||
const activeVisualization = suggestions[0];
|
||||
// compute the rest suggestions depending on the active one
|
||||
const newSuggestions = getSuggestions({
|
||||
datasourceMap,
|
||||
datasourceStates: {
|
||||
textBased: {
|
||||
isLoading: false,
|
||||
state: activeVisualization.datasourceState,
|
||||
},
|
||||
},
|
||||
visualizationMap,
|
||||
activeVisualization: visualizationMap[activeVisualization.visualizationId],
|
||||
visualizationState: activeVisualization.visualizationState,
|
||||
dataViews,
|
||||
});
|
||||
|
||||
return [activeVisualization, ...newSuggestions];
|
||||
};
|
|
@ -16,7 +16,7 @@ import { Storage } from '@kbn/kibana-utils-plugin/public';
|
|||
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import type { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
|
||||
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import type {
|
||||
|
@ -44,6 +44,7 @@ import {
|
|||
UiActionsStart,
|
||||
ACTION_VISUALIZE_FIELD,
|
||||
VISUALIZE_FIELD_TRIGGER,
|
||||
VisualizeFieldContext,
|
||||
} from '@kbn/ui-actions-plugin/public';
|
||||
import {
|
||||
VISUALIZE_EDITOR_TRIGGER,
|
||||
|
@ -89,6 +90,8 @@ import type {
|
|||
VisualizationType,
|
||||
EditorFrameSetup,
|
||||
LensTopNavMenuEntryGenerator,
|
||||
VisualizeEditorContext,
|
||||
Suggestion,
|
||||
} from './types';
|
||||
import { getLensAliasConfig } from './vis_type_alias';
|
||||
import { createOpenInDiscoverAction } from './trigger_actions/open_in_discover_action';
|
||||
|
@ -232,9 +235,15 @@ export interface LensPublicStart {
|
|||
stateHelperApi: () => Promise<{
|
||||
formula: FormulaPublicApi;
|
||||
chartInfo: ChartInfoApi;
|
||||
suggestions: LensSuggestionsApi;
|
||||
}>;
|
||||
}
|
||||
|
||||
export type LensSuggestionsApi = (
|
||||
context: VisualizeFieldContext | VisualizeEditorContext,
|
||||
dataViews: DataView
|
||||
) => Suggestion[] | undefined;
|
||||
|
||||
export class LensPlugin {
|
||||
private datatableVisualization: DatatableVisualizationType | undefined;
|
||||
private editorFrameService: EditorFrameServiceType | undefined;
|
||||
|
@ -585,13 +594,30 @@ export class LensPlugin {
|
|||
},
|
||||
|
||||
stateHelperApi: async () => {
|
||||
const { createFormulaPublicApi, createChartInfoApi } = await import('./async_services');
|
||||
const { createFormulaPublicApi, createChartInfoApi, suggestionsApi } = await import(
|
||||
'./async_services'
|
||||
);
|
||||
if (!this.editorFrameService) {
|
||||
await this.initDependenciesForApi();
|
||||
}
|
||||
const [visualizationMap, datasourceMap] = await Promise.all([
|
||||
this.editorFrameService!.loadVisualizations(),
|
||||
this.editorFrameService!.loadDatasources(),
|
||||
]);
|
||||
return {
|
||||
formula: createFormulaPublicApi(),
|
||||
chartInfo: createChartInfoApi(startDependencies.dataViews, this.editorFrameService),
|
||||
suggestions: (
|
||||
context: VisualizeFieldContext | VisualizeEditorContext,
|
||||
dataView: DataView
|
||||
) => {
|
||||
return suggestionsApi({
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
context,
|
||||
dataView,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { getSuggestions } from './suggestions';
|
||||
import { IconChartVerticalBullet, IconChartHorizontalBullet } from '@kbn/chart-icons';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import { GaugeShapes } from '@kbn/expression-gauge-plugin/common';
|
||||
import { GaugeVisualizationState } from './constants';
|
||||
|
@ -157,12 +158,12 @@ describe('shows suggestions', () => {
|
|||
},
|
||||
title: 'Gauge',
|
||||
hide: true,
|
||||
previewIcon: 'empty',
|
||||
previewIcon: IconChartHorizontalBullet,
|
||||
score: 0.5,
|
||||
},
|
||||
{
|
||||
hide: true,
|
||||
previewIcon: 'empty',
|
||||
previewIcon: IconChartVerticalBullet,
|
||||
title: 'Gauge',
|
||||
score: 0.5,
|
||||
state: {
|
||||
|
@ -204,7 +205,7 @@ describe('shows suggestions', () => {
|
|||
ticksPosition: 'auto',
|
||||
layerId: 'first',
|
||||
},
|
||||
previewIcon: 'empty',
|
||||
previewIcon: IconChartVerticalBullet,
|
||||
title: 'Gauge',
|
||||
hide: false, // shows suggestion when current is gauge
|
||||
score: 0.5,
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
GaugeTicksPositions,
|
||||
GaugeLabelMajorModes,
|
||||
} from '@kbn/expression-gauge-plugin/common';
|
||||
import { IconChartHorizontalBullet, IconChartVerticalBullet } from '@kbn/chart-icons';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import type { TableSuggestion, Visualization } from '../../types';
|
||||
import type { GaugeVisualizationState } from './constants';
|
||||
|
@ -64,7 +65,8 @@ export const getSuggestions: Visualization<GaugeVisualizationState>['getSuggesti
|
|||
title: i18n.translate('xpack.lens.gauge.gaugeLabel', {
|
||||
defaultMessage: 'Gauge',
|
||||
}),
|
||||
previewIcon: 'empty',
|
||||
previewIcon:
|
||||
shape === GaugeShapes.VERTICAL_BULLET ? IconChartVerticalBullet : IconChartHorizontalBullet,
|
||||
score: 0.5,
|
||||
hide: !isGauge || state?.metricAccessor === undefined, // only display for gauges for beta
|
||||
};
|
||||
|
@ -73,6 +75,10 @@ export const getSuggestions: Visualization<GaugeVisualizationState>['getSuggesti
|
|||
? [
|
||||
{
|
||||
...baseSuggestion,
|
||||
previewIcon:
|
||||
state?.shape === GaugeShapes.VERTICAL_BULLET
|
||||
? IconChartHorizontalBullet
|
||||
: IconChartVerticalBullet,
|
||||
state: {
|
||||
...baseSuggestion.state,
|
||||
...state,
|
||||
|
@ -93,6 +99,10 @@ export const getSuggestions: Visualization<GaugeVisualizationState>['getSuggesti
|
|||
},
|
||||
{
|
||||
...baseSuggestion,
|
||||
previewIcon:
|
||||
state?.shape === GaugeShapes.VERTICAL_BULLET
|
||||
? IconChartHorizontalBullet
|
||||
: IconChartVerticalBullet,
|
||||
state: {
|
||||
...baseSuggestion.state,
|
||||
metricAccessor: table.columns[0].columnId,
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { Position } from '@elastic/charts';
|
||||
import { IconChartHeatmap } from '@kbn/chart-icons';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import { getSuggestions } from './suggestions';
|
||||
import type { HeatmapVisualizationState } from './types';
|
||||
|
@ -298,7 +299,7 @@ describe('heatmap suggestions', () => {
|
|||
},
|
||||
title: 'Heat map',
|
||||
hide: true,
|
||||
previewIcon: 'empty',
|
||||
previewIcon: IconChartHeatmap,
|
||||
score: 0.3,
|
||||
},
|
||||
]);
|
||||
|
@ -351,7 +352,7 @@ describe('heatmap suggestions', () => {
|
|||
},
|
||||
title: 'Heat map',
|
||||
hide: true,
|
||||
previewIcon: 'empty',
|
||||
previewIcon: IconChartHeatmap,
|
||||
score: 0,
|
||||
},
|
||||
]);
|
||||
|
@ -404,7 +405,7 @@ describe('heatmap suggestions', () => {
|
|||
},
|
||||
title: 'Heat map',
|
||||
hide: true,
|
||||
previewIcon: 'empty',
|
||||
previewIcon: IconChartHeatmap,
|
||||
score: 0.3,
|
||||
},
|
||||
]);
|
||||
|
@ -468,7 +469,7 @@ describe('heatmap suggestions', () => {
|
|||
},
|
||||
title: 'Heat map',
|
||||
hide: true,
|
||||
previewIcon: 'empty',
|
||||
previewIcon: IconChartHeatmap,
|
||||
score: 0.3,
|
||||
},
|
||||
]);
|
||||
|
@ -534,7 +535,7 @@ describe('heatmap suggestions', () => {
|
|||
},
|
||||
title: 'Heat map',
|
||||
hide: false,
|
||||
previewIcon: 'empty',
|
||||
previewIcon: IconChartHeatmap,
|
||||
score: 0.6,
|
||||
},
|
||||
]);
|
||||
|
@ -608,7 +609,7 @@ describe('heatmap suggestions', () => {
|
|||
},
|
||||
title: 'Heat map',
|
||||
hide: false,
|
||||
previewIcon: 'empty',
|
||||
previewIcon: IconChartHeatmap,
|
||||
score: 0.3,
|
||||
},
|
||||
]);
|
||||
|
@ -682,7 +683,7 @@ describe('heatmap suggestions', () => {
|
|||
},
|
||||
title: 'Heat map',
|
||||
hide: false,
|
||||
previewIcon: 'empty',
|
||||
previewIcon: IconChartHeatmap,
|
||||
score: 0.9,
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { partition } from 'lodash';
|
||||
import { Position } from '@elastic/charts';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IconChartHeatmap } from '@kbn/chart-icons';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import type { Visualization } from '../../types';
|
||||
import type { HeatmapVisualizationState } from './types';
|
||||
|
@ -127,7 +128,7 @@ export const getSuggestions: Visualization<HeatmapVisualizationState>['getSugges
|
|||
defaultMessage: 'Heat map',
|
||||
}),
|
||||
hide,
|
||||
previewIcon: 'empty',
|
||||
previewIcon: IconChartHeatmap,
|
||||
score: Number(score.toFixed(1)),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1040,7 +1040,7 @@ describe('suggestions', () => {
|
|||
Array [
|
||||
Object {
|
||||
"hide": false,
|
||||
"previewIcon": "bullseye",
|
||||
"previewIcon": [Function],
|
||||
"score": 0.61,
|
||||
"state": Object {
|
||||
"layers": Array [
|
||||
|
@ -1069,7 +1069,7 @@ describe('suggestions', () => {
|
|||
"palette": undefined,
|
||||
"shape": "mosaic",
|
||||
},
|
||||
"title": "As Mosaic",
|
||||
"title": "Mosaic",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
@ -1149,7 +1149,7 @@ describe('suggestions', () => {
|
|||
Array [
|
||||
Object {
|
||||
"hide": false,
|
||||
"previewIcon": "bullseye",
|
||||
"previewIcon": [Function],
|
||||
"score": 0.46,
|
||||
"state": Object {
|
||||
"layers": Array [
|
||||
|
@ -1175,7 +1175,7 @@ describe('suggestions', () => {
|
|||
"palette": undefined,
|
||||
"shape": "waffle",
|
||||
},
|
||||
"title": "As Waffle",
|
||||
"title": "Waffle",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
|
|
@ -120,7 +120,7 @@ export function suggestions({
|
|||
const newShape = getNewShape(groups, subVisualizationId as PieVisualizationState['shape']);
|
||||
const baseSuggestion: VisualizationSuggestion<PieVisualizationState> = {
|
||||
title: i18n.translate('xpack.lens.pie.suggestionLabel', {
|
||||
defaultMessage: 'As {chartName}',
|
||||
defaultMessage: '{chartName}',
|
||||
values: { chartName: PartitionChartsMeta[newShape].label },
|
||||
description: 'chartName is already translated',
|
||||
}),
|
||||
|
@ -149,7 +149,7 @@ export function suggestions({
|
|||
},
|
||||
],
|
||||
},
|
||||
previewIcon: 'bullseye',
|
||||
previewIcon: PartitionChartsMeta[newShape].icon,
|
||||
// dont show suggestions for same type
|
||||
hide:
|
||||
table.changeType === 'reduced' ||
|
||||
|
@ -161,7 +161,7 @@ export function suggestions({
|
|||
results.push({
|
||||
...baseSuggestion,
|
||||
title: i18n.translate('xpack.lens.pie.suggestionLabel', {
|
||||
defaultMessage: 'As {chartName}',
|
||||
defaultMessage: '{chartName}',
|
||||
values: {
|
||||
chartName:
|
||||
PartitionChartsMeta[
|
||||
|
@ -185,7 +185,7 @@ export function suggestions({
|
|||
) {
|
||||
results.push({
|
||||
title: i18n.translate('xpack.lens.pie.treemapSuggestionLabel', {
|
||||
defaultMessage: 'As Treemap',
|
||||
defaultMessage: 'Treemap',
|
||||
}),
|
||||
// Use a higher score when currently active, to prevent chart type switching
|
||||
// on the user unintentionally
|
||||
|
@ -218,7 +218,7 @@ export function suggestions({
|
|||
},
|
||||
],
|
||||
},
|
||||
previewIcon: 'bullseye',
|
||||
previewIcon: PartitionChartsMeta.treemap.icon,
|
||||
// hide treemap suggestions from bottom bar, but keep them for chart switcher
|
||||
hide:
|
||||
table.changeType === 'reduced' ||
|
||||
|
@ -234,7 +234,7 @@ export function suggestions({
|
|||
) {
|
||||
results.push({
|
||||
title: i18n.translate('xpack.lens.pie.mosaicSuggestionLabel', {
|
||||
defaultMessage: 'As Mosaic',
|
||||
defaultMessage: 'Mosaic',
|
||||
}),
|
||||
score: state?.shape === PieChartTypes.MOSAIC ? 0.7 : 0.5,
|
||||
state: {
|
||||
|
@ -266,7 +266,7 @@ export function suggestions({
|
|||
},
|
||||
],
|
||||
},
|
||||
previewIcon: 'bullseye',
|
||||
previewIcon: PartitionChartsMeta.mosaic.icon,
|
||||
hide:
|
||||
groups.length !== 2 ||
|
||||
table.changeType === 'reduced' ||
|
||||
|
@ -281,7 +281,7 @@ export function suggestions({
|
|||
) {
|
||||
results.push({
|
||||
title: i18n.translate('xpack.lens.pie.waffleSuggestionLabel', {
|
||||
defaultMessage: 'As Waffle',
|
||||
defaultMessage: 'Waffle',
|
||||
}),
|
||||
score: state?.shape === PieChartTypes.WAFFLE ? 0.7 : 0.4,
|
||||
state: {
|
||||
|
@ -310,7 +310,7 @@ export function suggestions({
|
|||
},
|
||||
],
|
||||
},
|
||||
previewIcon: 'bullseye',
|
||||
previewIcon: PartitionChartsMeta.waffle.icon,
|
||||
hide:
|
||||
groups.length !== 1 ||
|
||||
table.changeType === 'reduced' ||
|
||||
|
|
|
@ -2467,7 +2467,6 @@
|
|||
"discover.sourceViewer.errorMessage": "Impossible de récupérer les données pour le moment. Actualisez l'onglet et réessayez.",
|
||||
"discover.sourceViewer.errorMessageTitle": "Une erreur s'est produite.",
|
||||
"discover.sourceViewer.refresh": "Actualiser",
|
||||
"discover.textBasedLanguages.visualize.label": "Visualiser dans Lens",
|
||||
"discover.toggleSidebarAriaLabel": "Activer/Désactiver la barre latérale",
|
||||
"discover.topNav.openOptionsPopover.documentExplorerDisabledHint": "Saviez-vous que Discover possède un nouvel Explorateur de documents avec un meilleur tri des données, des colonnes redimensionnables et une vue en plein écran ? Vous pouvez modifier le mode d'affichage dans les Paramètres avancés.",
|
||||
"discover.topNav.openOptionsPopover.documentExplorerEnabledHint": "Vous pouvez revenir à l'affichage Discover classique dans les Paramètres avancés.",
|
||||
|
|
|
@ -2467,7 +2467,6 @@
|
|||
"discover.sourceViewer.errorMessage": "現在データを取得できませんでした。タブを更新して、再試行してください。",
|
||||
"discover.sourceViewer.errorMessageTitle": "エラーが発生しました",
|
||||
"discover.sourceViewer.refresh": "更新",
|
||||
"discover.textBasedLanguages.visualize.label": "Lensで可視化",
|
||||
"discover.toggleSidebarAriaLabel": "サイドバーを切り替える",
|
||||
"discover.topNav.openOptionsPopover.documentExplorerDisabledHint": "Discoverの新しいドキュメントエクスプローラーでは、データの並べ替え、列のサイズ変更、全画面表示といった優れた機能をご利用いただけます。高度な設定で表示モードを変更できます。",
|
||||
"discover.topNav.openOptionsPopover.documentExplorerEnabledHint": "高度な設定でクラシックDiscoverビューに戻すことができます。",
|
||||
|
|
|
@ -2467,7 +2467,6 @@
|
|||
"discover.sourceViewer.errorMessage": "当前无法获取数据。请刷新选项卡以重试。",
|
||||
"discover.sourceViewer.errorMessageTitle": "发生错误",
|
||||
"discover.sourceViewer.refresh": "刷新",
|
||||
"discover.textBasedLanguages.visualize.label": "在 Lens 中可视化",
|
||||
"discover.toggleSidebarAriaLabel": "切换侧边栏",
|
||||
"discover.topNav.openOptionsPopover.documentExplorerDisabledHint": "您知道吗?Discover 有一种新的 Document Explorer,可提供更好的数据排序、可调整大小的列及全屏视图。您可以在高级设置中更改视图模式。",
|
||||
"discover.topNav.openOptionsPopover.documentExplorerEnabledHint": "您可以在高级设置中切换回经典 Discover 视图。",
|
||||
|
|
|
@ -130,7 +130,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should visualize correctly text based language queries', async () => {
|
||||
it('should visualize correctly text based language queries in Discover', async () => {
|
||||
await PageObjects.discover.selectTextBaseLang('SQL');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await monacoEditor.setCodeEditorValue(
|
||||
|
@ -138,8 +138,24 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
);
|
||||
await testSubjects.click('querySubmitButton');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
expect(await testSubjects.exists('unifiedHistogramChart')).to.be(true);
|
||||
expect(await testSubjects.exists('heatmapChart')).to.be(true);
|
||||
|
||||
await testSubjects.click('textBased-visualize');
|
||||
await PageObjects.discover.chooseLensChart('Donut');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
expect(await testSubjects.exists('partitionVisChart')).to.be(true);
|
||||
});
|
||||
|
||||
it('should visualize correctly text based language queries in Lens', 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('unifiedTextLangEditor-expand');
|
||||
await testSubjects.click('unifiedHistogramEditVisualization');
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
|
@ -157,8 +173,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
);
|
||||
await testSubjects.click('querySubmitButton');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
await testSubjects.click('textBased-visualize');
|
||||
await testSubjects.click('unifiedTextLangEditor-expand');
|
||||
await testSubjects.click('unifiedHistogramEditVisualization');
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
|
|
|
@ -315,7 +315,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
await PageObjects.lens.save('twolayerchart');
|
||||
await testSubjects.click('lnsSuggestion-asDonut > lnsSuggestion');
|
||||
await testSubjects.click('lnsSuggestion-donut > lnsSuggestion');
|
||||
|
||||
expect(await PageObjects.lens.getLayerCount()).to.eql(1);
|
||||
expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sliceByDimensionPanel')).to.eql(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue