[Lens][Discover] Visualize text based query (#141928)

* [Lens] Enable text-based languages-sql

* Display data

* Chart switcher and further fixes

* Drag and drop fixes

* Small fix

* Multiple improvements

* Errors implementation and save and exit

* Some cleanup

* Fix types failures

* Revert change

* Fix underlying data error

* Fix jest test

* Fixes app test

* Rename datasource to textBased

* display the dataview picker component but disabled

* Fix functional test

* Refactoring

* Populate the dataview to theembeddable

* Load sync

* sync load of the new dtsource

* Fix

* Fix bug when the dtaview is not found

* Refactoring

* Add some unit tests

* Add some unit tests

* Add more unit tests

* Add a functional test

* Add all files

* Update lens limit

* Fixes bug

* Bump lens size

* Fix flakiness

* Further fixes

* Fix check

* More fixes

* Fix test

* Wait for query to run

* More changes

* Fix

* Fix the function input to fetch from variable

* Initial implementation of visualizing fields

* Text based languages Visualization in Lens

* Fix due to bad conflict

* Fix types

* Revert from main

* Fix jest test

* Add unit tests

* Adds a functional test

* Visualize lens field

* Add a unit test

* Move switch datasource logic to loadInitial

* Cleanup
This commit is contained in:
Stratoula Kalafateli 2022-09-30 12:48:31 +03:00 committed by GitHub
parent b885d9381d
commit 802039ec34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 294 additions and 9 deletions

View file

@ -194,4 +194,14 @@ 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', () => {
const compInViewerMode = mountWithIntl(
<KibanaContextProvider services={mockDiscoverServices}>
<DiscoverSidebar {...props} onAddFilter={undefined} />
</KibanaContextProvider>
);
const visualizeField = findTestSubject(compInViewerMode, 'textBased-visualize');
expect(visualizeField.length).toBe(1);
});
});

View file

