[Discover] Add default app state extension and log integration data source profiles (#186347)

## Summary

This PR adds a new `getDefaultAppState` extension that allows profiles
to set default values for select app state properties, currently
`columns` and `rowHeight`. It also adds logs data source sub profiles
for the following integrations that consume the `getDefaultAppState`
extension (only `columns` are used currently):
- System logs  
- Kubernetes container logs 
- Windows logs
- AWS S3 Logs
- Nginx error logs 
- Nginx access logs
- Apache error logs 

The index patterns and default state for the integrations are hardcoded
for the initial implementation, but we should change this later to use
an API and state provided by the integrations if we continue this
approach.

For testing, you can ingest sample data for the integrations using
https://github.com/elastic/kibana-demo-data, but you'll need to reindex
the data into correctly named data streams for each:
```
log-system_error -> logs-system.system-test
log-k8s_container -> logs-kubernetes.container_logs-test
log-aws_s3 -> logs-aws.s3access-test
log-nginx_error -> logs-nginx.error-test
log-nqinx -> logs-nginx.access-test
log-apache_error -> logs-apache.error-test

POST /_reindex
{
  "source": {
    "index": "log-k8s_container"
  },
  "dest": {
    "index": "logs-kubernetes.container_logs-test",
    "op_type": "create"
  }
}
```


![default_state](https://github.com/user-attachments/assets/ed73f527-bb5a-470e-b132-f626f0562e18)

Resolves #186271.

### Checklist

- [ ] 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)
- [ ]
[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
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] 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))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] 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))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Julia Rechkunova <julia.rechkunova@elastic.co>
This commit is contained in:
Davis McPhee 2024-08-02 12:18:43 -03:00 committed by GitHub
parent fde808999b
commit ea71c10037
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
61 changed files with 2141 additions and 282 deletions

View file

@ -73,6 +73,10 @@ describe('test fetchAll', () => {
expandedDoc: undefined,
customFilters: [],
overriddenVisContextAfterInvalidation: undefined,
resetDefaultProfileState: {
columns: false,
rowHeight: false,
},
}),
searchSessionId: '123',
initialFetchStatus: FetchStatus.UNINITIALIZED,
@ -261,6 +265,10 @@ describe('test fetchAll', () => {
expandedDoc: undefined,
customFilters: [],
overriddenVisContextAfterInvalidation: undefined,
resetDefaultProfileState: {
columns: false,
rowHeight: false,
},
}),
};
fetchAll(subjects, false, deps);
@ -379,6 +387,10 @@ describe('test fetchAll', () => {
expandedDoc: undefined,
customFilters: [],
overriddenVisContextAfterInvalidation: undefined,
resetDefaultProfileState: {
columns: false,
rowHeight: false,
},
}),
};
fetchAll(subjects, false, deps);

View file

@ -8,7 +8,7 @@
import { Adapters } from '@kbn/inspector-plugin/common';
import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public';
import { BehaviorSubject, filter, firstValueFrom, map, merge, scan } from 'rxjs';
import { BehaviorSubject, combineLatest, filter, firstValueFrom, switchMap } from 'rxjs';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import { isEqual } from 'lodash';
import { isOfAggregateQueryType } from '@kbn/es-query';
@ -53,7 +53,8 @@ export interface FetchDeps {
export function fetchAll(
dataSubjects: SavedSearchData,
reset = false,
fetchDeps: FetchDeps
fetchDeps: FetchDeps,
onFetchRecordsComplete?: () => Promise<void>
): Promise<void> {
const {
initialFetchStatus,
@ -177,10 +178,10 @@ export function fetchAll(
// Return a promise that will resolve once all the requests have finished or failed
return firstValueFrom(
merge(
fetchStatusByType(dataSubjects.documents$, 'documents'),
fetchStatusByType(dataSubjects.totalHits$, 'totalHits')
).pipe(scan(toRequestFinishedMap, {}), filter(allRequestsFinished))
combineLatest([
isComplete(dataSubjects.documents$).pipe(switchMap(async () => onFetchRecordsComplete?.())),
isComplete(dataSubjects.totalHits$),
])
).then(() => {
// Send a complete message to main$ once all queries are done and if main$
// is not already in an ERROR state, e.g. because the document query has failed.
@ -250,16 +251,8 @@ export async function fetchMoreDocuments(
}
}
const fetchStatusByType = <T extends DataMsg>(subject: BehaviorSubject<T>, type: string) =>
subject.pipe(map(({ fetchStatus }) => ({ type, fetchStatus })));
const toRequestFinishedMap = (
currentMap: Record<string, boolean>,
{ type, fetchStatus }: { type: string; fetchStatus: FetchStatus }
) => ({
...currentMap,
[type]: [FetchStatus.COMPLETE, FetchStatus.ERROR].includes(fetchStatus),
});
const allRequestsFinished = (requests: Record<string, boolean>) =>
Object.values(requests).every((finished) => finished);
const isComplete = <T extends DataMsg>(subject: BehaviorSubject<T>) => {
return subject.pipe(
filter(({ fetchStatus }) => [FetchStatus.COMPLETE, FetchStatus.ERROR].includes(fetchStatus))
);
};

View file

@ -23,6 +23,7 @@ import { DiscoverAppState } from '../state_management/discover_app_state_contain
import { DiscoverStateContainer } from '../state_management/discover_state';
import { VIEW_MODE } from '@kbn/saved-search-plugin/public';
import { dataViewAdHoc } from '../../../__mocks__/data_view_complex';
import { buildDataTableRecord, EsHitRecord } from '@kbn/discover-utils';
function getHookProps(
query: AggregateQuery | Query | undefined,
@ -487,4 +488,95 @@ describe('useEsqlMode', () => {
});
});
});
it('should call setResetDefaultProfileState correctly when index pattern changes', async () => {
const { stateContainer } = renderHookWithContext(false);
const documents$ = stateContainer.dataState.data$.documents$;
expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({
columns: false,
rowHeight: false,
});
documents$.next({
fetchStatus: FetchStatus.PARTIAL,
query: { esql: 'from pattern1' },
});
await waitFor(() =>
expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({
columns: true,
rowHeight: true,
})
);
stateContainer.internalState.transitions.setResetDefaultProfileState({
columns: false,
rowHeight: false,
});
documents$.next({
fetchStatus: FetchStatus.PARTIAL,
query: { esql: 'from pattern1' },
});
await waitFor(() =>
expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({
columns: false,
rowHeight: false,
})
);
documents$.next({
fetchStatus: FetchStatus.PARTIAL,
query: { esql: 'from pattern2' },
});
await waitFor(() =>
expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({
columns: true,
rowHeight: true,
})
);
});
it('should call setResetDefaultProfileState correctly when columns change', async () => {
const { stateContainer } = renderHookWithContext(false);
const documents$ = stateContainer.dataState.data$.documents$;
const result1 = [buildDataTableRecord({ message: 'foo' } as EsHitRecord)];
const result2 = [buildDataTableRecord({ message: 'foo', extension: 'bar' } as EsHitRecord)];
expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({
columns: false,
rowHeight: false,
});
documents$.next({
fetchStatus: FetchStatus.PARTIAL,
query: { esql: 'from pattern' },
result: result1,
});
await waitFor(() =>
expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({
columns: true,
rowHeight: true,
})
);
stateContainer.internalState.transitions.setResetDefaultProfileState({
columns: false,
rowHeight: false,
});
documents$.next({
fetchStatus: FetchStatus.PARTIAL,
query: { esql: 'from pattern' },
result: result1,
});
await waitFor(() =>
expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({
columns: false,
rowHeight: false,
})
);
documents$.next({
fetchStatus: FetchStatus.PARTIAL,
query: { esql: 'from pattern' },
result: result2,
});
await waitFor(() =>
expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({
columns: true,
rowHeight: false,
})
);
});
});

View file

@ -18,6 +18,7 @@ import { getValidViewMode } from '../utils/get_valid_view_mode';
import { FetchStatus } from '../../types';
const MAX_NUM_OF_COLUMNS = 50;
/**
* Hook to take care of ES|QL state transformations when a new result is returned
* If necessary this is setting displayed columns and selected data view
@ -29,106 +30,122 @@ export function useEsqlMode({
stateContainer: DiscoverStateContainer;
dataViews: DataViewsContract;
}) {
const prev = useRef<{
query: string;
recentlyUpdatedToColumns: string[];
}>({
recentlyUpdatedToColumns: [],
query: '',
});
const initialFetch = useRef<boolean>(true);
const savedSearch = useSavedSearchInitial();
const prev = useRef<{
initialFetch: boolean;
query: string;
allColumns: string[];
defaultColumns: string[];
}>({
initialFetch: true,
query: '',
allColumns: [],
defaultColumns: [],
});
const cleanup = useCallback(() => {
if (prev.current.query) {
// cleanup when it's not an ES|QL query
prev.current = {
recentlyUpdatedToColumns: [],
query: '',
};
initialFetch.current = true;
if (!prev.current.query) {
return;
}
// cleanup when it's not an ES|QL query
prev.current = {
initialFetch: true,
query: '',
allColumns: [],
defaultColumns: [],
};
}, []);
useEffect(() => {
const subscription = stateContainer.dataState.data$.documents$
.pipe(
switchMap(async (next) => {
const { query } = next;
if (!query || next.fetchStatus === FetchStatus.ERROR) {
const { query: nextQuery } = next;
if (!nextQuery || next.fetchStatus === FetchStatus.ERROR) {
return;
}
const sendComplete = () => {
stateContainer.dataState.data$.documents$.next({
...next,
fetchStatus: FetchStatus.COMPLETE,
});
};
const { viewMode } = stateContainer.appState.getState();
const isEsqlQuery = isOfAggregateQueryType(query);
if (isEsqlQuery) {
const hasResults = Boolean(next.result?.length);
if (next.fetchStatus !== FetchStatus.PARTIAL) {
return;
}
let nextColumns: string[] = prev.current.recentlyUpdatedToColumns;
if (hasResults) {
const firstRow = next.result![0];
const firstRowColumns = Object.keys(firstRow.raw);
if (hasTransformationalCommand(query.esql)) {
nextColumns = firstRowColumns.slice(0, MAX_NUM_OF_COLUMNS);
} else {
nextColumns = [];
}
}
if (initialFetch.current) {
initialFetch.current = false;
prev.current.query = query.esql;
prev.current.recentlyUpdatedToColumns = nextColumns;
}
const indexPatternChanged =
getIndexPatternFromESQLQuery(query.esql) !==
getIndexPatternFromESQLQuery(prev.current.query);
const addColumnsToState =
indexPatternChanged || !isEqual(nextColumns, prev.current.recentlyUpdatedToColumns);
const changeViewMode = viewMode !== getValidViewMode({ viewMode, isEsqlMode: true });
if (!indexPatternChanged && !addColumnsToState && !changeViewMode) {
sendComplete();
return;
}
prev.current.query = query.esql;
prev.current.recentlyUpdatedToColumns = nextColumns;
// just change URL state if necessary
if (addColumnsToState || changeViewMode) {
const nextState = {
...(addColumnsToState && { columns: nextColumns }),
...(changeViewMode && { viewMode: undefined }),
};
await stateContainer.appState.replaceUrlState(nextState);
}
sendComplete();
} else {
if (!isOfAggregateQueryType(nextQuery)) {
// cleanup for a "regular" query
cleanup();
return;
}
if (next.fetchStatus !== FetchStatus.PARTIAL) {
return;
}
let nextAllColumns = prev.current.allColumns;
let nextDefaultColumns = prev.current.defaultColumns;
if (next.result?.length) {
nextAllColumns = Object.keys(next.result[0].raw);
if (hasTransformationalCommand(nextQuery.esql)) {
nextDefaultColumns = nextAllColumns.slice(0, MAX_NUM_OF_COLUMNS);
} else {
nextDefaultColumns = [];
}
}
if (prev.current.initialFetch) {
prev.current.initialFetch = false;
prev.current.query = nextQuery.esql;
prev.current.allColumns = nextAllColumns;
prev.current.defaultColumns = nextDefaultColumns;
}
const indexPatternChanged =
getIndexPatternFromESQLQuery(nextQuery.esql) !==
getIndexPatternFromESQLQuery(prev.current.query);
const allColumnsChanged = !isEqual(nextAllColumns, prev.current.allColumns);
const changeDefaultColumns =
indexPatternChanged || !isEqual(nextDefaultColumns, prev.current.defaultColumns);
const { viewMode } = stateContainer.appState.getState();
const changeViewMode = viewMode !== getValidViewMode({ viewMode, isEsqlMode: true });
if (indexPatternChanged) {
stateContainer.internalState.transitions.setResetDefaultProfileState({
columns: true,
rowHeight: true,
});
} else if (allColumnsChanged) {
stateContainer.internalState.transitions.setResetDefaultProfileState({
columns: true,
rowHeight: false,
});
}
prev.current.allColumns = nextAllColumns;
if (indexPatternChanged || changeDefaultColumns || changeViewMode) {
prev.current.query = nextQuery.esql;
prev.current.defaultColumns = nextDefaultColumns;
// just change URL state if necessary
if (changeDefaultColumns || changeViewMode) {
const nextState = {
...(changeDefaultColumns && { columns: nextDefaultColumns }),
...(changeViewMode && { viewMode: undefined }),
};
await stateContainer.appState.replaceUrlState(nextState);
}
}
stateContainer.dataState.data$.documents$.next({
...next,
fetchStatus: FetchStatus.COMPLETE,
});
})
)
.subscribe();
return () => {
// cleanup for e.g. when savedSearch is switched
cleanup();

View file

@ -8,39 +8,55 @@
import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
import {
createKbnUrlStateStorage,
IKbnUrlStateStorage,
withNotifyOnErrors,
} from '@kbn/kibana-utils-plugin/public';
import type { Filter } from '@kbn/es-query';
import { History } from 'history';
import { savedSearchMock } from '../../../__mocks__/saved_search';
import { discoverServiceMock } from '../../../__mocks__/services';
import {
DiscoverAppStateContainer,
getDiscoverAppStateContainer,
isEqualState,
} from './discover_app_state_container';
import { getDiscoverAppStateContainer, isEqualState } from './discover_app_state_container';
import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/common';
import { createDataViewDataSource } from '../../../../common/data_sources';
import { getInternalStateContainer } from './discover_internal_state_container';
import {
DiscoverSavedSearchContainer,
getSavedSearchContainer,
} from './discover_saved_search_container';
import { getDiscoverGlobalStateContainer } from './discover_global_state_container';
let history: History;
let state: DiscoverAppStateContainer;
let stateStorage: IKbnUrlStateStorage;
let internalState: ReturnType<typeof getInternalStateContainer>;
let savedSearchState: DiscoverSavedSearchContainer;
describe('Test discover app state container', () => {
beforeEach(async () => {
const storeInSessionStorage = discoverServiceMock.uiSettings.get('state:storeInSessionStorage');
const toasts = discoverServiceMock.core.notifications.toasts;
const stateStorage = createKbnUrlStateStorage({
stateStorage = createKbnUrlStateStorage({
useHash: storeInSessionStorage,
history,
...(toasts && withNotifyOnErrors(toasts)),
});
state = getDiscoverAppStateContainer({
stateStorage,
savedSearch: savedSearchMock,
internalState = getInternalStateContainer();
savedSearchState = getSavedSearchContainer({
services: discoverServiceMock,
globalStateContainer: getDiscoverGlobalStateContainer(stateStorage),
});
});
const getStateContainer = () =>
getDiscoverAppStateContainer({
stateStorage,
internalStateContainer: internalState,
savedSearchContainer: savedSearchState,
services: discoverServiceMock,
});
test('hasChanged returns whether the current state has changed', async () => {
const state = getStateContainer();
state.set({
dataSource: createDataViewDataSource({ dataViewId: 'modified' }),
});
@ -50,6 +66,7 @@ describe('Test discover app state container', () => {
});
test('getPrevious returns the state before the current', async () => {
const state = getStateContainer();
state.set({
dataSource: createDataViewDataSource({ dataViewId: 'first' }),
});
@ -110,6 +127,7 @@ describe('Test discover app state container', () => {
} as SavedSearch;
test('should return correct output', () => {
const state = getStateContainer();
const appState = state.getAppStateFromSavedSearch(localSavedSearchMock);
expect(appState).toMatchObject(
expect.objectContaining({
@ -133,6 +151,7 @@ describe('Test discover app state container', () => {
});
test('should return default query if query is undefined', () => {
const state = getStateContainer();
discoverServiceMock.data.query.queryString.getDefaultQuery = jest
.fn()
.mockReturnValue(defaultQuery);
@ -233,6 +252,7 @@ describe('Test discover app state container', () => {
});
test('should automatically set ES|QL data source when query is ES|QL', () => {
const state = getStateContainer();
state.update({
dataSource: createDataViewDataSource({ dataViewId: 'test' }),
});
@ -244,4 +264,70 @@ describe('Test discover app state container', () => {
});
expect(state.get().dataSource?.type).toBe('esql');
});
describe('initAndSync', () => {
it('should call setResetDefaultProfileState correctly with no initial state', () => {
const state = getStateContainer();
expect(internalState.get().resetDefaultProfileState).toEqual({
columns: false,
rowHeight: false,
});
state.initAndSync();
expect(internalState.get().resetDefaultProfileState).toEqual({
columns: true,
rowHeight: true,
});
});
it('should call setResetDefaultProfileState correctly with initial columns', () => {
const stateStorageGetSpy = jest.spyOn(stateStorage, 'get');
stateStorageGetSpy.mockReturnValue({ columns: ['test'] });
const state = getStateContainer();
expect(internalState.get().resetDefaultProfileState).toEqual({
columns: false,
rowHeight: false,
});
state.initAndSync();
expect(internalState.get().resetDefaultProfileState).toEqual({
columns: false,
rowHeight: true,
});
});
it('should call setResetDefaultProfileState correctly with initial rowHeight', () => {
const stateStorageGetSpy = jest.spyOn(stateStorage, 'get');
stateStorageGetSpy.mockReturnValue({ rowHeight: 5 });
const state = getStateContainer();
expect(internalState.get().resetDefaultProfileState).toEqual({
columns: false,
rowHeight: false,
});
state.initAndSync();
expect(internalState.get().resetDefaultProfileState).toEqual({
columns: true,
rowHeight: false,
});
});
it('should call setResetDefaultProfileState correctly with saved search', () => {
const stateStorageGetSpy = jest.spyOn(stateStorage, 'get');
stateStorageGetSpy.mockReturnValue({ columns: ['test'], rowHeight: 5 });
const savedSearchGetSpy = jest.spyOn(savedSearchState, 'getState');
savedSearchGetSpy.mockReturnValue({
id: 'test',
searchSource: createSearchSourceMock(),
managed: false,
});
const state = getStateContainer();
expect(internalState.get().resetDefaultProfileState).toEqual({
columns: false,
rowHeight: false,
});
state.initAndSync();
expect(internalState.get().resetDefaultProfileState).toEqual({
columns: false,
rowHeight: false,
});
});
});
});

View file

@ -38,6 +38,8 @@ import {
DiscoverDataSource,
isDataSourceType,
} from '../../../../common/data_sources';
import type { DiscoverInternalStateContainer } from './discover_internal_state_container';
import type { DiscoverSavedSearchContainer } from './discover_saved_search_container';
export const APP_STATE_URL_KEY = '_a';
export interface DiscoverAppStateContainer extends ReduxLikeStateContainer<DiscoverAppState> {
@ -54,10 +56,9 @@ export interface DiscoverAppStateContainer extends ReduxLikeStateContainer<Disco
*/
hasChanged: () => boolean;
/**
* Initializes the state by the given saved search and starts syncing the state with the URL
* @param currentSavedSearch
* Initializes the app state and starts syncing it with the URL
*/
initAndSync: (currentSavedSearch: SavedSearch) => () => void;
initAndSync: () => () => void;
/**
* Replaces the current state in URL with the given state
* @param newState
@ -82,11 +83,10 @@ export interface DiscoverAppStateContainer extends ReduxLikeStateContainer<Disco
* @param replace
*/
update: (newPartial: DiscoverAppState, replace?: boolean) => void;
/*
* Get updated AppState when given a saved search
*
* */
*/
getAppStateFromSavedSearch: (newSavedSearch: SavedSearch) => DiscoverAppState;
}
@ -157,6 +157,17 @@ export interface DiscoverAppState {
breakdownField?: string;
}
export interface AppStateUrl extends Omit<DiscoverAppState, 'sort'> {
/**
* Necessary to take care of legacy links [fieldName,direction]
*/
sort?: string[][] | [string, string];
/**
* Legacy data view ID prop
*/
index?: string;
}
export const { Provider: DiscoverAppStateProvider, useSelector: useAppStateSelector } =
createStateContainerReactHelpers<ReduxLikeStateContainer<DiscoverAppState>>();
@ -168,14 +179,20 @@ export const { Provider: DiscoverAppStateProvider, useSelector: useAppStateSelec
*/
export const getDiscoverAppStateContainer = ({
stateStorage,
savedSearch,
internalStateContainer,
savedSearchContainer,
services,
}: {
stateStorage: IKbnUrlStateStorage;
savedSearch: SavedSearch;
internalStateContainer: DiscoverInternalStateContainer;
savedSearchContainer: DiscoverSavedSearchContainer;
services: DiscoverServices;
}): DiscoverAppStateContainer => {
let initialState = getInitialState(stateStorage, savedSearch, services);
let initialState = getInitialState(
getCurrentUrlState(stateStorage, services),
savedSearchContainer.getState(),
services
);
let previousState = initialState;
const appStateContainer = createStateContainer<DiscoverAppState>(initialState);
@ -234,9 +251,20 @@ export const getDiscoverAppStateContainer = ({
});
};
const initializeAndSync = (currentSavedSearch: SavedSearch) => {
const initializeAndSync = () => {
const currentSavedSearch = savedSearchContainer.getState();
addLog('[appState] initialize state and sync with URL', currentSavedSearch);
if (!currentSavedSearch.id) {
const { columns, rowHeight } = getCurrentUrlState(stateStorage, services);
internalStateContainer.transitions.setResetDefaultProfileState({
columns: columns === undefined,
rowHeight: rowHeight === undefined,
});
}
const { data } = services;
const savedSearchDataView = currentSavedSearch.searchSource.getField('index');
const appState = enhancedAppContainer.getState();
@ -314,34 +342,24 @@ export const getDiscoverAppStateContainer = ({
};
};
export interface AppStateUrl extends Omit<DiscoverAppState, 'sort'> {
/**
* Necessary to take care of legacy links [fieldName,direction]
*/
sort?: string[][] | [string, string];
/**
* Legacy data view ID prop
*/
index?: string;
function getCurrentUrlState(stateStorage: IKbnUrlStateStorage, services: DiscoverServices) {
return cleanupUrlState(
stateStorage.get<AppStateUrl>(APP_STATE_URL_KEY) ?? {},
services.uiSettings
);
}
export function getInitialState(
stateStorage: IKbnUrlStateStorage | undefined,
initialUrlState: DiscoverAppState | undefined,
savedSearch: SavedSearch,
services: DiscoverServices
) {
const appStateFromUrl = stateStorage?.get<AppStateUrl>(APP_STATE_URL_KEY);
const defaultAppState = getStateDefaults({
savedSearch,
services,
});
return handleSourceColumnState(
appStateFromUrl == null
? defaultAppState
: {
...defaultAppState,
...cleanupUrlState(appStateFromUrl, services.uiSettings),
},
initialUrlState === undefined ? defaultAppState : { ...defaultAppState, ...initialUrlState },
services.uiSettings
);
}

View file

@ -165,4 +165,62 @@ describe('test getDataStateContainer', () => {
dataState.refetch$.next('fetch_more');
});
it('should update app state from default profile state', async () => {
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
const dataState = stateContainer.dataState;
const dataUnsub = dataState.subscribe();
const appUnsub = stateContainer.appState.initAndSync();
discoverServiceMock.profilesManager.resolveDataSourceProfile({});
stateContainer.actions.setDataView(dataViewMock);
stateContainer.internalState.transitions.setResetDefaultProfileState({
columns: true,
rowHeight: true,
});
dataState.data$.totalHits$.next({
fetchStatus: FetchStatus.COMPLETE,
result: 0,
});
dataState.refetch$.next(undefined);
await waitFor(() => {
expect(dataState.data$.main$.value.fetchStatus).toBe(FetchStatus.COMPLETE);
});
expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({
columns: false,
rowHeight: false,
});
expect(stateContainer.appState.get().columns).toEqual(['message', 'extension']);
expect(stateContainer.appState.get().rowHeight).toEqual(3);
dataUnsub();
appUnsub();
});
it('should not update app state from default profile state', async () => {
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
const dataState = stateContainer.dataState;
const dataUnsub = dataState.subscribe();
const appUnsub = stateContainer.appState.initAndSync();
discoverServiceMock.profilesManager.resolveDataSourceProfile({});
stateContainer.actions.setDataView(dataViewMock);
stateContainer.internalState.transitions.setResetDefaultProfileState({
columns: false,
rowHeight: false,
});
dataState.data$.totalHits$.next({
fetchStatus: FetchStatus.COMPLETE,
result: 0,
});
dataState.refetch$.next(undefined);
await waitFor(() => {
expect(dataState.data$.main$.value.fetchStatus).toBe(FetchStatus.COMPLETE);
});
expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({
columns: false,
rowHeight: false,
});
expect(stateContainer.appState.get().columns).toEqual(['default_column']);
expect(stateContainer.appState.get().rowHeight).toBeUndefined();
dataUnsub();
appUnsub();
});
});

View file

@ -10,24 +10,29 @@ import { BehaviorSubject, filter, map, mergeMap, Observable, share, Subject, tap
import type { AutoRefreshDoneFn } from '@kbn/data-plugin/public';
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import { RequestAdapter } from '@kbn/inspector-plugin/common';
import { SavedSearch } from '@kbn/saved-search-plugin/public';
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
import { AggregateQuery, isOfAggregateQueryType, Query } from '@kbn/es-query';
import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types';
import { DataView } from '@kbn/data-views-plugin/common';
import type { DataView } from '@kbn/data-views-plugin/common';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import type { SearchResponseWarning } from '@kbn/search-response-warnings';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import { SEARCH_FIELDS_FROM_SOURCE, SEARCH_ON_PAGE_LOAD_SETTING } from '@kbn/discover-utils';
import {
DEFAULT_COLUMNS_SETTING,
SEARCH_FIELDS_FROM_SOURCE,
SEARCH_ON_PAGE_LOAD_SETTING,
} from '@kbn/discover-utils';
import { getEsqlDataView } from './utils/get_esql_data_view';
import { DiscoverAppState } from './discover_app_state_container';
import { DiscoverServices } from '../../../build_services';
import { DiscoverSearchSessionManager } from './discover_search_session';
import type { DiscoverAppStateContainer } from './discover_app_state_container';
import type { DiscoverServices } from '../../../build_services';
import type { DiscoverSearchSessionManager } from './discover_search_session';
import { FetchStatus } from '../../types';
import { validateTimeRange } from './utils/validate_time_range';
import { fetchAll, fetchMoreDocuments } from '../data_fetching/fetch_all';
import { sendResetMsg } from '../hooks/use_saved_search_messages';
import { getFetch$ } from '../data_fetching/get_fetch_observable';
import { InternalState } from './discover_internal_state_container';
import type { DiscoverInternalStateContainer } from './discover_internal_state_container';
import { getDefaultProfileState } from './utils/get_default_profile_state';
export interface SavedSearchData {
main$: DataMain$;
@ -138,15 +143,15 @@ export interface DiscoverDataStateContainer {
export function getDataStateContainer({
services,
searchSessionManager,
getAppState,
getInternalState,
appStateContainer,
internalStateContainer,
getSavedSearch,
setDataView,
}: {
services: DiscoverServices;
searchSessionManager: DiscoverSearchSessionManager;
getAppState: () => DiscoverAppState;
getInternalState: () => InternalState;
appStateContainer: DiscoverAppStateContainer;
internalStateContainer: DiscoverInternalStateContainer;
getSavedSearch: () => SavedSearch;
setDataView: (dataView: DataView) => void;
}): DiscoverDataStateContainer {
@ -221,8 +226,8 @@ export function getDataStateContainer({
inspectorAdapters,
searchSessionId,
services,
getAppState,
getInternalState,
getAppState: appStateContainer.getState,
getInternalState: internalStateContainer.getState,
savedSearch: getSavedSearch(),
useNewFieldsApi: !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE),
};
@ -232,34 +237,65 @@ export function getDataStateContainer({
if (options.fetchMore) {
abortControllerFetchMore = new AbortController();
const fetchMoreStartTime = window.performance.now();
await fetchMoreDocuments(dataSubjects, {
abortController: abortControllerFetchMore,
...commonFetchDeps,
});
const fetchMoreDuration = window.performance.now() - fetchMoreStartTime;
reportPerformanceMetricEvent(services.analytics, {
eventName: 'discoverFetchMore',
duration: fetchMoreDuration,
});
return;
}
await profilesManager.resolveDataSourceProfile({
dataSource: getAppState().dataSource,
dataSource: appStateContainer.getState().dataSource,
dataView: getSavedSearch().searchSource.getField('index'),
query: getAppState().query,
query: appStateContainer.getState().query,
});
abortController = new AbortController();
const prevAutoRefreshDone = autoRefreshDone;
const fetchAllStartTime = window.performance.now();
await fetchAll(dataSubjects, options.reset, {
abortController,
...commonFetchDeps,
});
await fetchAll(
dataSubjects,
options.reset,
{
abortController,
...commonFetchDeps,
},
async () => {
const { resetDefaultProfileState, dataView } = internalStateContainer.getState();
const { esqlQueryColumns } = dataSubjects.documents$.getValue();
const defaultColumns = uiSettings.get<string[]>(DEFAULT_COLUMNS_SETTING, []);
if (dataView) {
const stateUpdate = getDefaultProfileState({
profilesManager,
resetDefaultProfileState,
defaultColumns,
dataView,
esqlQueryColumns,
});
if (stateUpdate) {
await appStateContainer.replaceUrlState(stateUpdate);
}
}
internalStateContainer.transitions.setResetDefaultProfileState({
columns: false,
rowHeight: false,
});
}
);
const fetchAllDuration = window.performance.now() - fetchAllStartTime;
reportPerformanceMetricEvent(services.analytics, {
eventName: 'discoverFetchAll',
@ -286,7 +322,7 @@ export function getDataStateContainer({
}
const fetchQuery = async (resetQuery?: boolean) => {
const query = getAppState().query;
const query = appStateContainer.getState().query;
const currentDataView = getSavedSearch().searchSource.getField('index');
if (isOfAggregateQueryType(query)) {
@ -301,6 +337,7 @@ export function getDataStateContainer({
} else {
refetch$.next(undefined);
}
return refetch$;
};

View file

@ -24,6 +24,7 @@ export interface InternalState {
expandedDoc: DataTableRecord | undefined;
customFilters: Filter[];
overriddenVisContextAfterInvalidation: UnifiedHistogramVisContext | {} | undefined; // it will be used during saved search saving
resetDefaultProfileState: { columns: boolean; rowHeight: boolean };
}
export interface InternalStateTransitions {
@ -48,6 +49,9 @@ export interface InternalStateTransitions {
overriddenVisContextAfterInvalidation: UnifiedHistogramVisContext | {} | undefined
) => InternalState;
resetOnSavedSearchChange: (state: InternalState) => () => InternalState;
setResetDefaultProfileState: (
state: InternalState
) => (resetDefaultProfileState: InternalState['resetDefaultProfileState']) => InternalState;
}
export type DiscoverInternalStateContainer = ReduxLikeStateContainer<
@ -68,6 +72,7 @@ export function getInternalStateContainer() {
expandedDoc: undefined,
customFilters: [],
overriddenVisContextAfterInvalidation: undefined,
resetDefaultProfileState: { columns: false, rowHeight: false },
},
{
setDataView: (prevState: InternalState) => (nextDataView: DataView) => ({
@ -134,6 +139,12 @@ export function getInternalStateContainer() {
overriddenVisContextAfterInvalidation: undefined,
expandedDoc: undefined,
}),
setResetDefaultProfileState:
(prevState: InternalState) =>
(resetDefaultProfileState: InternalState['resetDefaultProfileState']) => ({
...prevState,
resetDefaultProfileState,
}),
},
{},
{ freeze: (state) => state }

View file

@ -256,20 +256,21 @@ export function getDiscoverStateContainer({
globalStateContainer,
});
/**
* Internal State Container, state that's not persisted and not part of the URL
*/
const internalStateContainer = getInternalStateContainer();
/**
* App State Container, synced with the _a part URL
*/
const appStateContainer = getDiscoverAppStateContainer({
stateStorage,
savedSearch: savedSearchContainer.getState(),
internalStateContainer,
savedSearchContainer,
services,
});
/**
* Internal State Container, state that's not persisted and not part of the URL
*/
const internalStateContainer = getInternalStateContainer();
const pauseAutoRefreshInterval = async (dataView: DataView) => {
if (dataView && (!dataView.isTimeBased() || dataView.type === DataViewType.ROLLUP)) {
const state = globalStateContainer.get();
@ -281,6 +282,7 @@ export function getDiscoverStateContainer({
}
}
};
const setDataView = (dataView: DataView) => {
internalStateContainer.transitions.setDataView(dataView);
pauseAutoRefreshInterval(dataView);
@ -290,8 +292,8 @@ export function getDiscoverStateContainer({
const dataStateContainer = getDataStateContainer({
services,
searchSessionManager,
getAppState: appStateContainer.getState,
getInternalState: internalStateContainer.getState,
appStateContainer,
internalStateContainer,
getSavedSearch: savedSearchContainer.getState,
setDataView,
});
@ -403,9 +405,8 @@ export function getDiscoverStateContainer({
});
// initialize app state container, syncing with _g and _a part of the URL
const appStateInitAndSyncUnsubscribe = appStateContainer.initAndSync(
savedSearchContainer.getState()
);
const appStateInitAndSyncUnsubscribe = appStateContainer.initAndSync();
// subscribing to state changes of appStateContainer, triggering data fetching
const appStateUnsubscribe = appStateContainer.subscribe(
buildStateSubscribe({
@ -417,6 +418,7 @@ export function getDiscoverStateContainer({
setDataView,
})
);
// start subscribing to dataStateContainer, triggering data fetching
const unsubscribeData = dataStateContainer.subscribe();
@ -467,6 +469,7 @@ export function getDiscoverStateContainer({
await onChangeDataView(newDataView);
return newDataView;
};
/**
* Triggered when a user submits a query in the search bar
*/
@ -492,6 +495,7 @@ export function getDiscoverStateContainer({
appState: appStateContainer,
});
};
/**
* Undo all changes to the current saved search
*/
@ -518,6 +522,7 @@ export function getDiscoverStateContainer({
await appStateContainer.replaceUrlState(newAppState);
return nextSavedSearch;
};
const fetchData = (initial: boolean = false) => {
addLog('fetchData', { initial });
if (!initial || dataStateContainer.getInitialFetchStatus() === FetchStatus.LOADING) {

View file

@ -31,6 +31,7 @@ const setupTestParams = (dataView: DataView | undefined) => {
discoverState.appState.update = jest.fn();
discoverState.internalState.transitions = {
setIsDataViewLoading: jest.fn(),
setResetDefaultProfileState: jest.fn(),
} as unknown as Readonly<PureTransitionsToTransitions<InternalStateTransitions>>;
return {
services,
@ -71,4 +72,14 @@ describe('changeDataView', () => {
expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(1, true);
expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(2, false);
});
it('should call setResetDefaultProfileState correctly when switching data view', async () => {
const params = setupTestParams(dataViewComplexMock);
expect(params.internalState.transitions.setResetDefaultProfileState).not.toHaveBeenCalled();
await changeDataView(dataViewComplexMock.id!, params);
expect(params.internalState.transitions.setResetDefaultProfileState).toHaveBeenCalledWith({
columns: true,
rowHeight: true,
});
});
});

View file

@ -35,10 +35,12 @@ export async function changeDataView(
}
) {
addLog('[ui] changeDataView', { id });
const { dataViews, uiSettings } = services;
const dataView = internalState.getState().dataView;
const state = appState.getState();
let nextDataView: DataView | null = null;
internalState.transitions.setIsDataViewLoading(true);
try {
@ -60,9 +62,12 @@ export async function changeDataView(
);
appState.update(nextAppState);
if (internalState.getState().expandedDoc) {
internalState.transitions.setExpandedDoc(undefined);
}
}
internalState.transitions.setIsDataViewLoading(false);
internalState.transitions.setResetDefaultProfileState({ columns: true, rowHeight: true });
}

View file

@ -0,0 +1,99 @@
/*
* 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 { fieldList } from '@kbn/data-views-plugin/common';
import { buildDataViewMock } from '@kbn/discover-utils/src/__mocks__';
import { createContextAwarenessMocks } from '../../../../context_awareness/__mocks__';
import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield';
import { getDefaultProfileState } from './get_default_profile_state';
const emptyDataView = buildDataViewMock({
name: 'emptyDataView',
fields: fieldList(),
});
const { profilesManagerMock } = createContextAwarenessMocks();
profilesManagerMock.resolveDataSourceProfile({});
describe('getDefaultProfileState', () => {
it('should return expected columns', () => {
let appState = getDefaultProfileState({
profilesManager: profilesManagerMock,
resetDefaultProfileState: {
columns: true,
rowHeight: false,
},
defaultColumns: ['messsage', 'bytes'],
dataView: dataViewWithTimefieldMock,
esqlQueryColumns: undefined,
});
expect(appState).toEqual({
columns: ['message', 'extension', 'bytes'],
grid: {
columns: {
extension: {
width: 200,
},
message: {
width: 100,
},
},
},
});
appState = getDefaultProfileState({
profilesManager: profilesManagerMock,
resetDefaultProfileState: {
columns: true,
rowHeight: false,
},
defaultColumns: ['messsage', 'bytes'],
dataView: emptyDataView,
esqlQueryColumns: [{ id: '1', name: 'foo', meta: { type: 'string' } }],
});
expect(appState).toEqual({
columns: ['foo'],
grid: {
columns: {
foo: {
width: 300,
},
},
},
});
});
it('should return expected rowHeight', () => {
const appState = getDefaultProfileState({
profilesManager: profilesManagerMock,
resetDefaultProfileState: {
columns: false,
rowHeight: true,
},
defaultColumns: [],
dataView: dataViewWithTimefieldMock,
esqlQueryColumns: undefined,
});
expect(appState).toEqual({
rowHeight: 3,
});
});
it('should return undefined', () => {
const appState = getDefaultProfileState({
profilesManager: profilesManagerMock,
resetDefaultProfileState: {
columns: false,
rowHeight: false,
},
defaultColumns: [],
dataView: dataViewWithTimefieldMock,
esqlQueryColumns: undefined,
});
expect(appState).toEqual(undefined);
});
});

View file

@ -0,0 +1,81 @@
/*
* 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 { DataView } from '@kbn/data-views-plugin/common';
import type { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
import { uniqBy } from 'lodash';
import type { DiscoverAppState } from '../discover_app_state_container';
import {
DefaultAppStateColumn,
getMergedAccessor,
ProfilesManager,
} from '../../../../context_awareness';
import type { InternalState } from '../discover_internal_state_container';
import type { DataDocumentsMsg } from '../discover_data_state_container';
export const getDefaultProfileState = ({
profilesManager,
resetDefaultProfileState,
defaultColumns,
dataView,
esqlQueryColumns,
}: {
profilesManager: ProfilesManager;
resetDefaultProfileState: InternalState['resetDefaultProfileState'];
defaultColumns: string[];
dataView: DataView;
esqlQueryColumns: DataDocumentsMsg['esqlQueryColumns'];
}) => {
const stateUpdate: DiscoverAppState = {};
const defaultState = getDefaultState(profilesManager, dataView);
if (resetDefaultProfileState.columns) {
const mappedDefaultColumns = defaultColumns.map((name) => ({ name }));
const isValidColumn = getIsValidColumn(dataView, esqlQueryColumns);
const validColumns = uniqBy(
defaultState.columns?.concat(mappedDefaultColumns).filter(isValidColumn),
'name'
);
if (validColumns?.length) {
const columns = validColumns.reduce<DiscoverGridSettings['columns']>(
(acc, { name, width }) => (width ? { ...acc, [name]: { width } } : acc),
undefined
);
stateUpdate.grid = columns ? { columns } : undefined;
stateUpdate.columns = validColumns.map(({ name }) => name);
}
}
if (resetDefaultProfileState.rowHeight && defaultState.rowHeight !== undefined) {
stateUpdate.rowHeight = defaultState.rowHeight;
}
return Object.keys(stateUpdate).length ? stateUpdate : undefined;
};
const getDefaultState = (profilesManager: ProfilesManager, dataView: DataView) => {
const getDefaultAppState = getMergedAccessor(
profilesManager.getProfiles(),
'getDefaultAppState',
() => ({})
);
return getDefaultAppState({ dataView });
};
const getIsValidColumn =
(dataView: DataView, esqlQueryColumns: DataDocumentsMsg['esqlQueryColumns']) =>
(column: DefaultAppStateColumn) => {
const isValid = esqlQueryColumns
? esqlQueryColumns.some((esqlColumn) => esqlColumn.name === column.name)
: dataView.fields.getByName(column.name);
return Boolean(isValid);
};

View file

@ -49,6 +49,23 @@ export const createContextAwarenessMocks = ({
...prev(),
rootProfile: () => <>data-source-profile</>,
})),
getDefaultAppState: jest.fn(() => () => ({
columns: [
{
name: 'message',
width: 100,
},
{
name: 'extension',
width: 200,
},
{
name: 'foo',
width: 300,
},
],
rowHeight: 3,
})),
},
resolve: jest.fn(() => ({
isMatch: true,

View file

@ -71,6 +71,22 @@ export const exampleDataSourceProfileProvider: DataSourceProfileProvider = {
},
};
},
getDefaultAppState: () => () => ({
columns: [
{
name: '@timestamp',
width: 212,
},
{
name: 'log.level',
width: 150,
},
{
name: 'message',
},
],
rowHeight: 5,
}),
},
resolve: (params) => {
let indexPattern: string | undefined;

View file

@ -0,0 +1,34 @@
/*
* 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 { createContextAwarenessMocks } from '../__mocks__';
import { extendProfileProvider } from './extend_profile_provider';
const { dataSourceProfileProviderMock } = createContextAwarenessMocks();
describe('extendProfileProvider', () => {
it('should merge profiles and overwrite other properties', () => {
const resolve = jest.fn();
const getDefaultAppState = jest.fn();
const extendedProfileProvider = extendProfileProvider(dataSourceProfileProviderMock, {
profileId: 'extended-profile',
profile: { getDefaultAppState },
resolve,
});
expect(extendedProfileProvider).toEqual({
...dataSourceProfileProviderMock,
profileId: 'extended-profile',
profile: {
...dataSourceProfileProviderMock.profile,
getDefaultAppState,
},
resolve,
});
});
});

View file

@ -0,0 +1,21 @@
/*
* 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 { BaseProfileProvider } from '../profile_service';
export const extendProfileProvider = <TProvider extends BaseProfileProvider<{}>>(
baseProvider: TProvider,
extension: Partial<TProvider> & Pick<TProvider, 'profileId'>
): TProvider => ({
...baseProvider,
...extension,
profile: {
...baseProvider.profile,
...extension.profile,
},
});

View file

@ -0,0 +1,40 @@
/*
* 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 { createDataViewDataSource, createEsqlDataSource } from '../../../common/data_sources';
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
import { extractIndexPatternFrom } from './extract_index_pattern_from';
describe('extractIndexPatternFrom', () => {
it('should return index pattern from data view', () => {
const indexPattern = extractIndexPatternFrom({
dataSource: createDataViewDataSource({ dataViewId: dataViewWithTimefieldMock.id! }),
dataView: dataViewWithTimefieldMock,
});
expect(indexPattern).toBe(dataViewWithTimefieldMock.getIndexPattern());
});
it('should return index pattern from ES|QL query', () => {
const indexPattern = extractIndexPatternFrom({
dataSource: createEsqlDataSource(),
query: { esql: 'FROM index-pattern' },
});
expect(indexPattern).toBe('index-pattern');
});
it('should return null if no data view or ES|QL query', () => {
let indexPattern = extractIndexPatternFrom({
dataSource: createDataViewDataSource({ dataViewId: dataViewWithTimefieldMock.id! }),
});
expect(indexPattern).toBeNull();
indexPattern = extractIndexPatternFrom({
dataSource: createEsqlDataSource(),
});
expect(indexPattern).toBeNull();
});
});

View file

@ -0,0 +1,26 @@
/*
* 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 { isOfAggregateQueryType } from '@kbn/es-query';
import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
import { isDataViewSource, isEsqlSource } from '../../../common/data_sources';
import type { DataSourceProfileProviderParams } from '../profiles';
export const extractIndexPatternFrom = ({
dataSource,
dataView,
query,
}: Pick<DataSourceProfileProviderParams, 'dataSource' | 'dataView' | 'query'>) => {
if (isEsqlSource(dataSource) && isOfAggregateQueryType(query)) {
return getIndexPatternFromESQLQuery(query.esql);
} else if (isDataViewSource(dataSource) && dataView) {
return dataView.getIndexPattern();
}
return null;
};

View file

@ -0,0 +1,32 @@
/*
* 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 { DataSourceProfileProvider } from '../../../profiles';
import { DefaultAppStateColumn } from '../../../types';
export const createGetDefaultAppState = ({
defaultColumns,
}: {
defaultColumns?: DefaultAppStateColumn[];
}): DataSourceProfileProvider['profile']['getDefaultAppState'] => {
return (prev) => (params) => {
const appState = { ...prev(params) };
if (defaultColumns) {
appState.columns = [];
if (params.dataView.isTimeBased()) {
appState.columns.push({ name: params.dataView.timeFieldName, width: 212 });
}
appState.columns.push(...defaultColumns);
}
return appState;
};
};

View file

@ -7,4 +7,5 @@
*/
export { getRowIndicatorProvider } from './get_row_indicator_provider';
export { createGetDefaultAppState } from './get_default_app_state';
export { getCellRenderers } from './get_cell_renderers';

View file

@ -0,0 +1,14 @@
/*
* 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 { DefaultAppStateColumn } from '../../types';
export const LOG_LEVEL_COLUMN: DefaultAppStateColumn = { name: 'log.level', width: 150 };
export const MESSAGE_COLUMN: DefaultAppStateColumn = { name: 'message' };
export const CLIENT_IP_COLUMN: DefaultAppStateColumn = { name: 'client.ip', width: 150 };
export const HOST_NAME_COLUMN: DefaultAppStateColumn = { name: 'host.name', width: 250 };

View file

@ -0,0 +1,34 @@
/*
* 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 { ProfileProviderServices } from '../profile_provider_services';
import { createLogsDataSourceProfileProvider } from './profile';
import {
createApacheErrorLogsDataSourceProfileProvider,
createAwsS3accessLogsDataSourceProfileProvider,
createKubernetesContainerLogsDataSourceProfileProvider,
createNginxAccessLogsDataSourceProfileProvider,
createNginxErrorLogsDataSourceProfileProvider,
createSystemLogsDataSourceProfileProvider,
createWindowsLogsDataSourceProfileProvider,
} from './sub_profiles';
export const createLogsDataSourceProfileProviders = (providerServices: ProfileProviderServices) => {
const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(providerServices);
return [
createSystemLogsDataSourceProfileProvider(logsDataSourceProfileProvider),
createKubernetesContainerLogsDataSourceProfileProvider(logsDataSourceProfileProvider),
createWindowsLogsDataSourceProfileProvider(logsDataSourceProfileProvider),
createAwsS3accessLogsDataSourceProfileProvider(logsDataSourceProfileProvider),
createNginxErrorLogsDataSourceProfileProvider(logsDataSourceProfileProvider),
createNginxAccessLogsDataSourceProfileProvider(logsDataSourceProfileProvider),
createApacheErrorLogsDataSourceProfileProvider(logsDataSourceProfileProvider),
logsDataSourceProfileProvider,
];
};

View file

@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
export { createLogsDataSourceProfileProvider } from './profile';
export { createLogsDataSourceProfileProviders } from './create_profile_providers';

View file

@ -6,16 +6,10 @@
* Side Public License, v 1.
*/
import { isOfAggregateQueryType } from '@kbn/es-query';
import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
import { isDataViewSource, isEsqlSource } from '../../../../common/data_sources';
import {
DataSourceCategory,
DataSourceProfileProvider,
DataSourceProfileProviderParams,
} from '../../profiles';
import { DataSourceCategory, DataSourceProfileProvider } from '../../profiles';
import { ProfileProviderServices } from '../profile_provider_services';
import { getRowIndicatorProvider } from './accessors';
import { extractIndexPatternFrom } from '../extract_index_pattern_from';
import { getCellRenderers } from './accessors';
export const createLogsDataSourceProfileProvider = (
@ -39,17 +33,3 @@ export const createLogsDataSourceProfileProvider = (
};
},
});
const extractIndexPatternFrom = ({
dataSource,
dataView,
query,
}: DataSourceProfileProviderParams) => {
if (isEsqlSource(dataSource) && isOfAggregateQueryType(query)) {
return getIndexPatternFromESQLQuery(query.esql);
} else if (isDataViewSource(dataSource) && dataView) {
return dataView.getIndexPattern();
}
return null;
};

View file

@ -0,0 +1,53 @@
/*
* 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 { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield';
import { createEsqlDataSource } from '../../../../../common/data_sources';
import { DataSourceCategory, RootContext, SolutionType } from '../../../profiles';
import { createContextAwarenessMocks } from '../../../__mocks__';
import { createLogsDataSourceProfileProvider } from '../profile';
import { createApacheErrorLogsDataSourceProfileProvider } from './apache_error_logs';
const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default };
const { profileProviderServices } = createContextAwarenessMocks();
const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(profileProviderServices);
const dataSourceProfileProvider = createApacheErrorLogsDataSourceProfileProvider(
logsDataSourceProfileProvider
);
describe('createApacheErrorLogsDataSourceProfileProvider', () => {
it('should match a valid index pattern', async () => {
const result = await dataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: 'FROM logs-apache.error-*' },
});
expect(result).toEqual({ isMatch: true, context: { category: DataSourceCategory.Logs } });
});
it('should not match an invalid index pattern', async () => {
const result = await dataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: 'FROM logs-apache.access-*' },
});
expect(result).toEqual({ isMatch: false });
});
it('should return default app state', () => {
const getDefaultAppState = dataSourceProfileProvider.profile.getDefaultAppState?.(() => ({}));
expect(getDefaultAppState?.({ dataView: dataViewWithTimefieldMock })).toEqual({
columns: [
{ name: 'timestamp', width: 212 },
{ name: 'log.level', width: 150 },
{ name: 'client.ip', width: 150 },
{ name: 'message' },
],
});
});
});

View file

@ -0,0 +1,26 @@
/*
* 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 { DataSourceProfileProvider } from '../../../profiles';
import { extendProfileProvider } from '../../extend_profile_provider';
import { createGetDefaultAppState } from '../accessors';
import { CLIENT_IP_COLUMN, LOG_LEVEL_COLUMN, MESSAGE_COLUMN } from '../consts';
import { createResolve } from './create_resolve';
export const createApacheErrorLogsDataSourceProfileProvider = (
logsDataSourceProfileProvider: DataSourceProfileProvider
): DataSourceProfileProvider =>
extendProfileProvider(logsDataSourceProfileProvider, {
profileId: 'apache-error-logs-data-source',
profile: {
getDefaultAppState: createGetDefaultAppState({
defaultColumns: [LOG_LEVEL_COLUMN, CLIENT_IP_COLUMN, MESSAGE_COLUMN],
}),
},
resolve: createResolve('logs-apache.error'),
});

View file

@ -0,0 +1,55 @@
/*
* 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 { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield';
import { createEsqlDataSource } from '../../../../../common/data_sources';
import { DataSourceCategory, RootContext, SolutionType } from '../../../profiles';
import { createContextAwarenessMocks } from '../../../__mocks__';
import { createLogsDataSourceProfileProvider } from '../profile';
import { createAwsS3accessLogsDataSourceProfileProvider } from './aws_s3access_logs';
const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default };
const { profileProviderServices } = createContextAwarenessMocks();
const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(profileProviderServices);
const dataSourceProfileProvider = createAwsS3accessLogsDataSourceProfileProvider(
logsDataSourceProfileProvider
);
describe('createAwsS3accessLogsDataSourceProfileProvider', () => {
it('should match a valid index pattern', async () => {
const result = await dataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: 'FROM logs-aws.s3access-*' },
});
expect(result).toEqual({ isMatch: true, context: { category: DataSourceCategory.Logs } });
});
it('should not match an invalid index pattern', async () => {
const result = await dataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: 'FROM logs-aws.s3noaccess-*' },
});
expect(result).toEqual({ isMatch: false });
});
it('should return default app state', () => {
const getDefaultAppState = dataSourceProfileProvider.profile.getDefaultAppState?.(() => ({}));
expect(getDefaultAppState?.({ dataView: dataViewWithTimefieldMock })).toEqual({
columns: [
{ name: 'timestamp', width: 212 },
{ name: 'aws.s3.bucket.name', width: 200 },
{ name: 'aws.s3.object.key', width: 200 },
{ name: 'aws.s3access.operation', width: 200 },
{ name: 'client.ip', width: 150 },
{ name: 'message' },
],
});
});
});

View file

@ -0,0 +1,32 @@
/*
* 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 { DataSourceProfileProvider } from '../../../profiles';
import { extendProfileProvider } from '../../extend_profile_provider';
import { createGetDefaultAppState } from '../accessors';
import { CLIENT_IP_COLUMN, MESSAGE_COLUMN } from '../consts';
import { createResolve } from './create_resolve';
export const createAwsS3accessLogsDataSourceProfileProvider = (
logsDataSourceProfileProvider: DataSourceProfileProvider
): DataSourceProfileProvider =>
extendProfileProvider(logsDataSourceProfileProvider, {
profileId: 'aws-s3access-logs-data-source',
profile: {
getDefaultAppState: createGetDefaultAppState({
defaultColumns: [
{ name: 'aws.s3.bucket.name', width: 200 },
{ name: 'aws.s3.object.key', width: 200 },
{ name: 'aws.s3access.operation', width: 200 },
CLIENT_IP_COLUMN,
MESSAGE_COLUMN,
],
}),
},
resolve: createResolve('logs-aws.s3access'),
});

View file

@ -0,0 +1,30 @@
/*
* 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 { createRegExpPatternFrom, testPatternAgainstAllowedList } from '@kbn/data-view-utils';
import { DataSourceCategory, DataSourceProfileProvider } from '../../../profiles';
import { extractIndexPatternFrom } from '../../extract_index_pattern_from';
export const createResolve = (baseIndexPattern: string): DataSourceProfileProvider['resolve'] => {
const testIndexPattern = testPatternAgainstAllowedList([
createRegExpPatternFrom(baseIndexPattern),
]);
return (params) => {
const indexPattern = extractIndexPatternFrom(params);
if (!indexPattern || !testIndexPattern(indexPattern)) {
return { isMatch: false };
}
return {
isMatch: true,
context: { category: DataSourceCategory.Logs },
};
};
};

View file

@ -0,0 +1,15 @@
/*
* 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.
*/
export { createApacheErrorLogsDataSourceProfileProvider } from './apache_error_logs';
export { createAwsS3accessLogsDataSourceProfileProvider } from './aws_s3access_logs';
export { createKubernetesContainerLogsDataSourceProfileProvider } from './kubernetes_container_logs';
export { createNginxAccessLogsDataSourceProfileProvider } from './nginx_access_logs';
export { createNginxErrorLogsDataSourceProfileProvider } from './nginx_error_logs';
export { createSystemLogsDataSourceProfileProvider } from './system_logs';
export { createWindowsLogsDataSourceProfileProvider } from './windows_logs';

View file

@ -0,0 +1,55 @@
/*
* 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 { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield';
import { createEsqlDataSource } from '../../../../../common/data_sources';
import { DataSourceCategory, RootContext, SolutionType } from '../../../profiles';
import { createContextAwarenessMocks } from '../../../__mocks__';
import { createLogsDataSourceProfileProvider } from '../profile';
import { createKubernetesContainerLogsDataSourceProfileProvider } from './kubernetes_container_logs';
const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default };
const { profileProviderServices } = createContextAwarenessMocks();
const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(profileProviderServices);
const dataSourceProfileProvider = createKubernetesContainerLogsDataSourceProfileProvider(
logsDataSourceProfileProvider
);
describe('createKubernetesContainerLogsDataSourceProfileProvider', () => {
it('should match a valid index pattern', async () => {
const result = await dataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: 'FROM logs-kubernetes.container_logs-*' },
});
expect(result).toEqual({ isMatch: true, context: { category: DataSourceCategory.Logs } });
});
it('should not match an invalid index pattern', async () => {
const result = await dataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: 'FROM logs-kubernetes.access_logs-*' },
});
expect(result).toEqual({ isMatch: false });
});
it('should return default app state', () => {
const getDefaultAppState = dataSourceProfileProvider.profile.getDefaultAppState?.(() => ({}));
expect(getDefaultAppState?.({ dataView: dataViewWithTimefieldMock })).toEqual({
columns: [
{ name: 'timestamp', width: 212 },
{ name: 'log.level', width: 150 },
{ name: 'kubernetes.pod.name', width: 200 },
{ name: 'kubernetes.namespace', width: 200 },
{ name: 'orchestrator.cluster.name', width: 200 },
{ name: 'message' },
],
});
});
});

View file

@ -0,0 +1,32 @@
/*
* 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 { DataSourceProfileProvider } from '../../../profiles';
import { extendProfileProvider } from '../../extend_profile_provider';
import { createGetDefaultAppState } from '../accessors';
import { LOG_LEVEL_COLUMN, MESSAGE_COLUMN } from '../consts';
import { createResolve } from './create_resolve';
export const createKubernetesContainerLogsDataSourceProfileProvider = (
logsDataSourceProfileProvider: DataSourceProfileProvider
): DataSourceProfileProvider =>
extendProfileProvider(logsDataSourceProfileProvider, {
profileId: 'kubernetes-container-logs-data-source',
profile: {
getDefaultAppState: createGetDefaultAppState({
defaultColumns: [
LOG_LEVEL_COLUMN,
{ name: 'kubernetes.pod.name', width: 200 },
{ name: 'kubernetes.namespace', width: 200 },
{ name: 'orchestrator.cluster.name', width: 200 },
MESSAGE_COLUMN,
],
}),
},
resolve: createResolve('logs-kubernetes.container_logs'),
});

View file

@ -0,0 +1,55 @@
/*
* 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 { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield';
import { createEsqlDataSource } from '../../../../../common/data_sources';
import { DataSourceCategory, RootContext, SolutionType } from '../../../profiles';
import { createContextAwarenessMocks } from '../../../__mocks__';
import { createLogsDataSourceProfileProvider } from '../profile';
import { createNginxAccessLogsDataSourceProfileProvider } from './nginx_access_logs';
const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default };
const { profileProviderServices } = createContextAwarenessMocks();
const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(profileProviderServices);
const dataSourceProfileProvider = createNginxAccessLogsDataSourceProfileProvider(
logsDataSourceProfileProvider
);
describe('createNginxAccessLogsDataSourceProfileProvider', () => {
it('should match a valid index pattern', async () => {
const result = await dataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: 'FROM logs-nginx.access-*' },
});
expect(result).toEqual({ isMatch: true, context: { category: DataSourceCategory.Logs } });
});
it('should not match an invalid index pattern', async () => {
const result = await dataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: 'FROM logs-nginx.error-*' },
});
expect(result).toEqual({ isMatch: false });
});
it('should return default app state', () => {
const getDefaultAppState = dataSourceProfileProvider.profile.getDefaultAppState?.(() => ({}));
expect(getDefaultAppState?.({ dataView: dataViewWithTimefieldMock })).toEqual({
columns: [
{ name: 'timestamp', width: 212 },
{ name: 'url.path', width: 150 },
{ name: 'http.response.status_code', width: 200 },
{ name: 'client.ip', width: 150 },
{ name: 'host.name', width: 250 },
{ name: 'message' },
],
});
});
});

View file

@ -0,0 +1,32 @@
/*
* 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 { DataSourceProfileProvider } from '../../../profiles';
import { extendProfileProvider } from '../../extend_profile_provider';
import { createGetDefaultAppState } from '../accessors';
import { CLIENT_IP_COLUMN, HOST_NAME_COLUMN, MESSAGE_COLUMN } from '../consts';
import { createResolve } from './create_resolve';
export const createNginxAccessLogsDataSourceProfileProvider = (
logsDataSourceProfileProvider: DataSourceProfileProvider
): DataSourceProfileProvider =>
extendProfileProvider(logsDataSourceProfileProvider, {
profileId: 'nginx-access-logs-data-source',
profile: {
getDefaultAppState: createGetDefaultAppState({
defaultColumns: [
{ name: 'url.path', width: 150 },
{ name: 'http.response.status_code', width: 200 },
CLIENT_IP_COLUMN,
HOST_NAME_COLUMN,
MESSAGE_COLUMN,
],
}),
},
resolve: createResolve('logs-nginx.access'),
});

View file

@ -0,0 +1,52 @@
/*
* 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 { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield';
import { createEsqlDataSource } from '../../../../../common/data_sources';
import { DataSourceCategory, RootContext, SolutionType } from '../../../profiles';
import { createContextAwarenessMocks } from '../../../__mocks__';
import { createLogsDataSourceProfileProvider } from '../profile';
import { createNginxErrorLogsDataSourceProfileProvider } from './nginx_error_logs';
const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default };
const { profileProviderServices } = createContextAwarenessMocks();
const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(profileProviderServices);
const dataSourceProfileProvider = createNginxErrorLogsDataSourceProfileProvider(
logsDataSourceProfileProvider
);
describe('createNginxErrorLogsDataSourceProfileProvider', () => {
it('should match a valid index pattern', async () => {
const result = await dataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: 'FROM logs-nginx.error-*' },
});
expect(result).toEqual({ isMatch: true, context: { category: DataSourceCategory.Logs } });
});
it('should not match an invalid index pattern', async () => {
const result = await dataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: 'FROM logs-nginx.access-*' },
});
expect(result).toEqual({ isMatch: false });
});
it('should return default app state', () => {
const getDefaultAppState = dataSourceProfileProvider.profile.getDefaultAppState?.(() => ({}));
expect(getDefaultAppState?.({ dataView: dataViewWithTimefieldMock })).toEqual({
columns: [
{ name: 'timestamp', width: 212 },
{ name: 'log.level', width: 150 },
{ name: 'message' },
],
});
});
});

View file

@ -0,0 +1,26 @@
/*
* 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 { DataSourceProfileProvider } from '../../../profiles';
import { extendProfileProvider } from '../../extend_profile_provider';
import { createGetDefaultAppState } from '../accessors';
import { LOG_LEVEL_COLUMN, MESSAGE_COLUMN } from '../consts';
import { createResolve } from './create_resolve';
export const createNginxErrorLogsDataSourceProfileProvider = (
logsDataSourceProfileProvider: DataSourceProfileProvider
): DataSourceProfileProvider =>
extendProfileProvider(logsDataSourceProfileProvider, {
profileId: 'nginx-error-logs-data-source',
profile: {
getDefaultAppState: createGetDefaultAppState({
defaultColumns: [LOG_LEVEL_COLUMN, MESSAGE_COLUMN],
}),
},
resolve: createResolve('logs-nginx.error'),
});

View file

@ -0,0 +1,54 @@
/*
* 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 { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield';
import { createEsqlDataSource } from '../../../../../common/data_sources';
import { DataSourceCategory, RootContext, SolutionType } from '../../../profiles';
import { createContextAwarenessMocks } from '../../../__mocks__';
import { createLogsDataSourceProfileProvider } from '../profile';
import { createSystemLogsDataSourceProfileProvider } from './system_logs';
const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default };
const { profileProviderServices } = createContextAwarenessMocks();
const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(profileProviderServices);
const dataSourceProfileProvider = createSystemLogsDataSourceProfileProvider(
logsDataSourceProfileProvider
);
describe('createSystemLogsDataSourceProfileProvider', () => {
it('should match a valid index pattern', async () => {
const result = await dataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: 'FROM logs-system.syslog-*' },
});
expect(result).toEqual({ isMatch: true, context: { category: DataSourceCategory.Logs } });
});
it('should not match an invalid index pattern', async () => {
const result = await dataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: 'FROM logs-notsystem.syslog-*' },
});
expect(result).toEqual({ isMatch: false });
});
it('should return default app state', () => {
const getDefaultAppState = dataSourceProfileProvider.profile.getDefaultAppState?.(() => ({}));
expect(getDefaultAppState?.({ dataView: dataViewWithTimefieldMock })).toEqual({
columns: [
{ name: 'timestamp', width: 212 },
{ name: 'log.level', width: 150 },
{ name: 'process.name', width: 150 },
{ name: 'host.name', width: 250 },
{ name: 'message' },
],
});
});
});

View file

@ -0,0 +1,31 @@
/*
* 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 { DataSourceProfileProvider } from '../../../profiles';
import { extendProfileProvider } from '../../extend_profile_provider';
import { createGetDefaultAppState } from '../accessors';
import { HOST_NAME_COLUMN, LOG_LEVEL_COLUMN, MESSAGE_COLUMN } from '../consts';
import { createResolve } from './create_resolve';
export const createSystemLogsDataSourceProfileProvider = (
logsDataSourceProfileProvider: DataSourceProfileProvider
): DataSourceProfileProvider =>
extendProfileProvider(logsDataSourceProfileProvider, {
profileId: 'system-logs-data-source',
profile: {
getDefaultAppState: createGetDefaultAppState({
defaultColumns: [
LOG_LEVEL_COLUMN,
{ name: 'process.name', width: 150 },
HOST_NAME_COLUMN,
MESSAGE_COLUMN,
],
}),
},
resolve: createResolve('logs-system'),
});

View file

@ -0,0 +1,53 @@
/*
* 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 { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield';
import { createEsqlDataSource } from '../../../../../common/data_sources';
import { DataSourceCategory, RootContext, SolutionType } from '../../../profiles';
import { createContextAwarenessMocks } from '../../../__mocks__';
import { createLogsDataSourceProfileProvider } from '../profile';
import { createWindowsLogsDataSourceProfileProvider } from './windows_logs';
const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default };
const { profileProviderServices } = createContextAwarenessMocks();
const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(profileProviderServices);
const dataSourceProfileProvider = createWindowsLogsDataSourceProfileProvider(
logsDataSourceProfileProvider
);
describe('createWindowsLogsDataSourceProfileProvider', () => {
it('should match a valid index pattern', async () => {
const result = await dataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: 'FROM logs-windows.powershell-*' },
});
expect(result).toEqual({ isMatch: true, context: { category: DataSourceCategory.Logs } });
});
it('should not match an invalid index pattern', async () => {
const result = await dataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: 'FROM logs-notwindows.powershell-*' },
});
expect(result).toEqual({ isMatch: false });
});
it('should return default app state', () => {
const getDefaultAppState = dataSourceProfileProvider.profile.getDefaultAppState?.(() => ({}));
expect(getDefaultAppState?.({ dataView: dataViewWithTimefieldMock })).toEqual({
columns: [
{ name: 'timestamp', width: 212 },
{ name: 'log.level', width: 150 },
{ name: 'host.name', width: 250 },
{ name: 'message' },
],
});
});
});

View file

@ -0,0 +1,26 @@
/*
* 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 { DataSourceProfileProvider } from '../../../profiles';
import { extendProfileProvider } from '../../extend_profile_provider';
import { createGetDefaultAppState } from '../accessors';
import { HOST_NAME_COLUMN, LOG_LEVEL_COLUMN, MESSAGE_COLUMN } from '../consts';
import { createResolve } from './create_resolve';
export const createWindowsLogsDataSourceProfileProvider = (
logsDataSourceProfileProvider: DataSourceProfileProvider
): DataSourceProfileProvider =>
extendProfileProvider(logsDataSourceProfileProvider, {
profileId: 'windows-logs-data-source',
profile: {
getDefaultAppState: createGetDefaultAppState({
defaultColumns: [LOG_LEVEL_COLUMN, HOST_NAME_COLUMN, MESSAGE_COLUMN],
}),
},
resolve: createResolve('logs-windows'),
});

View file

@ -10,15 +10,19 @@ import { uniq } from 'lodash';
import type {
DataSourceProfileService,
DocumentProfileService,
RootProfileProvider,
RootProfileService,
} from '../profiles';
import type { BaseProfileProvider, BaseProfileService } from '../profile_service';
import { exampleDataSourceProfileProvider } from './example_data_source_profile';
import { exampleDocumentProfileProvider } from './example_document_profile';
import { exampleRootProfileProvider } from './example_root_pofile';
import { createLogsDataSourceProfileProvider } from './logs_data_source_profile';
import { createLogsDataSourceProfileProviders } from './logs_data_source_profile';
import { createLogDocumentProfileProvider } from './log_document_profile';
import { createProfileProviderServices } from './profile_provider_services';
import {
createProfileProviderServices,
ProfileProviderServices,
} from './profile_provider_services';
export const registerProfileProviders = ({
rootProfileService,
@ -32,35 +36,31 @@ export const registerProfileProviders = ({
experimentalProfileIds: string[];
}) => {
const providerServices = createProfileProviderServices();
const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(providerServices);
const logsDocumentProfileProvider = createLogDocumentProfileProvider(providerServices);
const rootProfileProviders = [exampleRootProfileProvider];
const dataSourceProfileProviders = [
exampleDataSourceProfileProvider,
logsDataSourceProfileProvider,
];
const documentProfileProviders = [exampleDocumentProfileProvider, logsDocumentProfileProvider];
const rootProfileProviders = createRootProfileProviders(providerServices);
const dataSourceProfileProviders = createDataSourceProfileProviders(providerServices);
const documentProfileProviders = createDocumentProfileProviders(providerServices);
const enabledProfileIds = uniq([
logsDataSourceProfileProvider.profileId,
logsDocumentProfileProvider.profileId,
...extractProfileIds(rootProfileProviders),
...extractProfileIds(dataSourceProfileProviders),
...extractProfileIds(documentProfileProviders),
...experimentalProfileIds,
]);
registerEnabledProfileProviders({
profileService: rootProfileService,
availableProviders: rootProfileProviders,
availableProviders: [exampleRootProfileProvider, ...rootProfileProviders],
enabledProfileIds,
});
registerEnabledProfileProviders({
profileService: dataSourceProfileService,
availableProviders: dataSourceProfileProviders,
availableProviders: [exampleDataSourceProfileProvider, ...dataSourceProfileProviders],
enabledProfileIds,
});
registerEnabledProfileProviders({
profileService: documentProfileService,
availableProviders: documentProfileProviders,
availableProviders: [exampleDocumentProfileProvider, ...documentProfileProviders],
enabledProfileIds,
});
};
@ -77,9 +77,23 @@ export const registerEnabledProfileProviders = <
availableProviders: TProvider[];
enabledProfileIds: string[];
}) => {
for (const profile of availableProviders) {
if (enabledProfileIds.includes(profile.profileId)) {
profileService.registerProvider(profile);
for (const provider of availableProviders) {
if (enabledProfileIds.includes(provider.profileId)) {
profileService.registerProvider(provider);
}
}
};
const extractProfileIds = (providers: Array<BaseProfileProvider<{}>>) =>
providers.map(({ profileId }) => profileId);
const createRootProfileProviders = (_providerServices: ProfileProviderServices) =>
[] as RootProfileProvider[];
const createDataSourceProfileProviders = (providerServices: ProfileProviderServices) => [
...createLogsDataSourceProfileProviders(providerServices),
];
const createDocumentProfileProviders = (providerServices: ProfileProviderServices) => [
createLogDocumentProfileProvider(providerServices),
];

View file

@ -17,7 +17,7 @@ export enum DocumentType {
Default = 'default',
}
export type DocumentProfile = Omit<Profile, 'getCellRenderers'>;
export type DocumentProfile = Pick<Profile, 'getDocViewer'>;
export interface DocumentProfileProviderParams {
rootContext: RootContext;

View file

@ -24,9 +24,24 @@ export interface RowIndicatorExtensionParams {
dataView: DataView;
}
export interface DefaultAppStateColumn {
name: string;
width?: number;
}
export interface DefaultAppStateExtensionParams {
dataView: DataView;
}
export interface DefaultAppStateExtension {
columns?: DefaultAppStateColumn[];
rowHeight?: number;
}
export interface Profile {
getCellRenderers: () => CustomCellRenderer;
getDocViewer: (params: DocViewerExtensionParams) => DocViewerExtension;
getDefaultAppState: (params: DefaultAppStateExtensionParams) => DefaultAppStateExtension;
getRowIndicatorProvider: (
params: RowIndicatorExtensionParams
) => UnifiedDataTableProps['getRowIndicator'] | undefined;

View file

@ -24,8 +24,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from my-example-* | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp');
@ -43,8 +43,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from my-example-logs | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp');
@ -66,8 +66,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from my-example-* | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle({ rowIndex: 0 });
@ -82,8 +82,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from my-example-logs | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle({ rowIndex: 0 });
@ -98,7 +98,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('data view mode', () => {
describe('cell renderers', () => {
it('should render custom @timestamp but not custom log.level', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-*');
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp');
@ -112,7 +114,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should render custom @timestamp and custom log.level', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-logs');
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp');
@ -130,7 +134,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('doc viewer extension', () => {
it('should not render custom doc viewer view', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-*');
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle({ rowIndex: 0 });
@ -141,7 +147,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should render custom doc viewer view', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-logs');
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle({ rowIndex: 0 });

View file

@ -23,8 +23,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from my-example-* | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp');
@ -38,7 +38,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('data view mode', () => {
describe('cell renderers', () => {
it('should render custom @timestamp', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-*');
await PageObjects.discover.waitUntilSearchingHasFinished();
const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp');

View file

@ -35,8 +35,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
esql: 'from my-example-logs,logstash* | sort @timestamp desc | where `log.level` is not null',
},
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level');
@ -55,8 +55,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
esql: 'from my-example* | sort @timestamp desc | where `log.level` is not null',
},
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level');
@ -68,7 +68,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('data view mode', () => {
it('should render log.level badge cell', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-logs,logstash*');
await queryBar.setQuery('log.level:*');
await queryBar.submitQuery();
@ -83,7 +85,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it("should not render log.level badge cell if it's not a logs data source", async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-*');
await queryBar.setQuery('log.level:*');
await queryBar.submitQuery();

View file

@ -0,0 +1,206 @@
/*
* 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 expect from '@kbn/expect';
import kbnRison from '@kbn/rison';
import type { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'discover', 'unifiedFieldList']);
const dataViews = getService('dataViews');
const dataGrid = getService('dataGrid');
const queryBar = getService('queryBar');
const monacoEditor = getService('monacoEditor');
const testSubjects = getService('testSubjects');
const kibanaServer = getService('kibanaServer');
describe('extension getDefaultAppState', () => {
afterEach(async () => {
await kibanaServer.uiSettings.unset('defaultColumns');
});
describe('ES|QL mode', () => {
it('should render default columns and row height', async () => {
const state = kbnRison.encode({
dataSource: { type: 'esql' },
query: {
esql: 'from my-example-logs',
},
});
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
const columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message']);
await dataGrid.clickGridSettings();
const rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
const rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(5);
});
it('should render default columns and row height when switching index patterns', async () => {
const state = kbnRison.encode({
dataSource: { type: 'esql' },
query: {
esql: 'from my-example-*',
},
});
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
let columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'Document']);
await dataGrid.clickGridSettings();
let rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
let rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(3);
await monacoEditor.setCodeEditorValue('from my-example-logs');
await queryBar.clickQuerySubmitButton();
await PageObjects.discover.waitUntilSearchingHasFinished();
columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message']);
await dataGrid.clickGridSettings();
rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(5);
});
it('should reset default columns and row height when clicking "New"', async () => {
const state = kbnRison.encode({
dataSource: { type: 'esql' },
query: {
esql: 'from my-example-logs',
},
});
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemRemove('log.level');
await PageObjects.unifiedFieldList.clickFieldListItemRemove('message');
let columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'Document']);
await dataGrid.clickGridSettings();
await dataGrid.changeRowHeightValue('Single');
let rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Single');
await testSubjects.click('discoverNewButton');
await PageObjects.discover.waitUntilSearchingHasFinished();
columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message']);
await dataGrid.clickGridSettings();
rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
const rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(5);
});
it('should merge and dedup configured default columns with default profile columns', async () => {
await kibanaServer.uiSettings.update({
defaultColumns: ['bad_column', 'data_stream.type', 'message'],
});
const state = kbnRison.encode({
dataSource: { type: 'esql' },
query: {
esql: 'from my-example-logs',
},
});
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
const columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message', 'data_stream.type']);
});
});
describe('data view mode', () => {
it('should render default columns and row height', async () => {
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-logs');
await PageObjects.discover.waitUntilSearchingHasFinished();
const columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message']);
await dataGrid.clickGridSettings();
const rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
const rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(5);
});
it('should render default columns and row height when switching data views', async () => {
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-*');
await PageObjects.discover.waitUntilSearchingHasFinished();
let columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'Document']);
await dataGrid.clickGridSettings();
let rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
let rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(3);
await dataViews.switchTo('my-example-logs');
await PageObjects.discover.waitUntilSearchingHasFinished();
columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message']);
await dataGrid.clickGridSettings();
rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(5);
});
it('should reset default columns and row height when clicking "New"', async () => {
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-logs');
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemRemove('log.level');
await PageObjects.unifiedFieldList.clickFieldListItemRemove('message');
let columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'Document']);
await dataGrid.clickGridSettings();
await dataGrid.changeRowHeightValue('Single');
let rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Single');
await testSubjects.click('discoverNewButton');
await PageObjects.discover.waitUntilSearchingHasFinished();
columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message']);
await dataGrid.clickGridSettings();
rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
const rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(5);
});
it('should merge and dedup configured default columns with default profile columns', async () => {
await kibanaServer.uiSettings.update({
defaultColumns: ['bad_column', 'data_stream.type', 'message'],
});
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-logs');
await PageObjects.discover.waitUntilSearchingHasFinished();
const columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message', 'data_stream.type']);
});
});
});
}

View file

@ -22,8 +22,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from my-example-logs | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle();
@ -38,8 +38,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from my-example-metrics | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle();
@ -50,7 +50,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('data view mode', () => {
it('should render logs overview tab for logs data source', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-logs');
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle();
@ -61,7 +63,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should not render logs overview tab for non-logs data source', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-metrics');
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle();

View file

@ -31,8 +31,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from logstash* | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.timePicker.setDefaultAbsoluteRange();
@ -51,8 +51,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from my-example* | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
// my-example* has a log.level field, but it's not matching the logs profile, so the color indicator should not be rendered
@ -67,8 +67,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
esql: 'from my-example-logs,logstash* | sort @timestamp desc | where `log.level` is not null',
},
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
// in this case it's matching the logs data source profile and has a log.level field, so the color indicator should be rendered

View file

@ -38,5 +38,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid
loadTestFile(require.resolve('./extensions/_get_row_indicator_provider'));
loadTestFile(require.resolve('./extensions/_get_doc_viewer'));
loadTestFile(require.resolve('./extensions/_get_cell_renderers'));
loadTestFile(require.resolve('./extensions/_get_default_app_state'));
});
}

View file

@ -469,6 +469,13 @@ export class DataGridService extends FtrService {
return value;
}
public async getCustomRowHeightNumber(scope: 'row' | 'header' = 'row') {
const input = await this.testSubjects.find(
`unifiedDataTable${scope === 'header' ? 'Header' : ''}RowHeightSettings_lineCountNumber`
);
return Number(await input.getAttribute('value'));
}
public async changeRowHeightValue(newValue: string) {
const buttonGroup = await this.testSubjects.find(
'unifiedDataTableRowHeightSettings_rowHeightButtonGroup'

View file

@ -125,7 +125,7 @@ export const useDiscoverInTimelineActions = (
newSavedSearchId
);
const savedSearchState = savedSearch ? getAppStateFromSavedSearch(savedSearch) : null;
discoverStateContainer.current?.appState.initAndSync(savedSearch);
discoverStateContainer.current?.appState.initAndSync();
await discoverStateContainer.current?.appState.replaceUrlState(
savedSearchState?.appState ?? {}
);

View file

@ -33,8 +33,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from my-example-* | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp');
@ -50,8 +50,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from my-example-logs | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp');
@ -71,8 +71,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from my-example-* | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle({ rowIndex: 0 });
@ -87,8 +87,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from my-example-logs | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle({ rowIndex: 0 });
@ -103,7 +103,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('data view mode', () => {
describe('cell renderers', () => {
it('should not render custom @timestamp or log.level', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-*');
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp');
@ -115,7 +117,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should not render custom @timestamp but should render custom log.level', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-logs');
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp');
@ -131,7 +135,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('doc viewer extension', () => {
it('should not render custom doc viewer view', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-*');
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle({ rowIndex: 0 });
@ -142,7 +148,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should render custom doc viewer view', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-logs');
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle({ rowIndex: 0 });

View file

@ -16,8 +16,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('root profile', () => {
before(async () => {
await PageObjects.svlCommonPage.loginAsViewer();
await PageObjects.svlCommonPage.loginAsAdmin();
});
describe('ES|QL mode', () => {
describe('cell renderers', () => {
it('should not render custom @timestamp', async () => {
@ -25,8 +26,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from my-example-* | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp', 2500);
@ -38,7 +39,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('data view mode', () => {
describe('cell renderers', () => {
it('should not render custom @timestamp', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-*');
await PageObjects.discover.waitUntilSearchingHasFinished();
const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp', 2500);

View file

@ -10,7 +10,7 @@ import expect from '@kbn/expect';
import type { FtrProviderContext } from '../../../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'discover', 'unifiedFieldList']);
const PageObjects = getPageObjects(['common', 'discover', 'unifiedFieldList', 'svlCommonPage']);
const esArchiver = getService('esArchiver');
const testSubjects = getService('testSubjects');
const dataGrid = getService('dataGrid');
@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('extension getCellRenderers', () => {
before(async () => {
await PageObjects.svlCommonPage.loginAsAdmin();
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
});
@ -34,8 +35,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
esql: 'from my-example-logs,logstash* | sort @timestamp desc | where `log.level` is not null',
},
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level');
@ -54,8 +55,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
esql: 'from my-example* | sort @timestamp desc | where `log.level` is not null',
},
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level');
@ -67,7 +68,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('data view mode', () => {
it('should render log.level badge cell', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-logs,logstash*');
await queryBar.setQuery('log.level:*');
await queryBar.submitQuery();
@ -82,7 +85,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it("should not render log.level badge cell if it's not a logs data source", async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-*');
await queryBar.setQuery('log.level:*');
await queryBar.submitQuery();

View file

@ -0,0 +1,209 @@
/*
* 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 expect from '@kbn/expect';
import kbnRison from '@kbn/rison';
import type { FtrProviderContext } from '../../../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'discover', 'svlCommonPage', 'unifiedFieldList']);
const dataViews = getService('dataViews');
const dataGrid = getService('dataGrid');
const queryBar = getService('queryBar');
const monacoEditor = getService('monacoEditor');
const testSubjects = getService('testSubjects');
const kibanaServer = getService('kibanaServer');
describe('extension getDefaultAppState', () => {
before(async () => {
await PageObjects.svlCommonPage.loginAsAdmin();
});
afterEach(async () => {
await kibanaServer.uiSettings.unset('defaultColumns');
});
describe('ES|QL mode', () => {
it('should render default columns and row height', async () => {
const state = kbnRison.encode({
dataSource: { type: 'esql' },
query: {
esql: 'from my-example-logs',
},
});
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
const columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message']);
await dataGrid.clickGridSettings();
const rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
const rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(5);
});
it('should render default columns and row height when switching index patterns', async () => {
const state = kbnRison.encode({
dataSource: { type: 'esql' },
query: {
esql: 'from my-example-*',
},
});
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
let columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'Document']);
await dataGrid.clickGridSettings();
let rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
let rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(3);
await monacoEditor.setCodeEditorValue('from my-example-logs');
await queryBar.clickQuerySubmitButton();
await PageObjects.discover.waitUntilSearchingHasFinished();
columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message']);
await dataGrid.clickGridSettings();
rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(5);
});
it('should reset default columns and row height when clicking "New"', async () => {
const state = kbnRison.encode({
dataSource: { type: 'esql' },
query: {
esql: 'from my-example-logs',
},
});
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemRemove('log.level');
await PageObjects.unifiedFieldList.clickFieldListItemRemove('message');
let columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'Document']);
await dataGrid.clickGridSettings();
await dataGrid.changeRowHeightValue('Single');
let rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Single');
await testSubjects.click('discoverNewButton');
await PageObjects.discover.waitUntilSearchingHasFinished();
columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message']);
await dataGrid.clickGridSettings();
rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
const rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(5);
});
it('should merge and dedup configured default columns with default profile columns', async () => {
await kibanaServer.uiSettings.update({
defaultColumns: ['bad_column', 'data_stream.type', 'message'],
});
const state = kbnRison.encode({
dataSource: { type: 'esql' },
query: {
esql: 'from my-example-logs',
},
});
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
const columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message', 'data_stream.type']);
});
});
describe('data view mode', () => {
it('should render default columns and row height', async () => {
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-logs');
await PageObjects.discover.waitUntilSearchingHasFinished();
const columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message']);
await dataGrid.clickGridSettings();
const rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
const rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(5);
});
it('should render default columns and row height when switching data views', async () => {
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-*');
await PageObjects.discover.waitUntilSearchingHasFinished();
let columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'Document']);
await dataGrid.clickGridSettings();
let rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
let rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(3);
await dataViews.switchTo('my-example-logs');
await PageObjects.discover.waitUntilSearchingHasFinished();
columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message']);
await dataGrid.clickGridSettings();
rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(5);
});
it('should reset default columns and row height when clicking "New"', async () => {
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-logs');
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.clickFieldListItemRemove('log.level');
await PageObjects.unifiedFieldList.clickFieldListItemRemove('message');
let columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'Document']);
await dataGrid.clickGridSettings();
await dataGrid.changeRowHeightValue('Single');
let rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Single');
await testSubjects.click('discoverNewButton');
await PageObjects.discover.waitUntilSearchingHasFinished();
columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message']);
await dataGrid.clickGridSettings();
rowHeightValue = await dataGrid.getCurrentRowHeightValue();
expect(rowHeightValue).to.be('Custom');
const rowHeightNumber = await dataGrid.getCustomRowHeightNumber();
expect(rowHeightNumber).to.be(5);
});
it('should merge and dedup configured default columns with default profile columns', async () => {
await kibanaServer.uiSettings.update({
defaultColumns: ['bad_column', 'data_stream.type', 'message'],
});
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-logs');
await PageObjects.discover.waitUntilSearchingHasFinished();
const columns = await PageObjects.discover.getColumnHeaders();
expect(columns).to.eql(['@timestamp', 'log.level', 'message', 'data_stream.type']);
});
});
});
}

View file

@ -18,14 +18,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
before(async () => {
await PageObjects.svlCommonPage.loginAsAdmin();
});
describe('ES|QL mode', () => {
it('should render logs overview tab for logs data source', async () => {
const state = kbnRison.encode({
dataSource: { type: 'esql' },
query: { esql: 'from my-example-logs | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle();
@ -40,8 +41,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from my-example-metrics | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle();
@ -52,7 +53,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('data view mode', () => {
it('should render logs overview tab for logs data source', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-logs');
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle();
@ -63,7 +66,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should not render logs overview tab for non-logs data source', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.switchTo('my-example-metrics');
await PageObjects.discover.waitUntilSearchingHasFinished();
await dataGrid.clickRowToggle();

View file

@ -37,8 +37,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from logstash* | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.timePicker.setDefaultAbsoluteRange();
@ -57,8 +57,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
dataSource: { type: 'esql' },
query: { esql: 'from my-example* | sort @timestamp desc' },
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
// my-example* has a log.level field, but it's not matching the logs profile, so the color indicator should not be rendered
@ -73,8 +73,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
esql: 'from my-example-logs,logstash* | sort @timestamp desc | where `log.level` is not null',
},
});
await PageObjects.common.navigateToApp('discover', {
hash: `/?_a=${state}`,
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
// in this case it's matching the logs data source profile and has a log.level field, so the color indicator should be rendered

View file

@ -40,5 +40,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid
loadTestFile(require.resolve('./extensions/_get_row_indicator_provider'));
loadTestFile(require.resolve('./extensions/_get_doc_viewer'));
loadTestFile(require.resolve('./extensions/_get_cell_renderers'));
loadTestFile(require.resolve('./extensions/_get_default_app_state'));
});
}