@ -22,6 +22,7 @@ import {
useResizeObserver,
EuiButton,
} from '@elastic/eui';
import { isOfAggregateQueryType } from '@kbn/es-query';
import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect';
import { isEqual } from 'lodash';
import { FormattedMessage } from '@kbn/i18n-react';
@ -39,6 +40,7 @@ import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive';
import { VIEW_MODE } from '../../../../components/view_mode_toggle';
import { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../../../../components/discover_tour';
import type { DataTableRecord } from '../../../../types';
import { triggerVisualizeActionsTextBasedLanguages } from './lib/visualize_trigger_utils';
/**
* Default number of available fields displayed and added on scroll
@ -309,6 +311,12 @@ export function DiscoverSidebarComponent({
const filterChanged = useMemo(() => isEqual(fieldFilter, getDefaultFieldFilter()), [fieldFilter]);
const visualizeAggregateQuery = useCallback(() => {
const aggregateQuery =
state.query && isOfAggregateQueryType(state.query) ? state.query : undefined;
triggerVisualizeActionsTextBasedLanguages(columns, selectedDataView, aggregateQuery);
}, [columns, selectedDataView, state.query]);
if (!selectedDataView) {
return null;
}
@ -532,6 +540,20 @@ 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>
)}
</EuiFlexGroup>
</EuiPageSideBar>
);

View file

@ -12,6 +12,7 @@ import {
visualizeFieldTrigger,
visualizeGeoFieldTrigger,
} from '@kbn/ui-actions-plugin/public';
import type { AggregateQuery } from '@kbn/es-query';
import type { DataViewField, DataView } from '@kbn/data-views-plugin/public';
import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public';
import { getUiActions } from '../../../../../kibana_services';
@ -59,6 +60,22 @@ export function triggerVisualizeActions(
getUiActions().getTrigger(trigger).exec(triggerOptions);
}
export function triggerVisualizeActionsTextBasedLanguages(
contextualFields: string[],
dataView?: DataView,
query?: AggregateQuery
) {
if (!dataView) return;
const triggerOptions = {
dataViewSpec: dataView.toSpec(false),
fieldName: '',
contextualFields,
originatingApp: PLUGIN_ID,
query,
};
getUiActions().getTrigger(VISUALIZE_FIELD_TRIGGER).exec(triggerOptions);
}
export interface VisualizeInformation {
field: DataViewField;
href?: string;

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import type { AggregateQuery } from '@kbn/es-query';
import type { DataViewSpec } from '@kbn/data-views-plugin/public';
import { ActionInternal } from './actions/action_internal';
import { TriggerInternal } from './triggers/trigger_internal';
@ -19,6 +20,7 @@ export interface VisualizeFieldContext {
dataViewSpec: DataViewSpec;
contextualFields?: string[];
originatingApp?: string;
query?: AggregateQuery;
}
export const ACTION_VISUALIZE_FIELD = 'ACTION_VISUALIZE_FIELD';

View file

@ -205,6 +205,12 @@ export function getVisualizeFieldSuggestions({
context: visualizeTriggerFieldContext,
});
}
// suggestions for visualizing textbased languages
if (visualizeTriggerFieldContext && 'query' in visualizeTriggerFieldContext) {
if (visualizeTriggerFieldContext.query) {
return suggestions.find((s) => s.datasourceId === 'textBasedLanguages');
}
}
if (suggestions.length) {
return suggestions.find((s) => s.visualizationId === activeVisualization?.id) || suggestions[0];

View file

@ -115,6 +115,11 @@ export function loadInitial(
defaultIndexPatternId: lensServices.uiSettings.get('defaultIndex'),
};
let activeDatasourceId: string | undefined;
if (initialContext && 'query' in initialContext) {
activeDatasourceId = 'textBasedLanguages';
}
if (
!initialInput ||
(attributeService.inputIsRefType(initialInput) &&
@ -141,6 +146,7 @@ export function loadInitial(
...emptyState,
dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs),
searchSessionId: data.search.session.getSessionId() || data.search.session.start(),
...(activeDatasourceId && { activeDatasourceId }),
datasourceStates: Object.entries(datasourceStates).reduce(
(state, [datasourceId, datasourceState]) => ({
...state,

View file

@ -12,6 +12,7 @@ import { TextBasedLanguagesPersistedState, TextBasedLanguagesPrivateState } from
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { getTextBasedLanguagesDatasource } from './text_based_languages';
import { generateId } from '../id_generator';
import { DatasourcePublicAPI, Datasource } from '../types';
jest.mock('../id_generator');
@ -272,6 +273,97 @@ describe('IndexPattern Data Source', () => {
});
});
describe('#getDatasourceSuggestionsForVisualizeField', () => {
(generateId as jest.Mock).mockReturnValue(`newid`);
it('should create the correct layers', () => {
const state = {
layers: {},
initialContext: {
contextualFields: ['bytes', 'dest'],
query: { sql: 'SELECT * FROM "foo"' },
dataViewSpec: {
title: 'foo',
id: '1',
name: 'Foo',
},
},
} as unknown as TextBasedLanguagesPrivateState;
const suggestions = textBasedLanguagesDatasource.getDatasourceSuggestionsForVisualizeField(
state,
'1',
'',
indexPatterns
);
expect(suggestions[0].state).toEqual({
...state,
layers: {
newid: {
allColumns: [
{
columnId: 'newid',
fieldName: 'bytes',
meta: {
type: 'number',
},
},
{
columnId: 'newid',
fieldName: 'dest',
meta: {
type: 'string',
},
},
],
columns: [
{
columnId: 'newid',
fieldName: 'bytes',
meta: {
type: 'number',
},
},
{
columnId: 'newid',
fieldName: 'dest',
meta: {
type: 'string',
},
},
],
index: 'foo',
query: {
sql: 'SELECT * FROM "foo"',
},
},
},
});
expect(suggestions[0].table).toEqual({
changeType: 'initial',
columns: [
{
columnId: 'newid',
operation: {
dataType: 'number',
isBucketed: false,
label: 'bytes',
},
},
{
columnId: 'newid',
operation: {
dataType: 'string',
isBucketed: true,
label: 'dest',
},
},
],
isMultiRow: false,
layerId: 'newid',
});
});
});
describe('#getErrorMessages', () => {
it('should use the results of getErrorMessages directly when single layer', () => {
const state = {

View file

@ -14,7 +14,7 @@ import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import type { AggregateQuery } from '@kbn/es-query';
import type { SavedObjectReference } from '@kbn/core/public';
import { EuiButtonEmpty, EuiFormRow } from '@elastic/eui';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
import type { ExpressionsStart, DatatableColumnType } from '@kbn/expressions-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import {
@ -36,7 +36,7 @@ import type {
TextBasedLanguageField,
} from './types';
import { FieldSelect } from './field_select';
import { Datasource } from '../types';
import type { Datasource, IndexPatternMap } from '../types';
import { LayerPanel } from './layerpanel';
function getLayerReferenceName(layerId: string) {
@ -82,6 +82,77 @@ export function getTextBasedLanguagesDatasource({
};
});
};
const getSuggestionsForVisualizeField = (
state: TextBasedLanguagesPrivateState,
indexPatternId: string,
fieldName: string,
indexPatterns: IndexPatternMap
) => {
const context = state.initialContext;
if (context && 'dataViewSpec' in context && context.dataViewSpec.title) {
const newLayerId = generateId();
const indexPattern = indexPatterns[indexPatternId];
const contextualFields = context.contextualFields;
const newColumns = contextualFields?.map((c) => {
let field = indexPattern?.getFieldByName(c);
if (!field) {
field = indexPattern?.fields.find((f) => f.name.includes(c));
}
const newId = generateId();
const type = field?.type ?? 'number';
return {
columnId: newId,
fieldName: c,
meta: {
type: type as DatatableColumnType,
},
};
});
const index = context.dataViewSpec.title;
const query = context.query;
const updatedState = {
...state,
layers: {
...state.layers,
[newLayerId]: {
index,
query,
columns: newColumns ?? [],
allColumns: newColumns ?? [],
},
},
};
return [
{
state: {
...updatedState,
},
table: {
changeType: 'initial' as TableChangeType,
isMultiRow: false,
layerId: newLayerId,
columns:
newColumns?.map((f) => {
return {
columnId: f.columnId,
operation: {
dataType: f?.meta?.type as DataType,
label: f.fieldName,
isBucketed: Boolean(f?.meta?.type !== 'number'),
},
};
}) ?? [],
},
keptLayerIds: [newLayerId],
},
];
}
return [];
};
const TextBasedLanguagesDatasource: Datasource<
TextBasedLanguagesPrivateState,
TextBasedLanguagesPersistedState
@ -137,6 +208,7 @@ export function getTextBasedLanguagesDatasource({
...initState,
fieldList: [],
indexPatternRefs: refs,
initialContext: context,
};
},
onRefreshIndexPattern() {},
@ -291,7 +363,11 @@ export function getTextBasedLanguagesDatasource({
const columnExists = props.state.fieldList.some((f) => f.name === selectedField?.fieldName);
render(
<EuiButtonEmpty color={columnExists ? 'primary' : 'danger'} onClick={() => {}}>
<EuiButtonEmpty
color={columnExists ? 'primary' : 'danger'}
onClick={() => {}}
data-test-subj="lns-dimensionTrigger-textBased"
>
{customLabel ??
i18n.translate('xpack.lens.textBasedLanguages.missingField', {
defaultMessage: 'Missing field',
@ -564,7 +640,7 @@ export function getTextBasedLanguagesDatasource({
});
return [];
},
getDatasourceSuggestionsForVisualizeField: getSuggestionsForState,
getDatasourceSuggestionsForVisualizeField: getSuggestionsForVisualizeField,
getDatasourceSuggestionsFromCurrentState: getSuggestionsForState,
getDatasourceSuggestionsForVisualizeCharts: getSuggestionsForState,
isEqual: () => true,

View file

@ -6,6 +6,8 @@
*/
import type { DatatableColumn } from '@kbn/expressions-plugin/public';
import type { AggregateQuery } from '@kbn/es-query';
import type { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
import type { VisualizeEditorContext } from '../types';
export interface TextBasedLanguagesLayerColumn {
columnId: string;
@ -34,6 +36,7 @@ export interface TextBasedLanguagesPersistedState {
export type TextBasedLanguagesPrivateState = TextBasedLanguagesPersistedState & {
indexPatternRefs: IndexPatternRef[];
fieldList: DatatableColumn[];
initialContext?: VisualizeFieldContext | VisualizeEditorContext;
};
export interface IndexPatternRef {

View file

@ -84,6 +84,18 @@ describe('Text based languages utils', () => {
index: '',
},
},
indexPatternRefs: [],
fieldList: [],
initialContext: {
contextualFields: ['bytes', 'dest'],
query: { sql: 'SELECT * FROM "foo"' },
fieldName: '',
dataViewSpec: {
title: 'foo',
id: '1',
name: 'Foo',
},
},
};
const dataViewsMock = dataViewPluginMocks.createStartContract();
const dataMock = dataPluginMock.createStartContract();
@ -113,6 +125,16 @@ describe('Text based languages utils', () => {
);
expect(updatedState).toStrictEqual({
initialContext: {
contextualFields: ['bytes', 'dest'],
query: { sql: 'SELECT * FROM "foo"' },
fieldName: '',
dataViewSpec: {
title: 'foo',
id: '1',
name: 'Foo',
},
},
fieldList: [
{
name: 'timestamp',

View file

@ -15,7 +15,7 @@ import { fetchDataFromAggregateQuery } from './fetch_data_from_aggregate_query';
import type {
IndexPatternRef,
TextBasedLanguagesPersistedState,
TextBasedLanguagesPrivateState,
TextBasedLanguagesLayerColumn,
} from './types';
@ -36,7 +36,7 @@ export async function loadIndexPatternRefs(
}
export async function getStateFromAggregateQuery(
state: TextBasedLanguagesPersistedState,
state: TextBasedLanguagesPrivateState,
query: AggregateQuery,
dataViews: DataViewsPublicPluginStart,
data: DataPublicPluginStart,
@ -45,13 +45,14 @@ export async function getStateFromAggregateQuery(
const indexPatternRefs: IndexPatternRef[] = await loadIndexPatternRefs(dataViews);
const errors: Error[] = [];
const layerIds = Object.keys(state.layers);
const context = state.initialContext;
const newLayerId = layerIds.length > 0 ? layerIds[0] : generateId();
// fetch the pattern from the query
const indexPattern = getIndexPatternFromTextBasedQuery(query);
// get the id of the dataview
const index = indexPatternRefs.find((r) => r.title === indexPattern)?.id ?? '';
let columnsFromQuery: DatatableColumn[] = [];
let columns: TextBasedLanguagesLayerColumn[] = [];
let allColumns: TextBasedLanguagesLayerColumn[] = [];
let timeFieldName;
try {
const table = await fetchDataFromAggregateQuery(query, dataViews, data, expressions);
@ -59,7 +60,8 @@ export async function getStateFromAggregateQuery(
timeFieldName = dataView.timeFieldName;
columnsFromQuery = table?.columns ?? [];
const existingColumns = state.layers[newLayerId].allColumns;
columns = [
allColumns = [
...existingColumns,
...columnsFromQuery.map((c) => ({ columnId: c.id, fieldName: c.id, meta: c.meta })),
];
@ -73,7 +75,7 @@ export async function getStateFromAggregateQuery(
index,
query,
columns: state.layers[newLayerId].columns ?? [],
allColumns: columns,
allColumns,
timeField: timeFieldName,
errors,
},
@ -84,6 +86,7 @@ export async function getStateFromAggregateQuery(
...tempState,
fieldList: columnsFromQuery ?? [],
indexPatternRefs,
initialContext: context,
};
}

View file

@ -26,12 +26,20 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
'spaceSelector',
'header',
]);
const monacoEditor = getService('monacoEditor');
const defaultSettings = {
'discover:enableSql': true,
};
async function setDiscoverTimeRange() {
await PageObjects.timePicker.setDefaultAbsoluteRange();
}
describe('discover field visualize button', () => {
before(async () => {
await kibanaServer.uiSettings.replace(defaultSettings);
});
beforeEach(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional');
await kibanaServer.importExport.load(
@ -95,5 +103,23 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
expect(selectedPattern).to.eql('logst*');
});
});
it('should visualize correctly text based language queries', 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('textBased-visualize');
await retry.try(async () => {
const dimensions = await testSubjects.findAll('lns-dimensionTrigger-textBased');
expect(dimensions).to.have.length(2);
expect(await dimensions[1].getVisibleText()).to.be('average');
});
});
});
}