diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx
index 98ce22cd14227..5d33a08557fed 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx
@@ -22,10 +22,10 @@ import {
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
- EuiPanel,
- EuiText,
+ EuiTitle,
EuiSpacer,
EuiLoadingSpinner,
+ EuiHorizontalRule,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ensureMinimumTime, extractTimeFields } from '../../lib';
@@ -43,6 +43,7 @@ interface StepTimeFieldProps {
goToPreviousStep: () => void;
createIndexPattern: (selectedTimeField: string | undefined, indexPatternId: string) => void;
indexPatternCreationType: IndexPatternCreationConfig;
+ selectedTimeField?: string;
}
interface StepTimeFieldState {
@@ -69,7 +70,7 @@ export class StepTimeField extends Component
-
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
);
}
@@ -236,7 +242,7 @@ export class StepTimeField extends Component
+ <>
-
+
-
+ >
);
}
}
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx
index 111be41cfc53a..cd76ca09ccb74 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx
@@ -19,10 +19,16 @@
import React, { ReactElement, Component } from 'react';
-import { EuiGlobalToastList, EuiGlobalToastListToast, EuiPanel } from '@elastic/eui';
+import {
+ EuiGlobalToastList,
+ EuiGlobalToastListToast,
+ EuiPageContent,
+ EuiHorizontalRule,
+} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { withRouter, RouteComponentProps } from 'react-router-dom';
+import { DocLinksStart } from 'src/core/public';
import { StepIndexPattern } from './components/step_index_pattern';
import { StepTimeField } from './components/step_time_field';
import { Header } from './components/header';
@@ -31,21 +37,21 @@ import { EmptyState } from './components/empty_state';
import { context as contextType } from '../../../../kibana_react/public';
import { getCreateBreadcrumbs } from '../breadcrumbs';
-import { MAX_SEARCH_SIZE } from './constants';
import { ensureMinimumTime, getIndices } from './lib';
import { IndexPatternCreationConfig } from '../..';
import { IndexPatternManagmentContextValue } from '../../types';
-import { MatchedIndex } from './types';
+import { MatchedItem } from './types';
interface CreateIndexPatternWizardState {
step: number;
indexPattern: string;
- allIndices: MatchedIndex[];
+ allIndices: MatchedItem[];
remoteClustersExist: boolean;
isInitiallyLoadingIndices: boolean;
- isIncludingSystemIndices: boolean;
toasts: EuiGlobalToastListToast[];
indexPatternCreationType: IndexPatternCreationConfig;
+ selectedTimeField?: string;
+ docLinks: DocLinksStart;
}
export class CreateIndexPatternWizard extends Component<
@@ -69,9 +75,9 @@ export class CreateIndexPatternWizard extends Component<
allIndices: [],
remoteClustersExist: false,
isInitiallyLoadingIndices: true,
- isIncludingSystemIndices: false,
toasts: [],
indexPatternCreationType: context.services.indexPatternManagementStart.creation.getType(type),
+ docLinks: context.services.docLinks,
};
}
@@ -80,7 +86,7 @@ export class CreateIndexPatternWizard extends Component<
}
catchAndWarn = async (
- asyncFn: Promise,
+ asyncFn: Promise,
errorValue: [] | string[],
errorMsg: ReactElement
) => {
@@ -102,12 +108,6 @@ export class CreateIndexPatternWizard extends Component<
};
fetchData = async () => {
- this.setState({
- allIndices: [],
- isInitiallyLoadingIndices: true,
- remoteClustersExist: false,
- });
-
const indicesFailMsg = (
+ ).then((allIndices: MatchedItem[]) =>
this.setState({ allIndices, isInitiallyLoadingIndices: false })
);
this.catchAndWarn(
// if we get an error from remote cluster query, supply fallback value that allows user entry.
// ['a'] is fallback value
- getIndices(
- this.context.services.data.search.__LEGACY.esClient,
- this.state.indexPatternCreationType,
- `*:*`,
- 1
- ),
+ getIndices(this.context.services.http, this.state.indexPatternCreationType, `*:*`, false),
['a'],
clustersFailMsg
- ).then((remoteIndices: string[] | MatchedIndex[]) =>
+ ).then((remoteIndices: string[] | MatchedItem[]) =>
this.setState({ remoteClustersExist: !!remoteIndices.length })
);
};
@@ -189,7 +179,7 @@ export class CreateIndexPatternWizard extends Component<
if (isConfirmed) {
return history.push(`/patterns/${indexPatternId}`);
} else {
- return false;
+ return;
}
}
@@ -201,31 +191,21 @@ export class CreateIndexPatternWizard extends Component<
history.push(`/patterns/${createdId}`);
};
- goToTimeFieldStep = (indexPattern: string) => {
- this.setState({ step: 2, indexPattern });
+ goToTimeFieldStep = (indexPattern: string, selectedTimeField?: string) => {
+ this.setState({ step: 2, indexPattern, selectedTimeField });
};
goToIndexPatternStep = () => {
this.setState({ step: 1 });
};
- onChangeIncludingSystemIndices = () => {
- this.setState((prevState) => ({
- isIncludingSystemIndices: !prevState.isIncludingSystemIndices,
- }));
- };
-
renderHeader() {
- const { isIncludingSystemIndices } = this.state;
-
return (
);
}
@@ -234,7 +214,6 @@ export class CreateIndexPatternWizard extends Component<
const {
allIndices,
isInitiallyLoadingIndices,
- isIncludingSystemIndices,
step,
indexPattern,
remoteClustersExist,
@@ -244,8 +223,8 @@ export class CreateIndexPatternWizard extends Component<
return ;
}
- const hasDataIndices = allIndices.some(({ name }: MatchedIndex) => !name.startsWith('.'));
- if (!hasDataIndices && !isIncludingSystemIndices && !remoteClustersExist) {
+ const hasDataIndices = allIndices.some(({ name }: MatchedItem) => !name.startsWith('.'));
+ if (!hasDataIndices && !remoteClustersExist) {
return (
+
+ {header}
+
+
+
);
}
if (step === 2) {
return (
-
+
+ {header}
+
+
+
);
}
@@ -290,15 +282,11 @@ export class CreateIndexPatternWizard extends Component<
};
render() {
- const header = this.renderHeader();
const content = this.renderContent();
return (
-
-
- {header}
- {content}
-
+ <>
+ {content}
{
@@ -306,7 +294,7 @@ export class CreateIndexPatternWizard extends Component<
}}
toastLifeTimeMs={6000}
/>
-
+ >
);
}
}
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/__snapshots__/get_indices.test.ts.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/__snapshots__/get_indices.test.ts.snap
new file mode 100644
index 0000000000000..99876383b4343
--- /dev/null
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/__snapshots__/get_indices.test.ts.snap
@@ -0,0 +1,69 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`getIndices response object to item array 1`] = `
+Array [
+ Object {
+ "item": Object {
+ "attributes": Array [
+ "frozen",
+ ],
+ "name": "frozen_index",
+ },
+ "name": "frozen_index",
+ "tags": Array [
+ Object {
+ "color": "default",
+ "key": "index",
+ "name": "Index",
+ },
+ Object {
+ "color": "danger",
+ "key": "frozen",
+ "name": "Frozen",
+ },
+ ],
+ },
+ Object {
+ "item": Object {
+ "indices": Array [],
+ "name": "test_alias",
+ },
+ "name": "test_alias",
+ "tags": Array [
+ Object {
+ "color": "default",
+ "key": "alias",
+ "name": "Alias",
+ },
+ ],
+ },
+ Object {
+ "item": Object {
+ "backing_indices": Array [],
+ "name": "test_data_stream",
+ "timestamp_field": "test_timestamp_field",
+ },
+ "name": "test_data_stream",
+ "tags": Array [
+ Object {
+ "color": "primary",
+ "key": "data_stream",
+ "name": "Data stream",
+ },
+ ],
+ },
+ Object {
+ "item": Object {
+ "name": "test_index",
+ },
+ "name": "test_index",
+ "tags": Array [
+ Object {
+ "color": "default",
+ "key": "index",
+ "name": "Index",
+ },
+ ],
+ },
+]
+`;
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts
index b1faca8a04964..8e4dd37284333 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts
@@ -17,66 +17,31 @@
* under the License.
*/
-import { getIndices } from './get_indices';
+import { getIndices, responseToItemArray } from './get_indices';
import { IndexPatternCreationConfig } from '../../../../../index_pattern_management/public';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { LegacyApiCaller } from '../../../../../data/public/search/legacy';
+import { httpServiceMock } from '../../../../../../core/public/mocks';
+import { ResolveIndexResponseItemIndexAttrs } from '../types';
export const successfulResponse = {
- hits: {
- total: 1,
- max_score: 0.0,
- hits: [],
- },
- aggregations: {
- indices: {
- doc_count_error_upper_bound: 0,
- sum_other_doc_count: 0,
- buckets: [
- {
- key: '1',
- doc_count: 1,
- },
- {
- key: '2',
- doc_count: 1,
- },
- ],
+ indices: [
+ {
+ name: 'remoteCluster1:bar-01',
+ attributes: ['open'],
},
- },
-};
-
-export const exceptionResponse = {
- body: {
- error: {
- root_cause: [
- {
- type: 'index_not_found_exception',
- reason: 'no such index',
- index_uuid: '_na_',
- 'resource.type': 'index_or_alias',
- 'resource.id': 't',
- index: 't',
- },
- ],
- type: 'transport_exception',
- reason: 'unable to communicate with remote cluster [cluster_one]',
- caused_by: {
- type: 'index_not_found_exception',
- reason: 'no such index',
- index_uuid: '_na_',
- 'resource.type': 'index_or_alias',
- 'resource.id': 't',
- index: 't',
- },
+ ],
+ aliases: [
+ {
+ name: 'f-alias',
+ indices: ['freeze-index', 'my-index'],
},
- },
- status: 500,
-};
-
-export const errorResponse = {
- statusCode: 400,
- error: 'Bad Request',
+ ],
+ data_streams: [
+ {
+ name: 'foo',
+ backing_indices: ['foo-000001'],
+ timestamp_field: '@timestamp',
+ },
+ ],
};
const mockIndexPatternCreationType = new IndexPatternCreationConfig({
@@ -87,81 +52,62 @@ const mockIndexPatternCreationType = new IndexPatternCreationConfig({
isBeta: false,
});
-function esClientFactory(search: (params: any) => any): LegacyApiCaller {
- return {
- search,
- msearch: () => ({
- abort: () => {},
- ...new Promise((resolve) => resolve({})),
- }),
- };
-}
-
-const es = esClientFactory(() => successfulResponse);
+const http = httpServiceMock.createStartContract();
+http.get.mockResolvedValue(successfulResponse);
describe('getIndices', () => {
it('should work in a basic case', async () => {
- const result = await getIndices(es, mockIndexPatternCreationType, 'kibana', 1);
- expect(result.length).toBe(2);
- expect(result[0].name).toBe('1');
- expect(result[1].name).toBe('2');
+ const result = await getIndices(http, mockIndexPatternCreationType, 'kibana', false);
+ expect(result.length).toBe(3);
+ expect(result[0].name).toBe('f-alias');
+ expect(result[1].name).toBe('foo');
});
it('should ignore ccs query-all', async () => {
- expect((await getIndices(es, mockIndexPatternCreationType, '*:', 10)).length).toBe(0);
+ expect((await getIndices(http, mockIndexPatternCreationType, '*:', false)).length).toBe(0);
});
it('should ignore a single comma', async () => {
- expect((await getIndices(es, mockIndexPatternCreationType, ',', 10)).length).toBe(0);
- expect((await getIndices(es, mockIndexPatternCreationType, ',*', 10)).length).toBe(0);
- expect((await getIndices(es, mockIndexPatternCreationType, ',foobar', 10)).length).toBe(0);
- });
-
- it('should trim the input', async () => {
- let index;
- const esClient = esClientFactory(
- jest.fn().mockImplementation((params) => {
- index = params.index;
- })
- );
-
- await getIndices(esClient, mockIndexPatternCreationType, 'kibana ', 1);
- expect(index).toBe('kibana');
+ expect((await getIndices(http, mockIndexPatternCreationType, ',', false)).length).toBe(0);
+ expect((await getIndices(http, mockIndexPatternCreationType, ',*', false)).length).toBe(0);
+ expect((await getIndices(http, mockIndexPatternCreationType, ',foobar', false)).length).toBe(0);
});
- it('should use the limit', async () => {
- let limit;
- const esClient = esClientFactory(
- jest.fn().mockImplementation((params) => {
- limit = params.body.aggs.indices.terms.size;
- })
- );
- await getIndices(esClient, mockIndexPatternCreationType, 'kibana', 10);
- expect(limit).toBe(10);
+ it('response object to item array', () => {
+ const result = {
+ indices: [
+ {
+ name: 'test_index',
+ },
+ {
+ name: 'frozen_index',
+ attributes: ['frozen' as ResolveIndexResponseItemIndexAttrs],
+ },
+ ],
+ aliases: [
+ {
+ name: 'test_alias',
+ indices: [],
+ },
+ ],
+ data_streams: [
+ {
+ name: 'test_data_stream',
+ backing_indices: [],
+ timestamp_field: 'test_timestamp_field',
+ },
+ ],
+ };
+ expect(responseToItemArray(result, mockIndexPatternCreationType)).toMatchSnapshot();
+ expect(responseToItemArray({}, mockIndexPatternCreationType)).toEqual([]);
});
describe('errors', () => {
it('should handle errors gracefully', async () => {
- const esClient = esClientFactory(() => errorResponse);
- const result = await getIndices(esClient, mockIndexPatternCreationType, 'kibana', 1);
- expect(result.length).toBe(0);
- });
-
- it('should throw exceptions', async () => {
- const esClient = esClientFactory(() => {
- throw new Error('Fail');
+ http.get.mockImplementationOnce(() => {
+ throw new Error('Test error');
});
-
- await expect(
- getIndices(esClient, mockIndexPatternCreationType, 'kibana', 1)
- ).rejects.toThrow();
- });
-
- it('should handle index_not_found_exception errors gracefully', async () => {
- const esClient = esClientFactory(
- () => new Promise((resolve, reject) => reject(exceptionResponse))
- );
- const result = await getIndices(esClient, mockIndexPatternCreationType, 'kibana', 1);
+ const result = await getIndices(http, mockIndexPatternCreationType, 'kibana', false);
expect(result.length).toBe(0);
});
});
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts
index 9f75dc39a654c..c6a11de1bc4fc 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts
@@ -17,17 +17,31 @@
* under the License.
*/
-import { get, sortBy } from 'lodash';
+import { sortBy } from 'lodash';
+import { HttpStart } from 'kibana/public';
+import { i18n } from '@kbn/i18n';
import { IndexPatternCreationConfig } from '../../../../../index_pattern_management/public';
-import { DataPublicPluginStart } from '../../../../../data/public';
-import { MatchedIndex } from '../types';
+import { MatchedItem, ResolveIndexResponse, ResolveIndexResponseItemIndexAttrs } from '../types';
+
+const aliasLabel = i18n.translate('indexPatternManagement.aliasLabel', { defaultMessage: 'Alias' });
+const dataStreamLabel = i18n.translate('indexPatternManagement.dataStreamLabel', {
+ defaultMessage: 'Data stream',
+});
+
+const indexLabel = i18n.translate('indexPatternManagement.indexLabel', {
+ defaultMessage: 'Index',
+});
+
+const frozenLabel = i18n.translate('indexPatternManagement.frozenLabel', {
+ defaultMessage: 'Frozen',
+});
export async function getIndices(
- es: DataPublicPluginStart['search']['__LEGACY']['esClient'],
+ http: HttpStart,
indexPatternCreationType: IndexPatternCreationConfig,
rawPattern: string,
- limit: number
-): Promise {
+ showAllIndices: boolean
+): Promise {
const pattern = rawPattern.trim();
// Searching for `*:` fails for CCS environments. The search request
@@ -48,54 +62,58 @@ export async function getIndices(
return [];
}
- // We need to always provide a limit and not rely on the default
- if (!limit) {
- throw new Error('`getIndices()` was called without the required `limit` parameter.');
- }
-
- const params = {
- ignoreUnavailable: true,
- index: pattern,
- ignore: [404],
- body: {
- size: 0, // no hits
- aggs: {
- indices: {
- terms: {
- field: '_index',
- size: limit,
- },
- },
- },
- },
- };
+ const query = showAllIndices ? { expand_wildcards: 'all' } : undefined;
try {
- const response = await es.search(params);
- if (!response || response.error || !response.aggregations) {
- return [];
- }
-
- return sortBy(
- response.aggregations.indices.buckets
- .map((bucket: { key: string; doc_count: number }) => {
- return bucket.key;
- })
- .map((indexName: string) => {
- return {
- name: indexName,
- tags: indexPatternCreationType.getIndexTags(indexName),
- };
- }),
- 'name'
+ const response = await http.get(
+ `/internal/index-pattern-management/resolve_index/${pattern}`,
+ { query }
);
- } catch (err) {
- const type = get(err, 'body.error.caused_by.type');
- if (type === 'index_not_found_exception') {
- // This happens in a CSS environment when the controlling node returns a 500 even though the data
- // nodes returned a 404. Remove this when/if this is handled: https://github.com/elastic/elasticsearch/issues/27461
+ if (!response) {
return [];
}
- throw err;
+
+ return responseToItemArray(response, indexPatternCreationType);
+ } catch {
+ return [];
}
}
+
+export const responseToItemArray = (
+ response: ResolveIndexResponse,
+ indexPatternCreationType: IndexPatternCreationConfig
+): MatchedItem[] => {
+ const source: MatchedItem[] = [];
+
+ (response.indices || []).forEach((index) => {
+ const tags: MatchedItem['tags'] = [{ key: 'index', name: indexLabel, color: 'default' }];
+ const isFrozen = (index.attributes || []).includes(ResolveIndexResponseItemIndexAttrs.FROZEN);
+
+ tags.push(...indexPatternCreationType.getIndexTags(index.name));
+ if (isFrozen) {
+ tags.push({ name: frozenLabel, key: 'frozen', color: 'danger' });
+ }
+
+ source.push({
+ name: index.name,
+ tags,
+ item: index,
+ });
+ });
+ (response.aliases || []).forEach((alias) => {
+ source.push({
+ name: alias.name,
+ tags: [{ key: 'alias', name: aliasLabel, color: 'default' }],
+ item: alias,
+ });
+ });
+ (response.data_streams || []).forEach((dataStream) => {
+ source.push({
+ name: dataStream.name,
+ tags: [{ key: 'data_stream', name: dataStreamLabel, color: 'primary' }],
+ item: dataStream,
+ });
+ });
+
+ return sortBy(source, 'name');
+};
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.test.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.test.ts
index 65840aa64046d..c27eaa5ebc99e 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.test.ts
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.test.ts
@@ -18,7 +18,7 @@
*/
import { getMatchedIndices } from './get_matched_indices';
-import { Tag } from '../types';
+import { Tag, MatchedItem } from '../types';
jest.mock('./../constants', () => ({
MAX_NUMBER_OF_MATCHING_INDICES: 6,
@@ -32,18 +32,18 @@ const indices = [
{ name: 'packetbeat', tags },
{ name: 'metricbeat', tags },
{ name: '.kibana', tags },
-];
+] as MatchedItem[];
const partialIndices = [
{ name: 'kibana', tags },
{ name: 'es', tags },
{ name: '.kibana', tags },
-];
+] as MatchedItem[];
const exactIndices = [
{ name: 'kibana', tags },
{ name: '.kibana', tags },
-];
+] as MatchedItem[];
describe('getMatchedIndices', () => {
it('should return all indices', () => {
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.ts
index 7e2eeb17ab387..dbb166597152e 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.ts
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.ts
@@ -33,7 +33,7 @@ function isSystemIndex(index: string): boolean {
return false;
}
-function filterSystemIndices(indices: MatchedIndex[], isIncludingSystemIndices: boolean) {
+function filterSystemIndices(indices: MatchedItem[], isIncludingSystemIndices: boolean) {
if (!indices) {
return indices;
}
@@ -65,12 +65,12 @@ function filterSystemIndices(indices: MatchedIndex[], isIncludingSystemIndices:
We call this `exact` matches because ES is telling us exactly what it matches
*/
-import { MatchedIndex } from '../types';
+import { MatchedItem } from '../types';
export function getMatchedIndices(
- unfilteredAllIndices: MatchedIndex[],
- unfilteredPartialMatchedIndices: MatchedIndex[],
- unfilteredExactMatchedIndices: MatchedIndex[],
+ unfilteredAllIndices: MatchedItem[],
+ unfilteredPartialMatchedIndices: MatchedItem[],
+ unfilteredExactMatchedIndices: MatchedItem[],
isIncludingSystemIndices: boolean = false
) {
const allIndices = filterSystemIndices(unfilteredAllIndices, isIncludingSystemIndices);
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/types.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/types.ts
index 634bbd856ea86..b23924837ffb7 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/types.ts
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/types.ts
@@ -17,12 +17,54 @@
* under the License.
*/
-export interface MatchedIndex {
+export interface MatchedItem {
name: string;
tags: Tag[];
+ item: {
+ name: string;
+ backing_indices?: string[];
+ timestamp_field?: string;
+ indices?: string[];
+ aliases?: string[];
+ attributes?: ResolveIndexResponseItemIndexAttrs[];
+ data_stream?: string;
+ };
+}
+
+export interface ResolveIndexResponse {
+ indices?: ResolveIndexResponseItemIndex[];
+ aliases?: ResolveIndexResponseItemAlias[];
+ data_streams?: ResolveIndexResponseItemDataStream[];
+}
+
+export interface ResolveIndexResponseItem {
+ name: string;
+}
+
+export interface ResolveIndexResponseItemDataStream extends ResolveIndexResponseItem {
+ backing_indices: string[];
+ timestamp_field: string;
+}
+
+export interface ResolveIndexResponseItemAlias extends ResolveIndexResponseItem {
+ indices: string[];
+}
+
+export interface ResolveIndexResponseItemIndex extends ResolveIndexResponseItem {
+ aliases?: string[];
+ attributes?: ResolveIndexResponseItemIndexAttrs[];
+ data_stream?: string;
+}
+
+export enum ResolveIndexResponseItemIndexAttrs {
+ OPEN = 'open',
+ CLOSED = 'closed',
+ HIDDEN = 'hidden',
+ FROZEN = 'frozen',
}
export interface Tag {
name: string;
key: string;
+ color: string;
}
diff --git a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap
index 6bc99c356592e..7a7545580d82a 100644
--- a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap
+++ b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap
@@ -836,7 +836,6 @@ exports[`FieldEditor should show deprecated lang warning 1`] = `
testlang
,
"painlessLink":
,
"scriptsInAggregation":
Please familiarize yourself with
-
-
+
and with
-
-
+
before using scripted fields.
diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts
index 93574cde7dc85..ec8100db42085 100644
--- a/src/plugins/index_pattern_management/public/mocks.ts
+++ b/src/plugins/index_pattern_management/public/mocks.ts
@@ -76,6 +76,13 @@ const createInstance = async () => {
};
};
+const docLinks = {
+ links: {
+ indexPatterns: {},
+ scriptedFields: {},
+ },
+};
+
const createIndexPatternManagmentContext = () => {
const {
chrome,
@@ -84,7 +91,6 @@ const createIndexPatternManagmentContext = () => {
uiSettings,
notifications,
overlays,
- docLinks,
} = coreMock.createStart();
const { http } = coreMock.createSetup();
const data = dataPluginMock.createStartContract();
diff --git a/src/plugins/index_pattern_management/public/service/creation/config.ts b/src/plugins/index_pattern_management/public/service/creation/config.ts
index 95a91fd7594ca..04510b1d64e1e 100644
--- a/src/plugins/index_pattern_management/public/service/creation/config.ts
+++ b/src/plugins/index_pattern_management/public/service/creation/config.ts
@@ -18,7 +18,7 @@
*/
import { i18n } from '@kbn/i18n';
-import { MatchedIndex } from '../../components/create_index_pattern_wizard/types';
+import { MatchedItem } from '../../components/create_index_pattern_wizard/types';
const indexPatternTypeName = i18n.translate(
'indexPatternManagement.editIndexPattern.createIndex.defaultTypeName',
@@ -105,7 +105,7 @@ export class IndexPatternCreationConfig {
return [];
}
- public checkIndicesForErrors(indices: MatchedIndex[]) {
+ public checkIndicesForErrors(indices: MatchedItem[]) {
return undefined;
}
diff --git a/src/plugins/data/public/search/expressions/utils/types.ts b/src/plugins/index_pattern_management/server/index.ts
similarity index 71%
rename from src/plugins/data/public/search/expressions/utils/types.ts
rename to src/plugins/index_pattern_management/server/index.ts
index b2311e664820e..02a4631589832 100644
--- a/src/plugins/data/public/search/expressions/utils/types.ts
+++ b/src/plugins/index_pattern_management/server/index.ts
@@ -17,18 +17,9 @@
* under the License.
*/
-interface InspectorStat {
- label: string;
- value: string;
- description: string;
-}
+import { PluginInitializerContext } from 'src/core/server';
+import { IndexPatternManagementPlugin } from './plugin';
-/** @internal */
-export interface RequestInspectorStats {
- indexPattern?: InspectorStat;
- indexPatternId?: InspectorStat;
- queryTime?: InspectorStat;
- hitsTotal?: InspectorStat;
- hits?: InspectorStat;
- requestTime?: InspectorStat;
+export function plugin(initializerContext: PluginInitializerContext) {
+ return new IndexPatternManagementPlugin(initializerContext);
}
diff --git a/src/plugins/index_pattern_management/server/plugin.ts b/src/plugins/index_pattern_management/server/plugin.ts
new file mode 100644
index 0000000000000..ecca45cbcc453
--- /dev/null
+++ b/src/plugins/index_pattern_management/server/plugin.ts
@@ -0,0 +1,70 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { PluginInitializerContext, CoreSetup, Plugin } from 'src/core/server';
+import { schema } from '@kbn/config-schema';
+
+export class IndexPatternManagementPlugin implements Plugin {
+ constructor(initializerContext: PluginInitializerContext) {}
+
+ public setup(core: CoreSetup) {
+ const router = core.http.createRouter();
+
+ router.get(
+ {
+ path: '/internal/index-pattern-management/resolve_index/{query}',
+ validate: {
+ params: schema.object({
+ query: schema.string(),
+ }),
+ query: schema.object({
+ expand_wildcards: schema.maybe(
+ schema.oneOf([
+ schema.literal('all'),
+ schema.literal('open'),
+ schema.literal('closed'),
+ schema.literal('hidden'),
+ schema.literal('none'),
+ ])
+ ),
+ }),
+ },
+ },
+ async (context, req, res) => {
+ const queryString = req.query.expand_wildcards
+ ? { expand_wildcards: req.query.expand_wildcards }
+ : null;
+ const result = await context.core.elasticsearch.legacy.client.callAsCurrentUser(
+ 'transport.request',
+ {
+ method: 'GET',
+ path: `/_resolve/index/${encodeURIComponent(req.params.query)}${
+ queryString ? '?' + new URLSearchParams(queryString).toString() : ''
+ }`,
+ }
+ );
+ return res.ok({ body: result });
+ }
+ );
+ }
+
+ public start() {}
+
+ public stop() {}
+}
diff --git a/src/plugins/inspector/common/adapters/index.ts b/src/plugins/inspector/common/adapters/index.ts
index 8e1979ab33275..2fc465e7d0b2d 100644
--- a/src/plugins/inspector/common/adapters/index.ts
+++ b/src/plugins/inspector/common/adapters/index.ts
@@ -18,4 +18,4 @@
*/
export { DataAdapter, FormattedData } from './data';
-export { RequestAdapter, RequestStatus } from './request';
+export { RequestAdapter, RequestStatistic, RequestStatistics, RequestStatus } from './request';
diff --git a/src/plugins/inspector/common/adapters/request/index.ts b/src/plugins/inspector/common/adapters/request/index.ts
index 7359c56999a94..5c93757e86d05 100644
--- a/src/plugins/inspector/common/adapters/request/index.ts
+++ b/src/plugins/inspector/common/adapters/request/index.ts
@@ -17,6 +17,5 @@
* under the License.
*/
-export { RequestStatus } from './types';
-
+export { RequestStatistic, RequestStatistics, RequestStatus } from './types';
export { RequestAdapter } from './request_adapter';
diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts
index 2911a9ae75689..e2d6ae647abb1 100644
--- a/src/plugins/kibana_utils/public/index.ts
+++ b/src/plugins/kibana_utils/public/index.ts
@@ -35,7 +35,6 @@ export {
export * from './core';
export * from '../common/errors';
export * from './field_wildcard';
-export * from './parse';
export * from './render_complete';
export * from './resize_checker';
export * from '../common/state_containers';
diff --git a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json
index cdbed7fa06367..470544cf35b30 100644
--- a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json
+++ b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json
@@ -406,6 +406,48 @@
"zh-tw": "國家"
}
},
+ {
+ "layer_id": "world_countries_with_compromised_attribution",
+ "created_at": "2017-04-26T17:12:15.978370",
+ "attribution": [
+ {
+ "label": {
+ "en": "Made with NaturalEarth
"
+ },
+ "url": {
+ "en": "http://www.naturalearthdata.com/about/terms-of-use"
+ }
+ },
+ {
+ "label": {
+ "en": "Elastic Maps Service"
+ },
+ "url": {
+ "en": "javascript:alert('foobar')"
+ }
+ }
+ ],
+ "formats": [
+ {
+ "type": "geojson",
+ "url": "/files/world_countries_v1.geo.json",
+ "legacy_default": true
+ }
+ ],
+ "fields": [
+ {
+ "type": "id",
+ "id": "iso2",
+ "label": {
+ "en": "ISO 3166-1 alpha-2 code"
+ }
+ }
+ ],
+ "legacy_ids": [],
+ "layer_name": {
+ "en": "World Countries (compromised)"
+ }
+ },
{
"layer_id": "australia_states",
"created_at": "2018-06-27T23:47:32.202380",
diff --git a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json
index c038bb411daec..1bbd94879b70c 100644
--- a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json
+++ b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json
@@ -11,7 +11,7 @@
{ "label": { "en": "OpenMapTiles" }, "url": { "en": "https://openmaptiles.org" } },
{ "label": { "en": "MapTiler" }, "url": { "en": "https://www.maptiler.com" } },
{
- "label": { "en": "Elastic Maps Service" },
+ "label": { "en": "" },
"url": { "en": "https://www.elastic.co/elastic-maps-service" }
}
],
diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js
index f4f88bd5807d5..ae40b2c92d40e 100644
--- a/src/plugins/maps_legacy/public/map/service_settings.js
+++ b/src/plugins/maps_legacy/public/map/service_settings.js
@@ -89,28 +89,31 @@ export class ServiceSettings {
};
}
+ _backfillSettings = (fileLayer) => {
+ // Older version of Kibana stored EMS state in the URL-params
+ // Creates object literal with required parameters as key-value pairs
+ const format = fileLayer.getDefaultFormatType();
+ const meta = fileLayer.getDefaultFormatMeta();
+
+ return {
+ name: fileLayer.getDisplayName(),
+ origin: fileLayer.getOrigin(),
+ id: fileLayer.getId(),
+ created_at: fileLayer.getCreatedAt(),
+ attribution: getAttributionString(fileLayer),
+ fields: fileLayer.getFieldsInLanguage(),
+ format: format, //legacy: format and meta are split up
+ meta: meta, //legacy, format and meta are split up
+ };
+ };
+
async getFileLayers() {
if (!this._mapConfig.includeElasticMapsService) {
return [];
}
const fileLayers = await this._emsClient.getFileLayers();
- return fileLayers.map((fileLayer) => {
- //backfill to older settings
- const format = fileLayer.getDefaultFormatType();
- const meta = fileLayer.getDefaultFormatMeta();
-
- return {
- name: fileLayer.getDisplayName(),
- origin: fileLayer.getOrigin(),
- id: fileLayer.getId(),
- created_at: fileLayer.getCreatedAt(),
- attribution: fileLayer.getHTMLAttribution(),
- fields: fileLayer.getFieldsInLanguage(),
- format: format, //legacy: format and meta are split up
- meta: meta, //legacy, format and meta are split up
- };
- });
+ return fileLayers.map(this._backfillSettings);
}
/**
@@ -139,7 +142,7 @@ export class ServiceSettings {
id: tmsService.getId(),
minZoom: await tmsService.getMinZoom(),
maxZoom: await tmsService.getMaxZoom(),
- attribution: tmsService.getHTMLAttribution(),
+ attribution: getAttributionString(tmsService),
};
})
);
@@ -159,16 +162,25 @@ export class ServiceSettings {
this._emsClient.addQueryParams(additionalQueryParams);
}
- async getEMSHotLink(fileLayerConfig) {
+ async getFileLayerFromConfig(fileLayerConfig) {
const fileLayers = await this._emsClient.getFileLayers();
- const layer = fileLayers.find((fileLayer) => {
+ return fileLayers.find((fileLayer) => {
const hasIdByName = fileLayer.hasId(fileLayerConfig.name); //legacy
const hasIdById = fileLayer.hasId(fileLayerConfig.id);
return hasIdByName || hasIdById;
});
+ }
+
+ async getEMSHotLink(fileLayerConfig) {
+ const layer = await this.getFileLayerFromConfig(fileLayerConfig);
return layer ? layer.getEMSHotLink() : null;
}
+ async loadFileLayerConfig(fileLayerConfig) {
+ const fileLayer = await this.getFileLayerFromConfig(fileLayerConfig);
+ return fileLayer ? this._backfillSettings(fileLayer) : null;
+ }
+
async _getAttributesForEMSTMSLayer(isDesaturated, isDarkMode) {
const tmsServices = await this._emsClient.getTMSServices();
const emsTileLayerId = this._mapConfig.emsTileLayerId;
@@ -189,7 +201,7 @@ export class ServiceSettings {
url: await tmsService.getUrlTemplate(),
minZoom: await tmsService.getMinZoom(),
maxZoom: await tmsService.getMaxZoom(),
- attribution: await tmsService.getHTMLAttribution(),
+ attribution: getAttributionString(tmsService),
origin: ORIGIN.EMS,
};
}
@@ -255,3 +267,17 @@ export class ServiceSettings {
return await response.json();
}
}
+
+function getAttributionString(emsService) {
+ const attributions = emsService.getAttributions();
+ const attributionSnippets = attributions.map((attribution) => {
+ const anchorTag = document.createElement('a');
+ anchorTag.setAttribute('rel', 'noreferrer noopener');
+ if (attribution.url.startsWith('http://') || attribution.url.startsWith('https://')) {
+ anchorTag.setAttribute('href', attribution.url);
+ }
+ anchorTag.textContent = attribution.label;
+ return anchorTag.outerHTML;
+ });
+ return attributionSnippets.join(' | '); //!!!this is the current convention used in Kibana
+}
diff --git a/src/plugins/maps_legacy/public/map/service_settings.test.js b/src/plugins/maps_legacy/public/map/service_settings.test.js
index 01facdc54137e..6e416f7fd5c84 100644
--- a/src/plugins/maps_legacy/public/map/service_settings.test.js
+++ b/src/plugins/maps_legacy/public/map/service_settings.test.js
@@ -98,6 +98,9 @@ describe('service_settings (FKA tile_map test)', function () {
expect(attrs.url.includes('{x}')).toEqual(true);
expect(attrs.url.includes('{y}')).toEqual(true);
expect(attrs.url.includes('{z}')).toEqual(true);
+ expect(attrs.attribution).toEqual(
+ 'OpenStreetMap contributors | OpenMapTiles | MapTiler | <iframe id=\'iframe\' style=\'position:fixed;height: 40%;width: 100%;top: 60%;left: 5%;right:5%;border: 0px;background:white;\' src=\'http://256.256.256.256\'></iframe> '
+ );
const urlObject = url.parse(attrs.url, true);
expect(urlObject.hostname).toEqual('tiles.foobar');
@@ -182,7 +185,7 @@ describe('service_settings (FKA tile_map test)', function () {
minZoom: 0,
maxZoom: 10,
attribution:
- 'OpenStreetMap contributors | OpenMapTiles | MapTiler | Elastic Maps Service ',
+ 'OpenStreetMap contributors | OpenMapTiles | MapTiler | <iframe id=\'iframe\' style=\'position:fixed;height: 40%;width: 100%;top: 60%;left: 5%;right:5%;border: 0px;background:white;\' src=\'http://256.256.256.256\'></iframe> ',
subdomains: [],
},
];
@@ -276,7 +279,6 @@ describe('service_settings (FKA tile_map test)', function () {
serviceSettings = makeServiceSettings({
includeElasticMapsService: false,
});
- // mapConfig.includeElasticMapsService = false;
const tilemapServices = await serviceSettings.getTMSServices();
const expected = [];
expect(tilemapServices).toEqual(expected);
@@ -289,7 +291,7 @@ describe('service_settings (FKA tile_map test)', function () {
const serviceSettings = makeServiceSettings();
serviceSettings.setQueryParams({ foo: 'bar' });
const fileLayers = await serviceSettings.getFileLayers();
- expect(fileLayers.length).toEqual(18);
+ expect(fileLayers.length).toEqual(19);
const assertions = fileLayers.map(async function (fileLayer) {
expect(fileLayer.origin).toEqual(ORIGIN.EMS);
const fileUrl = await serviceSettings.getUrlForRegionLayer(fileLayer);
@@ -343,5 +345,16 @@ describe('service_settings (FKA tile_map test)', function () {
const hotlink = await serviceSettings.getEMSHotLink(fileLayers[0]);
expect(hotlink).toEqual('?locale=en#file/world_countries'); //url host undefined becuase emsLandingPageUrl is set at kibana-load
});
+
+ it('should sanitize EMS attribution', async () => {
+ const serviceSettings = makeServiceSettings();
+ const fileLayers = await serviceSettings.getFileLayers();
+ const fileLayer = fileLayers.find((layer) => {
+ return layer.id === 'world_countries_with_compromised_attribution';
+ });
+ expect(fileLayer.attribution).toEqual(
+ '<div onclick=\'alert(1\')>Made with NaturalEarth</div> | Elastic Maps Service '
+ );
+ });
});
});
diff --git a/src/plugins/region_map/public/region_map_visualization.js b/src/plugins/region_map/public/region_map_visualization.js
index 002d020fcd568..43959c367558f 100644
--- a/src/plugins/region_map/public/region_map_visualization.js
+++ b/src/plugins/region_map/public/region_map_visualization.js
@@ -22,9 +22,11 @@ import ChoroplethLayer from './choropleth_layer';
import { getFormatService, getNotifications, getKibanaLegacy } from './kibana_services';
import { truncatedColorMaps } from '../../charts/public';
import { tooltipFormatter } from './tooltip_formatter';
-import { mapTooltipProvider } from '../../maps_legacy/public';
+import { mapTooltipProvider, ORIGIN } from '../../maps_legacy/public';
+import _ from 'lodash';
export function createRegionMapVisualization({
+ regionmapsConfig,
serviceSettings,
uiSettings,
BaseMapsVisualization,
@@ -60,17 +62,18 @@ export function createRegionMapVisualization({
});
}
- if (!this._params.selectedJoinField && this._params.selectedLayer) {
- this._params.selectedJoinField = this._params.selectedLayer.fields[0];
+ const selectedLayer = await this._loadConfig(this._params.selectedLayer);
+ if (!this._params.selectedJoinField && selectedLayer) {
+ this._params.selectedJoinField = selectedLayer.fields[0];
}
- if (!this._params.selectedLayer) {
+ if (!selectedLayer) {
return;
}
this._updateChoroplethLayerForNewMetrics(
- this._params.selectedLayer.name,
- this._params.selectedLayer.attribution,
+ selectedLayer.name,
+ selectedLayer.attribution,
this._params.showAllShapes,
results
);
@@ -90,29 +93,57 @@ export function createRegionMapVisualization({
this._kibanaMap.useUiStateFromVisualization(this._vis);
}
+ async _loadConfig(fileLayerConfig) {
+ // Load the selected layer from the metadata-service.
+ // Do not use the selectedLayer from the visState.
+ // These settings are stored in the URL and can be used to inject dirty display content.
+
+ if (
+ fileLayerConfig.isEMS || //Hosted by EMS. Metadata needs to be resolved through EMS
+ (fileLayerConfig.layerId && fileLayerConfig.layerId.startsWith(`${ORIGIN.EMS}.`)) //fallback for older saved objects
+ ) {
+ return await serviceSettings.loadFileLayerConfig(fileLayerConfig);
+ }
+
+ //Configured in the kibana.yml. Needs to be resolved through the settings.
+ const configuredLayer = regionmapsConfig.layers.find(
+ (layer) => layer.name === fileLayerConfig.name
+ );
+
+ if (configuredLayer) {
+ return {
+ ...configuredLayer,
+ attribution: _.escape(configuredLayer.attribution ? configuredLayer.attribution : ''),
+ };
+ }
+
+ return null;
+ }
+
async _updateParams() {
await super._updateParams();
- const visParams = this._params;
- if (!visParams.selectedJoinField && visParams.selectedLayer) {
- visParams.selectedJoinField = visParams.selectedLayer.fields[0];
+ const selectedLayer = await this._loadConfig(this._params.selectedLayer);
+
+ if (!this._params.selectedJoinField && selectedLayer) {
+ this._params.selectedJoinField = selectedLayer.fields[0];
}
- if (!visParams.selectedJoinField || !visParams.selectedLayer) {
+ if (!this._params.selectedJoinField || !selectedLayer) {
return;
}
this._updateChoroplethLayerForNewProperties(
- visParams.selectedLayer.name,
- visParams.selectedLayer.attribution,
+ selectedLayer.name,
+ selectedLayer.attribution,
this._params.showAllShapes
);
const metricFieldFormatter = getFormatService().deserialize(this._params.metric.format);
- this._choroplethLayer.setJoinField(visParams.selectedJoinField.name);
- this._choroplethLayer.setColorRamp(truncatedColorMaps[visParams.colorSchema].value);
- this._choroplethLayer.setLineWeight(visParams.outlineWeight);
+ this._choroplethLayer.setJoinField(this._params.selectedJoinField.name);
+ this._choroplethLayer.setColorRamp(truncatedColorMaps[this._params.colorSchema].value);
+ this._choroplethLayer.setLineWeight(this._params.outlineWeight);
this._choroplethLayer.setTooltipFormatter(
this._tooltipFormatter,
metricFieldFormatter,
diff --git a/src/plugins/usage_collection/server/collector/collector.test.ts b/src/plugins/usage_collection/server/collector/collector.test.ts
new file mode 100644
index 0000000000000..a3e2425c1f122
--- /dev/null
+++ b/src/plugins/usage_collection/server/collector/collector.test.ts
@@ -0,0 +1,213 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { loggingSystemMock } from '../../../../core/server/mocks';
+import { Collector } from './collector';
+import { UsageCollector } from './usage_collector';
+
+const logger = loggingSystemMock.createLogger();
+
+describe('collector', () => {
+ describe('options validations', () => {
+ it('should not accept an empty object', () => {
+ // @ts-expect-error
+ expect(() => new Collector(logger, {})).toThrowError(
+ 'Collector must be instantiated with a options.type string property'
+ );
+ });
+
+ it('should fail if init is not a function', () => {
+ expect(
+ () =>
+ new Collector(logger, {
+ type: 'my_test_collector',
+ // @ts-expect-error
+ init: 1,
+ })
+ ).toThrowError(
+ 'If init property is passed, Collector must be instantiated with a options.init as a function property'
+ );
+ });
+
+ it('should fail if fetch is not defined', () => {
+ expect(
+ () =>
+ // @ts-expect-error
+ new Collector(logger, {
+ type: 'my_test_collector',
+ isReady: () => false,
+ })
+ ).toThrowError('Collector must be instantiated with a options.fetch function property');
+ });
+
+ it('should fail if fetch is not a function', () => {
+ expect(
+ () =>
+ new Collector(logger, {
+ type: 'my_test_collector',
+ isReady: () => false,
+ // @ts-expect-error
+ fetch: 1,
+ })
+ ).toThrowError('Collector must be instantiated with a options.fetch function property');
+ });
+
+ it('should be OK with all mandatory properties', () => {
+ const collector = new Collector(logger, {
+ type: 'my_test_collector',
+ isReady: () => false,
+ fetch: () => ({ testPass: 100 }),
+ });
+ expect(collector).toBeDefined();
+ });
+
+ it('should fallback when isReady is not provided', () => {
+ const fetchOutput = { testPass: 100 };
+ // @ts-expect-error not providing isReady to test the logic fallback
+ const collector = new Collector(logger, {
+ type: 'my_test_collector',
+ fetch: () => fetchOutput,
+ });
+ expect(collector.isReady()).toBe(true);
+ });
+ });
+
+ describe('formatForBulkUpload', () => {
+ it('should use the default formatter', () => {
+ const fetchOutput = { testPass: 100 };
+ const collector = new Collector(logger, {
+ type: 'my_test_collector',
+ isReady: () => false,
+ fetch: () => fetchOutput,
+ });
+ expect(collector.formatForBulkUpload(fetchOutput)).toStrictEqual({
+ type: 'my_test_collector',
+ payload: fetchOutput,
+ });
+ });
+
+ it('should use a custom formatter', () => {
+ const fetchOutput = { testPass: 100 };
+ const collector = new Collector(logger, {
+ type: 'my_test_collector',
+ isReady: () => false,
+ fetch: () => fetchOutput,
+ formatForBulkUpload: (a) => ({ type: 'other_value', payload: { nested: a } }),
+ });
+ expect(collector.formatForBulkUpload(fetchOutput)).toStrictEqual({
+ type: 'other_value',
+ payload: { nested: fetchOutput },
+ });
+ });
+
+ it("should use UsageCollector's default formatter", () => {
+ const fetchOutput = { testPass: 100 };
+ const collector = new UsageCollector(logger, {
+ type: 'my_test_collector',
+ isReady: () => false,
+ fetch: () => fetchOutput,
+ });
+ expect(collector.formatForBulkUpload(fetchOutput)).toStrictEqual({
+ type: 'kibana_stats',
+ payload: { usage: { my_test_collector: fetchOutput } },
+ });
+ });
+ });
+
+ describe('schema TS validations', () => {
+ // These tests below are used to ensure types inference is working as expected.
+ // We don't intend to test any logic as such, just the relation between the types in `fetch` and `schema`.
+ // Using ts-expect-error when an error is expected will fail the compilation if there is not such error.
+
+ test('when fetch returns a simple object', () => {
+ const collector = new Collector(logger, {
+ type: 'my_test_collector',
+ isReady: () => false,
+ fetch: () => ({ testPass: 100 }),
+ schema: {
+ testPass: { type: 'long' },
+ },
+ });
+ expect(collector).toBeDefined();
+ });
+
+ test('when fetch returns array-properties and schema', () => {
+ const collector = new Collector(logger, {
+ type: 'my_test_collector',
+ isReady: () => false,
+ fetch: () => ({ testPass: [{ name: 'a', value: 100 }] }),
+ schema: {
+ testPass: { name: { type: 'keyword' }, value: { type: 'long' } },
+ },
+ });
+ expect(collector).toBeDefined();
+ });
+
+ test('TS should complain when schema is missing some properties', () => {
+ const collector = new Collector(logger, {
+ type: 'my_test_collector',
+ isReady: () => false,
+ fetch: () => ({ testPass: [{ name: 'a', value: 100 }], otherProp: 1 }),
+ // @ts-expect-error
+ schema: {
+ testPass: { name: { type: 'keyword' }, value: { type: 'long' } },
+ },
+ });
+ expect(collector).toBeDefined();
+ });
+
+ test('TS complains if schema misses any of the optional properties', () => {
+ const collector = new Collector(logger, {
+ type: 'my_test_collector',
+ isReady: () => false,
+ // Need to be explicit with the returned type because TS struggles to identify it
+ fetch: (): { testPass?: Array<{ name: string; value: number }>; otherProp?: number } => {
+ if (Math.random() > 0.5) {
+ return { testPass: [{ name: 'a', value: 100 }] };
+ }
+ return { otherProp: 1 };
+ },
+ // @ts-expect-error
+ schema: {
+ testPass: { name: { type: 'keyword' }, value: { type: 'long' } },
+ },
+ });
+ expect(collector).toBeDefined();
+ });
+
+ test('schema defines all the optional properties', () => {
+ const collector = new Collector(logger, {
+ type: 'my_test_collector',
+ isReady: () => false,
+ // Need to be explicit with the returned type because TS struggles to identify it
+ fetch: (): { testPass?: Array<{ name: string; value: number }>; otherProp?: number } => {
+ if (Math.random() > 0.5) {
+ return { testPass: [{ name: 'a', value: 100 }] };
+ }
+ return { otherProp: 1 };
+ },
+ schema: {
+ testPass: { name: { type: 'keyword' }, value: { type: 'long' } },
+ otherProp: { type: 'long' },
+ },
+ });
+ expect(collector).toBeDefined();
+ });
+ });
+});
diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts
index 9ae63b9f50e42..d57700024c088 100644
--- a/src/plugins/usage_collection/server/collector/collector.ts
+++ b/src/plugins/usage_collection/server/collector/collector.ts
@@ -34,20 +34,20 @@ export interface SchemaField {
type: string;
}
-type Purify = { [P in T]: T }[T];
+export type RecursiveMakeSchemaFrom = U extends object
+ ? MakeSchemaFrom
+ : { type: AllowedSchemaTypes };
export type MakeSchemaFrom = {
- [Key in Purify>]: Base[Key] extends Array
- ? { type: AllowedSchemaTypes }
- : Base[Key] extends object
- ? MakeSchemaFrom
- : { type: AllowedSchemaTypes };
+ [Key in keyof Base]: Base[Key] extends Array
+ ? RecursiveMakeSchemaFrom
+ : RecursiveMakeSchemaFrom ;
};
export interface CollectorOptions {
type: string;
init?: Function;
- schema?: MakeSchemaFrom;
+ schema?: MakeSchemaFrom>; // Using Required to enforce all optional keys in the object
fetch: (callCluster: LegacyAPICaller) => Promise | T;
/*
* A hook for allowing the fetched data payload to be organized into a typed
diff --git a/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx b/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx
index 8fe5cdb47a53d..c0c46f6714c2d 100644
--- a/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx
+++ b/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx
@@ -21,7 +21,7 @@ import React, { useCallback } from 'react';
import { EuiFieldText, EuiFlexItem, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { Ipv4Address } from '../../../../../kibana_utils/public';
+import { search } from '../../../../../data/public';
import { InputList, InputListConfig, InputModel, InputObject, InputItem } from './input_list';
const EMPTY_STRING = '';
@@ -49,7 +49,7 @@ const defaultConfig = {
from: { value: '0.0.0.0', model: '0.0.0.0', isInvalid: false },
to: { value: '255.255.255.255', model: '255.255.255.255', isInvalid: false },
},
- validateClass: Ipv4Address,
+ validateClass: search.aggs.Ipv4Address,
getModelValue: (item: FromToObject = {}) => ({
from: {
value: item.from || EMPTY_STRING,
diff --git a/src/plugins/vis_default_editor/public/components/controls/filter.tsx b/src/plugins/vis_default_editor/public/components/controls/filter.tsx
index 0228c79139f16..94fd2d9bc9151 100644
--- a/src/plugins/vis_default_editor/public/components/controls/filter.tsx
+++ b/src/plugins/vis_default_editor/public/components/controls/filter.tsx
@@ -22,6 +22,7 @@ import { EuiForm, EuiButtonIcon, EuiFieldText, EuiFormRow, EuiSpacer } from '@el
import { i18n } from '@kbn/i18n';
import { IAggConfig, Query, QueryStringInput } from '../../../../data/public';
+import { useKibana } from '../../../../kibana_react/public';
interface FilterRowProps {
id: string;
@@ -48,6 +49,7 @@ function FilterRow({
onChangeValue,
onRemoveFilter,
}: FilterRowProps) {
+ const { services } = useKibana();
const [showCustomLabel, setShowCustomLabel] = useState(false);
const filterLabel = i18n.translate('visDefaultEditor.controls.filters.filterLabel', {
defaultMessage: 'Filter {index}',
@@ -56,6 +58,13 @@ function FilterRow({
},
});
+ const onBlur = () => {
+ if (value.query.length > 0) {
+ // Store filter to the query log so that it is available in autocomplete.
+ services.data.query.addToQueryLog(services.appName, value);
+ }
+ };
+
const FilterControl = (
onChangeValue(id, query, customLabel)}
+ onBlur={onBlur}
disableAutoFocus={!autoFocus}
dataTestSubj={dataTestSubj}
bubbleSubmitEvent={true}
diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts
index 05644eddc5fca..e0ec4801b3caf 100644
--- a/src/plugins/visualizations/public/mocks.ts
+++ b/src/plugins/visualizations/public/mocks.ts
@@ -39,7 +39,9 @@ const createStartContract = (): VisualizationsStart => ({
get: jest.fn(),
all: jest.fn(),
getAliases: jest.fn(),
- savedVisualizationsLoader: {} as any,
+ savedVisualizationsLoader: {
+ get: jest.fn(),
+ } as any,
showNewVisModal: jest.fn(),
createVis: jest.fn(),
convertFromSerializedVis: jest.fn(),
diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts
index a6adaf1f3c62b..02ae1cc155dd2 100644
--- a/src/plugins/visualize/public/application/types.ts
+++ b/src/plugins/visualize/public/application/types.ts
@@ -50,7 +50,7 @@ export type PureVisState = SavedVisState;
export interface VisualizeAppState {
filters: Filter[];
- uiState: PersistedState;
+ uiState: Record;
vis: PureVisState;
query: Query;
savedQuery?: string;
diff --git a/src/plugins/visualize/public/application/utils/create_visualize_app_state.test.ts b/src/plugins/visualize/public/application/utils/create_visualize_app_state.test.ts
new file mode 100644
index 0000000000000..885eec8a68d2d
--- /dev/null
+++ b/src/plugins/visualize/public/application/utils/create_visualize_app_state.test.ts
@@ -0,0 +1,134 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { IKbnUrlStateStorage } from 'src/plugins/kibana_utils/public';
+import { createVisualizeAppState } from './create_visualize_app_state';
+import { migrateAppState } from './migrate_app_state';
+import { visualizeAppStateStub } from './stubs';
+
+const mockStartStateSync = jest.fn();
+const mockStopStateSync = jest.fn();
+
+jest.mock('../../../../kibana_utils/public', () => ({
+ createStateContainer: jest.fn(() => 'stateContainer'),
+ syncState: jest.fn(() => ({
+ start: mockStartStateSync,
+ stop: mockStopStateSync,
+ })),
+}));
+jest.mock('./migrate_app_state', () => ({
+ migrateAppState: jest.fn(() => 'migratedAppState'),
+}));
+
+const { createStateContainer, syncState } = jest.requireMock('../../../../kibana_utils/public');
+
+describe('createVisualizeAppState', () => {
+ const kbnUrlStateStorage = ({
+ set: jest.fn(),
+ get: jest.fn(() => ({ linked: false })),
+ } as unknown) as IKbnUrlStateStorage;
+
+ const { stateContainer, stopStateSync } = createVisualizeAppState({
+ stateDefaults: visualizeAppStateStub,
+ kbnUrlStateStorage,
+ });
+ const transitions = createStateContainer.mock.calls[0][1];
+
+ test('should initialize visualize app state', () => {
+ expect(kbnUrlStateStorage.get).toHaveBeenCalledWith('_a');
+ expect(migrateAppState).toHaveBeenCalledWith({
+ ...visualizeAppStateStub,
+ linked: false,
+ });
+ expect(kbnUrlStateStorage.set).toHaveBeenCalledWith('_a', 'migratedAppState', {
+ replace: true,
+ });
+ expect(createStateContainer).toHaveBeenCalled();
+ expect(syncState).toHaveBeenCalled();
+ expect(mockStartStateSync).toHaveBeenCalled();
+ });
+
+ test('should return the stateContainer and stopStateSync', () => {
+ expect(stateContainer).toBe('stateContainer');
+ stopStateSync();
+ expect(stopStateSync).toHaveBeenCalledTimes(1);
+ });
+
+ describe('stateContainer transitions', () => {
+ test('set', () => {
+ const newQuery = { query: '', language: '' };
+ expect(transitions.set(visualizeAppStateStub)('query', newQuery)).toEqual({
+ ...visualizeAppStateStub,
+ query: newQuery,
+ });
+ });
+
+ test('setVis', () => {
+ const newVis = { data: 'data' };
+ expect(transitions.setVis(visualizeAppStateStub)(newVis)).toEqual({
+ ...visualizeAppStateStub,
+ vis: {
+ ...visualizeAppStateStub.vis,
+ ...newVis,
+ },
+ });
+ });
+
+ test('unlinkSavedSearch', () => {
+ const params = {
+ query: { query: '', language: '' },
+ parentFilters: [{ test: 'filter2' }],
+ };
+ expect(transitions.unlinkSavedSearch(visualizeAppStateStub)(params)).toEqual({
+ ...visualizeAppStateStub,
+ query: params.query,
+ filters: [...visualizeAppStateStub.filters, { test: 'filter2' }],
+ linked: false,
+ });
+ });
+
+ test('updateVisState: should not include resctricted param types', () => {
+ const newVisState = {
+ a: 1,
+ _b: 2,
+ $c: 3,
+ d: () => {},
+ };
+ expect(transitions.updateVisState(visualizeAppStateStub)(newVisState)).toEqual({
+ ...visualizeAppStateStub,
+ vis: { a: 1 },
+ });
+ });
+
+ test('updateSavedQuery: add savedQuery', () => {
+ const savedQueryId = '123test';
+ expect(transitions.updateSavedQuery(visualizeAppStateStub)(savedQueryId)).toEqual({
+ ...visualizeAppStateStub,
+ savedQuery: savedQueryId,
+ });
+ });
+
+ test('updateSavedQuery: remove savedQuery from state', () => {
+ const savedQueryId = '123test';
+ expect(
+ transitions.updateSavedQuery({ ...visualizeAppStateStub, savedQuery: savedQueryId })()
+ ).toEqual(visualizeAppStateStub);
+ });
+ });
+});
diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts
new file mode 100644
index 0000000000000..31f0fc5f94479
--- /dev/null
+++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts
@@ -0,0 +1,124 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { createSavedSearchesLoader } from '../../../../discover/public';
+import { getVisualizationInstance } from './get_visualization_instance';
+import { createVisualizeServicesMock } from './mocks';
+import { VisualizeServices } from '../types';
+import { BehaviorSubject } from 'rxjs';
+
+const mockSavedSearchObj = {};
+const mockGetSavedSearch = jest.fn(() => mockSavedSearchObj);
+
+jest.mock('../../../../discover/public', () => ({
+ createSavedSearchesLoader: jest.fn(() => ({
+ get: mockGetSavedSearch,
+ })),
+}));
+
+describe('getVisualizationInstance', () => {
+ const serializedVisMock = {
+ type: 'area',
+ };
+ let savedVisMock: any;
+ let visMock: any;
+ let mockServices: jest.Mocked;
+ let subj: BehaviorSubject;
+
+ beforeEach(() => {
+ mockServices = createVisualizeServicesMock();
+ subj = new BehaviorSubject({});
+ visMock = {
+ type: {},
+ data: {},
+ };
+ savedVisMock = {};
+ // @ts-expect-error
+ mockServices.savedVisualizations.get.mockImplementation(() => savedVisMock);
+ // @ts-expect-error
+ mockServices.visualizations.convertToSerializedVis.mockImplementation(() => serializedVisMock);
+ // @ts-expect-error
+ mockServices.visualizations.createVis.mockImplementation(() => visMock);
+ // @ts-expect-error
+ mockServices.createVisEmbeddableFromObject.mockImplementation(() => ({
+ getOutput$: jest.fn(() => subj.asObservable()),
+ }));
+ });
+
+ test('should create new instances of savedVis, vis and embeddableHandler', async () => {
+ const opts = {
+ type: 'area',
+ indexPattern: 'my_index_pattern',
+ };
+ const { savedVis, savedSearch, vis, embeddableHandler } = await getVisualizationInstance(
+ mockServices,
+ opts
+ );
+
+ expect(mockServices.savedVisualizations.get).toHaveBeenCalledWith(opts);
+ expect(savedVisMock.searchSourceFields).toEqual({
+ index: opts.indexPattern,
+ });
+ expect(mockServices.visualizations.convertToSerializedVis).toHaveBeenCalledWith(savedVisMock);
+ expect(mockServices.visualizations.createVis).toHaveBeenCalledWith(
+ serializedVisMock.type,
+ serializedVisMock
+ );
+ expect(mockServices.createVisEmbeddableFromObject).toHaveBeenCalledWith(visMock, {
+ timeRange: undefined,
+ filters: undefined,
+ id: '',
+ });
+
+ expect(vis).toBe(visMock);
+ expect(savedVis).toBe(savedVisMock);
+ expect(embeddableHandler).toBeDefined();
+ expect(savedSearch).toBeUndefined();
+ });
+
+ test('should load existing vis by id and call vis type setup if exists', async () => {
+ const newVisObj = { data: {} };
+ visMock.type.setup = jest.fn(() => newVisObj);
+ const { vis } = await getVisualizationInstance(mockServices, 'saved_vis_id');
+
+ expect(mockServices.savedVisualizations.get).toHaveBeenCalledWith('saved_vis_id');
+ expect(savedVisMock.searchSourceFields).toBeUndefined();
+ expect(visMock.type.setup).toHaveBeenCalledWith(visMock);
+ expect(vis).toBe(newVisObj);
+ });
+
+ test('should create saved search instance if vis based on saved search id', async () => {
+ visMock.data.savedSearchId = 'saved_search_id';
+ const { savedSearch } = await getVisualizationInstance(mockServices, 'saved_vis_id');
+
+ expect(createSavedSearchesLoader).toHaveBeenCalled();
+ expect(mockGetSavedSearch).toHaveBeenCalledWith(visMock.data.savedSearchId);
+ expect(savedSearch).toBe(mockSavedSearchObj);
+ });
+
+ test('should subscribe on embeddable handler updates and send toasts on errors', async () => {
+ await getVisualizationInstance(mockServices, 'saved_vis_id');
+
+ subj.next({
+ error: 'error',
+ });
+
+ expect(mockServices.toastNotifications.addError).toHaveBeenCalled();
+ });
+});
diff --git a/src/plugins/visualize/public/application/utils/mocks.ts b/src/plugins/visualize/public/application/utils/mocks.ts
new file mode 100644
index 0000000000000..09e7ba23875ca
--- /dev/null
+++ b/src/plugins/visualize/public/application/utils/mocks.ts
@@ -0,0 +1,43 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { coreMock } from '../../../../../core/public/mocks';
+import { dataPluginMock } from '../../../../data/public/mocks';
+import { visualizationsPluginMock } from '../../../../visualizations/public/mocks';
+import { VisualizeServices } from '../types';
+
+export const createVisualizeServicesMock = () => {
+ const coreStartMock = coreMock.createStart();
+ const dataStartMock = dataPluginMock.createStartContract();
+ const toastNotifications = coreStartMock.notifications.toasts;
+ const visualizations = visualizationsPluginMock.createStartContract();
+
+ return ({
+ ...coreStartMock,
+ data: dataStartMock,
+ toastNotifications,
+ history: {
+ replace: jest.fn(),
+ location: { pathname: '' },
+ },
+ visualizations,
+ savedVisualizations: visualizations.savedVisualizationsLoader,
+ createVisEmbeddableFromObject: visualizations.__LEGACY.createVisEmbeddableFromObject,
+ } as unknown) as jest.Mocked;
+};
diff --git a/src/plugins/visualize/public/application/utils/stubs.ts b/src/plugins/visualize/public/application/utils/stubs.ts
new file mode 100644
index 0000000000000..1bbd738a739cf
--- /dev/null
+++ b/src/plugins/visualize/public/application/utils/stubs.ts
@@ -0,0 +1,89 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { VisualizeAppState } from '../types';
+
+export const visualizeAppStateStub: VisualizeAppState = {
+ uiState: {
+ vis: {
+ defaultColors: {
+ '0 - 2': 'rgb(165,0,38)',
+ '2 - 3': 'rgb(255,255,190)',
+ '3 - 4': 'rgb(0,104,55)',
+ },
+ },
+ },
+ query: { query: '', language: 'kuery' },
+ filters: [],
+ vis: {
+ title: '[eCommerce] Average Sold Quantity',
+ type: 'gauge',
+ aggs: [
+ {
+ id: '1',
+ enabled: true,
+ // @ts-expect-error
+ type: 'avg',
+ schema: 'metric',
+ params: { field: 'total_quantity', customLabel: 'average items' },
+ },
+ ],
+ params: {
+ type: 'gauge',
+ addTooltip: true,
+ addLegend: true,
+ isDisplayWarning: false,
+ gauge: {
+ extendRange: true,
+ percentageMode: false,
+ gaugeType: 'Circle',
+ gaugeStyle: 'Full',
+ backStyle: 'Full',
+ orientation: 'vertical',
+ colorSchema: 'Green to Red',
+ gaugeColorMode: 'Labels',
+ colorsRange: [
+ { from: 0, to: 2 },
+ { from: 2, to: 3 },
+ { from: 3, to: 4 },
+ ],
+ invertColors: true,
+ labels: { show: true, color: 'black' },
+ scale: { show: false, labels: false, color: '#333' },
+ type: 'meter',
+ style: {
+ bgWidth: 0.9,
+ width: 0.9,
+ mask: false,
+ bgMask: false,
+ maskBars: 50,
+ bgFill: '#eee',
+ bgColor: false,
+ subText: 'per order',
+ fontSize: 60,
+ labelColor: true,
+ },
+ minAngle: 0,
+ maxAngle: 6.283185307179586,
+ alignment: 'horizontal',
+ },
+ },
+ },
+ linked: false,
+};
diff --git a/src/plugins/visualize/public/application/utils/use/use_chrome_visibility.test.ts b/src/plugins/visualize/public/application/utils/use/use_chrome_visibility.test.ts
new file mode 100644
index 0000000000000..904816db22278
--- /dev/null
+++ b/src/plugins/visualize/public/application/utils/use/use_chrome_visibility.test.ts
@@ -0,0 +1,55 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { act, renderHook } from '@testing-library/react-hooks';
+
+import { chromeServiceMock } from '../../../../../../core/public/mocks';
+import { useChromeVisibility } from './use_chrome_visibility';
+
+describe('useChromeVisibility', () => {
+ const chromeMock = chromeServiceMock.createStartContract();
+
+ test('should set up a subscription for chrome visibility', () => {
+ const { result } = renderHook(() => useChromeVisibility(chromeMock));
+
+ expect(chromeMock.getIsVisible$).toHaveBeenCalled();
+ expect(result.current).toEqual(false);
+ });
+
+ test('should change chrome visibility to true if change was emitted', () => {
+ const { result } = renderHook(() => useChromeVisibility(chromeMock));
+ const behaviorSubj = chromeMock.getIsVisible$.mock.results[0].value;
+ act(() => {
+ behaviorSubj.next(true);
+ });
+
+ expect(result.current).toEqual(true);
+ });
+
+ test('should destroy a subscription', () => {
+ const { unmount } = renderHook(() => useChromeVisibility(chromeMock));
+ const behaviorSubj = chromeMock.getIsVisible$.mock.results[0].value;
+ const subscription = behaviorSubj.observers[0];
+ subscription.unsubscribe = jest.fn();
+
+ unmount();
+
+ expect(subscription.unsubscribe).toHaveBeenCalled();
+ });
+});
diff --git a/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts b/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts
new file mode 100644
index 0000000000000..3546ee7b321bb
--- /dev/null
+++ b/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts
@@ -0,0 +1,327 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { renderHook, act } from '@testing-library/react-hooks';
+import { EventEmitter } from 'events';
+
+import { useEditorUpdates } from './use_editor_updates';
+import {
+ VisualizeServices,
+ VisualizeAppStateContainer,
+ SavedVisInstance,
+ IEditorController,
+} from '../../types';
+import { visualizeAppStateStub } from '../stubs';
+import { createVisualizeServicesMock } from '../mocks';
+
+describe('useEditorUpdates', () => {
+ const eventEmitter = new EventEmitter();
+ const setHasUnsavedChangesMock = jest.fn();
+ let mockServices: jest.Mocked;
+
+ beforeEach(() => {
+ mockServices = createVisualizeServicesMock();
+ // @ts-expect-error
+ mockServices.visualizations.convertFromSerializedVis.mockImplementation(() => ({
+ visState: visualizeAppStateStub.vis,
+ }));
+ });
+
+ test('should not create any subscriptions if app state container is not ready', () => {
+ const { result } = renderHook(() =>
+ useEditorUpdates(
+ mockServices,
+ eventEmitter,
+ setHasUnsavedChangesMock,
+ null,
+ undefined,
+ undefined
+ )
+ );
+
+ expect(result.current).toEqual({
+ isEmbeddableRendered: false,
+ currentAppState: undefined,
+ });
+ });
+
+ let unsubscribeStateUpdatesMock: jest.Mock;
+ let appState: VisualizeAppStateContainer;
+ let savedVisInstance: SavedVisInstance;
+ let visEditorController: IEditorController;
+ let timeRange: any;
+ let mockFilters: any;
+
+ beforeEach(() => {
+ unsubscribeStateUpdatesMock = jest.fn();
+ appState = ({
+ getState: jest.fn(() => visualizeAppStateStub),
+ subscribe: jest.fn(() => unsubscribeStateUpdatesMock),
+ transitions: {
+ set: jest.fn(),
+ },
+ } as unknown) as VisualizeAppStateContainer;
+ savedVisInstance = ({
+ vis: {
+ uiState: {
+ on: jest.fn(),
+ off: jest.fn(),
+ setSilent: jest.fn(),
+ getChanges: jest.fn(() => visualizeAppStateStub.uiState),
+ },
+ data: {},
+ serialize: jest.fn(),
+ title: visualizeAppStateStub.vis.title,
+ setState: jest.fn(),
+ },
+ embeddableHandler: {
+ updateInput: jest.fn(),
+ reload: jest.fn(),
+ },
+ savedVis: {},
+ } as unknown) as SavedVisInstance;
+ visEditorController = {
+ render: jest.fn(),
+ destroy: jest.fn(),
+ };
+ timeRange = {
+ from: 'now-15m',
+ to: 'now',
+ };
+ mockFilters = ['mockFilters'];
+ // @ts-expect-error
+ mockServices.data.query.timefilter.timefilter.getTime.mockImplementation(() => timeRange);
+ // @ts-expect-error
+ mockServices.data.query.filterManager.getFilters.mockImplementation(() => mockFilters);
+ });
+
+ test('should set up current app state and render the editor', () => {
+ const { result } = renderHook(() =>
+ useEditorUpdates(
+ mockServices,
+ eventEmitter,
+ setHasUnsavedChangesMock,
+ appState,
+ savedVisInstance,
+ visEditorController
+ )
+ );
+
+ expect(result.current).toEqual({
+ isEmbeddableRendered: false,
+ currentAppState: visualizeAppStateStub,
+ });
+ expect(savedVisInstance.vis.uiState.setSilent).toHaveBeenCalledWith(
+ visualizeAppStateStub.uiState
+ );
+ expect(visEditorController.render).toHaveBeenCalledWith({
+ core: mockServices,
+ data: mockServices.data,
+ uiState: savedVisInstance.vis.uiState,
+ timeRange,
+ filters: mockFilters,
+ query: visualizeAppStateStub.query,
+ linked: false,
+ savedSearch: undefined,
+ });
+ });
+
+ test('should update embeddable handler in embeded mode', () => {
+ renderHook(() =>
+ useEditorUpdates(
+ mockServices,
+ eventEmitter,
+ setHasUnsavedChangesMock,
+ appState,
+ savedVisInstance,
+ undefined
+ )
+ );
+
+ expect(savedVisInstance.embeddableHandler.updateInput).toHaveBeenCalledWith({
+ timeRange,
+ filters: mockFilters,
+ query: visualizeAppStateStub.query,
+ });
+ });
+
+ test('should update isEmbeddableRendered value when embedabble is rendered', () => {
+ const { result } = renderHook(() =>
+ useEditorUpdates(
+ mockServices,
+ eventEmitter,
+ setHasUnsavedChangesMock,
+ appState,
+ savedVisInstance,
+ undefined
+ )
+ );
+
+ act(() => {
+ eventEmitter.emit('embeddableRendered');
+ });
+
+ expect(result.current.isEmbeddableRendered).toBe(true);
+ });
+
+ test('should destroy subscriptions on unmount', () => {
+ const { unmount } = renderHook(() =>
+ useEditorUpdates(
+ mockServices,
+ eventEmitter,
+ setHasUnsavedChangesMock,
+ appState,
+ savedVisInstance,
+ undefined
+ )
+ );
+
+ unmount();
+
+ expect(unsubscribeStateUpdatesMock).toHaveBeenCalledTimes(1);
+ expect(savedVisInstance.vis.uiState.off).toHaveBeenCalledTimes(1);
+ });
+
+ describe('subscribe on app state updates', () => {
+ test('should subscribe on appState updates', () => {
+ const { result } = renderHook(() =>
+ useEditorUpdates(
+ mockServices,
+ eventEmitter,
+ setHasUnsavedChangesMock,
+ appState,
+ savedVisInstance,
+ undefined
+ )
+ );
+ // @ts-expect-error
+ const listener = appState.subscribe.mock.calls[0][0];
+
+ act(() => {
+ listener(visualizeAppStateStub);
+ });
+
+ expect(result.current.currentAppState).toEqual(visualizeAppStateStub);
+ expect(setHasUnsavedChangesMock).toHaveBeenCalledWith(true);
+ expect(savedVisInstance.embeddableHandler.updateInput).toHaveBeenCalledTimes(2);
+ });
+
+ test('should update vis state and reload the editor if changes come from url', () => {
+ const { result } = renderHook(() =>
+ useEditorUpdates(
+ mockServices,
+ eventEmitter,
+ setHasUnsavedChangesMock,
+ appState,
+ savedVisInstance,
+ undefined
+ )
+ );
+ // @ts-expect-error
+ const listener = appState.subscribe.mock.calls[0][0];
+ const newAppState = {
+ ...visualizeAppStateStub,
+ vis: {
+ ...visualizeAppStateStub.vis,
+ title: 'New title',
+ },
+ };
+ const { aggs, ...visState } = newAppState.vis;
+ const updateEditorSpy = jest.fn();
+
+ eventEmitter.on('updateEditor', updateEditorSpy);
+
+ act(() => {
+ listener(newAppState);
+ });
+
+ expect(result.current.currentAppState).toEqual(newAppState);
+ expect(savedVisInstance.vis.setState).toHaveBeenCalledWith({
+ ...visState,
+ data: { aggs },
+ });
+ expect(savedVisInstance.embeddableHandler.reload).toHaveBeenCalled();
+ expect(updateEditorSpy).toHaveBeenCalled();
+ });
+
+ describe('handle linked search changes', () => {
+ test('should update saved search id in saved instance', () => {
+ // @ts-expect-error
+ savedVisInstance.savedSearch = {
+ id: 'saved_search_id',
+ };
+
+ renderHook(() =>
+ useEditorUpdates(
+ mockServices,
+ eventEmitter,
+ setHasUnsavedChangesMock,
+ appState,
+ savedVisInstance,
+ undefined
+ )
+ );
+ // @ts-expect-error
+ const listener = appState.subscribe.mock.calls[0][0];
+
+ act(() => {
+ listener({
+ ...visualizeAppStateStub,
+ linked: true,
+ });
+ });
+
+ expect(savedVisInstance.savedVis.savedSearchId).toEqual('saved_search_id');
+ expect(savedVisInstance.vis.data.savedSearchId).toEqual('saved_search_id');
+ });
+
+ test('should remove saved search id from vis instance', () => {
+ // @ts-expect-error
+ savedVisInstance.savedVis = {
+ savedSearchId: 'saved_search_id',
+ };
+ // @ts-expect-error
+ savedVisInstance.savedSearch = {
+ id: 'saved_search_id',
+ };
+ savedVisInstance.vis.data.savedSearchId = 'saved_search_id';
+
+ renderHook(() =>
+ useEditorUpdates(
+ mockServices,
+ eventEmitter,
+ setHasUnsavedChangesMock,
+ appState,
+ savedVisInstance,
+ undefined
+ )
+ );
+ // @ts-expect-error
+ const listener = appState.subscribe.mock.calls[0][0];
+
+ act(() => {
+ listener(visualizeAppStateStub);
+ });
+
+ expect(savedVisInstance.savedVis.savedSearchId).toBeUndefined();
+ expect(savedVisInstance.vis.data.savedSearchId).toBeUndefined();
+ });
+ });
+ });
+});
diff --git a/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.test.ts b/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.test.ts
new file mode 100644
index 0000000000000..4c9ebbc1d9abd
--- /dev/null
+++ b/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.test.ts
@@ -0,0 +1,76 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { renderHook } from '@testing-library/react-hooks';
+import { EventEmitter } from 'events';
+
+import { useLinkedSearchUpdates } from './use_linked_search_updates';
+import { VisualizeServices, SavedVisInstance, VisualizeAppStateContainer } from '../../types';
+import { createVisualizeServicesMock } from '../mocks';
+
+describe('useLinkedSearchUpdates', () => {
+ let mockServices: jest.Mocked;
+ const eventEmitter = new EventEmitter();
+ const savedVisInstance = ({
+ vis: {
+ data: {
+ searchSource: { setField: jest.fn(), setParent: jest.fn() },
+ },
+ },
+ savedVis: {},
+ embeddableHandler: {},
+ } as unknown) as SavedVisInstance;
+
+ beforeEach(() => {
+ mockServices = createVisualizeServicesMock();
+ });
+
+ it('should not subscribe on unlinkFromSavedSearch event if appState or savedSearch are not defined', () => {
+ renderHook(() => useLinkedSearchUpdates(mockServices, eventEmitter, null, savedVisInstance));
+
+ expect(mockServices.toastNotifications.addSuccess).not.toHaveBeenCalled();
+ });
+
+ it('should subscribe on unlinkFromSavedSearch event if vis is based on saved search', () => {
+ const mockAppState = ({
+ transitions: {
+ unlinkSavedSearch: jest.fn(),
+ },
+ } as unknown) as VisualizeAppStateContainer;
+ savedVisInstance.savedSearch = ({
+ searchSource: {
+ getParent: jest.fn(),
+ getField: jest.fn(),
+ getOwnField: jest.fn(),
+ },
+ title: 'savedSearch',
+ } as unknown) as SavedVisInstance['savedSearch'];
+
+ renderHook(() =>
+ useLinkedSearchUpdates(mockServices, eventEmitter, mockAppState, savedVisInstance)
+ );
+
+ eventEmitter.emit('unlinkFromSavedSearch');
+
+ expect(savedVisInstance.savedSearch?.searchSource?.getParent).toHaveBeenCalled();
+ expect(savedVisInstance.savedSearch?.searchSource?.getField).toHaveBeenCalledWith('index');
+ expect(mockAppState.transitions.unlinkSavedSearch).toHaveBeenCalled();
+ expect(mockServices.toastNotifications.addSuccess).toHaveBeenCalled();
+ });
+});
diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts
new file mode 100644
index 0000000000000..a6b6d8ca0e837
--- /dev/null
+++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts
@@ -0,0 +1,224 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { renderHook } from '@testing-library/react-hooks';
+import { EventEmitter } from 'events';
+
+import { coreMock } from '../../../../../../core/public/mocks';
+import { useSavedVisInstance } from './use_saved_vis_instance';
+import { redirectWhenMissing } from '../../../../../kibana_utils/public';
+import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs';
+import { VisualizeServices } from '../../types';
+import { VisualizeConstants } from '../../visualize_constants';
+
+const mockDefaultEditorControllerDestroy = jest.fn();
+const mockEmbeddableHandlerDestroy = jest.fn();
+const mockEmbeddableHandlerRender = jest.fn();
+const mockSavedVisDestroy = jest.fn();
+const savedVisId = '9ca7aa90-b892-11e8-a6d9-e546fe2bba5f';
+const mockSavedVisInstance = {
+ embeddableHandler: {
+ destroy: mockEmbeddableHandlerDestroy,
+ render: mockEmbeddableHandlerRender,
+ },
+ savedVis: {
+ id: savedVisId,
+ title: 'Test Vis',
+ destroy: mockSavedVisDestroy,
+ },
+ vis: {
+ type: {},
+ },
+};
+
+jest.mock('../get_visualization_instance', () => ({
+ getVisualizationInstance: jest.fn(() => mockSavedVisInstance),
+}));
+jest.mock('../breadcrumbs', () => ({
+ getEditBreadcrumbs: jest.fn((text) => text),
+ getCreateBreadcrumbs: jest.fn((text) => text),
+}));
+jest.mock('../../../../../vis_default_editor/public', () => ({
+ DefaultEditorController: jest.fn(() => ({ destroy: mockDefaultEditorControllerDestroy })),
+}));
+jest.mock('../../../../../kibana_utils/public');
+
+const mockGetVisualizationInstance = jest.requireMock('../get_visualization_instance')
+ .getVisualizationInstance;
+
+describe('useSavedVisInstance', () => {
+ const coreStartMock = coreMock.createStart();
+ const toastNotifications = coreStartMock.notifications.toasts;
+ let mockServices: VisualizeServices;
+ const eventEmitter = new EventEmitter();
+
+ beforeEach(() => {
+ mockServices = ({
+ ...coreStartMock,
+ toastNotifications,
+ history: {
+ location: {
+ pathname: VisualizeConstants.EDIT_PATH,
+ },
+ replace: () => {},
+ },
+ visualizations: {
+ all: jest.fn(() => [
+ {
+ name: 'area',
+ requiresSearch: true,
+ options: {
+ showIndexSelection: true,
+ },
+ },
+ { name: 'gauge' },
+ ]),
+ },
+ } as unknown) as VisualizeServices;
+
+ mockDefaultEditorControllerDestroy.mockClear();
+ mockEmbeddableHandlerDestroy.mockClear();
+ mockEmbeddableHandlerRender.mockClear();
+ mockSavedVisDestroy.mockClear();
+ toastNotifications.addWarning.mockClear();
+ mockGetVisualizationInstance.mockClear();
+ });
+
+ test('should not load instance until chrome is defined', () => {
+ const { result } = renderHook(() =>
+ useSavedVisInstance(mockServices, eventEmitter, undefined, undefined)
+ );
+ expect(mockGetVisualizationInstance).not.toHaveBeenCalled();
+ expect(result.current.visEditorController).toBeUndefined();
+ expect(result.current.savedVisInstance).toBeUndefined();
+ expect(result.current.visEditorRef).toBeDefined();
+ });
+
+ describe('edit saved visualization route', () => {
+ test('should load instance and initiate an editor if chrome is set up', async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useSavedVisInstance(mockServices, eventEmitter, true, savedVisId)
+ );
+
+ expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, savedVisId);
+ expect(mockGetVisualizationInstance.mock.calls.length).toBe(1);
+
+ await waitForNextUpdate();
+ expect(mockServices.chrome.setBreadcrumbs).toHaveBeenCalledWith('Test Vis');
+ expect(getEditBreadcrumbs).toHaveBeenCalledWith('Test Vis');
+ expect(getCreateBreadcrumbs).not.toHaveBeenCalled();
+ expect(mockEmbeddableHandlerRender).not.toHaveBeenCalled();
+ expect(result.current.visEditorController).toBeDefined();
+ expect(result.current.savedVisInstance).toBeDefined();
+ });
+
+ test('should destroy the editor and the savedVis on unmount if chrome exists', async () => {
+ const { unmount, waitForNextUpdate } = renderHook(() =>
+ useSavedVisInstance(mockServices, eventEmitter, true, savedVisId)
+ );
+
+ await waitForNextUpdate();
+ unmount();
+
+ expect(mockDefaultEditorControllerDestroy.mock.calls.length).toBe(1);
+ expect(mockEmbeddableHandlerDestroy).not.toHaveBeenCalled();
+ expect(mockSavedVisDestroy.mock.calls.length).toBe(1);
+ });
+ });
+
+ describe('create new visualization route', () => {
+ beforeEach(() => {
+ mockServices.history.location = {
+ ...mockServices.history.location,
+ pathname: VisualizeConstants.CREATE_PATH,
+ search: '?type=area&indexPattern=1a2b3c4d',
+ };
+ delete mockSavedVisInstance.savedVis.id;
+ });
+
+ test('should create new visualization based on search params', async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useSavedVisInstance(mockServices, eventEmitter, true, undefined)
+ );
+
+ expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, {
+ indexPattern: '1a2b3c4d',
+ type: 'area',
+ });
+
+ await waitForNextUpdate();
+
+ expect(getCreateBreadcrumbs).toHaveBeenCalled();
+ expect(mockEmbeddableHandlerRender).not.toHaveBeenCalled();
+ expect(result.current.visEditorController).toBeDefined();
+ expect(result.current.savedVisInstance).toBeDefined();
+ });
+
+ test('should throw error if vis type is invalid', async () => {
+ mockServices.history.location = {
+ ...mockServices.history.location,
+ search: '?type=myVisType&indexPattern=1a2b3c4d',
+ };
+
+ renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, undefined));
+
+ expect(mockGetVisualizationInstance).not.toHaveBeenCalled();
+ expect(redirectWhenMissing).toHaveBeenCalled();
+ expect(toastNotifications.addWarning).toHaveBeenCalled();
+ });
+
+ test("should throw error if index pattern or saved search id doesn't exist in search params", async () => {
+ mockServices.history.location = {
+ ...mockServices.history.location,
+ search: '?type=area',
+ };
+
+ renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, undefined));
+
+ expect(mockGetVisualizationInstance).not.toHaveBeenCalled();
+ expect(redirectWhenMissing).toHaveBeenCalled();
+ expect(toastNotifications.addWarning).toHaveBeenCalled();
+ });
+ });
+
+ describe('embeded mode', () => {
+ test('should create new visualization based on search params', async () => {
+ const { result, unmount, waitForNextUpdate } = renderHook(() =>
+ useSavedVisInstance(mockServices, eventEmitter, false, savedVisId)
+ );
+
+ // mock editor ref
+ // @ts-expect-error
+ result.current.visEditorRef.current = 'div';
+
+ expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, savedVisId);
+
+ await waitForNextUpdate();
+
+ expect(mockEmbeddableHandlerRender).toHaveBeenCalled();
+ expect(result.current.visEditorController).toBeUndefined();
+ expect(result.current.savedVisInstance).toBeDefined();
+
+ unmount();
+ expect(mockDefaultEditorControllerDestroy).not.toHaveBeenCalled();
+ expect(mockEmbeddableHandlerDestroy.mock.calls.length).toBe(1);
+ expect(mockSavedVisDestroy.mock.calls.length).toBe(1);
+ });
+ });
+});
diff --git a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts
new file mode 100644
index 0000000000000..e885067c58184
--- /dev/null
+++ b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts
@@ -0,0 +1,210 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { act, renderHook } from '@testing-library/react-hooks';
+import { EventEmitter } from 'events';
+import { Observable } from 'rxjs';
+
+import { useVisualizeAppState } from './use_visualize_app_state';
+import { VisualizeServices, SavedVisInstance } from '../../types';
+import { visualizeAppStateStub } from '../stubs';
+import { VisualizeConstants } from '../../visualize_constants';
+import { createVisualizeServicesMock } from '../mocks';
+
+jest.mock('../utils');
+jest.mock('../create_visualize_app_state');
+jest.mock('../../../../../data/public');
+
+describe('useVisualizeAppState', () => {
+ const { visStateToEditorState } = jest.requireMock('../utils');
+ const { createVisualizeAppState } = jest.requireMock('../create_visualize_app_state');
+ const { connectToQueryState } = jest.requireMock('../../../../../data/public');
+ const stopStateSyncMock = jest.fn();
+ const stateContainerGetStateMock = jest.fn(() => visualizeAppStateStub);
+ const stopSyncingAppFiltersMock = jest.fn();
+ const stateContainer = {
+ getState: stateContainerGetStateMock,
+ state$: new Observable(),
+ transitions: {
+ updateVisState: jest.fn(),
+ set: jest.fn(),
+ },
+ };
+
+ visStateToEditorState.mockImplementation(() => visualizeAppStateStub);
+ createVisualizeAppState.mockImplementation(() => ({
+ stateContainer,
+ stopStateSync: stopStateSyncMock,
+ }));
+ connectToQueryState.mockImplementation(() => stopSyncingAppFiltersMock);
+
+ const eventEmitter = new EventEmitter();
+ const savedVisInstance = ({
+ vis: {
+ setState: jest.fn().mockResolvedValue({}),
+ },
+ savedVis: {},
+ embeddableHandler: {},
+ } as unknown) as SavedVisInstance;
+ let mockServices: jest.Mocked;
+
+ beforeEach(() => {
+ mockServices = createVisualizeServicesMock();
+
+ stopStateSyncMock.mockClear();
+ stopSyncingAppFiltersMock.mockClear();
+ visStateToEditorState.mockClear();
+ });
+
+ it("should not create appState if vis instance isn't ready", () => {
+ const { result } = renderHook(() => useVisualizeAppState(mockServices, eventEmitter));
+
+ expect(result.current).toEqual({
+ appState: null,
+ hasUnappliedChanges: false,
+ });
+ });
+
+ it('should create appState and connect it to query search params', () => {
+ const { result } = renderHook(() =>
+ useVisualizeAppState(mockServices, eventEmitter, savedVisInstance)
+ );
+
+ expect(visStateToEditorState).toHaveBeenCalledWith(savedVisInstance, mockServices);
+ expect(createVisualizeAppState).toHaveBeenCalledWith({
+ stateDefaults: visualizeAppStateStub,
+ kbnUrlStateStorage: undefined,
+ });
+ expect(mockServices.data.query.filterManager.setAppFilters).toHaveBeenCalledWith(
+ visualizeAppStateStub.filters
+ );
+ expect(connectToQueryState).toHaveBeenCalledWith(mockServices.data.query, expect.any(Object), {
+ filters: 'appState',
+ });
+ expect(result.current).toEqual({
+ appState: stateContainer,
+ hasUnappliedChanges: false,
+ });
+ });
+
+ it('should stop state and app filters syncing with query on destroy', () => {
+ const { unmount } = renderHook(() =>
+ useVisualizeAppState(mockServices, eventEmitter, savedVisInstance)
+ );
+
+ unmount();
+
+ expect(stopStateSyncMock).toBeCalledTimes(1);
+ expect(stopSyncingAppFiltersMock).toBeCalledTimes(1);
+ });
+
+ it('should be subscribed on dirtyStateChange event from an editor', () => {
+ const { result } = renderHook(() =>
+ useVisualizeAppState(mockServices, eventEmitter, savedVisInstance)
+ );
+
+ act(() => {
+ eventEmitter.emit('dirtyStateChange', { isDirty: true });
+ });
+
+ expect(result.current.hasUnappliedChanges).toEqual(true);
+ expect(stateContainer.transitions.updateVisState).not.toHaveBeenCalled();
+ expect(visStateToEditorState).toHaveBeenCalledTimes(1);
+
+ act(() => {
+ eventEmitter.emit('dirtyStateChange', { isDirty: false });
+ });
+
+ expect(result.current.hasUnappliedChanges).toEqual(false);
+ expect(stateContainer.transitions.updateVisState).toHaveBeenCalledWith(
+ visualizeAppStateStub.vis
+ );
+ expect(visStateToEditorState).toHaveBeenCalledTimes(2);
+ });
+
+ describe('update vis state if the url params are not equal with the saved object vis state', () => {
+ const newAgg = {
+ id: '2',
+ enabled: true,
+ type: 'terms',
+ schema: 'group',
+ params: {
+ field: 'total_quantity',
+ orderBy: '1',
+ order: 'desc',
+ size: 5,
+ otherBucket: false,
+ otherBucketLabel: 'Other',
+ missingBucket: false,
+ missingBucketLabel: 'Missing',
+ customLabel: '',
+ },
+ };
+ const state = {
+ ...visualizeAppStateStub,
+ vis: {
+ ...visualizeAppStateStub.vis,
+ aggs: [...visualizeAppStateStub.vis.aggs, newAgg],
+ },
+ };
+
+ it('should successfully update vis state and set up app state container', async () => {
+ // @ts-expect-error
+ stateContainerGetStateMock.mockImplementation(() => state);
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useVisualizeAppState(mockServices, eventEmitter, savedVisInstance)
+ );
+
+ await waitForNextUpdate();
+
+ const { aggs, ...visState } = stateContainer.getState().vis;
+ const expectedNewVisState = {
+ ...visState,
+ data: { aggs: state.vis.aggs },
+ };
+
+ expect(savedVisInstance.vis.setState).toHaveBeenCalledWith(expectedNewVisState);
+ expect(result.current).toEqual({
+ appState: stateContainer,
+ hasUnappliedChanges: false,
+ });
+ });
+
+ it(`should add warning toast and redirect to the landing page
+ if setting new vis state was not successful, e.x. invalid query params`, async () => {
+ // @ts-expect-error
+ stateContainerGetStateMock.mockImplementation(() => state);
+ // @ts-expect-error
+ savedVisInstance.vis.setState.mockRejectedValue({
+ message: 'error',
+ });
+
+ renderHook(() => useVisualizeAppState(mockServices, eventEmitter, savedVisInstance));
+
+ await new Promise((res) => {
+ setTimeout(() => res());
+ });
+
+ expect(mockServices.toastNotifications.addWarning).toHaveBeenCalled();
+ expect(mockServices.history.replace).toHaveBeenCalledWith(
+ `${VisualizeConstants.LANDING_PAGE_PATH}?notFound=visualization`
+ );
+ });
+ });
+});
diff --git a/tasks/config/karma.js b/tasks/config/karma.js
index 7c4f75bea8801..fa4bdc8ed2266 100644
--- a/tasks/config/karma.js
+++ b/tasks/config/karma.js
@@ -110,7 +110,7 @@ module.exports = function (grunt) {
customLaunchers: {
Chrome_Headless: {
base: 'Chrome',
- flags: ['--headless', '--disable-gpu', '--remote-debugging-port=9222', '--no-sandbox'],
+ flags: ['--headless', '--disable-gpu', '--remote-debugging-port=9222'],
},
},
diff --git a/tasks/test_jest.js b/tasks/test_jest.js
index 810ed42324840..d8f51806e8ddc 100644
--- a/tasks/test_jest.js
+++ b/tasks/test_jest.js
@@ -22,7 +22,7 @@ const { resolve } = require('path');
module.exports = function (grunt) {
grunt.registerTask('test:jest', function () {
const done = this.async();
- runJest(resolve(__dirname, '../scripts/jest.js'), ['--maxWorkers=10']).then(done, done);
+ runJest(resolve(__dirname, '../scripts/jest.js')).then(done, done);
});
grunt.registerTask('test:jest_integration', function () {
@@ -30,10 +30,10 @@ module.exports = function (grunt) {
runJest(resolve(__dirname, '../scripts/jest_integration.js')).then(done, done);
});
- function runJest(jestScript, args = []) {
+ function runJest(jestScript) {
const serverCmd = {
cmd: 'node',
- args: [jestScript, '--ci', ...args],
+ args: [jestScript, '--ci'],
opts: { stdio: 'inherit' },
};
diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js
index 906f0b83e99e7..949a01ff7873a 100644
--- a/test/functional/apps/discover/_discover.js
+++ b/test/functional/apps/discover/_discover.js
@@ -218,6 +218,8 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.common.navigateToApp('discover');
await PageObjects.header.awaitKibanaChrome();
await queryBar.setQuery('');
+ // To remove focus of the of the search bar so date/time picker can show
+ await PageObjects.discover.selectIndexPattern(defaultSettings.defaultIndex);
await PageObjects.timePicker.setDefaultAbsoluteRange();
log.debug(
diff --git a/test/functional/apps/management/_create_index_pattern_wizard.js b/test/functional/apps/management/_create_index_pattern_wizard.js
index 8209f3e1ac9d6..cb8b5a6ddc65f 100644
--- a/test/functional/apps/management/_create_index_pattern_wizard.js
+++ b/test/functional/apps/management/_create_index_pattern_wizard.js
@@ -22,6 +22,7 @@ import expect from '@kbn/expect';
export default function ({ getService, getPageObjects }) {
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
+ const es = getService('legacyEs');
const PageObjects = getPageObjects(['settings', 'common']);
describe('"Create Index Pattern" wizard', function () {
@@ -48,5 +49,59 @@ export default function ({ getService, getPageObjects }) {
expect(isEnabled).to.be.ok();
});
});
+
+ describe('data streams', () => {
+ it('can be an index pattern', async () => {
+ await es.transport.request({
+ path: '/_index_template/generic-logs',
+ method: 'PUT',
+ body: {
+ index_patterns: ['logs-*', 'test_data_stream'],
+ template: {
+ mappings: {
+ properties: {
+ '@timestamp': {
+ type: 'date',
+ },
+ },
+ },
+ },
+ data_stream: {
+ timestamp_field: '@timestamp',
+ },
+ },
+ });
+
+ await es.transport.request({
+ path: '/_data_stream/test_data_stream',
+ method: 'PUT',
+ });
+
+ await PageObjects.settings.createIndexPattern('test_data_stream', false);
+
+ await es.transport.request({
+ path: '/_data_stream/test_data_stream',
+ method: 'DELETE',
+ });
+ });
+ });
+
+ describe('index alias', () => {
+ it('can be an index pattern', async () => {
+ await es.transport.request({
+ path: '/blogs/_doc',
+ method: 'POST',
+ body: { user: 'matt', message: 20 },
+ });
+
+ await es.transport.request({
+ path: '/_aliases',
+ method: 'POST',
+ body: { actions: [{ add: { index: 'blogs', alias: 'alias1' } }] },
+ });
+
+ await PageObjects.settings.createIndexPattern('alias1', false);
+ });
+ });
});
}
diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts
index 92482a3779771..7c325ba6d4aec 100644
--- a/test/functional/page_objects/dashboard_page.ts
+++ b/test/functional/page_objects/dashboard_page.ts
@@ -290,14 +290,16 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide
dashboardName: string,
saveOptions: SaveDashboardOptions = { waitDialogIsClosed: true }
) {
- await this.enterDashboardTitleAndClickSave(dashboardName, saveOptions);
+ await retry.try(async () => {
+ await this.enterDashboardTitleAndClickSave(dashboardName, saveOptions);
- if (saveOptions.needsConfirm) {
- await this.clickSave();
- }
+ if (saveOptions.needsConfirm) {
+ await this.clickSave();
+ }
- // Confirm that the Dashboard has actually been saved
- await testSubjects.existOrFail('saveDashboardSuccess');
+ // Confirm that the Dashboard has actually been saved
+ await testSubjects.existOrFail('saveDashboardSuccess');
+ });
const message = await PageObjects.common.closeToast();
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.common.waitForSaveModalToClose();
diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts
index 49133d8b13836..a08598fc42d68 100644
--- a/test/functional/page_objects/visualize_page.ts
+++ b/test/functional/page_objects/visualize_page.ts
@@ -257,7 +257,7 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide
public async openSavedVisualization(vizName: string) {
const dataTestSubj = `visListingTitleLink-${vizName.split(' ').join('-')}`;
- await testSubjects.click(dataTestSubj);
+ await testSubjects.click(dataTestSubj, 20000);
await header.waitUntilLoadingHasFinished();
}
diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts
index 27814060e70c1..78f659a064a0c 100644
--- a/test/functional/services/remote/webdriver.ts
+++ b/test/functional/services/remote/webdriver.ts
@@ -88,6 +88,7 @@ async function attemptToCreateCommand(
) {
const attemptId = ++attemptCounter;
log.debug('[webdriver] Creating session');
+ const remoteSessionUrl = process.env.REMOTE_SESSION_URL;
const buildDriverInstance = async () => {
switch (browserType) {
@@ -133,11 +134,20 @@ async function attemptToCreateCommand(
chromeCapabilities.set('goog:loggingPrefs', { browser: 'ALL' });
chromeCapabilities.setAcceptInsecureCerts(config.acceptInsecureCerts);
- const session = await new Builder()
- .forBrowser(browserType)
- .withCapabilities(chromeCapabilities)
- .setChromeService(new chrome.ServiceBuilder(chromeDriver.path).enableVerboseLogging())
- .build();
+ let session;
+ if (remoteSessionUrl) {
+ session = await new Builder()
+ .forBrowser(browserType)
+ .withCapabilities(chromeCapabilities)
+ .usingServer(remoteSessionUrl)
+ .build();
+ } else {
+ session = await new Builder()
+ .forBrowser(browserType)
+ .withCapabilities(chromeCapabilities)
+ .setChromeService(new chrome.ServiceBuilder(chromeDriver.path).enableVerboseLogging())
+ .build();
+ }
return {
session,
@@ -284,11 +294,19 @@ async function attemptToCreateCommand(
logLevel: 'TRACE',
});
- const session = await new Builder()
- .forBrowser(browserType)
- .withCapabilities(ieCapabilities)
- .build();
-
+ let session;
+ if (remoteSessionUrl) {
+ session = await new Builder()
+ .forBrowser(browserType)
+ .withCapabilities(ieCapabilities)
+ .usingServer(remoteSessionUrl)
+ .build();
+ } else {
+ session = await new Builder()
+ .forBrowser(browserType)
+ .withCapabilities(ieCapabilities)
+ .build();
+ }
return {
session,
consoleLog$: Rx.EMPTY,
diff --git a/test/scripts/checks/doc_api_changes.sh b/test/scripts/checks/doc_api_changes.sh
deleted file mode 100755
index 503d12b2f6d73..0000000000000
--- a/test/scripts/checks/doc_api_changes.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:checkDocApiChanges
diff --git a/test/scripts/checks/file_casing.sh b/test/scripts/checks/file_casing.sh
deleted file mode 100755
index 513664263791b..0000000000000
--- a/test/scripts/checks/file_casing.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:checkFileCasing
diff --git a/test/scripts/checks/i18n.sh b/test/scripts/checks/i18n.sh
deleted file mode 100755
index 7a6fd46c46c76..0000000000000
--- a/test/scripts/checks/i18n.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:i18nCheck
diff --git a/test/scripts/checks/licenses.sh b/test/scripts/checks/licenses.sh
deleted file mode 100755
index a08d7d07a24a1..0000000000000
--- a/test/scripts/checks/licenses.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:licenses
diff --git a/test/scripts/checks/lock_file_symlinks.sh b/test/scripts/checks/lock_file_symlinks.sh
deleted file mode 100755
index 1d43d32c9feb8..0000000000000
--- a/test/scripts/checks/lock_file_symlinks.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:checkLockfileSymlinks
diff --git a/test/scripts/checks/test_hardening.sh b/test/scripts/checks/test_hardening.sh
deleted file mode 100755
index 9184758577654..0000000000000
--- a/test/scripts/checks/test_hardening.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:test_hardening
diff --git a/test/scripts/checks/test_projects.sh b/test/scripts/checks/test_projects.sh
deleted file mode 100755
index 5f9aafe80e10e..0000000000000
--- a/test/scripts/checks/test_projects.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:test_projects
diff --git a/test/scripts/checks/ts_projects.sh b/test/scripts/checks/ts_projects.sh
deleted file mode 100755
index d667c753baec2..0000000000000
--- a/test/scripts/checks/ts_projects.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:checkTsProjects
diff --git a/test/scripts/checks/type_check.sh b/test/scripts/checks/type_check.sh
deleted file mode 100755
index 07c49638134be..0000000000000
--- a/test/scripts/checks/type_check.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:typeCheck
diff --git a/test/scripts/checks/verify_dependency_versions.sh b/test/scripts/checks/verify_dependency_versions.sh
deleted file mode 100755
index b73a71e7ff7fd..0000000000000
--- a/test/scripts/checks/verify_dependency_versions.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:verifyDependencyVersions
diff --git a/test/scripts/checks/verify_notice.sh b/test/scripts/checks/verify_notice.sh
deleted file mode 100755
index 9f8343e540861..0000000000000
--- a/test/scripts/checks/verify_notice.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:verifyNotice
diff --git a/test/scripts/jenkins_build_kbn_sample_panel_action.sh b/test/scripts/jenkins_build_kbn_sample_panel_action.sh
old mode 100755
new mode 100644
diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh
index f449986713f97..3e49edc8e6ae5 100755
--- a/test/scripts/jenkins_build_kibana.sh
+++ b/test/scripts/jenkins_build_kibana.sh
@@ -2,9 +2,19 @@
source src/dev/ci_setup/setup_env.sh
-if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then
- ./test/scripts/jenkins_build_plugins.sh
-fi
+echo " -> building examples separate from test plugins"
+node scripts/build_kibana_platform_plugins \
+ --oss \
+ --examples \
+ --verbose;
+
+echo " -> building test plugins"
+node scripts/build_kibana_platform_plugins \
+ --oss \
+ --no-examples \
+ --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \
+ --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \
+ --verbose;
# doesn't persist, also set in kibanaPipeline.groovy
export KBN_NP_PLUGINS_BUILT=true
@@ -16,7 +26,4 @@ yarn run grunt functionalTests:ensureAllTestsInCiGroup;
if [[ -z "$CODE_COVERAGE" ]] ; then
echo " -> building and extracting OSS Kibana distributable for use in functional tests"
node scripts/build --debug --oss
-
- mkdir -p "$WORKSPACE/kibana-build-oss"
- cp -pR build/oss/kibana-*-SNAPSHOT-linux-x86_64/. $WORKSPACE/kibana-build-oss/
fi
diff --git a/test/scripts/jenkins_build_plugins.sh b/test/scripts/jenkins_build_plugins.sh
deleted file mode 100755
index 32b3942074b34..0000000000000
--- a/test/scripts/jenkins_build_plugins.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-echo " -> building examples separate from test plugins"
-node scripts/build_kibana_platform_plugins \
- --oss \
- --examples \
- --workers 6 \
- --verbose
-
-echo " -> building kibana platform plugins"
-node scripts/build_kibana_platform_plugins \
- --oss \
- --no-examples \
- --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \
- --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \
- --workers 6 \
- --verbose
diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh
index 2542d7032e83b..60d7f0406f4c9 100755
--- a/test/scripts/jenkins_ci_group.sh
+++ b/test/scripts/jenkins_ci_group.sh
@@ -5,7 +5,7 @@ source test/scripts/jenkins_test_setup_oss.sh
if [[ -z "$CODE_COVERAGE" ]]; then
checks-reporter-with-killswitch "Functional tests / Group ${CI_GROUP}" yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}";
- if [[ ! "$TASK_QUEUE_PROCESS_ID" && "$CI_GROUP" == "1" ]]; then
+ if [ "$CI_GROUP" == "1" ]; then
source test/scripts/jenkins_build_kbn_sample_panel_action.sh
yarn run grunt run:pluginFunctionalTestsRelease --from=source;
yarn run grunt run:exampleFunctionalTestsRelease --from=source;
diff --git a/test/scripts/jenkins_plugin_functional.sh b/test/scripts/jenkins_plugin_functional.sh
deleted file mode 100755
index 1d691d98982de..0000000000000
--- a/test/scripts/jenkins_plugin_functional.sh
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env bash
-
-source test/scripts/jenkins_test_setup_oss.sh
-
-cd test/plugin_functional/plugins/kbn_sample_panel_action;
-if [[ ! -d "target" ]]; then
- yarn build;
-fi
-cd -;
-
-pwd
-
-yarn run grunt run:pluginFunctionalTestsRelease --from=source;
-yarn run grunt run:exampleFunctionalTestsRelease --from=source;
-yarn run grunt run:interpreterFunctionalTestsRelease;
diff --git a/test/scripts/jenkins_security_solution_cypress.sh b/test/scripts/jenkins_security_solution_cypress.sh
old mode 100755
new mode 100644
index a5a1a2103801f..204911a3eedaa
--- a/test/scripts/jenkins_security_solution_cypress.sh
+++ b/test/scripts/jenkins_security_solution_cypress.sh
@@ -1,6 +1,12 @@
#!/usr/bin/env bash
-source test/scripts/jenkins_test_setup_xpack.sh
+source test/scripts/jenkins_test_setup.sh
+
+installDir="$PARENT_DIR/install/kibana"
+destDir="${installDir}-${CI_WORKER_NUMBER}"
+cp -R "$installDir" "$destDir"
+
+export KIBANA_INSTALL_DIR="$destDir"
echo " -> Running security solution cypress tests"
cd "$XPACK_DIR"
diff --git a/test/scripts/jenkins_setup_parallel_workspace.sh b/test/scripts/jenkins_setup_parallel_workspace.sh
deleted file mode 100755
index 5274d05572e71..0000000000000
--- a/test/scripts/jenkins_setup_parallel_workspace.sh
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env bash
-set -e
-
-CURRENT_DIR=$(pwd)
-
-# Copy everything except node_modules into the current workspace
-rsync -a ${WORKSPACE}/kibana/* . --exclude node_modules
-rsync -a ${WORKSPACE}/kibana/.??* .
-
-# Symlink all non-root, non-fixture node_modules into our new workspace
-cd ${WORKSPACE}/kibana
-find . -type d -name node_modules -not -path '*__fixtures__*' -not -path './node_modules*' -prune -print0 | xargs -0I % ln -s "${WORKSPACE}/kibana/%" "${CURRENT_DIR}/%"
-find . -type d -wholename '*__fixtures__*node_modules' -not -path './node_modules*' -prune -print0 | xargs -0I % cp -R "${WORKSPACE}/kibana/%" "${CURRENT_DIR}/%"
-cd "${CURRENT_DIR}"
-
-# Symlink all of the individual root-level node_modules into the node_modules/ directory
-mkdir -p node_modules
-ln -s ${WORKSPACE}/kibana/node_modules/* node_modules/
-ln -s ${WORKSPACE}/kibana/node_modules/.??* node_modules/
-
-# Copy a few node_modules instead of symlinking them. They don't work correctly if symlinked
-unlink node_modules/@kbn
-unlink node_modules/css-loader
-unlink node_modules/style-loader
-
-# packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts will fail if this is a symlink
-unlink node_modules/val-loader
-
-cp -R ${WORKSPACE}/kibana/node_modules/@kbn node_modules/
-cp -R ${WORKSPACE}/kibana/node_modules/css-loader node_modules/
-cp -R ${WORKSPACE}/kibana/node_modules/style-loader node_modules/
-cp -R ${WORKSPACE}/kibana/node_modules/val-loader node_modules/
diff --git a/test/scripts/jenkins_test_setup.sh b/test/scripts/jenkins_test_setup.sh
old mode 100755
new mode 100644
index 7cced76eb650f..49ee8a6b526ca
--- a/test/scripts/jenkins_test_setup.sh
+++ b/test/scripts/jenkins_test_setup.sh
@@ -14,7 +14,3 @@ trap 'post_work' EXIT
export TEST_BROWSER_HEADLESS=1
source src/dev/ci_setup/setup_env.sh
-
-if [[ ! -d .es && -d "$WORKSPACE/kibana/.es" ]]; then
- cp -R $WORKSPACE/kibana/.es ./
-fi
diff --git a/test/scripts/jenkins_test_setup_oss.sh b/test/scripts/jenkins_test_setup_oss.sh
old mode 100755
new mode 100644
index b7eac33f35176..7bbb867526384
--- a/test/scripts/jenkins_test_setup_oss.sh
+++ b/test/scripts/jenkins_test_setup_oss.sh
@@ -2,17 +2,10 @@
source test/scripts/jenkins_test_setup.sh
-if [[ -z "$CODE_COVERAGE" ]]; then
-
- destDir="build/kibana-build-oss"
- if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then
- destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}"
- fi
-
- if [[ ! -d $destDir ]]; then
- mkdir -p $destDir
- cp -pR "$WORKSPACE/kibana-build-oss/." $destDir/
- fi
+if [[ -z "$CODE_COVERAGE" ]] ; then
+ installDir="$(realpath $PARENT_DIR/kibana/build/oss/kibana-*-SNAPSHOT-linux-x86_64)"
+ destDir=${installDir}-${CI_PARALLEL_PROCESS_NUMBER}
+ cp -R "$installDir" "$destDir"
export KIBANA_INSTALL_DIR="$destDir"
fi
diff --git a/test/scripts/jenkins_test_setup_xpack.sh b/test/scripts/jenkins_test_setup_xpack.sh
old mode 100755
new mode 100644
index 74a3de77e3a76..a72e9749ebbd5
--- a/test/scripts/jenkins_test_setup_xpack.sh
+++ b/test/scripts/jenkins_test_setup_xpack.sh
@@ -3,18 +3,11 @@
source test/scripts/jenkins_test_setup.sh
if [[ -z "$CODE_COVERAGE" ]]; then
+ installDir="$PARENT_DIR/install/kibana"
+ destDir="${installDir}-${CI_PARALLEL_PROCESS_NUMBER}"
+ cp -R "$installDir" "$destDir"
- destDir="build/kibana-build-xpack"
- if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then
- destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}"
- fi
-
- if [[ ! -d $destDir ]]; then
- mkdir -p $destDir
- cp -pR "$WORKSPACE/kibana-build-xpack/." $destDir/
- fi
-
- export KIBANA_INSTALL_DIR="$(realpath $destDir)"
+ export KIBANA_INSTALL_DIR="$destDir"
cd "$XPACK_DIR"
fi
diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh
index 2452e2f5b8c58..58ef6a42d3fe4 100755
--- a/test/scripts/jenkins_xpack_build_kibana.sh
+++ b/test/scripts/jenkins_xpack_build_kibana.sh
@@ -3,9 +3,21 @@
cd "$KIBANA_DIR"
source src/dev/ci_setup/setup_env.sh
-if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then
- ./test/scripts/jenkins_xpack_build_plugins.sh
-fi
+echo " -> building examples separate from test plugins"
+node scripts/build_kibana_platform_plugins \
+ --examples \
+ --verbose;
+
+echo " -> building test plugins"
+node scripts/build_kibana_platform_plugins \
+ --no-examples \
+ --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \
+ --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \
+ --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \
+ --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \
+ --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \
+ --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \
+ --verbose;
# doesn't persist, also set in kibanaPipeline.groovy
export KBN_NP_PLUGINS_BUILT=true
@@ -30,10 +42,7 @@ if [[ -z "$CODE_COVERAGE" ]] ; then
cd "$KIBANA_DIR"
node scripts/build --debug --no-oss
linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')"
- installDir="$KIBANA_DIR/install/kibana"
+ installDir="$PARENT_DIR/install/kibana"
mkdir -p "$installDir"
tar -xzf "$linuxBuild" -C "$installDir" --strip=1
-
- mkdir -p "$WORKSPACE/kibana-build-xpack"
- cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/
fi
diff --git a/test/scripts/jenkins_xpack_build_plugins.sh b/test/scripts/jenkins_xpack_build_plugins.sh
deleted file mode 100755
index fea30c547bd5f..0000000000000
--- a/test/scripts/jenkins_xpack_build_plugins.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-echo " -> building examples separate from test plugins"
-node scripts/build_kibana_platform_plugins \
- --workers 12 \
- --examples \
- --verbose
-
-echo " -> building kibana platform plugins"
-node scripts/build_kibana_platform_plugins \
- --no-examples \
- --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \
- --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \
- --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \
- --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \
- --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \
- --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \
- --workers 12 \
- --verbose
diff --git a/test/scripts/jenkins_xpack_page_load_metrics.sh b/test/scripts/jenkins_xpack_page_load_metrics.sh
old mode 100755
new mode 100644
diff --git a/test/scripts/lint/eslint.sh b/test/scripts/lint/eslint.sh
deleted file mode 100755
index c3211300b96c5..0000000000000
--- a/test/scripts/lint/eslint.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:eslint
diff --git a/test/scripts/lint/sasslint.sh b/test/scripts/lint/sasslint.sh
deleted file mode 100755
index b9c683bcb049e..0000000000000
--- a/test/scripts/lint/sasslint.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:sasslint
diff --git a/test/scripts/test/api_integration.sh b/test/scripts/test/api_integration.sh
deleted file mode 100755
index 152c97a3ca7df..0000000000000
--- a/test/scripts/test/api_integration.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:apiIntegrationTests
diff --git a/test/scripts/test/jest_integration.sh b/test/scripts/test/jest_integration.sh
deleted file mode 100755
index 73dbbddfb38f6..0000000000000
--- a/test/scripts/test/jest_integration.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:test_jest_integration
diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh
deleted file mode 100755
index e25452698cebc..0000000000000
--- a/test/scripts/test/jest_unit.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:test_jest
diff --git a/test/scripts/test/karma_ci.sh b/test/scripts/test/karma_ci.sh
deleted file mode 100755
index e9985300ba19d..0000000000000
--- a/test/scripts/test/karma_ci.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:test_karma_ci
diff --git a/test/scripts/test/mocha.sh b/test/scripts/test/mocha.sh
deleted file mode 100755
index 43c00f0a09dcf..0000000000000
--- a/test/scripts/test/mocha.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:mocha
diff --git a/test/scripts/test/xpack_jest_unit.sh b/test/scripts/test/xpack_jest_unit.sh
deleted file mode 100755
index 93d70ec355391..0000000000000
--- a/test/scripts/test/xpack_jest_unit.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-cd x-pack
-checks-reporter-with-killswitch "X-Pack Jest" node --max-old-space-size=6144 scripts/jest --ci --verbose --maxWorkers=10
diff --git a/test/scripts/test/xpack_karma.sh b/test/scripts/test/xpack_karma.sh
deleted file mode 100755
index 9078f01f1b870..0000000000000
--- a/test/scripts/test/xpack_karma.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-cd x-pack
-checks-reporter-with-killswitch "X-Pack Karma Tests" yarn test:karma
diff --git a/test/scripts/test/xpack_list_cyclic_dependency.sh b/test/scripts/test/xpack_list_cyclic_dependency.sh
deleted file mode 100755
index 493fe9f58d322..0000000000000
--- a/test/scripts/test/xpack_list_cyclic_dependency.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-cd x-pack
-checks-reporter-with-killswitch "X-Pack List cyclic dependency test" node plugins/lists/scripts/check_circular_deps
diff --git a/test/scripts/test/xpack_siem_cyclic_dependency.sh b/test/scripts/test/xpack_siem_cyclic_dependency.sh
deleted file mode 100755
index b21301f25ad08..0000000000000
--- a/test/scripts/test/xpack_siem_cyclic_dependency.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-cd x-pack
-checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node plugins/security_solution/scripts/check_circular_deps
diff --git a/vars/catchErrors.groovy b/vars/catchErrors.groovy
index 2a1b55d832606..460a90b8ec0c0 100644
--- a/vars/catchErrors.groovy
+++ b/vars/catchErrors.groovy
@@ -1,15 +1,8 @@
// Basically, this is a shortcut for catchError(catchInterruptions: false) {}
// By default, catchError will swallow aborts/timeouts, which we almost never want
-// Also, by wrapping it in an additional try/catch, we cut down on spam in Pipeline Steps
def call(Map params = [:], Closure closure) {
- try {
- closure()
- } catch (ex) {
- params.catchInterruptions = false
- catchError(params) {
- throw ex
- }
- }
+ params.catchInterruptions = false
+ return catchError(params, closure)
}
return this
diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy
index 0f11204311451..f3fc5f84583c9 100644
--- a/vars/kibanaPipeline.groovy
+++ b/vars/kibanaPipeline.groovy
@@ -16,34 +16,27 @@ def withPostBuildReporting(Closure closure) {
}
}
-def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) {
- // This can go away once everything that uses the deprecated workers.parallelProcesses() is moved to task queue
- def parallelId = env.TASK_QUEUE_PROCESS_ID ?: env.CI_PARALLEL_PROCESS_NUMBER
-
- def kibanaPort = "61${parallelId}1"
- def esPort = "61${parallelId}2"
- def esTransportPort = "61${parallelId}3"
- def ingestManagementPackageRegistryPort = "61${parallelId}4"
-
- withEnv([
- "CI_GROUP=${parallelId}",
- "REMOVE_KIBANA_INSTALL_DIR=1",
- "CI_PARALLEL_PROCESS_NUMBER=${parallelId}",
- "TEST_KIBANA_HOST=localhost",
- "TEST_KIBANA_PORT=${kibanaPort}",
- "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}",
- "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}",
- "TEST_ES_TRANSPORT_PORT=${esTransportPort}",
- "KBN_NP_PLUGINS_BUILT=true",
- "INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=${ingestManagementPackageRegistryPort}",
- ] + additionalEnvs) {
- closure()
- }
-}
-
def functionalTestProcess(String name, Closure closure) {
- return {
- withFunctionalTestEnv(["JOB=${name}"], closure)
+ return { processNumber ->
+ def kibanaPort = "61${processNumber}1"
+ def esPort = "61${processNumber}2"
+ def esTransportPort = "61${processNumber}3"
+ def ingestManagementPackageRegistryPort = "61${processNumber}4"
+
+ withEnv([
+ "CI_PARALLEL_PROCESS_NUMBER=${processNumber}",
+ "TEST_KIBANA_HOST=localhost",
+ "TEST_KIBANA_PORT=${kibanaPort}",
+ "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}",
+ "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}",
+ "TEST_ES_TRANSPORT_PORT=${esTransportPort}",
+ "INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=${ingestManagementPackageRegistryPort}",
+ "IS_PIPELINE_JOB=1",
+ "JOB=${name}",
+ "KBN_NP_PLUGINS_BUILT=true",
+ ]) {
+ closure()
+ }
}
}
@@ -107,17 +100,11 @@ def withGcsArtifactUpload(workerName, closure) {
def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}"
def ARTIFACT_PATTERNS = [
'target/kibana-*',
- 'target/test-metrics/*',
'target/kibana-security-solution/**/*.png',
'target/junit/**/*',
- 'target/test-suites-ci-plan.json',
- 'test/**/screenshots/session/*.png',
- 'test/**/screenshots/failure/*.png',
- 'test/**/screenshots/diff/*.png',
+ 'test/**/screenshots/**/*.png',
'test/functional/failure_debug/html/*.html',
- 'x-pack/test/**/screenshots/session/*.png',
- 'x-pack/test/**/screenshots/failure/*.png',
- 'x-pack/test/**/screenshots/diff/*.png',
+ 'x-pack/test/**/screenshots/**/*.png',
'x-pack/test/functional/failure_debug/html/*.html',
'x-pack/test/functional/apps/reporting/reports/session/*.pdf',
]
@@ -132,12 +119,6 @@ def withGcsArtifactUpload(workerName, closure) {
ARTIFACT_PATTERNS.each { pattern ->
uploadGcsArtifact(uploadPrefix, pattern)
}
-
- dir(env.WORKSPACE) {
- ARTIFACT_PATTERNS.each { pattern ->
- uploadGcsArtifact(uploadPrefix, "parallel/*/kibana/${pattern}")
- }
- }
}
}
})
@@ -150,11 +131,6 @@ def withGcsArtifactUpload(workerName, closure) {
def publishJunit() {
junit(testResults: 'target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true)
-
- // junit() is weird about paths for security reasons, so we need to actually change to an upper directory first
- dir(env.WORKSPACE) {
- junit(testResults: 'parallel/*/kibana/target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true)
- }
}
def sendMail() {
@@ -218,16 +194,12 @@ def doSetup() {
}
}
-def buildOss(maxWorkers = '') {
- withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) {
- runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana")
- }
+def buildOss() {
+ runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana")
}
-def buildXpack(maxWorkers = '') {
- withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) {
- runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana")
- }
+def buildXpack() {
+ runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana")
}
def runErrorReporter() {
@@ -276,100 +248,6 @@ def call(Map params = [:], Closure closure) {
}
}
-// Creates a task queue using withTaskQueue, and copies the bootstrapped kibana repo into each process's workspace
-// Note that node_modules are mostly symlinked to save time/space. See test/scripts/jenkins_setup_parallel_workspace.sh
-def withCiTaskQueue(Map options = [:], Closure closure) {
- def setupClosure = {
- // This can't use runbld, because it expects the source to be there, which isn't yet
- bash("${env.WORKSPACE}/kibana/test/scripts/jenkins_setup_parallel_workspace.sh", "Set up duplicate workspace for parallel process")
- }
-
- def config = [parallel: 24, setup: setupClosure] + options
-
- withTaskQueue(config) {
- closure.call()
- }
-}
-
-def scriptTask(description, script) {
- return {
- withFunctionalTestEnv {
- runbld(script, description)
- }
- }
-}
-
-def scriptTaskDocker(description, script) {
- return {
- withDocker(scriptTask(description, script))
- }
-}
-
-def buildDocker() {
- sh(
- script: """
- cp /usr/local/bin/runbld .ci/
- cp /usr/local/bin/bash_standard_lib.sh .ci/
- cd .ci
- docker build -t kibana-ci -f ./Dockerfile .
- """,
- label: 'Build CI Docker image'
- )
-}
-
-def withDocker(Closure closure) {
- docker
- .image('kibana-ci')
- .inside(
- "-v /etc/runbld:/etc/runbld:ro -v '${env.JENKINS_HOME}:${env.JENKINS_HOME}' -v '/dev/shm/workspace:/dev/shm/workspace' --shm-size 2GB --cpus 4",
- closure
- )
-}
-
-def buildOssPlugins() {
- runbld('./test/scripts/jenkins_build_plugins.sh', 'Build OSS Plugins')
-}
-
-def buildXpackPlugins() {
- runbld('./test/scripts/jenkins_xpack_build_plugins.sh', 'Build X-Pack Plugins')
-}
-
-def withTasks(Map params = [worker: [:]], Closure closure) {
- catchErrors {
- def config = [name: 'ci-worker', size: 'xxl', ramDisk: true] + (params.worker ?: [:])
-
- workers.ci(config) {
- withCiTaskQueue(parallel: 24) {
- parallel([
- docker: {
- retry(2) {
- buildDocker()
- }
- },
-
- // There are integration tests etc that require the plugins to be built first, so let's go ahead and build them before set up the parallel workspaces
- ossPlugins: { buildOssPlugins() },
- xpackPlugins: { buildXpackPlugins() },
- ])
-
- catchErrors {
- closure()
- }
- }
- }
- }
-}
-
-def allCiTasks() {
- withTasks {
- tasks.check()
- tasks.lint()
- tasks.test()
- tasks.functionalOss()
- tasks.functionalXpack()
- }
-}
-
def pipelineLibraryTests() {
whenChanged(['vars/', '.ci/pipeline-library/']) {
workers.base(size: 'flyweight', bootstrapped: false, ramDisk: false) {
@@ -380,4 +258,5 @@ def pipelineLibraryTests() {
}
}
+
return this
diff --git a/vars/task.groovy b/vars/task.groovy
deleted file mode 100644
index 0c07b519b6fef..0000000000000
--- a/vars/task.groovy
+++ /dev/null
@@ -1,5 +0,0 @@
-def call(Closure closure) {
- withTaskQueue.addTask(closure)
-}
-
-return this
diff --git a/vars/tasks.groovy b/vars/tasks.groovy
deleted file mode 100644
index 9de4c78322d3e..0000000000000
--- a/vars/tasks.groovy
+++ /dev/null
@@ -1,118 +0,0 @@
-def call(List closures) {
- withTaskQueue.addTasks(closures)
-}
-
-def check() {
- tasks([
- kibanaPipeline.scriptTask('Check TypeScript Projects', 'test/scripts/checks/ts_projects.sh'),
- kibanaPipeline.scriptTask('Check Doc API Changes', 'test/scripts/checks/doc_api_changes.sh'),
- kibanaPipeline.scriptTask('Check Types', 'test/scripts/checks/type_check.sh'),
- kibanaPipeline.scriptTask('Check i18n', 'test/scripts/checks/i18n.sh'),
- kibanaPipeline.scriptTask('Check File Casing', 'test/scripts/checks/file_casing.sh'),
- kibanaPipeline.scriptTask('Check Lockfile Symlinks', 'test/scripts/checks/lock_file_symlinks.sh'),
- kibanaPipeline.scriptTask('Check Licenses', 'test/scripts/checks/licenses.sh'),
- kibanaPipeline.scriptTask('Verify Dependency Versions', 'test/scripts/checks/verify_dependency_versions.sh'),
- kibanaPipeline.scriptTask('Verify NOTICE', 'test/scripts/checks/verify_notice.sh'),
- kibanaPipeline.scriptTask('Test Projects', 'test/scripts/checks/test_projects.sh'),
- kibanaPipeline.scriptTask('Test Hardening', 'test/scripts/checks/test_hardening.sh'),
- ])
-}
-
-def lint() {
- tasks([
- kibanaPipeline.scriptTask('Lint: eslint', 'test/scripts/lint/eslint.sh'),
- kibanaPipeline.scriptTask('Lint: sasslint', 'test/scripts/lint/sasslint.sh'),
- ])
-}
-
-def test() {
- tasks([
- // These 4 tasks require isolation because of hard-coded, conflicting ports and such, so let's use Docker here
- kibanaPipeline.scriptTaskDocker('Jest Integration Tests', 'test/scripts/test/jest_integration.sh'),
- kibanaPipeline.scriptTaskDocker('Mocha Tests', 'test/scripts/test/mocha.sh'),
- kibanaPipeline.scriptTaskDocker('Karma CI Tests', 'test/scripts/test/karma_ci.sh'),
- kibanaPipeline.scriptTaskDocker('X-Pack Karma Tests', 'test/scripts/test/xpack_karma.sh'),
-
- kibanaPipeline.scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh'),
- kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh'),
- kibanaPipeline.scriptTask('X-Pack SIEM cyclic dependency', 'test/scripts/test/xpack_siem_cyclic_dependency.sh'),
- kibanaPipeline.scriptTask('X-Pack List cyclic dependency', 'test/scripts/test/xpack_list_cyclic_dependency.sh'),
- kibanaPipeline.scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh'),
- ])
-}
-
-def functionalOss(Map params = [:]) {
- def config = params ?: [ciGroups: true, firefox: true, accessibility: true, pluginFunctional: true, visualRegression: false]
-
- task {
- kibanaPipeline.buildOss(6)
-
- if (config.ciGroups) {
- def ciGroups = 1..12
- tasks(ciGroups.collect { kibanaPipeline.ossCiGroupProcess(it) })
- }
-
- if (config.firefox) {
- task(kibanaPipeline.functionalTestProcess('oss-firefox', './test/scripts/jenkins_firefox_smoke.sh'))
- }
-
- if (config.accessibility) {
- task(kibanaPipeline.functionalTestProcess('oss-accessibility', './test/scripts/jenkins_accessibility.sh'))
- }
-
- if (config.pluginFunctional) {
- task(kibanaPipeline.functionalTestProcess('oss-pluginFunctional', './test/scripts/jenkins_plugin_functional.sh'))
- }
-
- if (config.visualRegression) {
- task(kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh'))
- }
- }
-}
-
-def functionalXpack(Map params = [:]) {
- def config = params ?: [
- ciGroups: true,
- firefox: true,
- accessibility: true,
- pluginFunctional: true,
- savedObjectsFieldMetrics: true,
- pageLoadMetrics: false,
- visualRegression: false,
- ]
-
- task {
- kibanaPipeline.buildXpack(10)
-
- if (config.ciGroups) {
- def ciGroups = 1..10
- tasks(ciGroups.collect { kibanaPipeline.xpackCiGroupProcess(it) })
- }
-
- if (config.firefox) {
- task(kibanaPipeline.functionalTestProcess('xpack-firefox', './test/scripts/jenkins_xpack_firefox_smoke.sh'))
- }
-
- if (config.accessibility) {
- task(kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'))
- }
-
- if (config.visualRegression) {
- task(kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'))
- }
-
- if (config.pageLoadMetrics) {
- task(kibanaPipeline.functionalTestProcess('xpack-pageLoadMetrics', './test/scripts/jenkins_xpack_page_load_metrics.sh'))
- }
-
- if (config.savedObjectsFieldMetrics) {
- task(kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh'))
- }
-
- whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/']) {
- task(kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh'))
- }
- }
-}
-
-return this
diff --git a/vars/withTaskQueue.groovy b/vars/withTaskQueue.groovy
deleted file mode 100644
index 8132d6264744f..0000000000000
--- a/vars/withTaskQueue.groovy
+++ /dev/null
@@ -1,154 +0,0 @@
-import groovy.transform.Field
-
-public static @Field TASK_QUEUES = [:]
-public static @Field TASK_QUEUES_COUNTER = 0
-
-/**
- withTaskQueue creates a queue of "tasks" (just plain closures to execute), and executes them with your desired level of concurrency.
- This way, you can define, for example, 40 things that need to execute, then only allow 10 of them to execute at once.
-
- Each "process" will execute in a separate, unique, empty directory.
- If you want each process to have a bootstrapped kibana repo, check out kibanaPipeline.withCiTaskQueue
-
- Using the queue currently requires an agent/worker.
-
- Usage:
-
- withTaskQueue(parallel: 10) {
- task { print "This is a task" }
-
- // This is the same as calling task() multiple times
- tasks([ { print "Another task" }, { print "And another task" } ])
-
- // Tasks can queue up subsequent tasks
- task {
- buildThing()
- task { print "I depend on buildThing()" }
- }
- }
-
- You can also define a setup task that each process should execute one time before executing tasks:
- withTaskQueue(parallel: 10, setup: { sh "my-setup-scrupt.sh" }) {
- ...
- }
-
-*/
-def call(Map options = [:], Closure closure) {
- def config = [ parallel: 10 ] + options
- def counter = ++TASK_QUEUES_COUNTER
-
- // We're basically abusing withEnv() to create a "scope" for all steps inside of a withTaskQueue block
- // This way, we could have multiple task queue instances in the same pipeline
- withEnv(["TASK_QUEUE_ID=${counter}"]) {
- withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID] = [
- tasks: [],
- tmpFile: sh(script: 'mktemp', returnStdout: true).trim()
- ]
-
- closure.call()
-
- def processesExecuting = 0
- def processes = [:]
- def iterationId = 0
-
- for(def i = 1; i <= config.parallel; i++) {
- def j = i
- processes["task-queue-process-${j}"] = {
- catchErrors {
- withEnv([
- "TASK_QUEUE_PROCESS_ID=${j}",
- "TASK_QUEUE_ITERATION_ID=${++iterationId}"
- ]) {
- dir("${WORKSPACE}/parallel/${j}/kibana") {
- if (config.setup) {
- config.setup.call(j)
- }
-
- def isDone = false
- while(!isDone) { // TODO some kind of timeout?
- catchErrors {
- if (!getTasks().isEmpty()) {
- processesExecuting++
- catchErrors {
- def task
- try {
- task = getTasks().pop()
- } catch (java.util.NoSuchElementException ex) {
- return
- }
-
- task.call()
- }
- processesExecuting--
- // If a task finishes, and no new tasks were queued up, and nothing else is executing
- // Then all of the processes should wake up and exit
- if (processesExecuting < 1 && getTasks().isEmpty()) {
- taskNotify()
- }
- return
- }
-
- if (processesExecuting > 0) {
- taskSleep()
- return
- }
-
- // Queue is empty, no processes are executing
- isDone = true
- }
- }
- }
- }
- }
- }
- }
- parallel(processes)
- }
-}
-
-// If we sleep in a loop using Groovy code, Pipeline Steps is flooded with Sleep steps
-// So, instead, we just watch a file and `touch` it whenever something happens that could modify the queue
-// There's a 20 minute timeout just in case something goes wrong,
-// in which case this method will get called again if the process is actually supposed to be waiting.
-def taskSleep() {
- sh(script: """#!/bin/bash
- TIMESTAMP=\$(date '+%s' -d "0 seconds ago")
- for (( i=1; i<=240; i++ ))
- do
- if [ "\$(stat -c %Y '${getTmpFile()}')" -ge "\$TIMESTAMP" ]
- then
- break
- else
- sleep 5
- if [[ \$i == 240 ]]; then
- echo "Waited for new tasks for 20 minutes, exiting in case something went wrong"
- fi
- fi
- done
- """, label: "Waiting for new tasks...")
-}
-
-// Used to let the task queue processes know that either a new task has been queued up, or work is complete
-def taskNotify() {
- sh "touch '${getTmpFile()}'"
-}
-
-def getTasks() {
- return withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID].tasks
-}
-
-def getTmpFile() {
- return withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID].tmpFile
-}
-
-def addTask(Closure closure) {
- getTasks() << closure
- taskNotify()
-}
-
-def addTasks(List closures) {
- closures.reverse().each {
- getTasks() << it
- }
- taskNotify()
-}
diff --git a/vars/workers.groovy b/vars/workers.groovy
index 2e94ce12f34c0..8b7e8525a7ce3 100644
--- a/vars/workers.groovy
+++ b/vars/workers.groovy
@@ -13,8 +13,6 @@ def label(size) {
return 'docker && tests-l'
case 'xl':
return 'docker && tests-xl'
- case 'xl-highmem':
- return 'docker && tests-xl-highmem'
case 'xxl':
return 'docker && tests-xxl'
}
@@ -57,11 +55,6 @@ def base(Map params, Closure closure) {
}
}
- sh(
- script: "mkdir -p ${env.WORKSPACE}/tmp",
- label: "Create custom temp directory"
- )
-
def checkoutInfo = [:]
if (config.scm) {
@@ -96,7 +89,6 @@ def base(Map params, Closure closure) {
"PR_AUTHOR=${env.ghprbPullAuthorLogin ?: ''}",
"TEST_BROWSER_HEADLESS=1",
"GIT_BRANCH=${checkoutInfo.branch}",
- "TMPDIR=${env.WORKSPACE}/tmp", // For Chrome and anything else that respects it
]) {
withCredentials([
string(credentialsId: 'vault-addr', variable: 'VAULT_ADDR'),
@@ -175,9 +167,7 @@ def parallelProcesses(Map params) {
sleep(delay)
}
- withEnv(["CI_PARALLEL_PROCESS_NUMBER=${processNumber}"]) {
- processClosure()
- }
+ processClosure(processNumber)
}
}
diff --git a/x-pack/.telemetryrc.json b/x-pack/.telemetryrc.json
index 2c16491c1096b..4da44667e167f 100644
--- a/x-pack/.telemetryrc.json
+++ b/x-pack/.telemetryrc.json
@@ -7,6 +7,7 @@
"plugins/apm/server/lib/apm_telemetry/index.ts",
"plugins/canvas/server/collectors/collector.ts",
"plugins/infra/server/usage/usage_collector.ts",
+ "plugins/ingest_manager/server/collectors/register.ts",
"plugins/lens/server/usage/collectors.ts",
"plugins/reporting/server/usage/reporting_usage_collector.ts",
"plugins/maps/server/maps_telemetry/collectors/register.ts"
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx
index e17a8046b5c6a..6c5b539fcecfa 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx
@@ -119,7 +119,7 @@ export function PageLoadDistChart({
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
data={data?.pageLoadDistribution ?? []}
- curve={CurveType.CURVE_NATURAL}
+ curve={CurveType.CURVE_CATMULL_ROM}
/>
{breakdowns.map(({ name, type }) => (
): LineAnnotationDatum[] {
return Object.entries(values ?? {}).map((value) => ({
- dataValue: Math.round(value[1] / 1000),
+ dataValue: value[1],
details: `${(+value[0]).toFixed(0)}`,
}));
}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
index 7d48cee49b104..81503e16f7bcf 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
@@ -68,7 +68,7 @@ export const PageLoadDistribution = () => {
);
const onPercentileChange = (min: number, max: number) => {
- setPercentileRange({ min: min * 1000, max: max * 1000 });
+ setPercentileRange({ min, max });
};
return (
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx
index 3ddaa66b8de5e..3380a81c7bfab 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx
@@ -46,7 +46,7 @@ export function RumOverview() {
(callApmApi) => {
if (start && end) {
return callApmApi({
- pathname: '/api/apm/services',
+ pathname: '/api/apm/rum-client/services',
params: {
query: {
start,
@@ -68,11 +68,7 @@ export function RumOverview() {
{!isRumServiceRoute && (
<>
- service.serviceName) ?? []
- }
- />
+
{' '}
>
diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts
index f31ad83666a17..6e3a29d9f3dbc 100644
--- a/x-pack/plugins/apm/public/plugin.ts
+++ b/x-pack/plugins/apm/public/plugin.ts
@@ -6,7 +6,6 @@
import { i18n } from '@kbn/i18n';
import { lazy } from 'react';
-import { euiThemeVars as theme } from '@kbn/ui-shared-deps/theme';
import { ConfigSchema } from '.';
import { ObservabilityPluginSetup } from '../../observability/public';
import {
@@ -83,7 +82,7 @@ export class ApmPlugin implements Plugin {
plugins.observability.dashboard.register({
appName: 'apm',
fetchData: async (params) => {
- return fetchLandingPageData(params, { theme });
+ return fetchLandingPageData(params);
},
hasData,
});
diff --git a/x-pack/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts b/x-pack/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts
deleted file mode 100644
index 299e8a2104282..0000000000000
--- a/x-pack/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-export const anomalyData = {
- dates: [
- 1530614880000,
- 1530614940000,
- 1530615000000,
- 1530615060000,
- 1530615120000,
- 1530615180000,
- 1530615240000,
- 1530615300000,
- 1530615360000,
- 1530615420000,
- 1530615480000,
- 1530615540000,
- 1530615600000,
- 1530615660000,
- 1530615720000,
- 1530615780000,
- 1530615840000,
- 1530615900000,
- 1530615960000,
- 1530616020000,
- 1530616080000,
- 1530616140000,
- 1530616200000,
- 1530616260000,
- 1530616320000,
- 1530616380000,
- 1530616440000,
- 1530616500000,
- 1530616560000,
- 1530616620000,
- ],
- buckets: [
- {
- anomalyScore: null,
- lower: 15669,
- upper: 54799,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: 0,
- lower: 17808,
- upper: 49874,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: 0,
- lower: 18012,
- upper: 49421,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: 0,
- lower: 17889,
- upper: 49654,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: 0,
- lower: 17713,
- upper: 50026,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: 0,
- lower: 18044,
- upper: 49371,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: 0,
- lower: 17713,
- upper: 50110,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: 0,
- lower: 17582,
- upper: 50419,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- {
- anomalyScore: null,
- lower: null,
- upper: null,
- },
- ],
-};
diff --git a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts
index a14d827eeaec5..fd407a8bf72ad 100644
--- a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts
+++ b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts
@@ -6,7 +6,6 @@
import { fetchLandingPageData, hasData } from './observability_dashboard';
import * as createCallApmApi from './createCallApmApi';
-import { euiThemeVars as theme } from '@kbn/ui-shared-deps/theme';
describe('Observability dashboard data', () => {
const callApmApiMock = jest.spyOn(createCallApmApi, 'callApmApi');
@@ -38,39 +37,31 @@ describe('Observability dashboard data', () => {
],
})
);
- const response = await fetchLandingPageData(
- {
- startTime: '1',
- endTime: '2',
- bucketSize: '3',
- },
- { theme }
- );
+ const response = await fetchLandingPageData({
+ startTime: '1',
+ endTime: '2',
+ bucketSize: '3',
+ });
expect(response).toEqual({
title: 'APM',
appLink: '/app/apm',
stats: {
services: {
type: 'number',
- label: 'Services',
value: 10,
},
transactions: {
type: 'number',
- label: 'Transactions',
value: 2,
- color: '#6092c0',
},
},
series: {
transactions: {
- label: 'Transactions',
coordinates: [
{ x: 1, y: 1 },
{ x: 2, y: 2 },
{ x: 3, y: 3 },
],
- color: '#6092c0',
},
},
});
@@ -82,35 +73,27 @@ describe('Observability dashboard data', () => {
transactionCoordinates: [],
})
);
- const response = await fetchLandingPageData(
- {
- startTime: '1',
- endTime: '2',
- bucketSize: '3',
- },
- { theme }
- );
+ const response = await fetchLandingPageData({
+ startTime: '1',
+ endTime: '2',
+ bucketSize: '3',
+ });
expect(response).toEqual({
title: 'APM',
appLink: '/app/apm',
stats: {
services: {
type: 'number',
- label: 'Services',
value: 0,
},
transactions: {
type: 'number',
- label: 'Transactions',
value: 0,
- color: '#6092c0',
},
},
series: {
transactions: {
- label: 'Transactions',
coordinates: [],
- color: '#6092c0',
},
},
});
@@ -122,35 +105,27 @@ describe('Observability dashboard data', () => {
transactionCoordinates: [{ x: 1 }, { x: 2 }, { x: 3 }],
})
);
- const response = await fetchLandingPageData(
- {
- startTime: '1',
- endTime: '2',
- bucketSize: '3',
- },
- { theme }
- );
+ const response = await fetchLandingPageData({
+ startTime: '1',
+ endTime: '2',
+ bucketSize: '3',
+ });
expect(response).toEqual({
title: 'APM',
appLink: '/app/apm',
stats: {
services: {
type: 'number',
- label: 'Services',
value: 0,
},
transactions: {
type: 'number',
- label: 'Transactions',
value: 0,
- color: '#6092c0',
},
},
series: {
transactions: {
- label: 'Transactions',
coordinates: [{ x: 1 }, { x: 2 }, { x: 3 }],
- color: '#6092c0',
},
},
});
diff --git a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts
index 79ccf8dbd6f9b..409cec8b9ce10 100644
--- a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts
+++ b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts
@@ -6,21 +6,17 @@
import { i18n } from '@kbn/i18n';
import { mean } from 'lodash';
-import { Theme } from '@kbn/ui-shared-deps/theme';
import {
ApmFetchDataResponse,
FetchDataParams,
} from '../../../../observability/public';
import { callApmApi } from './createCallApmApi';
-interface Options {
- theme: Theme;
-}
-
-export const fetchLandingPageData = async (
- { startTime, endTime, bucketSize }: FetchDataParams,
- { theme }: Options
-): Promise => {
+export const fetchLandingPageData = async ({
+ startTime,
+ endTime,
+ bucketSize,
+}: FetchDataParams): Promise => {
const data = await callApmApi({
pathname: '/api/apm/observability_dashboard',
params: { query: { start: startTime, end: endTime, bucketSize } },
@@ -36,34 +32,20 @@ export const fetchLandingPageData = async (
stats: {
services: {
type: 'number',
- label: i18n.translate(
- 'xpack.apm.observabilityDashboard.stats.services',
- { defaultMessage: 'Services' }
- ),
value: serviceCount,
},
transactions: {
type: 'number',
- label: i18n.translate(
- 'xpack.apm.observabilityDashboard.stats.transactions',
- { defaultMessage: 'Transactions' }
- ),
value:
mean(
transactionCoordinates
.map(({ y }) => y)
.filter((y) => y && isFinite(y))
) || 0,
- color: theme.euiColorVis1,
},
},
series: {
transactions: {
- label: i18n.translate(
- 'xpack.apm.observabilityDashboard.chart.transactions',
- { defaultMessage: 'Transactions' }
- ),
- color: theme.euiColorVis1,
coordinates: transactionCoordinates,
},
},
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts
index c648cf4cc116a..e3161b49b315d 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts
@@ -65,4 +65,40 @@ describe('data telemetry collection tasks', () => {
});
});
});
+
+ describe('integrations', () => {
+ const integrationsTask = tasks.find((task) => task.name === 'integrations');
+
+ it('returns the count of ML jobs', async () => {
+ const transportRequest = jest
+ .fn()
+ .mockResolvedValueOnce({ body: { count: 1 } });
+
+ expect(
+ await integrationsTask?.executor({ indices, transportRequest } as any)
+ ).toEqual({
+ integrations: {
+ ml: {
+ all_jobs_count: 1,
+ },
+ },
+ });
+ });
+
+ describe('with no data', () => {
+ it('returns a count of 0', async () => {
+ const transportRequest = jest.fn().mockResolvedValueOnce({});
+
+ expect(
+ await integrationsTask?.executor({ indices, transportRequest } as any)
+ ).toEqual({
+ integrations: {
+ ml: {
+ all_jobs_count: 0,
+ },
+ },
+ });
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
index f27af9a2cc516..4bbaaf3e86e78 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
@@ -4,31 +4,31 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { flatten, merge, sortBy, sum } from 'lodash';
-import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
+import { TelemetryTask } from '.';
import { AGENT_NAMES } from '../../../../common/agent_name';
-import { Transaction } from '../../../../typings/es_schemas/ui/transaction';
import {
- PROCESSOR_EVENT,
- SERVICE_NAME,
AGENT_NAME,
AGENT_VERSION,
+ CLOUD_AVAILABILITY_ZONE,
+ CLOUD_PROVIDER,
+ CLOUD_REGION,
ERROR_GROUP_ID,
- TRANSACTION_NAME,
PARENT_ID,
+ PROCESSOR_EVENT,
SERVICE_FRAMEWORK_NAME,
SERVICE_FRAMEWORK_VERSION,
SERVICE_LANGUAGE_NAME,
SERVICE_LANGUAGE_VERSION,
+ SERVICE_NAME,
SERVICE_RUNTIME_NAME,
SERVICE_RUNTIME_VERSION,
+ TRANSACTION_NAME,
USER_AGENT_ORIGINAL,
- CLOUD_AVAILABILITY_ZONE,
- CLOUD_PROVIDER,
- CLOUD_REGION,
} from '../../../../common/elasticsearch_fieldnames';
-import { Span } from '../../../../typings/es_schemas/ui/span';
import { APMError } from '../../../../typings/es_schemas/ui/apm_error';
-import { TelemetryTask } from '.';
+import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
+import { Span } from '../../../../typings/es_schemas/ui/span';
+import { Transaction } from '../../../../typings/es_schemas/ui/transaction';
import { APMTelemetry } from '../types';
const TIME_RANGES = ['1d', 'all'] as const;
@@ -465,17 +465,17 @@ export const tasks: TelemetryTask[] = [
{
name: 'integrations',
executor: async ({ transportRequest }) => {
- const apmJobs = ['*-high_mean_response_time'];
+ const apmJobs = ['apm-*', '*-high_mean_response_time'];
const response = (await transportRequest({
method: 'get',
path: `/_ml/anomaly_detectors/${apmJobs.join(',')}`,
- })) as { data?: { count: number } };
+ })) as { body?: { count: number } };
return {
integrations: {
ml: {
- all_jobs_count: response.data?.count ?? 0,
+ all_jobs_count: response.body?.count ?? 0,
},
},
};
diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
index c006d01637483..602eb88ba8940 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
+++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
@@ -70,6 +70,9 @@ Object {
"durPercentiles": Object {
"percentiles": Object {
"field": "transaction.duration.us",
+ "hdr": Object {
+ "number_of_significant_value_digits": 3,
+ },
"percents": Array [
50,
75,
@@ -179,3 +182,55 @@ Object {
"index": "myIndex",
}
`;
+
+exports[`rum client dashboard queries fetches rum services 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "services": Object {
+ "terms": Object {
+ "field": "service.name",
+ "size": 1000,
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "transaction",
+ },
+ },
+ Object {
+ "term": Object {
+ "transaction.type": "page-load",
+ },
+ },
+ Object {
+ "exists": Object {
+ "field": "transaction.marks.navigationTiming.fetchStart",
+ },
+ },
+ Object {
+ "term": Object {
+ "my.custom.ui.filter": "foo-bar",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts
index 43af18999547d..e847a87264759 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts
@@ -12,6 +12,12 @@ import {
SetupUIFilters,
} from '../helpers/setup_request';
+export const MICRO_TO_SEC = 1000000;
+
+export function microToSec(val: number) {
+ return Math.round((val / MICRO_TO_SEC + Number.EPSILON) * 100) / 100;
+}
+
export async function getPageLoadDistribution({
setup,
minPercentile,
@@ -42,6 +48,9 @@ export async function getPageLoadDistribution({
percentiles: {
field: 'transaction.duration.us',
percents: [50, 75, 90, 95, 99],
+ hdr: {
+ number_of_significant_value_digits: 3,
+ },
},
},
},
@@ -59,20 +68,29 @@ export async function getPageLoadDistribution({
return null;
}
- const minDuration = aggregations?.minDuration.value ?? 0;
+ const { durPercentiles, minDuration } = aggregations ?? {};
- const minPerc = minPercentile ? +minPercentile : minDuration;
+ const minPerc = minPercentile
+ ? +minPercentile * MICRO_TO_SEC
+ : minDuration?.value ?? 0;
- const maxPercQuery = aggregations?.durPercentiles.values['99.0'] ?? 10000;
+ const maxPercQuery = durPercentiles?.values['99.0'] ?? 10000;
- const maxPerc = maxPercentile ? +maxPercentile : maxPercQuery;
+ const maxPerc = maxPercentile ? +maxPercentile * MICRO_TO_SEC : maxPercQuery;
const pageDist = await getPercentilesDistribution(setup, minPerc, maxPerc);
+
+ Object.entries(durPercentiles?.values ?? {}).forEach(([key, val]) => {
+ if (durPercentiles?.values?.[key]) {
+ durPercentiles.values[key] = microToSec(val as number);
+ }
+ });
+
return {
pageLoadDistribution: pageDist,
- percentiles: aggregations?.durPercentiles.values,
- minDuration: minPerc,
- maxDuration: maxPerc,
+ percentiles: durPercentiles?.values,
+ minDuration: microToSec(minPerc),
+ maxDuration: microToSec(maxPerc),
};
}
@@ -81,9 +99,9 @@ const getPercentilesDistribution = async (
minDuration: number,
maxDuration: number
) => {
- const stepValue = (maxDuration - minDuration) / 50;
+ const stepValue = (maxDuration - minDuration) / 100;
const stepValues = [];
- for (let i = 1; i < 51; i++) {
+ for (let i = 1; i < 101; i++) {
stepValues.push((stepValue * i + minDuration).toFixed(2));
}
@@ -103,6 +121,9 @@ const getPercentilesDistribution = async (
field: 'transaction.duration.us',
values: stepValues,
keyed: false,
+ hdr: {
+ number_of_significant_value_digits: 3,
+ },
},
},
},
@@ -117,7 +138,7 @@ const getPercentilesDistribution = async (
return pageDist.map(({ key, value }, index: number, arr) => {
return {
- x: Math.round(key / 1000),
+ x: microToSec(key),
y: index === 0 ? value : value - arr[index - 1].value,
};
});
diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts
index 5ae6bd1540f7c..ea9d701e64c3d 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts
@@ -17,6 +17,7 @@ import {
USER_AGENT_NAME,
USER_AGENT_OS,
} from '../../../common/elasticsearch_fieldnames';
+import { MICRO_TO_SEC, microToSec } from './get_page_load_distribution';
export const getBreakdownField = (breakdown: string) => {
switch (breakdown) {
@@ -38,7 +39,9 @@ export const getPageLoadDistBreakdown = async (
maxDuration: number,
breakdown: string
) => {
- const stepValue = (maxDuration - minDuration) / 50;
+ // convert secs to micros
+ const stepValue =
+ (maxDuration * MICRO_TO_SEC - minDuration * MICRO_TO_SEC) / 50;
const stepValues = [];
for (let i = 1; i < 51; i++) {
@@ -67,6 +70,9 @@ export const getPageLoadDistBreakdown = async (
field: 'transaction.duration.us',
values: stepValues,
keyed: false,
+ hdr: {
+ number_of_significant_value_digits: 3,
+ },
},
},
},
@@ -86,7 +92,7 @@ export const getPageLoadDistBreakdown = async (
name: String(key),
data: pageDist.values?.map(({ key: pKey, value }, index: number, arr) => {
return {
- x: Math.round(pKey / 1000),
+ x: microToSec(pKey),
y: index === 0 ? value : value - arr[index - 1].value,
};
}),
diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts
new file mode 100644
index 0000000000000..5957a25239307
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getRumOverviewProjection } from '../../../common/projections/rum_overview';
+import { mergeProjection } from '../../../common/projections/util/merge_projection';
+import {
+ Setup,
+ SetupTimeRange,
+ SetupUIFilters,
+} from '../helpers/setup_request';
+
+export async function getRumServices({
+ setup,
+}: {
+ setup: Setup & SetupTimeRange & SetupUIFilters;
+}) {
+ const projection = getRumOverviewProjection({
+ setup,
+ });
+
+ const params = mergeProjection(projection, {
+ body: {
+ size: 0,
+ query: {
+ bool: projection.body.query.bool,
+ },
+ aggs: {
+ services: {
+ terms: {
+ field: 'service.name',
+ size: 1000,
+ },
+ },
+ },
+ },
+ });
+
+ const { client } = setup;
+
+ const response = await client.search(params);
+
+ const result = response.aggregations?.services.buckets ?? [];
+
+ return result.map(({ key }) => key as string);
+}
diff --git a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
index 5f5a48eced746..37432672c5d89 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
@@ -11,6 +11,7 @@ import {
import { getClientMetrics } from './get_client_metrics';
import { getPageViewTrends } from './get_page_view_trends';
import { getPageLoadDistribution } from './get_page_load_distribution';
+import { getRumServices } from './get_rum_services';
describe('rum client dashboard queries', () => {
let mock: SearchParamsMock;
@@ -49,4 +50,13 @@ describe('rum client dashboard queries', () => {
);
expect(mock.params).toMatchSnapshot();
});
+
+ it('fetches rum services', async () => {
+ mock = await inspectSearchParams((setup) =>
+ getRumServices({
+ setup,
+ })
+ );
+ expect(mock.params).toMatchSnapshot();
+ });
});
diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts
index c314debcd8049..513c44904683e 100644
--- a/x-pack/plugins/apm/server/routes/create_apm_api.ts
+++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts
@@ -76,6 +76,7 @@ import {
rumPageViewsTrendRoute,
rumPageLoadDistributionRoute,
rumPageLoadDistBreakdownRoute,
+ rumServicesRoute,
} from './rum_client';
import {
observabilityDashboardHasDataRoute,
@@ -172,6 +173,7 @@ const createApmApi = () => {
.add(rumPageLoadDistributionRoute)
.add(rumPageLoadDistBreakdownRoute)
.add(rumClientMetricsRoute)
+ .add(rumServicesRoute)
// Observability dashboard
.add(observabilityDashboardHasDataRoute)
diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts
index 75651f646a50d..01e549632a0bc 100644
--- a/x-pack/plugins/apm/server/routes/rum_client.ts
+++ b/x-pack/plugins/apm/server/routes/rum_client.ts
@@ -12,6 +12,7 @@ import { rangeRt, uiFiltersRt } from './default_api_types';
import { getPageViewTrends } from '../lib/rum_client/get_page_view_trends';
import { getPageLoadDistribution } from '../lib/rum_client/get_page_load_distribution';
import { getPageLoadDistBreakdown } from '../lib/rum_client/get_pl_dist_breakdown';
+import { getRumServices } from '../lib/rum_client/get_rum_services';
export const percentileRangeRt = t.partial({
minPercentile: t.string,
@@ -91,3 +92,15 @@ export const rumPageViewsTrendRoute = createRoute(() => ({
return getPageViewTrends({ setup, breakdowns });
},
}));
+
+export const rumServicesRoute = createRoute(() => ({
+ path: '/api/apm/rum-client/services',
+ params: {
+ query: t.intersection([uiFiltersRt, rangeRt]),
+ },
+ handler: async ({ context, request }) => {
+ const setup = await setupRequest(context, request);
+
+ return getRumServices({ setup });
+ },
+}));
diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
index a340aa24aebfb..ac7499c23e926 100644
--- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
+++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
@@ -150,6 +150,7 @@ export interface AggregationOptionsByType {
field: string;
values: string[];
keyed?: boolean;
+ hdr?: { number_of_significant_value_digits: number };
};
}
diff --git a/x-pack/plugins/canvas/.storybook/storyshots.test.js b/x-pack/plugins/canvas/.storybook/storyshots.test.js
index e3217ad4dbe58..b9fe0914b3698 100644
--- a/x-pack/plugins/canvas/.storybook/storyshots.test.js
+++ b/x-pack/plugins/canvas/.storybook/storyshots.test.js
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import fs from 'fs';
import path from 'path';
import moment from 'moment';
import 'moment-timezone';
@@ -77,12 +76,6 @@ import { RenderedElement } from '../shareable_runtime/components/rendered_elemen
jest.mock('../shareable_runtime/components/rendered_element');
RenderedElement.mockImplementation(() => 'RenderedElement');
-// Some of the code requires that this directory exists, but the tests don't actually require any css to be present
-const cssDir = path.resolve(__dirname, '../../../../built_assets/css');
-if (!fs.existsSync(cssDir)) {
- fs.mkdirSync(cssDir, { recursive: true });
-}
-
addSerializer(styleSheetSerializer);
// Initialize Storyshots and build the Jest Snapshots
diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx
index 577f04a4a7efd..a0381557db21e 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx
@@ -8,35 +8,35 @@ import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButton,
- EuiFlyout,
- EuiFlyoutHeader,
- EuiTitle,
- EuiFlyoutBody,
- EuiFlyoutFooter,
- EuiFlexGroup,
- EuiFlexItem,
EuiButtonEmpty,
EuiDescriptionList,
- EuiDescriptionListTitle,
EuiDescriptionListDescription,
+ EuiDescriptionListTitle,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiIconTip,
+ EuiLink,
+ EuiTitle,
} from '@elastic/eui';
+import { reactRouterNavigate } from '../../../../../shared_imports';
import { SectionLoading, SectionError, Error } from '../../../../components';
import { useLoadDataStream } from '../../../../services/api';
import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal';
interface Props {
dataStreamName: string;
+ backingIndicesLink: ReturnType;
onClose: (shouldReload?: boolean) => void;
}
-/**
- * NOTE: This currently isn't in use by data_stream_list.tsx because it doesn't contain any
- * information that doesn't already exist in the table. We'll use it once we add additional
- * info, e.g. storage size, docs count.
- */
export const DataStreamDetailPanel: React.FunctionComponent = ({
dataStreamName,
+ backingIndicesLink,
onClose,
}) => {
const { error, data: dataStream, isLoading } = useLoadDataStream(dataStreamName);
@@ -68,28 +68,95 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({
/>
);
} else if (dataStream) {
- const { timeStampField, generation } = dataStream;
+ const { indices, timeStampField, generation } = dataStream;
content = (
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ }
+ position="top"
+ />
+
+
+
- {timeStampField.name}
+
+ {indices.length}
+
-
-
-
-
- {generation}
-
+
+
+
+
+
+
+
+
+ }
+ position="top"
+ />
+
+
+
+
+ {timeStampField.name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ position="top"
+ />
+
+
+
+
+ {generation}
+
+
+
);
}
diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx
index adfaa7820aff3..239b119051c06 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx
@@ -8,14 +8,15 @@ import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { EuiTitle, EuiText, EuiSpacer, EuiEmptyPrompt, EuiLink } from '@elastic/eui';
+import { EuiText, EuiSpacer, EuiEmptyPrompt, EuiLink } from '@elastic/eui';
import { ScopedHistory } from 'kibana/public';
import { reactRouterNavigate, extractQueryParams } from '../../../../shared_imports';
import { useAppContext } from '../../../app_context';
import { SectionError, SectionLoading, Error } from '../../../components';
import { useLoadDataStreams } from '../../../services/api';
-import { decodePathFromReactRouter } from '../../../services/routing';
+import { encodePathForReactRouter, decodePathFromReactRouter } from '../../../services/routing';
+import { documentationService } from '../../../services/documentation';
import { Section } from '../../home';
import { DataStreamTable } from './data_stream_table';
import { DataStreamDetailPanel } from './data_stream_detail_panel';
@@ -79,7 +80,7 @@ export const DataStreamList: React.FunctionComponent
{' ' /* We need this space to separate these two sentences. */}
{ingestManager ? (
@@ -134,14 +135,25 @@ export const DataStreamList: React.FunctionComponent
{/* TODO: Add a switch for toggling on data streams created by Ingest Manager */}
-
-
-
-
-
+
+
+ {i18n.translate('xpack.idxMgmt.dataStreamListDescription.learnMoreLinkText', {
+ defaultMessage: 'Learn more.',
+ })}
+
+ ),
+ }}
+ />
+
@@ -170,6 +182,12 @@ export const DataStreamList: React.FunctionComponent {
history.push(`/${Section.DataStreams}`);
diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js
index 5f10eebc9d270..9122c6524d05d 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js
+++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js
@@ -33,7 +33,6 @@ import {
EuiTableRowCell,
EuiTableRowCellCheckbox,
EuiText,
- EuiTitle,
} from '@elastic/eui';
import { UIM_SHOW_DETAILS_CLICK } from '../../../../../../common/constants';
@@ -41,6 +40,7 @@ import { reactRouterNavigate } from '../../../../../shared_imports';
import { REFRESH_RATE_INDEX_LIST } from '../../../../constants';
import { healthToColor } from '../../../../services';
import { encodePathForReactRouter } from '../../../../services/routing';
+import { documentationService } from '../../../../services/documentation';
import { AppContextConsumer } from '../../../../app_context';
import { renderBadges } from '../../../../lib/render_badges';
import { NoMatch, PageErrorForbidden } from '../../../../components';
@@ -121,6 +121,11 @@ export class IndexTable extends Component {
}
componentWillUnmount() {
+ // When you deep-link to an index from the data streams tab, the hidden indices are toggled on.
+ // However, this state is lost when you navigate away. We need to clear the filter too, or else
+ // navigating back to this tab would just show an empty list because the backing indices
+ // would be hidden.
+ this.props.filterChanged('');
clearInterval(this.interval);
}
@@ -494,14 +499,28 @@ export class IndexTable extends Component {
-
-
-
-
-
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.indexTableDescription.learnMoreLinkText',
+ {
+ defaultMessage: 'Learn more.',
+ }
+ )}
+
+ ),
+ }}
+ />
+
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
index afa8fa5b4ee04..18a65407ee20d 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
@@ -17,12 +17,14 @@ import {
EuiFlexItem,
EuiFlexGroup,
EuiButton,
+ EuiLink,
} from '@elastic/eui';
import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants';
import { TemplateListItem } from '../../../../../common';
import { SectionError, SectionLoading, Error } from '../../../components';
import { useLoadIndexTemplates } from '../../../services/api';
+import { documentationService } from '../../../services/documentation';
import { useServices } from '../../../app_context';
import {
getTemplateEditLink,
@@ -109,14 +111,28 @@ export const TemplateList: React.FunctionComponent (
-
-
-
-
-
+
+
+ {i18n.translate(
+ 'xpack.idxMgmt.home.indexTemplatesDescription.learnMoreLinkText',
+ {
+ defaultMessage: 'Learn more.',
+ }
+ )}
+
+ ),
+ }}
+ />
+
filters={filters} onChange={setFilters} />
diff --git a/x-pack/plugins/index_management/public/application/services/documentation.ts b/x-pack/plugins/index_management/public/application/services/documentation.ts
index ccccccce19766..972b4f4b25680 100644
--- a/x-pack/plugins/index_management/public/application/services/documentation.ts
+++ b/x-pack/plugins/index_management/public/application/services/documentation.ts
@@ -36,6 +36,10 @@ class DocumentationService {
return `${this.esDocsBase}/mapping-routing-field.html`;
}
+ public getDataStreamsDocumentationLink() {
+ return `${this.esDocsBase}/data-streams.html`;
+ }
+
public getTemplatesDocumentationLink() {
return `${this.esDocsBase}/indices-templates.html`;
}
diff --git a/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap b/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap
index 99ab129fc36e3..4680414493a2c 100644
--- a/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap
+++ b/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap
@@ -91,7 +91,6 @@ Object {
"y": 3.5,
},
],
- "label": "Inbound traffic",
},
"outboundTraffic": Object {
"coordinates": Array [
@@ -180,32 +179,26 @@ Object {
"y": 4,
},
],
- "label": "Outbound traffic",
},
},
"stats": Object {
"cpu": Object {
- "label": "CPU usage",
"type": "percent",
"value": 0.0015,
},
"hosts": Object {
- "label": "Hosts",
"type": "number",
"value": 2,
},
"inboundTraffic": Object {
- "label": "Inbound traffic",
"type": "bytesPerSecond",
"value": 3.5,
},
"memory": Object {
- "label": "Memory usage",
"type": "percent",
"value": 0.0015,
},
"outboundTraffic": Object {
- "label": "Outbound traffic",
"type": "bytesPerSecond",
"value": 3,
},
diff --git a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts
index 15751fab39abc..25b334d03c4f7 100644
--- a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts
+++ b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts
@@ -103,14 +103,6 @@ export const createMetricsFetchData = (
body: JSON.stringify(snapshotRequest),
});
- const inboundLabel = i18n.translate('xpack.infra.observabilityHomepage.metrics.rxLabel', {
- defaultMessage: 'Inbound traffic',
- });
-
- const outboundLabel = i18n.translate('xpack.infra.observabilityHomepage.metrics.txLabel', {
- defaultMessage: 'Outbound traffic',
- });
-
return {
title: i18n.translate('xpack.infra.observabilityHomepage.metrics.title', {
defaultMessage: 'Metrics',
@@ -119,43 +111,30 @@ export const createMetricsFetchData = (
stats: {
hosts: {
type: 'number',
- label: i18n.translate('xpack.infra.observabilityHomepage.metrics.hostsLabel', {
- defaultMessage: 'Hosts',
- }),
value: results.nodes.length,
},
cpu: {
type: 'percent',
- label: i18n.translate('xpack.infra.observabilityHomepage.metrics.cpuLabel', {
- defaultMessage: 'CPU usage',
- }),
value: combineNodesBy('cpu', results.nodes, average),
},
memory: {
type: 'percent',
- label: i18n.translate('xpack.infra.observabilityHomepage.metrics.memoryLabel', {
- defaultMessage: 'Memory usage',
- }),
value: combineNodesBy('memory', results.nodes, average),
},
inboundTraffic: {
type: 'bytesPerSecond',
- label: inboundLabel,
value: combineNodesBy('rx', results.nodes, average),
},
outboundTraffic: {
type: 'bytesPerSecond',
- label: outboundLabel,
value: combineNodesBy('tx', results.nodes, average),
},
},
series: {
inboundTraffic: {
- label: inboundLabel,
coordinates: combineNodeTimeseriesBy('rx', results.nodes, average),
},
outboundTraffic: {
- label: outboundLabel,
coordinates: combineNodeTimeseriesBy('tx', results.nodes, average),
},
},
diff --git a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
index c374cbb3bb146..4b10dab5d1ae5 100644
--- a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
+++ b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
@@ -4146,9 +4146,6 @@
"config_revision": {
"type": ["number", "null"]
},
- "config_newest_revision": {
- "type": "number"
- },
"last_checkin": {
"type": "string"
},
diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts
index 27f0c61685fd4..1f4718acc2c1f 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts
@@ -81,7 +81,6 @@ interface AgentBase {
default_api_key_id?: string;
config_id?: string;
config_revision?: number | null;
- config_newest_revision?: number;
last_checkin?: string;
user_provided_metadata: AgentMetadata;
local_metadata: AgentMetadata;
diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
index 23e31227cbf3c..a34038d4fba04 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
@@ -42,6 +42,8 @@ export enum AgentAssetType {
input = 'input',
}
+export type RegistryRelease = 'ga' | 'beta' | 'experimental';
+
// from /package/{name}
// type Package struct at https://github.com/elastic/package-registry/blob/master/util/package.go
// https://github.com/elastic/package-registry/blob/master/docs/api/package.json
@@ -49,6 +51,7 @@ export interface RegistryPackage {
name: string;
title?: string;
version: string;
+ release?: RegistryRelease;
readme?: string;
description: string;
type: string;
@@ -114,6 +117,7 @@ export type RegistrySearchResult = Pick<
| 'name'
| 'title'
| 'version'
+ | 'release'
| 'description'
| 'type'
| 'icons'
diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts
index c5035d2d44432..1901b8c0c7039 100644
--- a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts
+++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts
@@ -12,13 +12,21 @@ import {
PackageInfo,
} from '../models/epm';
+export interface GetCategoriesRequest {
+ query: {
+ experimental?: boolean;
+ };
+}
+
export interface GetCategoriesResponse {
response: CategorySummaryList;
success: boolean;
}
+
export interface GetPackagesRequest {
query: {
category?: string;
+ experimental?: boolean;
};
}
diff --git a/x-pack/plugins/ingest_manager/kibana.json b/x-pack/plugins/ingest_manager/kibana.json
index 35447139607a6..181b93a9e2425 100644
--- a/x-pack/plugins/ingest_manager/kibana.json
+++ b/x-pack/plugins/ingest_manager/kibana.json
@@ -5,6 +5,6 @@
"ui": true,
"configPath": ["xpack", "ingestManager"],
"requiredPlugins": ["licensing", "data", "encryptedSavedObjects"],
- "optionalPlugins": ["security", "features", "cloud"],
+ "optionalPlugins": ["security", "features", "cloud", "usageCollection"],
"extraPublicDirs": ["common"]
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts
index 011e0c69f2683..e5a7191372e9c 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts
@@ -6,7 +6,7 @@
import { useEffect, useState } from 'react';
import { ICON_TYPES } from '@elastic/eui';
-import { PackageInfo, PackageListItem } from '../../../../common/types/models';
+import { PackageInfo, PackageListItem } from '../types';
import { useLinks } from '../sections/epm/hooks';
import { sendGetPackageInfoByKey } from './index';
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts
index 64bee1763b08b..40a22f6b44d50 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts
@@ -4,11 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { HttpFetchQuery } from 'src/core/public';
import { useRequest, sendRequest } from './use_request';
import { epmRouteService } from '../../services';
import {
+ GetCategoriesRequest,
GetCategoriesResponse,
+ GetPackagesRequest,
GetPackagesResponse,
GetLimitedPackagesResponse,
GetInfoResponse,
@@ -16,18 +17,19 @@ import {
DeletePackageResponse,
} from '../../types';
-export const useGetCategories = () => {
+export const useGetCategories = (query: GetCategoriesRequest['query'] = {}) => {
return useRequest({
path: epmRouteService.getCategoriesPath(),
method: 'get',
+ query: { experimental: true, ...query },
});
};
-export const useGetPackages = (query: HttpFetchQuery = {}) => {
+export const useGetPackages = (query: GetPackagesRequest['query'] = {}) => {
return useRequest({
path: epmRouteService.getListPath(),
method: 'get',
- query,
+ query: { experimental: true, ...query },
});
};
@@ -52,6 +54,13 @@ export const sendGetPackageInfoByKey = (pkgkey: string) => {
});
};
+export const useGetFileByPath = (filePath: string) => {
+ return useRequest({
+ path: epmRouteService.getFilePath(filePath),
+ method: 'get',
+ });
+};
+
export const sendGetFileByPath = (filePath: string) => {
return sendRequest({
path: epmRouteService.getFilePath(filePath),
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx
index ac74b09ab4391..24b4baeaa092b 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx
@@ -30,19 +30,24 @@ import {
ServiceTitleMap,
} from '../constants';
-export function AssetsFacetGroup({ assets }: { assets: AssetsGroupedByServiceByType }) {
- const FirstHeaderRow = styled(EuiFlexGroup)`
- padding: 0 0 ${(props) => props.theme.eui.paddingSizes.m} 0;
- `;
+const FirstHeaderRow = styled(EuiFlexGroup)`
+ padding: 0 0 ${(props) => props.theme.eui.paddingSizes.m} 0;
+`;
+
+const HeaderRow = styled(EuiFlexGroup)`
+ padding: ${(props) => props.theme.eui.paddingSizes.m} 0;
+`;
- const HeaderRow = styled(EuiFlexGroup)`
- padding: ${(props) => props.theme.eui.paddingSizes.m} 0;
- `;
+const FacetGroup = styled(EuiFacetGroup)`
+ flex-grow: 0;
+`;
- const FacetGroup = styled(EuiFacetGroup)`
- flex-grow: 0;
- `;
+const FacetButton = styled(EuiFacetButton)`
+ padding: '${(props) => props.theme.eui.paddingSizes.xs} 0';
+ height: 'unset';
+`;
+export function AssetsFacetGroup({ assets }: { assets: AssetsGroupedByServiceByType }) {
return (
{entries(assets).map(([service, typeToParts], index) => {
@@ -77,10 +82,6 @@ export function AssetsFacetGroup({ assets }: { assets: AssetsGroupedByServiceByT
// only kibana assets have icons
const iconType = type in AssetIcons && AssetIcons[type];
const iconNode = iconType ? : '';
- const FacetButton = styled(EuiFacetButton)`
- padding: '${(props) => props.theme.eui.paddingSizes.xs} 0';
- height: 'unset';
- `;
return (
+ parseFloat(props.theme.eui.euiSize) * 6 + parseFloat(props.theme.eui.spacerSizes.xl) * 2}px;
+ height: 1px;
+`;
+
+const Panel = styled(EuiPanel)`
+ padding: ${(props) => props.theme.eui.spacerSizes.xl};
+ margin-bottom: -100%;
+ svg,
+ img {
+ height: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px;
+ width: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px;
+ }
+ .euiFlexItem {
+ height: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px;
+ justify-content: center;
+ }
+`;
+
+export function IconPanel({
+ packageName,
+ version,
+ icons,
+}: Pick) {
+ const iconType = usePackageIconType({ packageName, version, icons });
-export function IconPanel({ iconType }: { iconType: IconType }) {
- const Panel = styled(EuiPanel)`
- /* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */
- &&& {
- position: absolute;
- text-align: center;
- vertical-align: middle;
- padding: ${(props) => props.theme.eui.spacerSizes.xl};
- svg,
- img {
- height: ${(props) => props.theme.eui.euiKeyPadMenuSize};
- width: ${(props) => props.theme.eui.euiKeyPadMenuSize};
- }
- }
- `;
+ return (
+
+
+
+
+
+ );
+}
+export function LoadingIconPanel() {
return (
-
-
-
+
+
+
+
+
);
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx
index acdcd5b9a3406..3f0803af6daae 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx
@@ -3,13 +3,20 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiIcon } from '@elastic/eui';
import React from 'react';
-import styled from 'styled-components';
+import { EuiIconTip, EuiIconProps } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
-export const StyledAlert = styled(EuiIcon)`
- color: ${(props) => props.theme.eui.euiColorWarning};
- padding: 0 5px;
-`;
-
-export const UpdateIcon = () => ;
+export const UpdateIcon = ({ size = 'm' }: { size?: EuiIconProps['size'] }) => (
+
+);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/nav_button_back.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/nav_button_back.tsx
deleted file mode 100644
index 3fcf9758368de..0000000000000
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/nav_button_back.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import { EuiButtonEmpty } from '@elastic/eui';
-import React from 'react';
-import styled from 'styled-components';
-
-export function NavButtonBack({ href, text }: { href: string; text: string }) {
- const ButtonEmpty = styled(EuiButtonEmpty)`
- margin-right: ${(props) => props.theme.eui.spacerSizes.xl};
- `;
- return (
-
- {text}
-
- );
-}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx
index e3d8cdc8f4985..cf98f9dc90230 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx
@@ -9,12 +9,9 @@ import { EuiCard } from '@elastic/eui';
import { PackageInfo, PackageListItem } from '../../../types';
import { useLink } from '../../../hooks';
import { PackageIcon } from '../../../components/package_icon';
+import { RELEASE_BADGE_LABEL, RELEASE_BADGE_DESCRIPTION } from './release_badge';
-export interface BadgeProps {
- showInstalledBadge?: boolean;
-}
-
-type PackageCardProps = (PackageListItem | PackageInfo) & BadgeProps;
+type PackageCardProps = PackageListItem | PackageInfo;
// adding the `href` causes EuiCard to use a `a` instead of a `button`
// `a` tags use `euiLinkColor` which results in blueish Badge text
@@ -27,7 +24,7 @@ export function PackageCard({
name,
title,
version,
- showInstalledBadge,
+ release,
status,
icons,
...restProps
@@ -41,12 +38,14 @@ export function PackageCard({
return (
}
href={getHref('integration_details', { pkgkey: `${name}-${urlVersion}` })}
+ betaBadgeLabel={release && release !== 'ga' ? RELEASE_BADGE_LABEL[release] : undefined}
+ betaBadgeTooltipContent={
+ release && release !== 'ga' ? RELEASE_BADGE_DESCRIPTION[release] : undefined
+ }
/>
);
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx
index dbf454acd2b74..0c1199f7c8867 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx
@@ -20,22 +20,16 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { Loading } from '../../../components';
import { PackageList } from '../../../types';
import { useLocalSearch, searchIdField } from '../hooks';
-import { BadgeProps, PackageCard } from './package_card';
+import { PackageCard } from './package_card';
-type ListProps = {
+interface ListProps {
isLoading?: boolean;
controls?: ReactNode;
title: string;
list: PackageList;
-} & BadgeProps;
+}
-export function PackageListGrid({
- isLoading,
- controls,
- title,
- list,
- showInstalledBadge,
-}: ListProps) {
+export function PackageListGrid({ isLoading, controls, title, list }: ListProps) {
const initialQuery = EuiSearchBar.Query.MATCH_ALL;
const [query, setQuery] = useState(initialQuery);
@@ -71,7 +65,7 @@ export function PackageListGrid({
.includes(item[searchIdField])
)
: list;
- gridContent = ;
+ gridContent = ;
}
return (
@@ -108,16 +102,16 @@ function ControlsColumn({ controls, title }: ControlsColumnProps) {
- {controls}
+ {controls}
);
}
-type GridColumnProps = {
+interface GridColumnProps {
list: PackageList;
-} & BadgeProps;
+}
function GridColumn({ list }: GridColumnProps) {
return (
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/release_badge.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/release_badge.ts
new file mode 100644
index 0000000000000..f3520b4e7a9b3
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/release_badge.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { i18n } from '@kbn/i18n';
+import { RegistryRelease } from '../../../types';
+
+export const RELEASE_BADGE_LABEL: { [key in Exclude]: string } = {
+ beta: i18n.translate('xpack.ingestManager.epm.releaseBadge.betaLabel', {
+ defaultMessage: 'Beta',
+ }),
+ experimental: i18n.translate('xpack.ingestManager.epm.releaseBadge.experimentalLabel', {
+ defaultMessage: 'Experimental',
+ }),
+};
+
+export const RELEASE_BADGE_DESCRIPTION: { [key in Exclude]: string } = {
+ beta: i18n.translate('xpack.ingestManager.epm.releaseBadge.betaDescription', {
+ defaultMessage: 'This integration is not recommended for use in production environments.',
+ }),
+ experimental: i18n.translate('xpack.ingestManager.epm.releaseBadge.experimentalDescription', {
+ defaultMessage: 'This integration may have breaking changes or be removed in a future release.',
+ }),
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx
index c9a8cabdf414b..f53b4e9150ca1 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx
@@ -16,22 +16,22 @@ import { SideNavLinks } from './side_nav_links';
import { PackageConfigsPanel } from './package_configs_panel';
import { SettingsPanel } from './settings_panel';
-type ContentProps = PackageInfo & Pick & { hasIconPanel: boolean };
-export function Content(props: ContentProps) {
- const { hasIconPanel, name, panel, version } = props;
- const SideNavColumn = hasIconPanel
- ? styled(LeftColumn)`
- /* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */
- &&& {
- margin-top: 77px;
- }
- `
- : LeftColumn;
+type ContentProps = PackageInfo & Pick;
+
+const SideNavColumn = styled(LeftColumn)`
+ /* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */
+ &&& {
+ margin-top: 77px;
+ }
+`;
+
+// fixes IE11 problem with nested flex items
+const ContentFlexGroup = styled(EuiFlexGroup)`
+ flex: 0 0 auto !important;
+`;
- // fixes IE11 problem with nested flex items
- const ContentFlexGroup = styled(EuiFlexGroup)`
- flex: 0 0 auto !important;
- `;
+export function Content(props: ContentProps) {
+ const { name, panel, version } = props;
return (
@@ -75,13 +75,13 @@ function RightColumnContent(props: RightColumnContentProps) {
const { assets, panel } = props;
switch (panel) {
case 'overview':
- return (
+ return assets ? (
- );
+ ) : null;
default:
return ;
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx
deleted file mode 100644
index 875a8f5c5c127..0000000000000
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React, { Fragment } from 'react';
-import styled from 'styled-components';
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiTitle, IconType, EuiButton } from '@elastic/eui';
-import { PackageInfo } from '../../../../types';
-import { useCapabilities, useLink } from '../../../../hooks';
-import { IconPanel } from '../../components/icon_panel';
-import { NavButtonBack } from '../../components/nav_button_back';
-import { CenterColumn, LeftColumn, RightColumn } from './layout';
-import { UpdateIcon } from '../../components/icons';
-
-const FullWidthNavRow = styled(EuiPage)`
- /* no left padding so link is against column left edge */
- padding-left: 0;
-`;
-
-const Text = styled.span`
- margin-right: ${(props) => props.theme.eui.euiSizeM};
-`;
-
-type HeaderProps = PackageInfo & { iconType?: IconType };
-
-export function Header(props: HeaderProps) {
- const { iconType, name, title, version, latestVersion } = props;
-
- let installedVersion;
- if ('savedObject' in props) {
- installedVersion = props.savedObject.attributes.version;
- }
- const hasWriteCapabilites = useCapabilities().write;
- const { getHref } = useLink();
- const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false;
- return (
-
-
-
-
-
- {iconType ? (
-
-
-
- ) : null}
-
-
-
- {title}
-
-
- {version} {updateAvailable && }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx
index 505687068cf42..3267fbbe3733c 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx
@@ -3,15 +3,37 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiPage, EuiPageBody, EuiPageProps } from '@elastic/eui';
-import React, { Fragment, useEffect, useState } from 'react';
+import React, { useEffect, useState, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import styled from 'styled-components';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiText,
+ EuiSpacer,
+ EuiBetaBadge,
+ EuiButton,
+ EuiDescriptionList,
+ EuiDescriptionListTitle,
+ EuiDescriptionListDescription,
+} from '@elastic/eui';
import { DetailViewPanelName, InstallStatus, PackageInfo } from '../../../../types';
-import { sendGetPackageInfoByKey, usePackageIconType, useBreadcrumbs } from '../../../../hooks';
+import { Loading, Error } from '../../../../components';
+import {
+ useGetPackageInfoByKey,
+ useBreadcrumbs,
+ useLink,
+ useCapabilities,
+} from '../../../../hooks';
+import { WithHeaderLayout } from '../../../../layouts';
import { useSetPackageInstallStatus } from '../../hooks';
+import { IconPanel, LoadingIconPanel } from '../../components/icon_panel';
+import { RELEASE_BADGE_LABEL, RELEASE_BADGE_DESCRIPTION } from '../../components/release_badge';
+import { UpdateIcon } from '../../components/icons';
import { Content } from './content';
-import { Header } from './header';
export const DEFAULT_PANEL: DetailViewPanelName = 'overview';
@@ -20,66 +42,202 @@ export interface DetailParams {
panel?: DetailViewPanelName;
}
+const Divider = styled.div`
+ width: 0;
+ height: 100%;
+ border-left: ${(props) => props.theme.eui.euiBorderThin};
+`;
+
+// Allows child text to be truncated
+const FlexItemWithMinWidth = styled(EuiFlexItem)`
+ min-width: 0px;
+`;
+
+function Breadcrumbs({ packageTitle }: { packageTitle: string }) {
+ useBreadcrumbs('integration_details', { pkgTitle: packageTitle });
+ return null;
+}
+
export function Detail() {
// TODO: fix forced cast if possible
const { pkgkey, panel = DEFAULT_PANEL } = useParams() as DetailParams;
+ const { getHref } = useLink();
+ const hasWriteCapabilites = useCapabilities().write;
- const [info, setInfo] = useState(null);
+ // Package info state
+ const [packageInfo, setPackageInfo] = useState(null);
const setPackageInstallStatus = useSetPackageInstallStatus();
+ const updateAvailable =
+ packageInfo &&
+ 'savedObject' in packageInfo &&
+ packageInfo.savedObject &&
+ packageInfo.savedObject.attributes.version < packageInfo.latestVersion;
+
+ // Fetch package info
+ const { data: packageInfoData, error: packageInfoError, isLoading } = useGetPackageInfoByKey(
+ pkgkey
+ );
+
+ // Track install status state
useEffect(() => {
- sendGetPackageInfoByKey(pkgkey).then((response) => {
- const packageInfo = response.data?.response;
- const title = packageInfo?.title;
- const name = packageInfo?.name;
+ if (packageInfoData?.response) {
+ const packageInfoResponse = packageInfoData.response;
+ setPackageInfo(packageInfoResponse);
+
let installedVersion;
- if (packageInfo && 'savedObject' in packageInfo) {
- installedVersion = packageInfo.savedObject.attributes.version;
+ const { name } = packageInfoData.response;
+ if ('savedObject' in packageInfoResponse) {
+ installedVersion = packageInfoResponse.savedObject.attributes.version;
}
- const status: InstallStatus = packageInfo?.status as any;
-
- // track install status state
+ const status: InstallStatus = packageInfoResponse?.status as any;
if (name) {
setPackageInstallStatus({ name, status, version: installedVersion || null });
}
- if (packageInfo) {
- setInfo({ ...packageInfo, title: title || '' });
- }
- });
- }, [pkgkey, setPackageInstallStatus]);
-
- if (!info) return null;
-
- return ;
-}
+ }
+ }, [packageInfoData, setPackageInstallStatus, setPackageInfo]);
-const FullWidthHeader = styled(EuiPage)`
- border-bottom: ${(props) => props.theme.eui.euiBorderThin};
- padding-bottom: ${(props) => props.theme.eui.paddingSizes.xl};
-`;
+ const headerLeftContent = useMemo(
+ () => (
+
+
+ {/* Allows button to break out of full width */}
+
+
+
+
+
+
+
+
+
+ {isLoading || !packageInfo ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {/* Render space in place of package name while package info loads to prevent layout from jumping around */}
+ {packageInfo?.title || '\u00A0'}
+
+
+ {packageInfo?.release && packageInfo.release !== 'ga' ? (
+
+
+
+ ) : null}
+
+
+
+
+
+ ),
+ [getHref, isLoading, packageInfo]
+ );
-const FullWidthContent = styled(EuiPage)`
- background-color: ${(props) => props.theme.eui.euiColorEmptyShade};
- padding-top: ${(props) => parseInt(props.theme.eui.paddingSizes.xl, 10) * 1.25}px;
- flex-grow: 1;
-`;
+ const headerRightContent = useMemo(
+ () =>
+ packageInfo ? (
+ <>
+
+
+ {[
+ {
+ label: i18n.translate('xpack.ingestManager.epm.versionLabel', {
+ defaultMessage: 'Version',
+ }),
+ content: (
+
+ {packageInfo.version}
+ {updateAvailable ? (
+
+
+
+ ) : null}
+
+ ),
+ },
+ { isDivider: true },
+ {
+ content: (
+
+
+
+ ),
+ },
+ ].map((item, index) => (
+
+ {item.isDivider ?? false ? (
+
+ ) : item.label ? (
+
+ {item.label}
+ {item.content}
+
+ ) : (
+ item.content
+ )}
+
+ ))}
+
+ >
+ ) : undefined,
+ [getHref, hasWriteCapabilites, packageInfo, pkgkey, updateAvailable]
+ );
-type LayoutProps = PackageInfo & Pick & Pick;
-export function DetailLayout(props: LayoutProps) {
- const { name: packageName, version, icons, restrictWidth, title: packageTitle } = props;
- const iconType = usePackageIconType({ packageName, version, icons });
- useBreadcrumbs('integration_details', { pkgTitle: packageTitle });
return (
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {packageInfo ? : null}
+ {packageInfoError ? (
+
+ }
+ error={packageInfoError}
+ />
+ ) : isLoading || !packageInfo ? (
+
+ ) : (
+
+ )}
+
);
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/layout.tsx
index a802e35add7db..c329596384730 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/layout.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/layout.tsx
@@ -22,7 +22,7 @@ export const LeftColumn: FunctionComponent = ({ children, ...rest }
export const CenterColumn: FunctionComponent = ({ children, ...rest }) => {
return (
-
+
{children}
);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx
index 696af14604c5b..d8388a71556d6 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx
@@ -3,9 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import React, { Fragment } from 'react';
import styled from 'styled-components';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { ScreenshotItem } from '../../../../types';
import { useLinks } from '../../hooks';
@@ -13,6 +14,29 @@ interface ScreenshotProps {
images: ScreenshotItem[];
}
+const getHorizontalPadding = (styledProps: any): number =>
+ parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 2;
+const getVerticalPadding = (styledProps: any): number =>
+ parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 1.75;
+const getPadding = (styledProps: any) =>
+ styledProps.hascaption
+ ? `${styledProps.theme.eui.paddingSizes.xl} ${getHorizontalPadding(
+ styledProps
+ )}px ${getVerticalPadding(styledProps)}px`
+ : `${getHorizontalPadding(styledProps)}px ${getVerticalPadding(styledProps)}px`;
+const ScreenshotsContainer = styled(EuiFlexGroup)`
+ background: linear-gradient(360deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%),
+ ${(styledProps) => styledProps.theme.eui.euiColorPrimary};
+ padding: ${(styledProps) => getPadding(styledProps)};
+ flex: 0 0 auto;
+ border-radius: ${(styledProps) => styledProps.theme.eui.euiBorderRadius};
+`;
+
+// fixes ie11 problems with nested flex items
+const NestedEuiFlexItem = styled(EuiFlexItem)`
+ flex: 0 0 auto !important;
+`;
+
export function Screenshots(props: ScreenshotProps) {
const { toImage } = useLinks();
const { images } = props;
@@ -21,36 +45,23 @@ export function Screenshots(props: ScreenshotProps) {
const image = images[0];
const hasCaption = image.title ? true : false;
- const getHorizontalPadding = (styledProps: any): number =>
- parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 2;
- const getVerticalPadding = (styledProps: any): number =>
- parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 1.75;
- const getPadding = (styledProps: any) =>
- hasCaption
- ? `${styledProps.theme.eui.paddingSizes.xl} ${getHorizontalPadding(
- styledProps
- )}px ${getVerticalPadding(styledProps)}px`
- : `${getHorizontalPadding(styledProps)}px ${getVerticalPadding(styledProps)}px`;
-
- const ScreenshotsContainer = styled(EuiFlexGroup)`
- background: linear-gradient(360deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%),
- ${(styledProps) => styledProps.theme.eui.euiColorPrimary};
- padding: ${(styledProps) => getPadding(styledProps)};
- flex: 0 0 auto;
- border-radius: ${(styledProps) => styledProps.theme.eui.euiBorderRadius};
- `;
-
- // fixes ie11 problems with nested flex items
- const NestedEuiFlexItem = styled(EuiFlexItem)`
- flex: 0 0 auto !important;
- `;
return (
- Screenshots
+
+
+
-
+
{hasCaption && (
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx
index 125289ce3ee8d..4832a89479026 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx
@@ -33,7 +33,7 @@ const NoteLabel = () => (
);
const UpdatesAvailableMsg = () => (
-
+
{entries(PanelDisplayNames).map(([panel, display]) => {
- const Link = styled(EuiButtonEmpty).attrs({
- href: getHref('integration_details', { pkgkey: `${name}-${version}`, panel }),
- })`
- font-weight: ${(p) =>
- active === panel
- ? p.theme.eui.euiFontWeightSemiBold
- : p.theme.eui.euiFontWeightRegular};
- `;
// Don't display usages tab as we haven't implemented this yet
// FIXME: Restore when we implement usages page
if (panel === 'usages' && (true || packageInstallStatus.status !== InstallStatus.installed))
@@ -50,7 +41,11 @@ export function SideNavLinks({ name, version, active }: NavLinkProps) {
return (
- {display}
+
+ {active === panel ? {display} : display}
+
);
})}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx
index c378e5a47a9b9..363b1ede89e9e 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx
@@ -39,22 +39,26 @@ export const HeroCopy = memo(() => {
);
});
+const Illustration = styled(EuiImage)`
+ margin-bottom: -68px;
+ width: 80%;
+`;
+
export const HeroImage = memo(() => {
const { toAssets } = useLinks();
const { uiSettings } = useCore();
const IS_DARK_THEME = uiSettings.get('theme:darkMode');
- const Illustration = styled(EuiImage).attrs((props) => ({
- alt: i18n.translate('xpack.ingestManager.epm.illustrationAltText', {
- defaultMessage: 'Illustration of an integration',
- }),
- url: IS_DARK_THEME
- ? toAssets('illustration_integrations_darkmode.svg')
- : toAssets('illustration_integrations_lightmode.svg'),
- }))`
- margin-bottom: -68px;
- width: 80%;
- `;
-
- return ;
+ return (
+
+ );
});
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx
index c68833c1b2d95..a8e4d0105066b 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx
@@ -61,7 +61,9 @@ export function EPMHomePage() {
function InstalledPackages() {
useBreadcrumbs('integrations_installed');
- const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages();
+ const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages({
+ experimental: true,
+ });
const [selectedCategory, setSelectedCategory] = useState('');
const title = i18n.translate('xpack.ingestManager.epmList.installedTitle', {
@@ -118,7 +120,8 @@ function AvailablePackages() {
const queryParams = new URLSearchParams(useLocation().search);
const initialCategory = queryParams.get('category') || '';
const [selectedCategory, setSelectedCategory] = useState(initialCategory);
- const { data: categoryPackagesRes, isLoading: isLoadingPackages } = useGetPackages({
+ const { data: allPackagesRes, isLoading: isLoadingAllPackages } = useGetPackages();
+ const { data: categoryPackagesRes, isLoading: isLoadingCategoryPackages } = useGetPackages({
category: selectedCategory,
});
const { data: categoriesRes, isLoading: isLoadingCategories } = useGetCategories();
@@ -126,7 +129,7 @@ function AvailablePackages() {
categoryPackagesRes && categoryPackagesRes.response ? categoryPackagesRes.response : [];
const title = i18n.translate('xpack.ingestManager.epmList.allTitle', {
- defaultMessage: 'All integrations',
+ defaultMessage: 'Browse by category',
});
const categories = [
@@ -135,13 +138,13 @@ function AvailablePackages() {
title: i18n.translate('xpack.ingestManager.epmList.allPackagesFilterLinkText', {
defaultMessage: 'All',
}),
- count: packages.length,
+ count: allPackagesRes?.response?.length || 0,
},
...(categoriesRes ? categoriesRes.response : []),
];
const controls = categories ? (
{
@@ -156,7 +159,7 @@ function AvailablePackages() {
return (
;
- allPackages: PackageList;
-}
-
-export function SearchPackages({ searchTerm, localSearchRef, allPackages }: SearchPackagesProps) {
- // this means the search index hasn't been built yet.
- // i.e. the intial fetch of all packages hasn't finished
- if (!localSearchRef.current) return Still fetching matches. Try again in a moment.
;
-
- const matches = localSearchRef.current.search(searchTerm) as PackageList;
- const matchingIds = matches.map((match) => match[searchIdField]);
- const filtered = allPackages.filter((item) => matchingIds.includes(item[searchIdField]));
-
- return ;
-}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/search_results.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/search_results.tsx
deleted file mode 100644
index fbdcaac01931b..0000000000000
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/search_results.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { EuiText, EuiTitle } from '@elastic/eui';
-import React from 'react';
-import { PackageList } from '../../../../types';
-import { PackageListGrid } from '../../components/package_list_grid';
-
-interface SearchResultsProps {
- term: string;
- results: PackageList;
-}
-
-export function SearchResults({ term, results }: SearchResultsProps) {
- const title = 'Search results';
- return (
-
-
- {results.length} results for "{term}"
-
-
- }
- />
- );
-}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx
index ec58789becb72..30204603e764c 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useState } from 'react';
+import React, { useState, useMemo } from 'react';
import {
EuiBasicTable,
EuiButton,
@@ -25,7 +25,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react';
import { CSSProperties } from 'styled-components';
import { AgentEnrollmentFlyout } from '../components';
-import { Agent } from '../../../types';
+import { Agent, AgentConfig } from '../../../types';
import {
usePagination,
useCapabilities,
@@ -220,6 +220,13 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
});
const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : [];
+ const agentConfigsIndexedById = useMemo(() => {
+ return agentConfigs.reduce((acc, config) => {
+ acc[config.id] = config;
+
+ return acc;
+ }, {} as { [k: string]: AgentConfig });
+ }, [agentConfigs]);
const { isLoading: isAgentConfigsLoading } = agentConfigsRequest;
const columns = [
@@ -271,9 +278,10 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
)}
- {agent.config_revision &&
- agent.config_newest_revision &&
- agent.config_newest_revision > agent.config_revision && (
+ {agent.config_id &&
+ agent.config_revision &&
+ agentConfigsIndexedById[agent.config_id] &&
+ agentConfigsIndexedById[agent.config_id].revision > agent.config_revision && (
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts
index 9cd8a75642296..170a9cedc08d9 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts
@@ -91,7 +91,9 @@ export {
RequirementVersion,
ScreenshotItem,
ServiceName,
+ GetCategoriesRequest,
GetCategoriesResponse,
+ GetPackagesRequest,
GetPackagesResponse,
GetLimitedPackagesResponse,
GetInfoResponse,
@@ -101,6 +103,7 @@ export {
InstallStatus,
InstallationStatus,
Installable,
+ RegistryRelease,
} from '../../../../common';
export * from './intra_app_route_state';
diff --git a/x-pack/plugins/ingest_manager/server/collectors/agent_collectors.ts b/x-pack/plugins/ingest_manager/server/collectors/agent_collectors.ts
new file mode 100644
index 0000000000000..920b336297171
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/collectors/agent_collectors.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectsClient } from 'kibana/server';
+import * as AgentService from '../services/agents';
+export interface AgentUsage {
+ total: number;
+ online: number;
+ error: number;
+ offline: number;
+}
+
+export const getAgentUsage = async (soClient?: SavedObjectsClient): Promise => {
+ // TODO: unsure if this case is possible at all.
+ if (!soClient) {
+ return {
+ total: 0,
+ online: 0,
+ error: 0,
+ offline: 0,
+ };
+ }
+ const { total, online, error, offline } = await AgentService.getAgentStatusForConfig(soClient);
+ return {
+ total,
+ online,
+ error,
+ offline,
+ };
+};
diff --git a/x-pack/plugins/ingest_manager/server/collectors/config_collectors.ts b/x-pack/plugins/ingest_manager/server/collectors/config_collectors.ts
new file mode 100644
index 0000000000000..514984f7f859d
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/collectors/config_collectors.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { IngestManagerConfigType } from '..';
+
+export const getIsFleetEnabled = (config: IngestManagerConfigType) => {
+ return config.fleet.enabled;
+};
diff --git a/x-pack/plugins/ingest_manager/server/collectors/helpers.ts b/x-pack/plugins/ingest_manager/server/collectors/helpers.ts
new file mode 100644
index 0000000000000..c8ed54d5074fd
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/collectors/helpers.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { CoreSetup } from 'kibana/server';
+import { SavedObjectsClient } from '../../../../../src/core/server';
+
+export async function getInternalSavedObjectsClient(core: CoreSetup) {
+ return core.getStartServices().then(async ([coreStart]) => {
+ const savedObjectsRepo = coreStart.savedObjects.createInternalRepository();
+ return new SavedObjectsClient(savedObjectsRepo);
+ });
+}
diff --git a/x-pack/plugins/ingest_manager/server/collectors/package_collectors.ts b/x-pack/plugins/ingest_manager/server/collectors/package_collectors.ts
new file mode 100644
index 0000000000000..399e38f1919ba
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/collectors/package_collectors.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectsClient } from 'kibana/server';
+import _ from 'lodash';
+import { getPackageSavedObjects } from '../services/epm/packages/get';
+import { agentConfigService } from '../services';
+import { NewPackageConfig } from '../types';
+
+export interface PackageUsage {
+ name: string;
+ version: string;
+ enabled: boolean;
+}
+
+export const getPackageUsage = async (soClient?: SavedObjectsClient): Promise => {
+ if (!soClient) {
+ return [];
+ }
+ const packagesSavedObjects = await getPackageSavedObjects(soClient);
+ const agentConfigs = await agentConfigService.list(soClient, {
+ perPage: 1000, // avoiding pagination
+ withPackageConfigs: true,
+ });
+
+ // Once we provide detailed telemetry on agent configs, this logic should probably be moved
+ // to the (then to be created) agent config collector, so we only query and loop over these
+ // objects once.
+
+ const packagesInConfigs = agentConfigs.items.map((agentConfig) => {
+ const packageConfigs: NewPackageConfig[] = agentConfig.package_configs as NewPackageConfig[];
+ return packageConfigs
+ .map((packageConfig) => packageConfig.package?.name)
+ .filter((packageName): packageName is string => packageName !== undefined);
+ });
+
+ const enabledPackages = _.uniq(_.flatten(packagesInConfigs));
+
+ return packagesSavedObjects.saved_objects.map((p) => {
+ return {
+ name: p.attributes.name,
+ version: p.attributes.version,
+ enabled: enabledPackages.includes(p.attributes.name),
+ };
+ });
+};
diff --git a/x-pack/plugins/ingest_manager/server/collectors/register.ts b/x-pack/plugins/ingest_manager/server/collectors/register.ts
new file mode 100644
index 0000000000000..aad59ee74433c
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/collectors/register.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
+import { CoreSetup } from 'kibana/server';
+import { getIsFleetEnabled } from './config_collectors';
+import { AgentUsage, getAgentUsage } from './agent_collectors';
+import { getInternalSavedObjectsClient } from './helpers';
+import { PackageUsage, getPackageUsage } from './package_collectors';
+import { IngestManagerConfigType } from '..';
+
+interface Usage {
+ fleet_enabled: boolean;
+ agents: AgentUsage;
+ packages: PackageUsage[];
+}
+
+export function registerIngestManagerUsageCollector(
+ core: CoreSetup,
+ config: IngestManagerConfigType,
+ usageCollection: UsageCollectionSetup | undefined
+): void {
+ // usageCollection is an optional dependency, so make sure to return if it is not registered.
+ // if for any reason the saved objects client is not available, also return
+ if (!usageCollection) {
+ return;
+ }
+
+ // create usage collector
+ const ingestManagerCollector = usageCollection.makeUsageCollector({
+ type: 'ingest_manager',
+ isReady: () => true,
+ fetch: async () => {
+ const soClient = await getInternalSavedObjectsClient(core);
+ return {
+ fleet_enabled: getIsFleetEnabled(config),
+ agents: await getAgentUsage(soClient),
+ packages: await getPackageUsage(soClient),
+ };
+ },
+ // schema: { // temporarily disabled because of type errors
+ // fleet_enabled: { type: 'boolean' },
+ // agents: {
+ // total: { type: 'number' },
+ // online: { type: 'number' },
+ // error: { type: 'number' },
+ // offline: { type: 'number' },
+ // },
+ // packages: {
+ // name: { type: 'keyword' },
+ // version: { type: 'keyword' },
+ // enabled: { type: boolean },
+ // },
+ // },
+ });
+
+ // register usage collector
+ usageCollection.registerCollector(ingestManagerCollector);
+}
diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts
index 91201dbf9848b..d1adbd8b2f65d 100644
--- a/x-pack/plugins/ingest_manager/server/plugin.ts
+++ b/x-pack/plugins/ingest_manager/server/plugin.ts
@@ -14,6 +14,7 @@ import {
SavedObjectsServiceStart,
HttpServiceSetup,
} from 'kibana/server';
+import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { LicensingPluginSetup, ILicense } from '../../licensing/server';
import {
EncryptedSavedObjectsPluginStart,
@@ -62,6 +63,7 @@ import {
} from './services/agents';
import { CloudSetup } from '../../cloud/server';
import { agentCheckinState } from './services/agents/checkin/state';
+import { registerIngestManagerUsageCollector } from './collectors/register';
export interface IngestManagerSetupDeps {
licensing: LicensingPluginSetup;
@@ -69,6 +71,7 @@ export interface IngestManagerSetupDeps {
features?: FeaturesPluginSetup;
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup;
cloud?: CloudSetup;
+ usageCollection?: UsageCollectionSetup;
}
export type IngestManagerStartDeps = object;
@@ -198,6 +201,9 @@ export class IngestManagerPlugin
const router = core.http.createRouter();
const config = await this.config$.pipe(first()).toPromise();
+ // Register usage collection
+ registerIngestManagerUsageCollector(core, config, deps.usageCollection);
+
// Always register app routes for permissions checking
registerAppRoutes(router);
diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
index a50b3b13faeab..fe813f29b72e6 100644
--- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
@@ -14,6 +14,7 @@ import {
GetLimitedPackagesResponse,
} from '../../../common';
import {
+ GetCategoriesRequestSchema,
GetPackagesRequestSchema,
GetFileRequestSchema,
GetInfoRequestSchema,
@@ -30,9 +31,12 @@ import {
getLimitedPackages,
} from '../../services/epm/packages';
-export const getCategoriesHandler: RequestHandler = async (context, request, response) => {
+export const getCategoriesHandler: RequestHandler<
+ undefined,
+ TypeOf
+> = async (context, request, response) => {
try {
- const res = await getCategories();
+ const res = await getCategories(request.query);
const body: GetCategoriesResponse = {
response: res,
success: true,
@@ -54,7 +58,7 @@ export const getListHandler: RequestHandler<
const savedObjectsClient = context.core.savedObjects.client;
const res = await getPackages({
savedObjectsClient,
- category: request.query.category,
+ ...request.query,
});
const body: GetPackagesResponse = {
response: res,
diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts
index ffaf0ce46c89a..b524a7b33923e 100644
--- a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts
@@ -15,6 +15,7 @@ import {
deletePackageHandler,
} from './handlers';
import {
+ GetCategoriesRequestSchema,
GetPackagesRequestSchema,
GetFileRequestSchema,
GetInfoRequestSchema,
@@ -26,7 +27,7 @@ export const registerRoutes = (router: IRouter) => {
router.get(
{
path: EPM_API_ROUTES.CATEGORIES_PATTERN,
- validate: false,
+ validate: GetCategoriesRequestSchema,
options: { tags: [`access:${PLUGIN_ID}-read`] },
},
getCategoriesHandler
diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts
index b47cf4f7e7c3b..a5b5cc4337908 100644
--- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts
+++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts
@@ -64,7 +64,6 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = {
last_updated: { type: 'date' },
last_checkin: { type: 'date' },
config_revision: { type: 'integer' },
- config_newest_revision: { type: 'integer' },
default_api_key_id: { type: 'keyword' },
default_api_key: { type: 'binary', index: false },
updated_at: { type: 'date' },
diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts b/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts
index 1cca165906732..3d40d128afda8 100644
--- a/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts
@@ -6,7 +6,7 @@
import { SavedObjectsClientContract } from 'src/core/server';
import { generateEnrollmentAPIKey, deleteEnrollmentApiKeyForConfigId } from './api_keys';
-import { updateAgentsForConfigId, unenrollForConfigId } from './agents';
+import { unenrollForConfigId } from './agents';
import { outputService } from './output';
export async function agentConfigUpdateEventHandler(
@@ -26,10 +26,6 @@ export async function agentConfigUpdateEventHandler(
});
}
- if (action === 'updated') {
- await updateAgentsForConfigId(soClient, configId);
- }
-
if (action === 'deleted') {
await unenrollForConfigId(soClient, configId);
await deleteEnrollmentApiKeyForConfigId(soClient, configId);
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts
index f8142af376eb3..ecc2c987d04b6 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts
@@ -23,6 +23,5 @@ export async function reassignAgent(
await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, {
config_id: newConfigId,
config_revision: null,
- config_newest_revision: config.revision,
});
}
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/update.ts b/x-pack/plugins/ingest_manager/server/services/agents/update.ts
index ec7a42ff11b7a..11ad76fe81784 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/update.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/update.ts
@@ -8,38 +8,6 @@ import { SavedObjectsClientContract } from 'src/core/server';
import { listAgents } from './crud';
import { AGENT_SAVED_OBJECT_TYPE } from '../../constants';
import { unenrollAgent } from './unenroll';
-import { agentConfigService } from '../agent_config';
-
-export async function updateAgentsForConfigId(
- soClient: SavedObjectsClientContract,
- configId: string
-) {
- const config = await agentConfigService.get(soClient, configId);
- if (!config) {
- throw new Error('Config not found');
- }
- let hasMore = true;
- let page = 1;
- while (hasMore) {
- const { agents } = await listAgents(soClient, {
- kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id:"${configId}"`,
- page: page++,
- perPage: 1000,
- showInactive: true,
- });
- if (agents.length === 0) {
- hasMore = false;
- break;
- }
- const agentUpdate = agents.map((agent) => ({
- id: agent.id,
- type: AGENT_SAVED_OBJECT_TYPE,
- attributes: { config_newest_revision: config.revision },
- }));
-
- await soClient.bulkUpdate(agentUpdate);
- }
-}
export async function unenrollForConfigId(soClient: SavedObjectsClientContract, configId: string) {
let hasMore = true;
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
index 848e65b7931eb..7437321163749 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
@@ -99,7 +99,8 @@ exports[`tests loading base.yml: base.yml 1`] = `
"package": {
"name": "nginx"
},
- "managed_by": "ingest-manager"
+ "managed_by": "ingest-manager",
+ "managed": true
}
}
`;
@@ -203,7 +204,8 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = `
"package": {
"name": "coredns"
},
- "managed_by": "ingest-manager"
+ "managed_by": "ingest-manager",
+ "managed": true
}
}
`;
@@ -1691,7 +1693,8 @@ exports[`tests loading system.yml: system.yml 1`] = `
"package": {
"name": "system"
},
- "managed_by": "ingest-manager"
+ "managed_by": "ingest-manager",
+ "managed": true
}
}
`;
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
index e7867532ed176..77ad96952269f 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
@@ -317,6 +317,7 @@ function getBaseTemplate(
name: packageName,
},
managed_by: 'ingest-manager',
+ managed: true,
},
};
}
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
index ad9635cc02e06..78aa513d1a1dc 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
@@ -17,8 +17,8 @@ function nameAsTitle(name: string) {
return name.charAt(0).toUpperCase() + name.substr(1).toLowerCase();
}
-export async function getCategories() {
- return Registry.fetchCategories();
+export async function getCategories(options: Registry.CategoriesParams) {
+ return Registry.fetchCategories(options);
}
export async function getPackages(
@@ -26,8 +26,8 @@ export async function getPackages(
savedObjectsClient: SavedObjectsClientContract;
} & Registry.SearchParams
) {
- const { savedObjectsClient } = options;
- const registryItems = await Registry.fetchList({ category: options.category }).then((items) => {
+ const { savedObjectsClient, experimental, category } = options;
+ const registryItems = await Registry.fetchList({ category, experimental }).then((items) => {
return items.map((item) =>
Object.assign({}, item, { title: item.title || nameAsTitle(item.name) })
);
@@ -56,7 +56,7 @@ export async function getLimitedPackages(options: {
savedObjectsClient: SavedObjectsClientContract;
}): Promise {
const { savedObjectsClient } = options;
- const allPackages = await getPackages({ savedObjectsClient });
+ const allPackages = await getPackages({ savedObjectsClient, experimental: true });
const installedPackages = allPackages.filter(
(pkg) => (pkg.status = InstallationStatus.installed)
);
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts
index 0393cabca8ba2..ea906517f6dec 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts
@@ -26,6 +26,11 @@ export { ArchiveEntry } from './extract';
export interface SearchParams {
category?: CategoryId;
+ experimental?: boolean;
+}
+
+export interface CategoriesParams {
+ experimental?: boolean;
}
export const pkgToPkgKey = ({ name, version }: { name: string; version: string }) =>
@@ -34,19 +39,23 @@ export const pkgToPkgKey = ({ name, version }: { name: string; version: string }
export async function fetchList(params?: SearchParams): Promise {
const registryUrl = getRegistryUrl();
const url = new URL(`${registryUrl}/search`);
- if (params && params.category) {
- url.searchParams.set('category', params.category);
+ if (params) {
+ if (params.category) {
+ url.searchParams.set('category', params.category);
+ }
+ if (params.experimental) {
+ url.searchParams.set('experimental', params.experimental.toString());
+ }
}
return fetchUrl(url.toString()).then(JSON.parse);
}
-export async function fetchFindLatestPackage(
- packageName: string,
- internal: boolean = true
-): Promise {
+export async function fetchFindLatestPackage(packageName: string): Promise {
const registryUrl = getRegistryUrl();
- const url = new URL(`${registryUrl}/search?package=${packageName}&internal=${internal}`);
+ const url = new URL(
+ `${registryUrl}/search?package=${packageName}&internal=true&experimental=true`
+ );
const res = await fetchUrl(url.toString());
const searchResults = JSON.parse(res);
if (searchResults.length) {
@@ -66,9 +75,16 @@ export async function fetchFile(filePath: string): Promise {
return getResponse(`${registryUrl}${filePath}`);
}
-export async function fetchCategories(): Promise {
+export async function fetchCategories(params?: CategoriesParams): Promise {
const registryUrl = getRegistryUrl();
- return fetchUrl(`${registryUrl}/categories`).then(JSON.parse);
+ const url = new URL(`${registryUrl}/categories`);
+ if (params) {
+ if (params.experimental) {
+ url.searchParams.set('experimental', params.experimental.toString());
+ }
+ }
+
+ return fetchUrl(url.toString()).then(JSON.parse);
}
export async function getArchiveInfo(
diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts
index 3ed6ee553a507..08f47a8f1caaa 100644
--- a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts
+++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts
@@ -5,9 +5,16 @@
*/
import { schema } from '@kbn/config-schema';
+export const GetCategoriesRequestSchema = {
+ query: schema.object({
+ experimental: schema.maybe(schema.boolean()),
+ }),
+};
+
export const GetPackagesRequestSchema = {
query: schema.object({
category: schema.maybe(schema.string()),
+ experimental: schema.maybe(schema.boolean()),
}),
};
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap
index a3bb32337f9f8..096f26eb22fe3 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features , request an extension now.
"`;
+exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome subscription features , request an extension now.
"`;
-exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features , request an extension now.
"`;
+exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome subscription features , request an extension now.
"`;
-exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features , request an extension now.
"`;
+exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome subscription features , request an extension now.
"`;
-exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features , request an extension now.
"`;
+exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome subscription features , request an extension now.
"`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap
index cb2a41dadbe9e..0a5656aa266bc 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`RevertToBasic component should display when license is about to expire 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features .
"`;
+exports[`RevertToBasic component should display when license is about to expire 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features .
"`;
-exports[`RevertToBasic component should display when license is expired 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features .
"`;
+exports[`RevertToBasic component should display when license is expired 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features .
"`;
-exports[`RevertToBasic component should display when trial is active 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features .
"`;
+exports[`RevertToBasic component should display when trial is active 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features .
"`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap
index 9370b77e29560..9da8bb958941b 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`StartTrial component when trial is allowed display for basic license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other Platinum features have to offer.
"`;
+exports[`StartTrial component when trial is allowed display for basic license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other subscription features have to offer.
"`;
-exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other Platinum features have to offer.
"`;
+exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other subscription features have to offer.
"`;
-exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other Platinum features have to offer.
"`;
+exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other subscription features have to offer.
"`;
-exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other Platinum features have to offer.
"`;
+exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other subscription features have to offer.
"`;
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js
index fb1ea026abaa0..77fb10c71091d 100644
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js
@@ -19,13 +19,13 @@ export const RequestTrialExtension = ({ shouldShowRequestTrialExtension }) => {
),
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js
index a1a46d8616554..24b51cccb4e45 100644
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js
@@ -82,13 +82,13 @@ export class RevertToBasic extends React.PureComponent {
),
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx
index 65d40f1de2009..7220f377cf386 100644
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx
@@ -94,14 +94,14 @@ export class StartTrial extends Component {
),
@@ -236,15 +236,15 @@ export class StartTrial extends Component {
const description = (
),
diff --git a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx
index 83e7b82986cf8..d71a180cd2206 100644
--- a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx
+++ b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx
@@ -11,13 +11,17 @@ import { mount } from 'enzyme';
import { EuiSelect } from '@elastic/eui';
+import { UrlStateProvider } from '../../../util/url_state';
+
import { SelectInterval } from './select_interval';
describe('SelectInterval', () => {
test('creates correct initial selected value', () => {
const wrapper = mount(
-
+
+
+
);
const select = wrapper.find(EuiSelect);
@@ -29,7 +33,9 @@ describe('SelectInterval', () => {
test('currently selected value is updated correctly on click', (done) => {
const wrapper = mount(
-
+
+
+
);
const select = wrapper.find(EuiSelect).first();
diff --git a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx
index 484a0c395f3f8..cb4f80bfe6809 100644
--- a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx
+++ b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx
@@ -11,13 +11,17 @@ import { mount } from 'enzyme';
import { EuiSuperSelect } from '@elastic/eui';
+import { UrlStateProvider } from '../../../util/url_state';
+
import { SelectSeverity } from './select_severity';
describe('SelectSeverity', () => {
test('creates correct severity options and initial selected value', () => {
const wrapper = mount(
-
+
+
+
);
const select = wrapper.find(EuiSuperSelect);
@@ -65,7 +69,9 @@ describe('SelectSeverity', () => {
test('state for currently selected value is updated correctly on click', (done) => {
const wrapper = mount(
-
+
+
+
);
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
index aa637f71db1cc..618ea5184007d 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
@@ -121,16 +121,24 @@ export interface DfAnalyticsExplainResponse {
}
export interface Eval {
- meanSquaredError: number | string;
+ mse: number | string;
+ msle: number | string;
+ huber: number | string;
rSquared: number | string;
error: null | string;
}
export interface RegressionEvaluateResponse {
regression: {
+ huber: {
+ value: number;
+ };
mse: {
value: number;
};
+ msle: {
+ value: number;
+ };
r_squared: {
value: number;
};
@@ -414,19 +422,37 @@ export const useRefreshAnalyticsList = (
const DEFAULT_SIG_FIGS = 3;
-export function getValuesFromResponse(response: RegressionEvaluateResponse) {
- let meanSquaredError = response?.regression?.mse?.value;
+interface RegressionEvaluateExtractedResponse {
+ mse: number | string;
+ msle: number | string;
+ huber: number | string;
+ r_squared: number | string;
+}
- if (meanSquaredError) {
- meanSquaredError = Number(meanSquaredError.toPrecision(DEFAULT_SIG_FIGS));
- }
+export const EMPTY_STAT = '--';
- let rSquared = response?.regression?.r_squared?.value;
- if (rSquared) {
- rSquared = Number(rSquared.toPrecision(DEFAULT_SIG_FIGS));
+export function getValuesFromResponse(response: RegressionEvaluateResponse) {
+ const results: RegressionEvaluateExtractedResponse = {
+ mse: EMPTY_STAT,
+ msle: EMPTY_STAT,
+ huber: EMPTY_STAT,
+ r_squared: EMPTY_STAT,
+ };
+
+ if (response?.regression) {
+ for (const statType in response.regression) {
+ if (response.regression.hasOwnProperty(statType)) {
+ let currentStatValue =
+ response.regression[statType as keyof RegressionEvaluateResponse['regression']]?.value;
+ if (currentStatValue) {
+ currentStatValue = Number(currentStatValue.toPrecision(DEFAULT_SIG_FIGS));
+ }
+ results[statType as keyof RegressionEvaluateExtractedResponse] = currentStatValue;
+ }
+ }
}
- return { meanSquaredError, rSquared };
+ return results;
}
interface ResultsSearchBoolQuery {
bool: Dictionary;
@@ -490,13 +516,22 @@ export function getEvalQueryBody({
return query;
}
+export enum REGRESSION_STATS {
+ MSE = 'mse',
+ MSLE = 'msle',
+ R_SQUARED = 'rSquared',
+ HUBER = 'huber',
+}
+
interface EvaluateMetrics {
classification: {
multiclass_confusion_matrix: object;
};
regression: {
r_squared: object;
- mean_squared_error: object;
+ mse: object;
+ msle: object;
+ huber: object;
};
}
@@ -541,7 +576,9 @@ export const loadEvalData = async ({
},
regression: {
r_squared: {},
- mean_squared_error: {},
+ mse: {},
+ msle: {},
+ huber: {},
},
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
index b83dd2e4329e0..9dae54b6537b3 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
@@ -71,12 +71,8 @@ export const ConfigurationStepForm: FC = ({
EuiComboBoxOptionOption[]
>([]);
const [includesTableItems, setIncludesTableItems] = useState([]);
- const [maxDistinctValuesError, setMaxDistinctValuesError] = useState(
- undefined
- );
- const [unsupportedFieldsError, setUnsupportedFieldsError] = useState(
- undefined
- );
+ const [maxDistinctValuesError, setMaxDistinctValuesError] = useState();
+ const [unsupportedFieldsError, setUnsupportedFieldsError] = useState();
const { setEstimatedModelMemoryLimit, setFormState } = actions;
const { estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx
index a4d86b48006e8..8a41eb4b8a865 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx
@@ -26,7 +26,7 @@ export const DetailsStepDetails: FC<{ setCurrentStep: any; state: State }> = ({
state,
}) => {
const { form, isJobCreated } = state;
- const { description, jobId, destinationIndex } = form;
+ const { description, jobId, destinationIndex, resultsField } = form;
const detailsFirstCol: ListItems[] = [
{
@@ -37,6 +37,19 @@ export const DetailsStepDetails: FC<{ setCurrentStep: any; state: State }> = ({
},
];
+ if (
+ resultsField !== undefined &&
+ typeof resultsField === 'string' &&
+ resultsField.trim() !== ''
+ ) {
+ detailsFirstCol.push({
+ title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.resultsField', {
+ defaultMessage: 'Results field',
+ }),
+ description: resultsField,
+ });
+ }
+
const detailsSecondCol: ListItems[] = [
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.jobDescription', {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx
index d846ae95c2c7e..168d5e31f57c3 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx
@@ -47,6 +47,7 @@ export const DetailsStepForm: FC = ({
jobIdExists,
jobIdInvalidMaxLength,
jobIdValid,
+ resultsField,
} = form;
const forceInput = useRef(null);
@@ -195,6 +196,22 @@ export const DetailsStepForm: FC = ({
data-test-subj="mlAnalyticsCreateJobFlyoutDestinationIndexInput"
/>
+
+ setFormState({ resultsField: e.target.value })}
+ data-test-subj="mlAnalyticsCreateJobWizardResultsFieldInput"
+ />
+
= ({ jobConfig, jobStatus, searchQuery }) => {
const {
@@ -82,18 +94,19 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
genErrorEval.eval &&
isRegressionEvaluateResponse(genErrorEval.eval)
) {
- const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval);
+ const { mse, msle, huber, r_squared } = getValuesFromResponse(genErrorEval.eval);
setGeneralizationEval({
- meanSquaredError,
- rSquared,
+ mse,
+ msle,
+ huber,
+ rSquared: r_squared,
error: null,
});
setIsLoadingGeneralization(false);
} else {
setIsLoadingGeneralization(false);
setGeneralizationEval({
- meanSquaredError: '--',
- rSquared: '--',
+ ...EMPTY_STATS,
error: genErrorEval.error,
});
}
@@ -118,18 +131,19 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
trainingErrorEval.eval &&
isRegressionEvaluateResponse(trainingErrorEval.eval)
) {
- const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval);
+ const { mse, msle, huber, r_squared } = getValuesFromResponse(trainingErrorEval.eval);
setTrainingEval({
- meanSquaredError,
- rSquared,
+ mse,
+ msle,
+ huber,
+ rSquared: r_squared,
error: null,
});
setIsLoadingTraining(false);
} else {
setIsLoadingTraining(false);
setTrainingEval({
- meanSquaredError: '--',
- rSquared: '--',
+ ...EMPTY_STATS,
error: trainingErrorEval.error,
});
}
@@ -274,22 +288,48 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
-
+
+ {/* First row stats */}
-
+
+
+
+
+
+
+
+
+ {/* Second row stats */}
-
+
+
+
+
+
+
+
+
@@ -331,22 +371,48 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
-
+
+ {/* First row stats */}
-
+
+
+
+
+
+
+
+
+ {/* Second row stats */}
-
+
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx
index 1b4461b2bb075..114ec75efb2e7 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx
@@ -6,58 +6,99 @@
import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
+import { REGRESSION_STATS } from '../../../../common/analytics';
interface Props {
isLoading: boolean;
title: number | string;
- isMSE: boolean;
+ statType: REGRESSION_STATS;
dataTestSubj: string;
}
-const meanSquaredErrorText = i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText',
- {
- defaultMessage: 'Mean squared error',
- }
-);
-const rSquaredText = i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText',
- {
- defaultMessage: 'R squared',
- }
-);
-const meanSquaredErrorTooltipContent = i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent',
- {
- defaultMessage:
- 'Measures how well the regression analysis model is performing. Mean squared sum of the difference between true and predicted values.',
- }
-);
-const rSquaredTooltipContent = i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent',
- {
- defaultMessage:
- 'Represents the goodness of fit. Measures how well the observed outcomes are replicated by the model.',
- }
-);
+const statDescriptions = {
+ [REGRESSION_STATS.MSE]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText',
+ {
+ defaultMessage: 'Mean squared error',
+ }
+ ),
+ [REGRESSION_STATS.MSLE]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.msleText',
+ {
+ defaultMessage: 'Mean squared logarithmic error',
+ }
+ ),
+ [REGRESSION_STATS.R_SQUARED]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText',
+ {
+ defaultMessage: 'R squared',
+ }
+ ),
+ [REGRESSION_STATS.HUBER]: (
+
+ {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.huberLinkText', {
+ defaultMessage: 'Pseudo Huber loss function',
+ })}
+
+ ),
+ }}
+ />
+ ),
+};
+
+const tooltipContent = {
+ [REGRESSION_STATS.MSE]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent',
+ {
+ defaultMessage:
+ 'Measures how well the regression analysis model is performing. Mean squared sum of the difference between true and predicted values.',
+ }
+ ),
+ [REGRESSION_STATS.MSLE]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.msleTooltipContent',
+ {
+ defaultMessage:
+ 'Average squared difference between the logarithm of the predicted values and the logarithm of the actual (ground truth) value',
+ }
+ ),
+ [REGRESSION_STATS.R_SQUARED]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent',
+ {
+ defaultMessage:
+ 'Represents the goodness of fit. Measures how well the observed outcomes are replicated by the model.',
+ }
+ ),
+};
-export const EvaluateStat: FC = ({ isLoading, isMSE, title, dataTestSubj }) => (
+export const EvaluateStat: FC = ({ isLoading, statType, title, dataTestSubj }) => (
-
+ {statType !== REGRESSION_STATS.HUBER && (
+
+ )}
);
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts
similarity index 98%
rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts
rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts
index 4227c19fec5af..9db32e298691e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { isAdvancedConfig } from './action_clone';
+import { isAdvancedConfig } from './clone_button';
describe('Analytics job clone action', () => {
describe('isAdvancedConfig', () => {
@@ -131,7 +131,7 @@ describe('Analytics job clone action', () => {
},
analyzed_fields: {
includes: [],
- excludes: [],
+ excludes: ['excluded_field'],
},
model_memory_limit: '350mb',
allow_lazy_start: false,
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx
similarity index 98%
rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx
rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx
index bff54bc283296..280ec544c1e5e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx
@@ -19,7 +19,7 @@ import {
DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES,
} from '../../hooks/use_create_analytics_form';
import { State } from '../../hooks/use_create_analytics_form/state';
-import { DataFrameAnalyticsListRow } from './common';
+import { DataFrameAnalyticsListRow } from '../analytics_list/common';
import { checkPermission } from '../../../../../capabilities/check_capabilities';
import { extractErrorMessage } from '../../../../../../../common/util/errors';
@@ -247,6 +247,7 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo
},
results_field: {
optional: true,
+ formKey: 'resultsField',
defaultValue: DEFAULT_RESULTS_FIELD,
},
},
@@ -343,7 +344,7 @@ export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) {
};
}
-interface CloneActionProps {
+interface CloneButtonProps {
item: DataFrameAnalyticsListRow;
createAnalyticsForm: CreateAnalyticsFormProps;
}
@@ -353,7 +354,7 @@ interface CloneActionProps {
* Replace with {@link getCloneAction} as soon as all the actions are refactored
* to support EuiContext with a valid DOM structure without nested buttons.
*/
-export const CloneAction: FC = ({ createAnalyticsForm, item }) => {
+export const CloneButton: FC = ({ createAnalyticsForm, item }) => {
const canCreateDataFrameAnalytics: boolean = checkPermission('canCreateDataFrameAnalytics');
const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.cloneJobButtonLabel', {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/index.ts
new file mode 100644
index 0000000000000..b3d7189ff8cda
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/index.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export {
+ extractCloningConfig,
+ isAdvancedConfig,
+ CloneButton,
+ CloneDataFrameAnalyticsConfig,
+} from './clone_button';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/action_delete.test.tsx
similarity index 78%
rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx
rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/action_delete.test.tsx
index 33217f127f998..8d6272c5df860 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/action_delete.test.tsx
@@ -7,14 +7,17 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import * as CheckPrivilige from '../../../../../capabilities/check_capabilities';
-import mockAnalyticsListItem from './__mocks__/analytics_list_item.json';
-import { DeleteAction } from './action_delete';
+import mockAnalyticsListItem from '../analytics_list/__mocks__/analytics_list_item.json';
import { I18nProvider } from '@kbn/i18n/react';
import {
coreMock as mockCoreServices,
i18nServiceMock,
} from '../../../../../../../../../../src/core/public/mocks';
+import { DeleteButton } from './delete_button';
+import { DeleteButtonModal } from './delete_button_modal';
+import { useDeleteAction } from './use_delete_action';
+
jest.mock('../../../../../capabilities/check_capabilities', () => ({
checkPermission: jest.fn(() => false),
createPermissionFailureMessage: jest.fn(),
@@ -41,14 +44,18 @@ describe('DeleteAction', () => {
});
test('When canDeleteDataFrameAnalytics permission is false, button should be disabled.', () => {
- const { getByTestId } = render( );
+ const { getByTestId } = render(
+ {}} />
+ );
expect(getByTestId('mlAnalyticsJobDeleteButton')).toHaveAttribute('disabled');
});
test('When canDeleteDataFrameAnalytics permission is true, button should not be disabled.', () => {
const mock = jest.spyOn(CheckPrivilige, 'checkPermission');
mock.mockImplementation((p) => p === 'canDeleteDataFrameAnalytics');
- const { getByTestId } = render( );
+ const { getByTestId } = render(
+ {}} />
+ );
expect(getByTestId('mlAnalyticsJobDeleteButton')).not.toHaveAttribute('disabled');
@@ -57,11 +64,12 @@ describe('DeleteAction', () => {
test('When job is running, delete button should be disabled.', () => {
const { getByTestId } = render(
- {}}
/>
);
@@ -72,9 +80,21 @@ describe('DeleteAction', () => {
test('should allow to delete target index by default.', () => {
const mock = jest.spyOn(CheckPrivilige, 'checkPermission');
mock.mockImplementation((p) => p === 'canDeleteDataFrameAnalytics');
+
+ const TestComponent = () => {
+ const deleteAction = useDeleteAction();
+
+ return (
+ <>
+ {deleteAction.isModalVisible && }
+
+ >
+ );
+ };
+
const { getByTestId, queryByTestId } = render(
-
+
);
const deleteButton = getByTestId('mlAnalyticsJobDeleteButton');
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button.tsx
new file mode 100644
index 0000000000000..7da3bced48576
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button.tsx
@@ -0,0 +1,64 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui';
+import {
+ checkPermission,
+ createPermissionFailureMessage,
+} from '../../../../../capabilities/check_capabilities';
+import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from '../analytics_list/common';
+
+interface DeleteButtonProps {
+ item: DataFrameAnalyticsListRow;
+ onClick: (item: DataFrameAnalyticsListRow) => void;
+}
+
+export const DeleteButton: FC = ({ item, onClick }) => {
+ const disabled = isDataFrameAnalyticsRunning(item.stats.state);
+ const canDeleteDataFrameAnalytics: boolean = checkPermission('canDeleteDataFrameAnalytics');
+
+ const buttonDeleteText = i18n.translate('xpack.ml.dataframe.analyticsList.deleteActionName', {
+ defaultMessage: 'Delete',
+ });
+
+ const buttonDisabled = disabled || !canDeleteDataFrameAnalytics;
+ let deleteButton = (
+ onClick(item)}
+ aria-label={buttonDeleteText}
+ style={{ padding: 0 }}
+ >
+ {buttonDeleteText}
+
+ );
+
+ if (disabled || !canDeleteDataFrameAnalytics) {
+ deleteButton = (
+
+ {deleteButton}
+
+ );
+ }
+
+ return deleteButton;
+};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button_modal.tsx
new file mode 100644
index 0000000000000..f94dccee479bd
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button_modal.tsx
@@ -0,0 +1,108 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC } from 'react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiConfirmModal,
+ EuiOverlayMask,
+ EuiSwitch,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EUI_MODAL_CONFIRM_BUTTON,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { DeleteAction } from './use_delete_action';
+
+export const DeleteButtonModal: FC = ({
+ closeModal,
+ deleteAndCloseModal,
+ deleteTargetIndex,
+ deleteIndexPattern,
+ indexPatternExists,
+ item,
+ toggleDeleteIndex,
+ toggleDeleteIndexPattern,
+ userCanDeleteIndex,
+}) => {
+ if (item === undefined) {
+ return null;
+ }
+
+ const indexName = item.config.dest.index;
+
+ return (
+
+
+
+
+
+
+
+
+ {userCanDeleteIndex && (
+
+ )}
+
+
+ {userCanDeleteIndex && indexPatternExists && (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/index.ts
new file mode 100644
index 0000000000000..ef891d7c4a128
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { DeleteButton } from './delete_button';
+export { DeleteButtonModal } from './delete_button_modal';
+export { useDeleteAction } from './use_delete_action';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.ts
new file mode 100644
index 0000000000000..f924cf3afcba5
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.ts
@@ -0,0 +1,140 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useEffect, useState } from 'react';
+
+import { i18n } from '@kbn/i18n';
+
+import { IIndexPattern } from 'src/plugins/data/common';
+
+import { extractErrorMessage } from '../../../../../../../common/util/errors';
+
+import { useMlKibana } from '../../../../../contexts/kibana';
+
+import {
+ deleteAnalytics,
+ deleteAnalyticsAndDestIndex,
+ canDeleteIndex,
+} from '../../services/analytics_service';
+
+import { DataFrameAnalyticsListRow } from '../analytics_list/common';
+
+export type DeleteAction = ReturnType;
+export const useDeleteAction = () => {
+ const [item, setItem] = useState();
+
+ const [isModalVisible, setModalVisible] = useState(false);
+ const [deleteTargetIndex, setDeleteTargetIndex] = useState(true);
+ const [deleteIndexPattern, setDeleteIndexPattern] = useState(true);
+ const [userCanDeleteIndex, setUserCanDeleteIndex] = useState(false);
+ const [indexPatternExists, setIndexPatternExists] = useState(false);
+
+ const { savedObjects, notifications } = useMlKibana().services;
+ const savedObjectsClient = savedObjects.client;
+
+ const indexName = item?.config.dest.index ?? '';
+
+ const checkIndexPatternExists = async () => {
+ try {
+ const response = await savedObjectsClient.find({
+ type: 'index-pattern',
+ perPage: 10,
+ search: `"${indexName}"`,
+ searchFields: ['title'],
+ fields: ['title'],
+ });
+ const ip = response.savedObjects.find(
+ (obj) => obj.attributes.title.toLowerCase() === indexName.toLowerCase()
+ );
+ if (ip !== undefined) {
+ setIndexPatternExists(true);
+ }
+ } catch (e) {
+ const { toasts } = notifications;
+ const error = extractErrorMessage(e);
+
+ toasts.addDanger(
+ i18n.translate(
+ 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage',
+ {
+ defaultMessage:
+ 'An error occurred checking if index pattern {indexPattern} exists: {error}',
+ values: { indexPattern: indexName, error },
+ }
+ )
+ );
+ }
+ };
+ const checkUserIndexPermission = () => {
+ try {
+ const userCanDelete = canDeleteIndex(indexName);
+ if (userCanDelete) {
+ setUserCanDeleteIndex(true);
+ }
+ } catch (e) {
+ const { toasts } = notifications;
+ const error = extractErrorMessage(e);
+
+ toasts.addDanger(
+ i18n.translate(
+ 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage',
+ {
+ defaultMessage:
+ 'An error occurred checking if user can delete {destinationIndex}: {error}',
+ values: { destinationIndex: indexName, error },
+ }
+ )
+ );
+ }
+ };
+
+ useEffect(() => {
+ // Check if an index pattern exists corresponding to current DFA job
+ // if pattern does exist, show it to user
+ checkIndexPatternExists();
+
+ // Check if an user has permission to delete the index & index pattern
+ checkUserIndexPermission();
+ }, []);
+
+ const closeModal = () => setModalVisible(false);
+ const deleteAndCloseModal = () => {
+ setModalVisible(false);
+
+ if (item !== undefined) {
+ if ((userCanDeleteIndex && deleteTargetIndex) || (userCanDeleteIndex && deleteIndexPattern)) {
+ deleteAnalyticsAndDestIndex(
+ item,
+ deleteTargetIndex,
+ indexPatternExists && deleteIndexPattern
+ );
+ } else {
+ deleteAnalytics(item);
+ }
+ }
+ };
+ const toggleDeleteIndex = () => setDeleteTargetIndex(!deleteTargetIndex);
+ const toggleDeleteIndexPattern = () => setDeleteIndexPattern(!deleteIndexPattern);
+
+ const openModal = (newItem: DataFrameAnalyticsListRow) => {
+ setItem(newItem);
+ setModalVisible(true);
+ };
+
+ return {
+ closeModal,
+ deleteAndCloseModal,
+ deleteTargetIndex,
+ deleteIndexPattern,
+ indexPatternExists,
+ isModalVisible,
+ item,
+ openModal,
+ toggleDeleteIndex,
+ toggleDeleteIndexPattern,
+ userCanDeleteIndex,
+ };
+};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button.tsx
similarity index 55%
rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx
rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button.tsx
index 041b52d0322c4..0acb215336faf 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button.tsx
@@ -4,44 +4,37 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useState, FC } from 'react';
+import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
+import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui';
import { checkPermission } from '../../../../../capabilities/check_capabilities';
-import { DataFrameAnalyticsListRow } from './common';
-import { EditAnalyticsFlyout } from './edit_analytics_flyout';
-
-interface EditActionProps {
- item: DataFrameAnalyticsListRow;
+interface EditButtonProps {
+ onClick: () => void;
}
-export const EditAction: FC = ({ item }) => {
+export const EditButton: FC = ({ onClick }) => {
const canCreateDataFrameAnalytics: boolean = checkPermission('canCreateDataFrameAnalytics');
- const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
- const closeFlyout = () => setIsFlyoutVisible(false);
- const showFlyout = () => setIsFlyoutVisible(true);
-
const buttonEditText = i18n.translate('xpack.ml.dataframe.analyticsList.editActionName', {
defaultMessage: 'Edit',
});
+ const buttonDisabled = !canCreateDataFrameAnalytics;
const editButton = (
-
- {buttonEditText}
-
+ {buttonEditText}
+
);
if (!canCreateDataFrameAnalytics) {
@@ -57,10 +50,5 @@ export const EditAction: FC = ({ item }) => {
);
}
- return (
- <>
- {editButton}
- {isFlyoutVisible && }
- >
- );
+ return editButton;
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx
similarity index 97%
rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx
rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx
index b6aed9321e4e3..728f53bf69ee2 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx
@@ -32,20 +32,17 @@ import {
MemoryInputValidatorResult,
} from '../../../../../../../common/util/validators';
import { extractErrorMessage } from '../../../../../../../common/util/errors';
-import { DataFrameAnalyticsListRow, DATA_FRAME_TASK_STATE } from './common';
+import { DATA_FRAME_TASK_STATE } from '../analytics_list/common';
import {
useRefreshAnalyticsList,
UpdateDataFrameAnalyticsConfig,
} from '../../../../common/analytics';
-interface EditAnalyticsJobFlyoutProps {
- closeFlyout: () => void;
- item: DataFrameAnalyticsListRow;
-}
+import { EditAction } from './use_edit_action';
let mmLValidator: (value: any) => MemoryInputValidatorResult;
-export const EditAnalyticsFlyout: FC = ({ closeFlyout, item }) => {
+export const EditButtonFlyout: FC> = ({ closeFlyout, item }) => {
const { id: jobId, config } = item;
const { state } = item.stats;
const initialAllowLazyStart =
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/index.ts
new file mode 100644
index 0000000000000..cfb0bba16ca18
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { EditButton } from './edit_button';
+export { EditButtonFlyout } from './edit_button_flyout';
+export { isEditActionFlyoutVisible, useEditAction } from './use_edit_action';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/use_edit_action.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/use_edit_action.ts
new file mode 100644
index 0000000000000..82a7bcc91997a
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/use_edit_action.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useState } from 'react';
+
+import { DataFrameAnalyticsListRow } from '../analytics_list/common';
+
+export const isEditActionFlyoutVisible = (editAction: any): editAction is Required => {
+ return editAction.isFlyoutVisible === true && editAction.item !== undefined;
+};
+
+export interface EditAction {
+ isFlyoutVisible: boolean;
+ item?: DataFrameAnalyticsListRow;
+ closeFlyout: () => void;
+ openFlyout: (newItem: DataFrameAnalyticsListRow) => void;
+}
+export const useEditAction = () => {
+ const [item, setItem] = useState();
+
+ const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
+ const closeFlyout = () => setIsFlyoutVisible(false);
+ const openFlyout = (newItem: DataFrameAnalyticsListRow) => {
+ setItem(newItem);
+ setIsFlyoutVisible(true);
+ };
+
+ return {
+ isFlyoutVisible,
+ item,
+ closeFlyout,
+ openFlyout,
+ };
+};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/index.ts
new file mode 100644
index 0000000000000..df6bbb7c61908
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { StartButton } from './start_button';
+export { StartButtonModal } from './start_button_modal';
+export { useStartAction } from './use_start_action';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button.tsx
new file mode 100644
index 0000000000000..279a335de8f42
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui';
+
+import {
+ checkPermission,
+ createPermissionFailureMessage,
+} from '../../../../../capabilities/check_capabilities';
+
+import { DataFrameAnalyticsListRow, isCompletedAnalyticsJob } from '../analytics_list/common';
+
+interface StartButtonProps {
+ item: DataFrameAnalyticsListRow;
+ onClick: (item: DataFrameAnalyticsListRow) => void;
+}
+
+export const StartButton: FC = ({ item, onClick }) => {
+ const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics');
+
+ const buttonStartText = i18n.translate('xpack.ml.dataframe.analyticsList.startActionName', {
+ defaultMessage: 'Start',
+ });
+
+ // Disable start for analytics jobs which have completed.
+ const completeAnalytics = isCompletedAnalyticsJob(item.stats);
+
+ const disabled = !canStartStopDataFrameAnalytics || completeAnalytics;
+
+ let startButton = (
+ onClick(item)}
+ aria-label={buttonStartText}
+ data-test-subj="mlAnalyticsJobStartButton"
+ >
+ {buttonStartText}
+
+ );
+
+ if (!canStartStopDataFrameAnalytics || completeAnalytics) {
+ startButton = (
+
+ {startButton}
+
+ );
+ }
+
+ return startButton;
+};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button_modal.tsx
new file mode 100644
index 0000000000000..664dbe5c62b2f
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button_modal.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiConfirmModal, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui';
+
+import { StartAction } from './use_start_action';
+
+export const StartButtonModal: FC = ({ closeModal, item, startAndCloseModal }) => {
+ return (
+ <>
+ {item !== undefined && (
+
+
+
+ {i18n.translate('xpack.ml.dataframe.analyticsList.startModalBody', {
+ defaultMessage:
+ 'A data frame analytics job will increase search and indexing load in your cluster. Please stop the analytics job if excessive load is experienced. Are you sure you want to start this analytics job?',
+ })}
+
+
+
+ )}
+ >
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.ts
new file mode 100644
index 0000000000000..8eb6b990827ac
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useState } from 'react';
+
+import { DataFrameAnalyticsListRow } from '../analytics_list/common';
+import { startAnalytics } from '../../services/analytics_service';
+
+export type StartAction = ReturnType;
+export const useStartAction = () => {
+ const [isModalVisible, setModalVisible] = useState(false);
+
+ const [item, setItem] = useState();
+
+ const closeModal = () => setModalVisible(false);
+ const startAndCloseModal = () => {
+ if (item !== undefined) {
+ setModalVisible(false);
+ startAnalytics(item);
+ }
+ };
+
+ const openModal = (newItem: DataFrameAnalyticsListRow) => {
+ setItem(newItem);
+ setModalVisible(true);
+ };
+
+ return {
+ closeModal,
+ isModalVisible,
+ item,
+ openModal,
+ startAndCloseModal,
+ };
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/index.ts
similarity index 84%
rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/index.ts
rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/index.ts
index 41bc2aa258807..858b6c70501b3 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/index.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/index.ts
@@ -3,3 +3,5 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+
+export { StopButton } from './stop_button';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button.tsx
new file mode 100644
index 0000000000000..b8395f2f7c2a0
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button.tsx
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
+
+import {
+ checkPermission,
+ createPermissionFailureMessage,
+} from '../../../../../capabilities/check_capabilities';
+
+import { stopAnalytics } from '../../services/analytics_service';
+
+import { DataFrameAnalyticsListRow } from '../analytics_list/common';
+
+const buttonStopText = i18n.translate('xpack.ml.dataframe.analyticsList.stopActionName', {
+ defaultMessage: 'Stop',
+});
+
+interface StopButtonProps {
+ item: DataFrameAnalyticsListRow;
+}
+
+export const StopButton: FC = ({ item }) => {
+ const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics');
+
+ const stopButton = (
+ stopAnalytics(item)}
+ aria-label={buttonStopText}
+ data-test-subj="mlAnalyticsJobStopButton"
+ >
+ {buttonStopText}
+
+ );
+ if (!canStartStopDataFrameAnalytics) {
+ return (
+
+ {stopButton}
+
+ );
+ }
+
+ return stopButton;
+};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx
new file mode 100644
index 0000000000000..e31670ea42ceb
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { EuiTableActionsColumnType } from '@elastic/eui';
+
+import { DataFrameAnalyticsListRow } from '../analytics_list/common';
+
+import { ViewButton } from './view_button';
+
+export const getViewAction = (
+ isManagementTable: boolean = false
+): EuiTableActionsColumnType['actions'][number] => ({
+ isPrimary: true,
+ render: (item: DataFrameAnalyticsListRow) => (
+
+ ),
+});
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/index.ts
new file mode 100644
index 0000000000000..5ac12c12071fd
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { getViewAction } from './get_view_action';
+export { ViewButton } from './view_button';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx
new file mode 100644
index 0000000000000..17a18c374dfa6
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiButtonEmpty } from '@elastic/eui';
+
+import {
+ getAnalysisType,
+ isRegressionAnalysis,
+ isOutlierAnalysis,
+ isClassificationAnalysis,
+} from '../../../../common/analytics';
+import { useMlKibana } from '../../../../../contexts/kibana';
+
+import { getResultsUrl, DataFrameAnalyticsListRow } from '../analytics_list/common';
+
+interface ViewButtonProps {
+ item: DataFrameAnalyticsListRow;
+ isManagementTable: boolean;
+}
+
+export const ViewButton: FC = ({ item, isManagementTable }) => {
+ const {
+ services: {
+ application: { navigateToUrl, navigateToApp },
+ },
+ } = useMlKibana();
+
+ const analysisType = getAnalysisType(item.config.analysis);
+ const isDisabled =
+ !isRegressionAnalysis(item.config.analysis) &&
+ !isOutlierAnalysis(item.config.analysis) &&
+ !isClassificationAnalysis(item.config.analysis);
+
+ const url = getResultsUrl(item.id, analysisType);
+ const navigator = isManagementTable
+ ? () => navigateToApp('ml', { path: url })
+ : () => navigateToUrl(url);
+
+ return (
+
+ {i18n.translate('xpack.ml.dataframe.analyticsList.viewActionName', {
+ defaultMessage: 'View',
+ })}
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx
deleted file mode 100644
index 38ef00914e8fb..0000000000000
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx
+++ /dev/null
@@ -1,248 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React, { Fragment, FC, useState, useEffect } from 'react';
-import { i18n } from '@kbn/i18n';
-import {
- EuiButtonEmpty,
- EuiConfirmModal,
- EuiOverlayMask,
- EuiToolTip,
- EuiSwitch,
- EuiFlexGroup,
- EuiFlexItem,
- EUI_MODAL_CONFIRM_BUTTON,
-} from '@elastic/eui';
-import { IIndexPattern } from 'src/plugins/data/common';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { extractErrorMessage } from '../../../../../../../common/util/errors';
-import {
- deleteAnalytics,
- deleteAnalyticsAndDestIndex,
- canDeleteIndex,
-} from '../../services/analytics_service';
-import {
- checkPermission,
- createPermissionFailureMessage,
-} from '../../../../../capabilities/check_capabilities';
-import { useMlKibana } from '../../../../../contexts/kibana';
-import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common';
-
-interface DeleteActionProps {
- item: DataFrameAnalyticsListRow;
-}
-
-export const DeleteAction: FC = ({ item }) => {
- const disabled = isDataFrameAnalyticsRunning(item.stats.state);
- const canDeleteDataFrameAnalytics: boolean = checkPermission('canDeleteDataFrameAnalytics');
-
- const [isModalVisible, setModalVisible] = useState(false);
- const [deleteTargetIndex, setDeleteTargetIndex] = useState(true);
- const [deleteIndexPattern, setDeleteIndexPattern] = useState(true);
- const [userCanDeleteIndex, setUserCanDeleteIndex] = useState(false);
- const [indexPatternExists, setIndexPatternExists] = useState(false);
-
- const { savedObjects, notifications } = useMlKibana().services;
- const savedObjectsClient = savedObjects.client;
-
- const indexName = item.config.dest.index;
-
- const checkIndexPatternExists = async () => {
- try {
- const response = await savedObjectsClient.find({
- type: 'index-pattern',
- perPage: 10,
- search: `"${indexName}"`,
- searchFields: ['title'],
- fields: ['title'],
- });
- const ip = response.savedObjects.find(
- (obj) => obj.attributes.title.toLowerCase() === indexName.toLowerCase()
- );
- if (ip !== undefined) {
- setIndexPatternExists(true);
- }
- } catch (e) {
- const { toasts } = notifications;
- const error = extractErrorMessage(e);
-
- toasts.addDanger(
- i18n.translate(
- 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage',
- {
- defaultMessage:
- 'An error occurred checking if index pattern {indexPattern} exists: {error}',
- values: { indexPattern: indexName, error },
- }
- )
- );
- }
- };
- const checkUserIndexPermission = () => {
- try {
- const userCanDelete = canDeleteIndex(indexName);
- if (userCanDelete) {
- setUserCanDeleteIndex(true);
- }
- } catch (e) {
- const { toasts } = notifications;
- const error = extractErrorMessage(e);
-
- toasts.addDanger(
- i18n.translate(
- 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage',
- {
- defaultMessage:
- 'An error occurred checking if user can delete {destinationIndex}: {error}',
- values: { destinationIndex: indexName, error },
- }
- )
- );
- }
- };
-
- useEffect(() => {
- // Check if an index pattern exists corresponding to current DFA job
- // if pattern does exist, show it to user
- checkIndexPatternExists();
-
- // Check if an user has permission to delete the index & index pattern
- checkUserIndexPermission();
- }, []);
-
- const closeModal = () => setModalVisible(false);
- const deleteAndCloseModal = () => {
- setModalVisible(false);
-
- if ((userCanDeleteIndex && deleteTargetIndex) || (userCanDeleteIndex && deleteIndexPattern)) {
- deleteAnalyticsAndDestIndex(
- item,
- deleteTargetIndex,
- indexPatternExists && deleteIndexPattern
- );
- } else {
- deleteAnalytics(item);
- }
- };
- const openModal = () => setModalVisible(true);
- const toggleDeleteIndex = () => setDeleteTargetIndex(!deleteTargetIndex);
- const toggleDeleteIndexPattern = () => setDeleteIndexPattern(!deleteIndexPattern);
-
- const buttonDeleteText = i18n.translate('xpack.ml.dataframe.analyticsList.deleteActionName', {
- defaultMessage: 'Delete',
- });
-
- let deleteButton = (
-
- {buttonDeleteText}
-
- );
-
- if (disabled || !canDeleteDataFrameAnalytics) {
- deleteButton = (
-
- {deleteButton}
-
- );
- }
-
- return (
-
- {deleteButton}
- {isModalVisible && (
-
-
-
-
-
-
-
-
- {userCanDeleteIndex && (
-
- )}
-
-
- {userCanDeleteIndex && indexPatternExists && (
-
- )}
-
-
-
-
- )}
-
- );
-};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx
deleted file mode 100644
index 74eb1d0b02782..0000000000000
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React, { Fragment, FC, useState } from 'react';
-import { i18n } from '@kbn/i18n';
-import {
- EuiButtonEmpty,
- EuiConfirmModal,
- EuiOverlayMask,
- EuiToolTip,
- EUI_MODAL_CONFIRM_BUTTON,
-} from '@elastic/eui';
-
-import { startAnalytics } from '../../services/analytics_service';
-
-import {
- checkPermission,
- createPermissionFailureMessage,
-} from '../../../../../capabilities/check_capabilities';
-
-import { DataFrameAnalyticsListRow, isCompletedAnalyticsJob } from './common';
-
-interface StartActionProps {
- item: DataFrameAnalyticsListRow;
-}
-
-export const StartAction: FC = ({ item }) => {
- const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics');
-
- const [isModalVisible, setModalVisible] = useState(false);
-
- const closeModal = () => setModalVisible(false);
- const startAndCloseModal = () => {
- setModalVisible(false);
- startAnalytics(item);
- };
- const openModal = () => setModalVisible(true);
-
- const buttonStartText = i18n.translate('xpack.ml.dataframe.analyticsList.startActionName', {
- defaultMessage: 'Start',
- });
-
- // Disable start for analytics jobs which have completed.
- const completeAnalytics = isCompletedAnalyticsJob(item.stats);
-
- let startButton = (
-
- {buttonStartText}
-
- );
-
- if (!canStartStopDataFrameAnalytics || completeAnalytics) {
- startButton = (
-
- {startButton}
-
- );
- }
-
- return (
-
- {startButton}
- {isModalVisible && (
-
-
-
- {i18n.translate('xpack.ml.dataframe.analyticsList.startModalBody', {
- defaultMessage:
- 'A data frame analytics job will increase search and indexing load in your cluster. Please stop the analytics job if excessive load is experienced. Are you sure you want to start this analytics job?',
- })}
-
-
-
- )}
-
- );
-};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx
deleted file mode 100644
index b03a3a4c4edb2..0000000000000
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React, { FC } from 'react';
-import { i18n } from '@kbn/i18n';
-import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
-
-import {
- checkPermission,
- createPermissionFailureMessage,
-} from '../../../../../capabilities/check_capabilities';
-
-import {
- getAnalysisType,
- isRegressionAnalysis,
- isOutlierAnalysis,
- isClassificationAnalysis,
-} from '../../../../common/analytics';
-import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form';
-import { useMlKibana } from '../../../../../contexts/kibana';
-import { CloneAction } from './action_clone';
-
-import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common';
-import { stopAnalytics } from '../../services/analytics_service';
-
-import { StartAction } from './action_start';
-import { EditAction } from './action_edit';
-import { DeleteAction } from './action_delete';
-
-interface Props {
- item: DataFrameAnalyticsListRow;
- isManagementTable: boolean;
-}
-
-const AnalyticsViewButton: FC = ({ item, isManagementTable }) => {
- const {
- services: {
- application: { navigateToUrl, navigateToApp },
- },
- } = useMlKibana();
-
- const analysisType = getAnalysisType(item.config.analysis);
- const isDisabled =
- !isRegressionAnalysis(item.config.analysis) &&
- !isOutlierAnalysis(item.config.analysis) &&
- !isClassificationAnalysis(item.config.analysis);
-
- const url = getResultsUrl(item.id, analysisType);
- const navigator = isManagementTable
- ? () => navigateToApp('ml', { path: url })
- : () => navigateToUrl(url);
-
- return (
-
- {i18n.translate('xpack.ml.dataframe.analyticsList.viewActionName', {
- defaultMessage: 'View',
- })}
-
- );
-};
-
-interface Action {
- isPrimary?: boolean;
- render: (item: DataFrameAnalyticsListRow) => any;
-}
-
-export const getAnalyticsViewAction = (isManagementTable: boolean = false): Action => ({
- isPrimary: true,
- render: (item: DataFrameAnalyticsListRow) => (
-
- ),
-});
-
-export const getActions = (
- createAnalyticsForm: CreateAnalyticsFormProps,
- isManagementTable: boolean
-) => {
- const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics');
- const actions: Action[] = [getAnalyticsViewAction(isManagementTable)];
-
- if (isManagementTable === false) {
- actions.push(
- ...[
- {
- render: (item: DataFrameAnalyticsListRow) => {
- if (!isDataFrameAnalyticsRunning(item.stats.state)) {
- return ;
- }
-
- const buttonStopText = i18n.translate(
- 'xpack.ml.dataframe.analyticsList.stopActionName',
- {
- defaultMessage: 'Stop',
- }
- );
-
- const stopButton = (
- stopAnalytics(item)}
- aria-label={buttonStopText}
- data-test-subj="mlAnalyticsJobStopButton"
- >
- {buttonStopText}
-
- );
- if (!canStartStopDataFrameAnalytics) {
- return (
-
- {stopButton}
-
- );
- }
-
- return stopButton;
- },
- },
- {
- render: (item: DataFrameAnalyticsListRow) => {
- return ;
- },
- },
- {
- render: (item: DataFrameAnalyticsListRow) => {
- return ;
- },
- },
- {
- render: (item: DataFrameAnalyticsListRow) => {
- return ;
- },
- },
- ]
- );
- }
-
- return actions;
-};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
index dac0de4c7a533..405231aef5774 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment, FC, useState, useEffect } from 'react';
+import React, { FC, useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
@@ -25,7 +25,6 @@ import {
ANALYSIS_CONFIG_TYPE,
} from '../../../../common';
import { checkPermission } from '../../../../../capabilities/check_capabilities';
-import { getTaskStateBadge, getJobTypeBadge } from './columns';
import {
DataFrameAnalyticsListColumn,
@@ -38,7 +37,7 @@ import {
FieldClause,
} from './common';
import { getAnalyticsFactory } from '../../services/analytics_service';
-import { getColumns } from './columns';
+import { getTaskStateBadge, getJobTypeBadge, useColumns } from './use_columns';
import { ExpandedRow } from './expanded_row';
import {
ProgressBar,
@@ -232,6 +231,14 @@ export const DataFrameAnalyticsList: FC = ({
setIsLoading(false);
};
+ const { columns, modals } = useColumns(
+ expandedRowItemIds,
+ setExpandedRowItemIds,
+ isManagementTable,
+ isMlEnabledInSpace,
+ createAnalyticsForm
+ );
+
// Before the analytics have been loaded for the first time, display the loading indicator only.
// Otherwise a user would see 'No data frame analytics found' during the initial loading.
if (!isInitialized) {
@@ -240,7 +247,7 @@ export const DataFrameAnalyticsList: FC = ({
if (typeof errorMessage !== 'undefined') {
return (
-
+ <>
= ({
>
{JSON.stringify(errorMessage)}
-
+ >
);
}
if (analytics.length === 0) {
return (
-
+ <>
= ({
{isSourceIndexModalVisible === true && (
setIsSourceIndexModalVisible(false)} />
)}
-
+ >
);
}
- const columns = getColumns(
- expandedRowItemIds,
- setExpandedRowItemIds,
- isManagementTable,
- isMlEnabledInSpace,
- createAnalyticsForm
- );
-
const sorting = {
sort: {
field: sortField,
@@ -349,26 +348,6 @@ export const DataFrameAnalyticsList: FC = ({
view: getTaskStateBadge(val),
})),
},
- // For now analytics jobs are batch only
- /*
- {
- type: 'field_value_selection',
- field: 'mode',
- name: i18n.translate('xpack.ml.dataframe.analyticsList.modeFilter', {
- defaultMessage: 'Mode',
- }),
- multiSelect: false,
- options: Object.values(DATA_FRAME_MODE).map(val => ({
- value: val,
- name: val,
- view: (
-
- {val}
-
- ),
- })),
- },
- */
],
};
@@ -386,7 +365,8 @@ export const DataFrameAnalyticsList: FC = ({
};
return (
-
+ <>
+ {modals}
{analyticsStats && (
@@ -435,6 +415,6 @@ export const DataFrameAnalyticsList: FC = ({
{isSourceIndexModalVisible === true && (
setIsSourceIndexModalVisible(false)} />
)}
-
+ >
);
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx
index 0ee57fe5be141..5276fedff0fde 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx
@@ -24,11 +24,12 @@ import {
loadEvalData,
Eval,
} from '../../../../common';
-import { getTaskStateBadge } from './columns';
+import { getTaskStateBadge } from './use_columns';
import { getDataFrameAnalyticsProgressPhase, isCompletedAnalyticsJob } from './common';
import {
isRegressionAnalysis,
ANALYSIS_CONFIG_TYPE,
+ REGRESSION_STATS,
isRegressionEvaluateResponse,
} from '../../../../common/analytics';
import { ExpandedRowMessagesPane } from './expanded_row_messages_pane';
@@ -44,7 +45,7 @@ function getItemDescription(value: any) {
interface LoadedStatProps {
isLoading: boolean;
evalData: Eval;
- resultProperty: 'meanSquaredError' | 'rSquared';
+ resultProperty: REGRESSION_STATS;
}
const LoadedStat: FC = ({ isLoading, evalData, resultProperty }) => {
@@ -61,7 +62,7 @@ interface Props {
item: DataFrameAnalyticsListRow;
}
-const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null };
+const defaultEval: Eval = { mse: '', msle: '', huber: '', rSquared: '', error: null };
export const ExpandedRow: FC = ({ item }) => {
const [trainingEval, setTrainingEval] = useState(defaultEval);
@@ -94,17 +95,21 @@ export const ExpandedRow: FC = ({ item }) => {
genErrorEval.eval &&
isRegressionEvaluateResponse(genErrorEval.eval)
) {
- const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval);
+ const { mse, msle, huber, r_squared } = getValuesFromResponse(genErrorEval.eval);
setGeneralizationEval({
- meanSquaredError,
- rSquared,
+ mse,
+ msle,
+ huber,
+ rSquared: r_squared,
error: null,
});
setIsLoadingGeneralization(false);
} else {
setIsLoadingGeneralization(false);
setGeneralizationEval({
- meanSquaredError: '',
+ mse: '',
+ msle: '',
+ huber: '',
rSquared: '',
error: genErrorEval.error,
});
@@ -124,17 +129,21 @@ export const ExpandedRow: FC = ({ item }) => {
trainingErrorEval.eval &&
isRegressionEvaluateResponse(trainingErrorEval.eval)
) {
- const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval);
+ const { mse, msle, huber, r_squared } = getValuesFromResponse(trainingErrorEval.eval);
setTrainingEval({
- meanSquaredError,
- rSquared,
+ mse,
+ msle,
+ huber,
+ rSquared: r_squared,
error: null,
});
setIsLoadingTraining(false);
} else {
setIsLoadingTraining(false);
setTrainingEval({
- meanSquaredError: '',
+ mse: '',
+ msle: '',
+ huber: '',
rSquared: '',
error: genErrorEval.error,
});
@@ -221,7 +230,17 @@ export const ExpandedRow: FC = ({ item }) => {
+ ),
+ },
+ {
+ title: 'generalization mean squared logarithmic error',
+ description: (
+
),
},
@@ -231,7 +250,17 @@ export const ExpandedRow: FC = ({ item }) => {
+ ),
+ },
+ {
+ title: 'generalization pseudo huber loss function',
+ description: (
+
),
},
@@ -241,7 +270,17 @@ export const ExpandedRow: FC = ({ item }) => {
+ ),
+ },
+ {
+ title: 'training mean squared logarithmic error',
+ description: (
+
),
},
@@ -251,7 +290,17 @@ export const ExpandedRow: FC = ({ item }) => {
+ ),
+ },
+ {
+ title: 'training pseudo huber loss function',
+ description: (
+
),
}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx
new file mode 100644
index 0000000000000..e75d938116991
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { EuiTableActionsColumnType } from '@elastic/eui';
+
+import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form';
+import { CloneButton } from '../action_clone';
+import { useDeleteAction, DeleteButton, DeleteButtonModal } from '../action_delete';
+import {
+ isEditActionFlyoutVisible,
+ useEditAction,
+ EditButton,
+ EditButtonFlyout,
+} from '../action_edit';
+import { useStartAction, StartButton, StartButtonModal } from '../action_start';
+import { StopButton } from '../action_stop';
+import { getViewAction } from '../action_view';
+
+import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common';
+
+export const useActions = (
+ createAnalyticsForm: CreateAnalyticsFormProps,
+ isManagementTable: boolean
+): {
+ actions: EuiTableActionsColumnType['actions'];
+ modals: JSX.Element | null;
+} => {
+ const deleteAction = useDeleteAction();
+ const editAction = useEditAction();
+ const startAction = useStartAction();
+
+ let modals: JSX.Element | null = null;
+
+ const actions: EuiTableActionsColumnType['actions'] = [
+ getViewAction(isManagementTable),
+ ];
+
+ if (isManagementTable === false) {
+ modals = (
+ <>
+ {startAction.isModalVisible && }
+ {deleteAction.isModalVisible && }
+ {isEditActionFlyoutVisible(editAction) && }
+ >
+ );
+ actions.push(
+ ...[
+ {
+ render: (item: DataFrameAnalyticsListRow) => {
+ if (!isDataFrameAnalyticsRunning(item.stats.state)) {
+ return ;
+ }
+ return ;
+ },
+ },
+ {
+ render: (item: DataFrameAnalyticsListRow) => {
+ return editAction.openFlyout(item)} />;
+ },
+ },
+ {
+ render: (item: DataFrameAnalyticsListRow) => {
+ return ;
+ },
+ },
+ {
+ render: (item: DataFrameAnalyticsListRow) => {
+ return ;
+ },
+ },
+ ]
+ );
+ }
+
+ return { actions, modals };
+};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx
similarity index 93%
rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx
rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx
index a3d2e65386c19..fa88396461cd7 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx
@@ -33,7 +33,7 @@ import {
DataFrameAnalyticsListRow,
DataFrameAnalyticsStats,
} from './common';
-import { getActions } from './actions';
+import { useActions } from './use_actions';
enum TASK_STATE_COLOR {
analyzing = 'primary',
@@ -141,14 +141,14 @@ export const getDFAnalyticsJobIdLink = (item: DataFrameAnalyticsListRow) => (
{item.id}
);
-export const getColumns = (
+export const useColumns = (
expandedRowItemIds: DataFrameAnalyticsId[],
setExpandedRowItemIds: React.Dispatch>,
isManagementTable: boolean = false,
isMlEnabledInSpace: boolean = true,
createAnalyticsForm?: CreateAnalyticsFormProps
) => {
- const actions = getActions(createAnalyticsForm!, isManagementTable);
+ const { actions, modals } = useActions(createAnalyticsForm!, isManagementTable);
function toggleDetails(item: DataFrameAnalyticsListRow) {
const index = expandedRowItemIds.indexOf(item.config.id);
@@ -253,20 +253,6 @@ export const getColumns = (
width: '100px',
'data-test-subj': 'mlAnalyticsTableColumnStatus',
},
- // For now there is batch mode only so we hide this column for now.
- /*
- {
- name: i18n.translate('xpack.ml.dataframe.analyticsList.mode', { defaultMessage: 'Mode' }),
- sortable: (item: DataFrameAnalyticsListRow) => item.mode,
- truncateText: true,
- render(item: DataFrameAnalyticsListRow) {
- const mode = item.mode;
- const color = 'hollow';
- return {mode} ;
- },
- width: '100px',
- },
- */
progressColumn,
{
name: i18n.translate('xpack.ml.dataframe.analyticsList.tableActionLabel', {
@@ -293,5 +279,5 @@ export const getColumns = (
}
}
- return columns;
+ return { columns, modals };
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
index 81d35679443b8..b344e44c97d59 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
@@ -144,6 +144,11 @@ export const validateAdvancedEditor = (state: State): State => {
const destinationIndexNameValid = isValidIndexName(destinationIndexName);
const destinationIndexPatternTitleExists =
state.indexPatternsMap[destinationIndexName] !== undefined;
+
+ const resultsFieldEmptyString =
+ typeof jobConfig?.dest?.results_field === 'string' &&
+ jobConfig?.dest?.results_field.trim() === '';
+
const mml = jobConfig.model_memory_limit;
const modelMemoryLimitEmpty = mml === '' || mml === undefined;
if (!modelMemoryLimitEmpty && mml !== undefined) {
@@ -292,6 +297,18 @@ export const validateAdvancedEditor = (state: State): State => {
});
}
+ if (resultsFieldEmptyString) {
+ state.advancedEditorMessages.push({
+ error: i18n.translate(
+ 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.resultsFieldEmptyString',
+ {
+ defaultMessage: 'The results field must not be an empty string.',
+ }
+ ),
+ message: '',
+ });
+ }
+
if (dependentVariableEmpty) {
state.advancedEditorMessages.push({
error: i18n.translate(
@@ -336,6 +353,7 @@ export const validateAdvancedEditor = (state: State): State => {
sourceIndexNameValid &&
!destinationIndexNameEmpty &&
destinationIndexNameValid &&
+ !resultsFieldEmptyString &&
!dependentVariableEmpty &&
!modelMemoryLimitEmpty &&
numTopFeatureImportanceValuesValid &&
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
index da6e2e440a26e..0d425c8ead4a2 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
@@ -13,7 +13,7 @@ import {
DataFrameAnalyticsConfig,
ANALYSIS_CONFIG_TYPE,
} from '../../../../common/analytics';
-import { CloneDataFrameAnalyticsConfig } from '../../components/analytics_list/action_clone';
+import { CloneDataFrameAnalyticsConfig } from '../../components/action_clone';
export enum DEFAULT_MODEL_MEMORY_LIMIT {
regression = '100mb',
@@ -82,6 +82,7 @@ export interface State {
previousJobType: null | AnalyticsJobType;
requiredFieldsError: string | undefined;
randomizeSeed: undefined | number;
+ resultsField: undefined | string;
sourceIndex: EsIndexName;
sourceIndexNameEmpty: boolean;
sourceIndexNameValid: boolean;
@@ -147,6 +148,7 @@ export const getInitialState = (): State => ({
previousJobType: null,
requiredFieldsError: undefined,
randomizeSeed: undefined,
+ resultsField: undefined,
sourceIndex: '',
sourceIndexNameEmpty: true,
sourceIndexNameValid: false,
@@ -198,6 +200,13 @@ export const getJobConfigFromFormState = (
model_memory_limit: formState.modelMemoryLimit,
};
+ const resultsFieldEmpty =
+ typeof formState?.resultsField === 'string' && formState?.resultsField.trim() === '';
+
+ if (jobConfig.dest && !resultsFieldEmpty) {
+ jobConfig.dest.results_field = formState.resultsField;
+ }
+
if (
formState.jobType === ANALYSIS_CONFIG_TYPE.REGRESSION ||
formState.jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION
@@ -277,6 +286,7 @@ export function getCloneFormStateFromJobConfig(
const resultState: Partial = {
jobType,
description: analyticsJobConfig.description ?? '',
+ resultsField: analyticsJobConfig.dest.results_field,
sourceIndex: Array.isArray(analyticsJobConfig.source.index)
? analyticsJobConfig.source.index.join(',')
: analyticsJobConfig.source.index,
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts
index f95d2f572a406..4c312be26613c 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts
@@ -18,10 +18,7 @@ import {
DataFrameAnalyticsId,
DataFrameAnalyticsConfig,
} from '../../../../common';
-import {
- extractCloningConfig,
- isAdvancedConfig,
-} from '../../components/analytics_list/action_clone';
+import { extractCloningConfig, isAdvancedConfig } from '../../components/action_clone';
import { ActionDispatchers, ACTION } from './actions';
import { reducer } from './reducer';
diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
index 068f43a140c90..f356d79c0a8e1 100644
--- a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
+++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
@@ -9,12 +9,10 @@ import { useUrlState } from '../../util/url_state';
import { SWIMLANE_TYPE } from '../explorer_constants';
import { AppStateSelectedCells } from '../explorer_utils';
-export const useSelectedCells = (): [
- AppStateSelectedCells | undefined,
- (swimlaneSelectedCells: AppStateSelectedCells) => void
-] => {
- const [appState, setAppState] = useUrlState('_a');
-
+export const useSelectedCells = (
+ appState: any,
+ setAppState: ReturnType[1]
+): [AppStateSelectedCells | undefined, (swimlaneSelectedCells: AppStateSelectedCells) => void] => {
// keep swimlane selection, restore selectedCells from AppState
const selectedCells = useMemo(() => {
return appState?.mlExplorerSwimlane?.selectedType !== undefined
diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx
index f2e6ff7885b16..1eeff6287867d 100644
--- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx
+++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx
@@ -22,8 +22,8 @@ import {
import {
getTaskStateBadge,
progressColumn,
-} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/columns';
-import { getAnalyticsViewAction } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/actions';
+} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns';
+import { getViewAction } from '../../../data_frame_analytics/pages/analytics_management/components/action_view';
import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils';
const MlInMemoryTable = mlInMemoryTableFactory();
@@ -82,7 +82,7 @@ export const AnalyticsTable: FC = ({ items }) => {
name: i18n.translate('xpack.ml.overview.analyticsList.tableActionLabel', {
defaultMessage: 'Actions',
}),
- actions: [getAnalyticsViewAction()],
+ actions: [getViewAction()],
width: '100px',
},
];
diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx
index 281493c4e31b7..f1b8083f19ccf 100644
--- a/x-pack/plugins/ml/public/application/routing/router.tsx
+++ b/x-pack/plugins/ml/public/application/routing/router.tsx
@@ -12,6 +12,7 @@ import { IUiSettingsClient, ChromeStart } from 'kibana/public';
import { ChromeBreadcrumb } from 'kibana/public';
import { IndexPatternsContract } from 'src/plugins/data/public';
import { MlContext, MlContextValue } from '../contexts/ml';
+import { UrlStateProvider } from '../util/url_state';
import * as routes from './routes';
@@ -48,21 +49,23 @@ export const MlRouter: FC<{ pageDeps: PageDependencies }> = ({ pageDeps }) => {
return (
-
- {Object.entries(routes).map(([name, route]) => (
- {
- window.setTimeout(() => {
- setBreadcrumbs(route.breadcrumbs);
- });
- return route.render(props, pageDeps);
- }}
- />
- ))}
-
+
+
+ {Object.entries(routes).map(([name, route]) => (
+ {
+ window.setTimeout(() => {
+ setBreadcrumbs(route.breadcrumbs);
+ });
+ return route.render(props, pageDeps);
+ }}
+ />
+ ))}
+
+
);
};
diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
index 52b4408d1ac5b..7a7865c9bd738 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
@@ -152,7 +152,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim
const [tableInterval] = useTableInterval();
const [tableSeverity] = useTableSeverity();
- const [selectedCells, setSelectedCells] = useSelectedCells();
+ const [selectedCells, setSelectedCells] = useSelectedCells(appState, setAppState);
useEffect(() => {
explorerService.setSelectedCells(selectedCells);
}, [JSON.stringify(selectedCells)]);
diff --git a/x-pack/plugins/ml/public/application/util/url_state.test.ts b/x-pack/plugins/ml/public/application/util/url_state.test.tsx
similarity index 82%
rename from x-pack/plugins/ml/public/application/util/url_state.test.ts
rename to x-pack/plugins/ml/public/application/util/url_state.test.tsx
index 0813f2e3da97f..9c03369648554 100644
--- a/x-pack/plugins/ml/public/application/util/url_state.test.ts
+++ b/x-pack/plugins/ml/public/application/util/url_state.test.tsx
@@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { renderHook, act } from '@testing-library/react-hooks';
-import { getUrlState, useUrlState } from './url_state';
+import React, { FC } from 'react';
+import { render, act } from '@testing-library/react';
+import { parseUrlState, useUrlState, UrlStateProvider } from './url_state';
const mockHistoryPush = jest.fn();
@@ -22,7 +23,7 @@ jest.mock('react-router-dom', () => ({
describe('getUrlState', () => {
test('properly decode url with _g and _a', () => {
expect(
- getUrlState(
+ parseUrlState(
"?_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFieldName:action),query:(query_string:(analyze_wildcard:!t,query:'*')))&_g=(ml:(jobIds:!(dec-2)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2019-01-01T00:03:40.000Z',mode:absolute,to:'2019-08-30T11:55:07.000Z'))&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d"
)
).toEqual({
@@ -64,13 +65,19 @@ describe('useUrlState', () => {
});
test('pushes a properly encoded search string to history', () => {
- const { result } = renderHook(() => useUrlState('_a'));
+ const TestComponent: FC = () => {
+ const [, setUrlState] = useUrlState('_a');
+ return setUrlState({ query: {} })}>ButtonText ;
+ };
+
+ const { getByText } = render(
+
+
+
+ );
act(() => {
- const [, setUrlState] = result.current;
- setUrlState({
- query: {},
- });
+ getByText('ButtonText').click();
});
expect(mockHistoryPush).toHaveBeenCalledWith({
diff --git a/x-pack/plugins/ml/public/application/util/url_state.ts b/x-pack/plugins/ml/public/application/util/url_state.tsx
similarity index 54%
rename from x-pack/plugins/ml/public/application/util/url_state.ts
rename to x-pack/plugins/ml/public/application/util/url_state.tsx
index beff5340ce7e4..c288a00bb06da 100644
--- a/x-pack/plugins/ml/public/application/util/url_state.ts
+++ b/x-pack/plugins/ml/public/application/util/url_state.tsx
@@ -5,7 +5,7 @@
*/
import { parse, stringify } from 'query-string';
-import { useCallback } from 'react';
+import React, { createContext, useCallback, useContext, useMemo, FC } from 'react';
import { isEqual } from 'lodash';
import { decode, encode } from 'rison-node';
import { useHistory, useLocation } from 'react-router-dom';
@@ -14,8 +14,16 @@ import { Dictionary } from '../../../common/types/common';
import { getNestedProperty } from './object_utils';
-export type SetUrlState = (attribute: string | Dictionary, value?: any) => void;
-export type UrlState = [Dictionary, SetUrlState];
+type Accessor = '_a' | '_g';
+export type SetUrlState = (
+ accessor: Accessor,
+ attribute: string | Dictionary,
+ value?: any
+) => void;
+export interface UrlState {
+ searchString: string;
+ setUrlState: SetUrlState;
+}
/**
* Set of URL query parameters that require the rison serialization.
@@ -30,7 +38,7 @@ function isRisonSerializationRequired(queryParam: string): boolean {
return risonSerializedParams.has(queryParam);
}
-export function getUrlState(search: string): Dictionary {
+export function parseUrlState(search: string): Dictionary {
const urlState: Dictionary = {};
const parsedQueryString = parse(search, { sort: false });
@@ -56,14 +64,23 @@ export function getUrlState(search: string): Dictionary {
// - `history.push()` is the successor of `save`.
// - The exposed state and set call make use of the above and make sure that
// different urlStates(e.g. `_a` / `_g`) don't overwrite each other.
-export const useUrlState = (accessor: string): UrlState => {
+// This uses a context to be able to maintain only one instance
+// of the url state. It gets passed down with `UrlStateProvider`
+// and can be used via `useUrlState`.
+export const urlStateStore = createContext({
+ searchString: '',
+ setUrlState: () => {},
+});
+const { Provider } = urlStateStore;
+export const UrlStateProvider: FC = ({ children }) => {
const history = useHistory();
- const { search } = useLocation();
+ const { search: searchString } = useLocation();
- const setUrlState = useCallback(
- (attribute: string | Dictionary, value?: any) => {
- const urlState = getUrlState(search);
- const parsedQueryString = parse(search, { sort: false });
+ const setUrlState: SetUrlState = useCallback(
+ (accessor: Accessor, attribute: string | Dictionary, value?: any) => {
+ const prevSearchString = searchString;
+ const urlState = parseUrlState(prevSearchString);
+ const parsedQueryString = parse(prevSearchString, { sort: false });
if (!Object.prototype.hasOwnProperty.call(urlState, accessor)) {
urlState[accessor] = {};
@@ -71,7 +88,7 @@ export const useUrlState = (accessor: string): UrlState => {
if (typeof attribute === 'string') {
if (isEqual(getNestedProperty(urlState, `${accessor}.${attribute}`), value)) {
- return;
+ return prevSearchString;
}
urlState[accessor][attribute] = value;
@@ -83,7 +100,10 @@ export const useUrlState = (accessor: string): UrlState => {
}
try {
- const oldLocationSearch = stringify(parsedQueryString, { sort: false, encode: false });
+ const oldLocationSearchString = stringify(parsedQueryString, {
+ sort: false,
+ encode: false,
+ });
Object.keys(urlState).forEach((a) => {
if (isRisonSerializationRequired(a)) {
@@ -92,20 +112,41 @@ export const useUrlState = (accessor: string): UrlState => {
parsedQueryString[a] = urlState[a];
}
});
- const newLocationSearch = stringify(parsedQueryString, { sort: false, encode: false });
+ const newLocationSearchString = stringify(parsedQueryString, {
+ sort: false,
+ encode: false,
+ });
- if (oldLocationSearch !== newLocationSearch) {
- history.push({
- search: stringify(parsedQueryString, { sort: false }),
- });
+ if (oldLocationSearchString !== newLocationSearchString) {
+ const newSearchString = stringify(parsedQueryString, { sort: false });
+ history.push({ search: newSearchString });
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('Could not save url state', error);
}
},
- [search]
+ [searchString]
);
- return [getUrlState(search)[accessor], setUrlState];
+ return {children} ;
+};
+
+export const useUrlState = (accessor: Accessor) => {
+ const { searchString, setUrlState: setUrlStateContext } = useContext(urlStateStore);
+
+ const urlState = useMemo(() => {
+ const fullUrlState = parseUrlState(searchString);
+ if (typeof fullUrlState === 'object') {
+ return fullUrlState[accessor];
+ }
+ return undefined;
+ }, [searchString]);
+
+ const setUrlState = useCallback(
+ (attribute: string | Dictionary, value?: any) =>
+ setUrlStateContext(accessor, attribute, value),
+ [accessor, setUrlStateContext]
+ );
+ return [urlState, setUrlState];
};
diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts
index 16eaab20fe8cb..196e17d0984f9 100644
--- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts
+++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts
@@ -70,6 +70,7 @@ export const anomalyDetectionUpdateJobSchema = schema.object({
),
groups: schema.maybe(schema.arrayOf(schema.maybe(schema.string()))),
model_snapshot_retention_days: schema.maybe(schema.number()),
+ daily_model_snapshot_retention_after_days: schema.maybe(schema.number()),
});
export const analysisConfigSchema = schema.object({
diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx
index 21a9fabf445f1..5bc8d96656ed4 100644
--- a/x-pack/plugins/observability/public/application/index.tsx
+++ b/x-pack/plugins/observability/public/application/index.tsx
@@ -3,23 +3,64 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import { createHashHistory } from 'history';
+import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
-import { EuiThemeProvider } from '../../../../legacy/common/eui_styled_components';
+import { Route, Router, Switch } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
+import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public';
import { AppMountParameters, CoreStart } from '../../../../../src/core/public';
-import { Home } from '../pages/home';
+import { EuiThemeProvider } from '../../../../legacy/common/eui_styled_components';
import { PluginContext } from '../context/plugin_context';
+import { useUrlParams } from '../hooks/use_url_params';
+import { routes } from '../routes';
+import { usePluginContext } from '../hooks/use_plugin_context';
+
+const App = () => {
+ return (
+ <>
+
+ {Object.keys(routes).map((key) => {
+ const path = key as keyof typeof routes;
+ const route = routes[path];
+ const Wrapper = () => {
+ const { core } = usePluginContext();
+ useEffect(() => {
+ core.chrome.setBreadcrumbs([
+ {
+ text: i18n.translate('xpack.observability.observability.breadcrumb.', {
+ defaultMessage: 'Observability',
+ }),
+ },
+ ...route.breadcrumb,
+ ]);
+ }, [core]);
+
+ const { query, path: pathParams } = useUrlParams(route.params);
+ return route.handler({ query, path: pathParams });
+ };
+ return ;
+ })}
+
+ >
+ );
+};
export const renderApp = (core: CoreStart, { element }: AppMountParameters) => {
const i18nCore = core.i18n;
const isDarkMode = core.uiSettings.get('theme:darkMode');
+ const history = createHashHistory();
ReactDOM.render(
-
-
-
-
-
+
+
+
+
+
+
+
+
+
,
element
);
diff --git a/x-pack/plugins/observability/public/components/app/chart_container/index.test.tsx b/x-pack/plugins/observability/public/components/app/chart_container/index.test.tsx
new file mode 100644
index 0000000000000..d09d535a49340
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/chart_container/index.test.tsx
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { render } from '@testing-library/react';
+import React from 'react';
+import { ChartContainer } from './';
+
+describe('chart container', () => {
+ it('shows loading indicator', () => {
+ const component = render(
+
+ My amazing component
+
+ );
+ expect(component.getByTestId('loading')).toBeInTheDocument();
+ expect(component.queryByText('My amazing component')).not.toBeInTheDocument();
+ });
+ it("doesn't show loading indicator", () => {
+ const component = render(
+
+ My amazing component
+
+ );
+ expect(component.queryByTestId('loading')).not.toBeInTheDocument();
+ expect(component.getByText('My amazing component')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/app/chart_container/index.tsx b/x-pack/plugins/observability/public/components/app/chart_container/index.tsx
new file mode 100644
index 0000000000000..2a0c25773eae5
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/chart_container/index.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { Chart } from '@elastic/charts';
+import { EuiLoadingChart } from '@elastic/eui';
+import { EuiLoadingChartSize } from '@elastic/eui/src/components/loading/loading_chart';
+import React from 'react';
+
+interface Props {
+ isInitialLoad: boolean;
+ height?: number;
+ width?: number;
+ iconSize?: EuiLoadingChartSize;
+ children: React.ReactNode;
+}
+
+const CHART_HEIGHT = 170;
+
+export const ChartContainer = ({
+ isInitialLoad,
+ children,
+ iconSize = 'xl',
+ height = CHART_HEIGHT,
+}: Props) => {
+ if (isInitialLoad) {
+ return (
+
+
+
+ );
+ }
+ return {children} ;
+};
diff --git a/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx b/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx
new file mode 100644
index 0000000000000..e04e8f050006a
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { ISection } from '../../../typings/section';
+import { render } from '../../../utils/test_helper';
+import { EmptySection } from './';
+
+describe('EmptySection', () => {
+ it('renders without action button', () => {
+ const section: ISection = {
+ id: 'apm',
+ title: 'APM',
+ icon: 'logoAPM',
+ description: 'foo bar',
+ };
+ const { getByText, queryAllByText } = render( );
+
+ expect(getByText('APM')).toBeInTheDocument();
+ expect(getByText('foo bar')).toBeInTheDocument();
+ expect(queryAllByText('Install agent')).toEqual([]);
+ });
+ it('renders with action button', () => {
+ const section: ISection = {
+ id: 'apm',
+ title: 'APM',
+ icon: 'logoAPM',
+ description: 'foo bar',
+ linkTitle: 'install agent',
+ href: 'https://www.elastic.co',
+ };
+ const { getByText, getByTestId } = render( );
+
+ expect(getByText('APM')).toBeInTheDocument();
+ expect(getByText('foo bar')).toBeInTheDocument();
+ const linkButton = getByTestId('empty-apm') as HTMLAnchorElement;
+ expect(linkButton.href).toEqual('https://www.elastic.co/');
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/app/empty_section/index.tsx b/x-pack/plugins/observability/public/components/app/empty_section/index.tsx
new file mode 100644
index 0000000000000..e19bf1678bc01
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/empty_section/index.tsx
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { EuiButton, EuiEmptyPrompt, EuiText } from '@elastic/eui';
+import React from 'react';
+import { ISection } from '../../../typings/section';
+
+interface Props {
+ section: ISection;
+}
+
+export const EmptySection = ({ section }: Props) => {
+ return (
+ {section.title}}
+ titleSize="xs"
+ body={{section.description} }
+ actions={
+ <>
+ {section.linkTitle && (
+
+ {section.linkTitle}
+
+ )}
+ >
+ }
+ />
+ );
+};
diff --git a/x-pack/plugins/observability/public/components/app/header/index.test.tsx b/x-pack/plugins/observability/public/components/app/header/index.test.tsx
new file mode 100644
index 0000000000000..59b6fbe9caf7a
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/header/index.test.tsx
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { render } from '../../../utils/test_helper';
+import { Header } from './';
+
+describe('Header', () => {
+ it('renders without add data button', () => {
+ const { getByText, queryAllByText, getByTestId } = render();
+ expect(getByTestId('observability-logo')).toBeInTheDocument();
+ expect(getByText('Observability')).toBeInTheDocument();
+ expect(queryAllByText('Add data')).toEqual([]);
+ });
+ it('renders with add data button', () => {
+ const { getByText, getByTestId } = render();
+ expect(getByTestId('observability-logo')).toBeInTheDocument();
+ expect(getByText('Observability')).toBeInTheDocument();
+ expect(getByText('Add data')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/app/header/index.tsx b/x-pack/plugins/observability/public/components/app/header/index.tsx
new file mode 100644
index 0000000000000..1c6ce766d0901
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/header/index.tsx
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ EuiBetaBadge,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiSpacer,
+ EuiTitle,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import styled from 'styled-components';
+import { usePluginContext } from '../../../hooks/use_plugin_context';
+
+const Container = styled.div<{ color: string }>`
+ background: ${(props) => props.color};
+ border-bottom: ${(props) => props.theme.eui.euiBorderThin};
+`;
+
+const Wrapper = styled.div<{ restrictWidth?: number }>`
+ width: 100%;
+ max-width: ${(props) => `${props.restrictWidth}px`};
+ margin: 0 auto;
+ overflow: hidden;
+ padding: ${(props) => (props.restrictWidth ? 0 : '0 24px')};
+`;
+
+interface Props {
+ color: string;
+ showAddData?: boolean;
+ restrictWidth?: number;
+ showGiveFeedback?: boolean;
+}
+
+export const Header = ({
+ color,
+ restrictWidth,
+ showAddData = false,
+ showGiveFeedback = false,
+}: Props) => {
+ const { core } = usePluginContext();
+ return (
+
+
+
+
+
+
+
+
+
+
+ {i18n.translate('xpack.observability.home.title', {
+ defaultMessage: 'Observability',
+ })}{' '}
+
+
+
+
+ {showGiveFeedback && (
+
+
+ {i18n.translate('xpack.observability.home.feedback', {
+ defaultMessage: 'Give us feedback',
+ })}
+
+
+ )}
+ {showAddData && (
+
+
+ {i18n.translate('xpack.observability.home.addData', { defaultMessage: 'Add data' })}
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/observability/public/components/app/layout/with_header.tsx b/x-pack/plugins/observability/public/components/app/layout/with_header.tsx
new file mode 100644
index 0000000000000..27b25f0056055
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/layout/with_header.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiPage, EuiPageBody, EuiPageProps } from '@elastic/eui';
+import React from 'react';
+import styled from 'styled-components';
+import { Header } from '../header/index';
+
+const getPaddingSize = (props: EuiPageProps) => (props.restrictWidth ? 0 : '24px');
+
+const Page = styled(EuiPage)`
+ background: transparent;
+ padding-right: ${getPaddingSize};
+ padding-left: ${getPaddingSize};
+`;
+
+const Container = styled.div<{ color?: string }>`
+ overflow-y: hidden;
+ min-height: calc(100vh - ${(props) => props.theme.eui.euiHeaderChildSize});
+ background: ${(props) => props.color};
+`;
+
+interface Props {
+ headerColor: string;
+ bodyColor: string;
+ children?: React.ReactNode;
+ restrictWidth?: number;
+ showAddData?: boolean;
+ showGiveFeedback?: boolean;
+}
+
+export const WithHeaderLayout = ({
+ headerColor,
+ bodyColor,
+ children,
+ restrictWidth,
+ showAddData,
+ showGiveFeedback,
+}: Props) => (
+
+
+
+ {children}
+
+
+);
diff --git a/x-pack/plugins/observability/public/components/app/news/index.scss b/x-pack/plugins/observability/public/components/app/news/index.scss
new file mode 100644
index 0000000000000..1222fe489c732
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/news/index.scss
@@ -0,0 +1,3 @@
+.obsNewsFeed__itemImg{
+ @include euiBottomShadowSmall;
+}
\ No newline at end of file
diff --git a/x-pack/plugins/observability/public/components/app/news/index.test.tsx b/x-pack/plugins/observability/public/components/app/news/index.test.tsx
new file mode 100644
index 0000000000000..cae6b4aec0c62
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/news/index.test.tsx
@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { render } from '../../../utils/test_helper';
+import { News } from './';
+
+describe('News', () => {
+ it('renders resources with all elements', () => {
+ const { getByText, getAllByText } = render( );
+ expect(getByText("What's new")).toBeInTheDocument();
+ expect(getAllByText('Read full story')).not.toEqual([]);
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/app/news/index.tsx b/x-pack/plugins/observability/public/components/app/news/index.tsx
new file mode 100644
index 0000000000000..41a4074f47976
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/news/index.tsx
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiLink,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React, { useContext } from 'react';
+import { ThemeContext } from 'styled-components';
+import './index.scss';
+import { truncate } from 'lodash';
+import { news as newsMockData } from './mock/news.mock.data';
+
+interface NewsItem {
+ title: string;
+ description: string;
+ link_url: string;
+ image_url: string;
+}
+
+export const News = () => {
+ const newsItems: NewsItem[] = newsMockData;
+ return (
+
+
+
+
+ {i18n.translate('xpack.observability.news.title', {
+ defaultMessage: "What's new",
+ })}
+
+
+
+ {newsItems.map((item, index) => (
+
+
+
+ ))}
+
+ );
+};
+
+const limitString = (string: string, limit: number) => truncate(string, { length: limit });
+
+const NewsItem = ({ item }: { item: NewsItem }) => {
+ const theme = useContext(ThemeContext);
+
+ return (
+
+
+
+ {item.title}
+
+
+
+
+
+
+
+
+ {limitString(item.description, 128)}
+
+
+
+
+
+ {i18n.translate('xpack.observability.news.readFullStory', {
+ defaultMessage: 'Read full story',
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/observability/public/components/app/news/mock/news.mock.data.ts b/x-pack/plugins/observability/public/components/app/news/mock/news.mock.data.ts
new file mode 100644
index 0000000000000..5c623bb9134eb
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/news/mock/news.mock.data.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const news = [
+ {
+ title: 'Have SIEM questions?',
+ description:
+ 'Join our growing community of Elastic SIEM users to discuss the configuration and use of Elastic SIEM for threat detection and response.',
+ link_url: 'https://discuss.elastic.co/c/security/siem/?blade=securitysolutionfeed',
+ image_url:
+ 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed',
+ },
+ {
+ title: 'Elastic SIEM on-demand training course — free for a limited time',
+ description:
+ 'With this self-paced, on-demand course, you will learn how to leverage Elastic SIEM to drive your security operations and threat hunting. This course is designed for security analysts and practitioners who have used other SIEMs or are familiar with SIEM concepts.',
+ link_url:
+ 'https://training.elastic.co/elearning/security-analytics/elastic-siem-fundamentals-promo?blade=securitysolutionfeed',
+ image_url:
+ 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt50f58e0358ebea9d/5c30508693d9791a70cd73ad/illustration-specialization-course-page-security.svg?blade=securitysolutionfeed',
+ },
+ {
+ title: 'New to Elastic SIEM? Take our on-demand training course',
+ description:
+ 'With this self-paced, on-demand course, you will learn how to leverage Elastic SIEM to drive your security operations and threat hunting. This course is designed for security analysts and practitioners who have used other SIEMs or are familiar with SIEM concepts.',
+ link_url:
+ 'https://www.elastic.co/training/specializations/security-analytics/elastic-siem-fundamentals?blade=securitysolutionfeed',
+ image_url:
+ 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt50f58e0358ebea9d/5c30508693d9791a70cd73ad/illustration-specialization-course-page-security.svg?blade=securitysolutionfeed',
+ },
+];
diff --git a/x-pack/plugins/observability/public/components/app/resources/index.test.tsx b/x-pack/plugins/observability/public/components/app/resources/index.test.tsx
new file mode 100644
index 0000000000000..570aa3954424f
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/resources/index.test.tsx
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { render } from '@testing-library/react';
+import React from 'react';
+import { Resources } from './';
+
+describe('Resources', () => {
+ it('renders resources with all elements', () => {
+ const { getByText } = render( );
+ expect(getByText('Documentation')).toBeInTheDocument();
+ expect(getByText('Discuss forum')).toBeInTheDocument();
+ expect(getByText('Observability fundamentals')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/app/resources/index.tsx b/x-pack/plugins/observability/public/components/app/resources/index.tsx
new file mode 100644
index 0000000000000..c330c358d022a
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/resources/index.tsx
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { EuiFlexGroup, EuiFlexItem, EuiListGroup, EuiTitle } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+
+const resources = [
+ {
+ iconType: 'documents',
+ label: i18n.translate('xpack.observability.resources.documentation', {
+ defaultMessage: 'Documentation',
+ }),
+ href: 'https://www.elastic.co/guide/en/observability/current/observability-ui.html',
+ },
+ {
+ iconType: 'editorComment',
+ label: i18n.translate('xpack.observability.resources.forum', {
+ defaultMessage: 'Discuss forum',
+ }),
+ href: 'https://discuss.elastic.co/c/observability/',
+ },
+ {
+ iconType: 'training',
+ label: i18n.translate('xpack.observability.resources.training', {
+ defaultMessage: 'Observability fundamentals',
+ }),
+ href: 'https://www.elastic.co/training/observability-fundamentals',
+ },
+];
+
+export const Resources = () => {
+ return (
+
+
+
+
+ {i18n.translate('xpack.observability.resources.title', {
+ defaultMessage: 'Resources',
+ })}
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx b/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx
new file mode 100644
index 0000000000000..4c80195d33ace
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx
@@ -0,0 +1,129 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import {
+ EuiBadge,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiIconTip,
+ EuiLink,
+ EuiText,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import moment from 'moment';
+import React, { useState } from 'react';
+import { EuiSelect } from '@elastic/eui';
+import { uniqBy } from 'lodash';
+import { Alert } from '../../../../../../alerts/common';
+import { usePluginContext } from '../../../../hooks/use_plugin_context';
+import { SectionContainer } from '..';
+
+const ALL_TYPES = 'ALL_TYPES';
+const allTypes = {
+ value: ALL_TYPES,
+ text: i18n.translate('xpack.observability.overview.alert.allTypes', {
+ defaultMessage: 'All types',
+ }),
+};
+
+interface Props {
+ alerts: Alert[];
+}
+
+export const AlertsSection = ({ alerts }: Props) => {
+ const { core } = usePluginContext();
+ const [filter, setFilter] = useState(ALL_TYPES);
+
+ const filterOptions = uniqBy(alerts, (alert) => alert.consumer).map(({ consumer }) => ({
+ value: consumer,
+ text: consumer,
+ }));
+
+ return (
+
+
+
+
+
+ setFilter(e.target.value)}
+ prepend={i18n.translate('xpack.observability.overview.alert.view', {
+ defaultMessage: 'View',
+ })}
+ />
+
+
+
+
+
+ {alerts
+ .filter((alert) => filter === ALL_TYPES || alert.consumer === filter)
+ .map((alert, index) => {
+ const isLastElement = index === alerts.length - 1;
+ return (
+
+
+
+ {alert.name}
+
+
+
+
+
+ {alert.alertTypeId}
+
+ {alert.tags.map((tag, idx) => {
+ return (
+
+ {tag}
+
+ );
+ })}
+
+
+
+
+
+
+ Updated {moment.duration(moment().diff(alert.updatedAt)).humanize()} ago
+
+
+ {alert.muteAll && (
+
+
+
+ )}
+
+
+ {!isLastElement && }
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx
new file mode 100644
index 0000000000000..d4b8236e0ef49
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import * as fetcherHook from '../../../../hooks/use_fetcher';
+import { render } from '../../../../utils/test_helper';
+import { APMSection } from './';
+import { response } from './mock_data/apm.mock';
+
+describe('APMSection', () => {
+ it('renders with transaction series and stats', () => {
+ jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
+ data: response,
+ status: fetcherHook.FETCH_STATUS.SUCCESS,
+ refetch: jest.fn(),
+ });
+ const { getByText, queryAllByTestId } = render(
+
+ );
+
+ expect(getByText('APM')).toBeInTheDocument();
+ expect(getByText('View in app')).toBeInTheDocument();
+ expect(getByText('Services 11')).toBeInTheDocument();
+ expect(getByText('Transactions per minute 312.00k')).toBeInTheDocument();
+ expect(queryAllByTestId('loading')).toEqual([]);
+ });
+ it('shows loading state', () => {
+ jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
+ data: undefined,
+ status: fetcherHook.FETCH_STATUS.LOADING,
+ refetch: jest.fn(),
+ });
+ const { getByText, queryAllByText, getByTestId } = render(
+
+ );
+
+ expect(getByText('APM')).toBeInTheDocument();
+ expect(getByTestId('loading')).toBeInTheDocument();
+ expect(queryAllByText('View in app')).toEqual([]);
+ expect(queryAllByText('Services 11')).toEqual([]);
+ expect(queryAllByText('Transactions per minute 312.00k')).toEqual([]);
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx
new file mode 100644
index 0000000000000..697d4adfa0b75
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx
@@ -0,0 +1,112 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Axis, BarSeries, niceTimeFormatter, Position, ScaleType, Settings } from '@elastic/charts';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import numeral from '@elastic/numeral';
+import { i18n } from '@kbn/i18n';
+import moment from 'moment';
+import React, { useContext } from 'react';
+import { useHistory } from 'react-router-dom';
+import { ThemeContext } from 'styled-components';
+import { SectionContainer } from '../';
+import { getDataHandler } from '../../../../data_handler';
+import { useChartTheme } from '../../../../hooks/use_chart_theme';
+import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
+import { ChartContainer } from '../../chart_container';
+import { StyledStat } from '../../styled_stat';
+import { onBrushEnd } from '../helper';
+
+interface Props {
+ startTime?: string;
+ endTime?: string;
+ bucketSize?: string;
+}
+
+function formatTpm(value?: number) {
+ return numeral(value).format('0.00a');
+}
+
+export const APMSection = ({ startTime, endTime, bucketSize }: Props) => {
+ const theme = useContext(ThemeContext);
+ const history = useHistory();
+
+ const { data, status } = useFetcher(() => {
+ if (startTime && endTime && bucketSize) {
+ return getDataHandler('apm')?.fetchData({ startTime, endTime, bucketSize });
+ }
+ }, [startTime, endTime, bucketSize]);
+
+ const { title = 'APM', appLink, stats, series } = data || {};
+
+ const min = moment.utc(startTime).valueOf();
+ const max = moment.utc(endTime).valueOf();
+
+ const formatter = niceTimeFormatter([min, max]);
+
+ const isLoading = status === FETCH_STATUS.LOADING;
+
+ const transactionsColor = theme.eui.euiColorVis1;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ onBrushEnd({ x, history })}
+ theme={useChartTheme()}
+ showLegend={false}
+ xDomain={{ min, max }}
+ />
+ {series?.transactions.coordinates && (
+ <>
+
+ `${formatTpm(value)} tpm`}
+ />
+
+ >
+ )}
+
+
+ );
+};
diff --git a/x-pack/plugins/observability/public/components/app/section/apm/mock_data/apm.mock.ts b/x-pack/plugins/observability/public/components/app/section/apm/mock_data/apm.mock.ts
new file mode 100644
index 0000000000000..5857021b1537f
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/section/apm/mock_data/apm.mock.ts
@@ -0,0 +1,168 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ApmFetchDataResponse } from '../../../../../typings';
+
+export const response: ApmFetchDataResponse = {
+ title: 'APM',
+
+ appLink: '/app/apm',
+ stats: {
+ services: { value: 11, type: 'number' },
+ transactions: { value: 312000, type: 'number' },
+ },
+ series: {
+ transactions: {
+ coordinates: [
+ { x: 1591365600000, y: 32 },
+ { x: 1591366200000, y: 43 },
+ { x: 1591366800000, y: 22 },
+ { x: 1591367400000, y: 29 },
+ { x: 1591368000000, y: 39 },
+ { x: 1591368600000, y: 36 },
+ { x: 1591369200000, y: 50 },
+ { x: 1591369800000, y: 31 },
+ { x: 1591370400000, y: 39 },
+ { x: 1591371000000, y: 26 },
+ { x: 1591371600000, y: 45 },
+ { x: 1591372200000, y: 27 },
+ { x: 1591372800000, y: 37 },
+ { x: 1591373400000, y: 55 },
+ { x: 1591374000000, y: 31 },
+ { x: 1591374600000, y: 26 },
+ { x: 1591375200000, y: 57 },
+ { x: 1591375800000, y: 25 },
+ { x: 1591376400000, y: 28 },
+ { x: 1591377000000, y: 40 },
+ { x: 1591377600000, y: 33 },
+ { x: 1591378200000, y: 33 },
+ { x: 1591378800000, y: 31 },
+ { x: 1591379400000, y: 32 },
+ { x: 1591380000000, y: 34 },
+ { x: 1591380600000, y: 31 },
+ { x: 1591381200000, y: 16 },
+ { x: 1591381800000, y: 34 },
+ { x: 1591382400000, y: 33 },
+ { x: 1591383000000, y: 35 },
+ { x: 1591383600000, y: 47 },
+ { x: 1591384200000, y: 44 },
+ { x: 1591384800000, y: 21 },
+ { x: 1591385400000, y: 25 },
+ { x: 1591386000000, y: 34 },
+ { x: 1591386600000, y: 37 },
+ { x: 1591387200000, y: 38 },
+ { x: 1591387800000, y: 28 },
+ { x: 1591388400000, y: 32 },
+ { x: 1591389000000, y: 37 },
+ { x: 1591389600000, y: 25 },
+ { x: 1591390200000, y: 33 },
+ { x: 1591390800000, y: 34 },
+ { x: 1591391400000, y: 30 },
+ { x: 1591392000000, y: 45 },
+ { x: 1591392600000, y: 42 },
+ { x: 1591393200000, y: 23 },
+ { x: 1591393800000, y: 33 },
+ { x: 1591394400000, y: 38 },
+ { x: 1591395000000, y: 30 },
+ { x: 1591395600000, y: 25 },
+ { x: 1591396200000, y: 33 },
+ { x: 1591396800000, y: 37 },
+ { x: 1591397400000, y: 43 },
+ { x: 1591398000000, y: 30 },
+ { x: 1591398600000, y: 36 },
+ { x: 1591399200000, y: 28 },
+ { x: 1591399800000, y: 39 },
+ { x: 1591400400000, y: 27 },
+ { x: 1591401000000, y: 41 },
+ { x: 1591401600000, y: 25 },
+ { x: 1591402200000, y: 31 },
+ { x: 1591402800000, y: 28 },
+ { x: 1591403400000, y: 29 },
+ { x: 1591404000000, y: 49 },
+ { x: 1591404600000, y: 24 },
+ { x: 1591405200000, y: 41 },
+ { x: 1591405800000, y: 30 },
+ { x: 1591406400000, y: 36 },
+ { x: 1591407000000, y: 39 },
+ { x: 1591407600000, y: 23 },
+ { x: 1591408200000, y: 40 },
+ { x: 1591408800000, y: 34 },
+ { x: 1591409400000, y: 28 },
+ { x: 1591410000000, y: 33 },
+ { x: 1591410600000, y: 31 },
+ { x: 1591411200000, y: 39 },
+ { x: 1591411800000, y: 33 },
+ { x: 1591412400000, y: 35 },
+ { x: 1591413000000, y: 31 },
+ { x: 1591413600000, y: 35 },
+ { x: 1591414200000, y: 37 },
+ { x: 1591414800000, y: 26 },
+ { x: 1591415400000, y: 27 },
+ { x: 1591416000000, y: 26 },
+ { x: 1591416600000, y: 34 },
+ { x: 1591417200000, y: 33 },
+ { x: 1591417800000, y: 38 },
+ { x: 1591418400000, y: 34 },
+ { x: 1591419000000, y: 37 },
+ { x: 1591419600000, y: 24 },
+ { x: 1591420200000, y: 25 },
+ { x: 1591420800000, y: 20 },
+ { x: 1591421400000, y: 35 },
+ { x: 1591422000000, y: 41 },
+ { x: 1591422600000, y: 40 },
+ { x: 1591423200000, y: 33 },
+ { x: 1591423800000, y: 24 },
+ { x: 1591424400000, y: 44 },
+ { x: 1591425000000, y: 24 },
+ { x: 1591425600000, y: 32 },
+ { x: 1591426200000, y: 37 },
+ { x: 1591426800000, y: 34 },
+ { x: 1591427400000, y: 28 },
+ { x: 1591428000000, y: 26 },
+ { x: 1591428600000, y: 37 },
+ { x: 1591429200000, y: 36 },
+ { x: 1591429800000, y: 37 },
+ { x: 1591430400000, y: 23 },
+ { x: 1591431000000, y: 47 },
+ { x: 1591431600000, y: 41 },
+ { x: 1591432200000, y: 24 },
+ { x: 1591432800000, y: 34 },
+ { x: 1591433400000, y: 27 },
+ { x: 1591434000000, y: 34 },
+ { x: 1591434600000, y: 44 },
+ { x: 1591435200000, y: 20 },
+ { x: 1591435800000, y: 34 },
+ { x: 1591436400000, y: 29 },
+ { x: 1591437000000, y: 28 },
+ { x: 1591437600000, y: 36 },
+ { x: 1591438200000, y: 34 },
+ { x: 1591438800000, y: 26 },
+ { x: 1591439400000, y: 29 },
+ { x: 1591440000000, y: 45 },
+ { x: 1591440600000, y: 34 },
+ { x: 1591441200000, y: 25 },
+ { x: 1591441800000, y: 34 },
+ { x: 1591442400000, y: 28 },
+ { x: 1591443000000, y: 34 },
+ { x: 1591443600000, y: 31 },
+ { x: 1591444200000, y: 24 },
+ { x: 1591444800000, y: 34 },
+ { x: 1591445400000, y: 21 },
+ { x: 1591446000000, y: 40 },
+ { x: 1591446600000, y: 37 },
+ { x: 1591447200000, y: 31 },
+ { x: 1591447800000, y: 21 },
+ { x: 1591448400000, y: 24 },
+ { x: 1591449000000, y: 30 },
+ { x: 1591449600000, y: 22 },
+ { x: 1591450200000, y: 27 },
+ { x: 1591450800000, y: 30 },
+ { x: 1591451400000, y: 22 },
+ { x: 1591452000000, y: 9 },
+ ],
+ },
+ },
+};
diff --git a/x-pack/plugins/observability/public/components/app/section/error_panel/index.tsx b/x-pack/plugins/observability/public/components/app/section/error_panel/index.tsx
new file mode 100644
index 0000000000000..8f0781b8f0269
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/section/error_panel/index.tsx
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+
+export const ErrorPanel = () => {
+ return (
+
+
+
+ {i18n.translate('xpack.observability.section.errorPanel', {
+ defaultMessage: 'An error happened when trying to fetch data. Please try again',
+ })}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/observability/public/components/app/section/helper.test.ts b/x-pack/plugins/observability/public/components/app/section/helper.test.ts
new file mode 100644
index 0000000000000..6a8cd27753a8d
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/section/helper.test.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { onBrushEnd } from './helper';
+import { History } from 'history';
+
+describe('Chart helper', () => {
+ describe('onBrushEnd', () => {
+ const history = ({
+ push: jest.fn(),
+ location: {
+ search: '',
+ },
+ } as unknown) as History;
+ it("doesn't push a new history when x is not defined", () => {
+ onBrushEnd({ x: undefined, history });
+ expect(history.push).not.toBeCalled();
+ });
+
+ it('pushes a new history with time range converted to ISO', () => {
+ onBrushEnd({ x: [1593409448167, 1593415727797], history });
+ expect(history.push).toBeCalledWith({
+ search: 'rangeFrom=2020-06-29T05:44:08.167Z&rangeTo=2020-06-29T07:28:47.797Z',
+ });
+ });
+
+ it('pushes a new history keeping current search', () => {
+ history.location.search = '?foo=bar';
+ onBrushEnd({ x: [1593409448167, 1593415727797], history });
+ expect(history.push).toBeCalledWith({
+ search: 'foo=bar&rangeFrom=2020-06-29T05:44:08.167Z&rangeTo=2020-06-29T07:28:47.797Z',
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/app/section/helper.ts b/x-pack/plugins/observability/public/components/app/section/helper.ts
new file mode 100644
index 0000000000000..81fa92cb87782
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/section/helper.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { XYBrushArea } from '@elastic/charts';
+import { History } from 'history';
+import { fromQuery, toQuery } from '../../../utils/url';
+
+export const onBrushEnd = ({ x, history }: { x: XYBrushArea['x']; history: History }) => {
+ if (x) {
+ const start = x[0];
+ const end = x[1];
+
+ const currentSearch = toQuery(history.location.search);
+ const nextSearch = {
+ rangeFrom: new Date(start).toISOString(),
+ rangeTo: new Date(end).toISOString(),
+ };
+ history.push({
+ ...history.location,
+ search: fromQuery({
+ ...currentSearch,
+ ...nextSearch,
+ }),
+ });
+ }
+};
diff --git a/x-pack/plugins/observability/public/components/app/section/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/index.test.tsx
new file mode 100644
index 0000000000000..49cb175d0c094
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/section/index.test.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { render } from '../../../utils/test_helper';
+import { SectionContainer } from './';
+
+describe('SectionContainer', () => {
+ it('renders section without app link', () => {
+ const component = render(
+
+ I am a very nice component
+
+ );
+ expect(component.getByText('I am a very nice component')).toBeInTheDocument();
+ expect(component.getByText('Foo')).toBeInTheDocument();
+ expect(component.queryAllByText('View in app')).toEqual([]);
+ });
+ it('renders section with app link', () => {
+ const component = render(
+
+ I am a very nice component
+
+ );
+ expect(component.getByText('I am a very nice component')).toBeInTheDocument();
+ expect(component.getByText('Foo')).toBeInTheDocument();
+ expect(component.getByText('View in app')).toBeInTheDocument();
+ });
+ it('renders section with error', () => {
+ const component = render(
+
+ I am a very nice component
+
+ );
+ expect(component.queryByText('I am a very nice component')).not.toBeInTheDocument();
+ expect(component.getByText('Foo')).toBeInTheDocument();
+ expect(
+ component.getByText('An error happened when trying to fetch data. Please try again')
+ ).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/app/section/index.tsx b/x-pack/plugins/observability/public/components/app/section/index.tsx
new file mode 100644
index 0000000000000..3556e8c01ab30
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/section/index.tsx
@@ -0,0 +1,62 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { EuiAccordion, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { ErrorPanel } from './error_panel';
+import { usePluginContext } from '../../../hooks/use_plugin_context';
+
+interface Props {
+ title: string;
+ hasError: boolean;
+ children: React.ReactNode;
+ minHeight?: number;
+ appLink?: string;
+ appLinkName?: string;
+}
+
+export const SectionContainer = ({ title, appLink, children, hasError, appLinkName }: Props) => {
+ const { core } = usePluginContext();
+ return (
+
+ {title}
+
+ }
+ extraAction={
+ appLink && (
+
+
+ {appLinkName
+ ? appLinkName
+ : i18n.translate('xpack.observability.chart.viewInAppLabel', {
+ defaultMessage: 'View in app',
+ })}
+
+
+ )
+ }
+ >
+ <>
+
+
+ {hasError ? (
+
+ ) : (
+ <>
+
+ {children}
+ >
+ )}
+
+ >
+
+ );
+};
diff --git a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx
new file mode 100644
index 0000000000000..f3ba2ef6fa83a
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx
@@ -0,0 +1,151 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Axis, BarSeries, niceTimeFormatter, Position, ScaleType, Settings } from '@elastic/charts';
+import { EuiFlexGroup, EuiFlexItem, euiPaletteColorBlind } from '@elastic/eui';
+import numeral from '@elastic/numeral';
+import { isEmpty } from 'lodash';
+import moment from 'moment';
+import React, { Fragment } from 'react';
+import { useHistory } from 'react-router-dom';
+import { EuiTitle } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { EuiSpacer } from '@elastic/eui';
+import { SectionContainer } from '../';
+import { getDataHandler } from '../../../../data_handler';
+import { useChartTheme } from '../../../../hooks/use_chart_theme';
+import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
+import { LogsFetchDataResponse } from '../../../../typings';
+import { formatStatValue } from '../../../../utils/format_stat_value';
+import { ChartContainer } from '../../chart_container';
+import { StyledStat } from '../../styled_stat';
+import { onBrushEnd } from '../helper';
+
+interface Props {
+ startTime?: string;
+ endTime?: string;
+ bucketSize?: string;
+}
+
+function getColorPerItem(series?: LogsFetchDataResponse['series']) {
+ if (!series) {
+ return {};
+ }
+ const availableColors = euiPaletteColorBlind({
+ rotations: Math.ceil(Object.keys(series).length / 10),
+ });
+ const colorsPerItem = Object.keys(series).reduce((acc: Record, key, index) => {
+ acc[key] = availableColors[index];
+ return acc;
+ }, {});
+
+ return colorsPerItem;
+}
+
+export const LogsSection = ({ startTime, endTime, bucketSize }: Props) => {
+ const history = useHistory();
+
+ const { data, status } = useFetcher(() => {
+ if (startTime && endTime && bucketSize) {
+ return getDataHandler('infra_logs')?.fetchData({ startTime, endTime, bucketSize });
+ }
+ }, [startTime, endTime, bucketSize]);
+
+ const min = moment.utc(startTime).valueOf();
+ const max = moment.utc(endTime).valueOf();
+
+ const formatter = niceTimeFormatter([min, max]);
+
+ const { title, appLink, stats, series } = data || {};
+
+ const colorsPerItem = getColorPerItem(series);
+
+ const isLoading = status === FETCH_STATUS.LOADING;
+
+ return (
+
+
+
+ {i18n.translate('xpack.observability.overview.logs.subtitle', {
+ defaultMessage: 'Logs rate per minute',
+ })}
+
+
+
+
+ {!stats || isEmpty(stats) ? (
+
+
+
+ ) : (
+ Object.keys(stats).map((key) => {
+ const stat = stats[key];
+ return (
+
+
+
+ );
+ })
+ )}
+
+
+ onBrushEnd({ x, history })}
+ theme={useChartTheme()}
+ showLegend
+ legendPosition={Position.Right}
+ xDomain={{ min, max }}
+ showLegendExtra
+ />
+ {series &&
+ Object.keys(series).map((key) => {
+ const serie = series[key];
+ const chartData = serie.coordinates.map((coordinate) => ({
+ ...coordinate,
+ g: serie.label,
+ }));
+ return (
+
+
+
+ numeral(d).format('0a')}
+ />
+
+ );
+ })}
+
+
+ );
+};
diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx
new file mode 100644
index 0000000000000..6276e1ba1baca
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx
@@ -0,0 +1,182 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { AreaSeries, ScaleType, Settings } from '@elastic/charts';
+import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer } from '@elastic/eui';
+import numeral from '@elastic/numeral';
+import { i18n } from '@kbn/i18n';
+import React, { useContext } from 'react';
+import styled, { ThemeContext } from 'styled-components';
+import { SectionContainer } from '../';
+import { getDataHandler } from '../../../../data_handler';
+import { useChartTheme } from '../../../../hooks/use_chart_theme';
+import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
+import { Series } from '../../../../typings';
+import { ChartContainer } from '../../chart_container';
+import { StyledStat } from '../../styled_stat';
+
+interface Props {
+ startTime?: string;
+ endTime?: string;
+ bucketSize?: string;
+}
+
+/**
+ * EuiProgress doesn't support custom color, when it does this component can be removed.
+ */
+const StyledProgress = styled.div<{ color?: string }>`
+ progress {
+ &.euiProgress--native {
+ &::-webkit-progress-value {
+ background-color: ${(props) => props.color};
+ }
+
+ &::-moz-progress-bar {
+ background-color: ${(props) => props.color};
+ }
+ }
+
+ &.euiProgress--indeterminate {
+ &:before {
+ background-color: ${(props) => props.color};
+ }
+ }
+ }
+`;
+
+export const MetricsSection = ({ startTime, endTime, bucketSize }: Props) => {
+ const theme = useContext(ThemeContext);
+ const { data, status } = useFetcher(() => {
+ if (startTime && endTime && bucketSize) {
+ return getDataHandler('infra_metrics')?.fetchData({ startTime, endTime, bucketSize });
+ }
+ }, [startTime, endTime, bucketSize]);
+
+ const isLoading = status === FETCH_STATUS.LOADING;
+
+ const { title = 'Metrics', appLink, stats, series } = data || {};
+
+ const cpuColor = theme.eui.euiColorVis7;
+ const memoryColor = theme.eui.euiColorVis0;
+ const inboundTrafficColor = theme.eui.euiColorVis3;
+ const outboundTrafficColor = theme.eui.euiColorVis2;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const AreaChart = ({
+ serie,
+ isLoading,
+ color,
+}: {
+ serie?: Series;
+ isLoading: boolean;
+ color: string;
+}) => {
+ const chartTheme = useChartTheme();
+
+ return (
+
+
+ {serie && (
+
+ )}
+
+ );
+};
diff --git a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx
new file mode 100644
index 0000000000000..1f8ca6e61f132
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx
@@ -0,0 +1,179 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ Axis,
+ BarSeries,
+ niceTimeFormatter,
+ Position,
+ ScaleType,
+ Settings,
+ TickFormatter,
+} from '@elastic/charts';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import numeral from '@elastic/numeral';
+import { i18n } from '@kbn/i18n';
+import moment from 'moment';
+import React, { useContext } from 'react';
+import { useHistory } from 'react-router-dom';
+import { ThemeContext } from 'styled-components';
+import { SectionContainer } from '../';
+import { getDataHandler } from '../../../../data_handler';
+import { useChartTheme } from '../../../../hooks/use_chart_theme';
+import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
+import { Series } from '../../../../typings';
+import { ChartContainer } from '../../chart_container';
+import { StyledStat } from '../../styled_stat';
+import { onBrushEnd } from '../helper';
+
+interface Props {
+ startTime?: string;
+ endTime?: string;
+ bucketSize?: string;
+}
+
+export const UptimeSection = ({ startTime, endTime, bucketSize }: Props) => {
+ const theme = useContext(ThemeContext);
+ const history = useHistory();
+
+ const { data, status } = useFetcher(() => {
+ if (startTime && endTime && bucketSize) {
+ return getDataHandler('uptime')?.fetchData({ startTime, endTime, bucketSize });
+ }
+ }, [startTime, endTime, bucketSize]);
+
+ const min = moment.utc(startTime).valueOf();
+ const max = moment.utc(endTime).valueOf();
+ const formatter = niceTimeFormatter([min, max]);
+
+ const isLoading = status === FETCH_STATUS.LOADING;
+
+ const { title = 'Uptime', appLink, stats, series } = data || {};
+
+ const downColor = theme.eui.euiColorVis2;
+ const upColor = theme.eui.euiColorLightShade;
+
+ return (
+
+
+ {/* Stats section */}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Chart section */}
+
+ onBrushEnd({ x, history })}
+ theme={useChartTheme()}
+ showLegend={false}
+ legendPosition={Position.Right}
+ xDomain={{ min, max }}
+ />
+
+
+
+
+ );
+};
+
+const UptimeBarSeries = ({
+ id,
+ label,
+ series,
+ color,
+ ticktFormatter,
+}: {
+ id: string;
+ label: string;
+ series?: Series;
+ color: string;
+ ticktFormatter: TickFormatter;
+}) => {
+ if (!series) {
+ return null;
+ }
+ const chartData = series.coordinates.map((coordinate) => ({
+ ...coordinate,
+ g: label,
+ }));
+ return (
+ <>
+
+
+ numeral(x).format('0a')}
+ />
+ >
+ );
+};
diff --git a/x-pack/plugins/observability/public/components/app/styled_stat/index.tsx b/x-pack/plugins/observability/public/components/app/styled_stat/index.tsx
new file mode 100644
index 0000000000000..fe38df6484c29
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/styled_stat/index.tsx
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import styled from 'styled-components';
+import { EuiStat } from '@elastic/eui';
+import React from 'react';
+import { EuiStatProps } from '@elastic/eui/src/components/stat/stat';
+
+const Stat = styled(EuiStat)`
+ .euiStat__title {
+ color: ${(props) => props.color};
+ }
+`;
+
+interface Props extends Partial {
+ children?: React.ReactNode;
+ color?: string;
+}
+
+const EMPTY_VALUE = '--';
+
+export const StyledStat = (props: Props) => {
+ const { description = EMPTY_VALUE, title = EMPTY_VALUE, ...rest } = props;
+ return ;
+};
diff --git a/x-pack/plugins/observability/public/components/action_menu.tsx b/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx
similarity index 100%
rename from x-pack/plugins/observability/public/components/action_menu.tsx
rename to x-pack/plugins/observability/public/components/shared/action_menu/index.tsx
diff --git a/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx
new file mode 100644
index 0000000000000..cc77c1ed72b4a
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx
@@ -0,0 +1,89 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiSuperDatePicker } from '@elastic/eui';
+import React from 'react';
+import { useHistory, useLocation } from 'react-router-dom';
+import { UI_SETTINGS, useKibanaUISettings } from '../../../hooks/use_kibana_ui_settings';
+import { fromQuery, toQuery } from '../../../utils/url';
+
+export interface TimePickerTime {
+ from: string;
+ to: string;
+}
+
+export interface TimePickerQuickRange extends TimePickerTime {
+ display: string;
+}
+
+export interface TimePickerRefreshInterval {
+ pause: boolean;
+ value: number;
+}
+
+interface Props {
+ rangeFrom: string;
+ rangeTo: string;
+ refreshPaused: boolean;
+ refreshInterval: number;
+}
+
+export const DatePicker = ({ rangeFrom, rangeTo, refreshPaused, refreshInterval }: Props) => {
+ const location = useLocation();
+ const history = useHistory();
+
+ const timePickerQuickRanges = useKibanaUISettings(
+ UI_SETTINGS.TIMEPICKER_QUICK_RANGES
+ );
+
+ const commonlyUsedRanges = timePickerQuickRanges.map(({ from, to, display }) => ({
+ start: from,
+ end: to,
+ label: display,
+ }));
+
+ function updateUrl(nextQuery: {
+ rangeFrom?: string;
+ rangeTo?: string;
+ refreshPaused?: boolean;
+ refreshInterval?: number;
+ }) {
+ history.push({
+ ...location,
+ search: fromQuery({
+ ...toQuery(location.search),
+ ...nextQuery,
+ }),
+ });
+ }
+
+ function onRefreshChange({
+ isPaused,
+ refreshInterval: interval,
+ }: {
+ isPaused: boolean;
+ refreshInterval: number;
+ }) {
+ updateUrl({ refreshPaused: isPaused, refreshInterval: interval });
+ }
+
+ function onTimeChange({ start, end }: { start: string; end: string }) {
+ updateUrl({ rangeFrom: start, rangeTo: end });
+ }
+
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/observability/public/data_handler.ts b/x-pack/plugins/observability/public/data_handler.ts
index 39e702a332a8e..d7f8c471ad9aa 100644
--- a/x-pack/plugins/observability/public/data_handler.ts
+++ b/x-pack/plugins/observability/public/data_handler.ts
@@ -17,9 +17,20 @@ export function registerDataHandler({
dataHandlers[appName] = { fetchData, hasData };
}
+export function unregisterDataHandler({ appName }: { appName: T }) {
+ delete dataHandlers[appName];
+}
+
export function getDataHandler(appName: T) {
const dataHandler = dataHandlers[appName];
if (dataHandler) {
return dataHandler as DataHandler;
}
}
+
+export async function fetchHasData() {
+ const apps: ObservabilityApp[] = ['apm', 'uptime', 'infra_logs', 'infra_metrics'];
+ const promises = apps.map((app) => getDataHandler(app)?.hasData());
+ const [apm, uptime, logs, metrics] = await Promise.all(promises);
+ return { apm, uptime, infra_logs: logs, infra_metrics: metrics };
+}
diff --git a/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx b/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx
new file mode 100644
index 0000000000000..13f7159ba6043
--- /dev/null
+++ b/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
+import { useContext } from 'react';
+import { ThemeContext } from 'styled-components';
+
+export function useChartTheme() {
+ const theme = useContext(ThemeContext);
+ return theme.darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme;
+}
diff --git a/x-pack/plugins/observability/public/hooks/use_fetcher.tsx b/x-pack/plugins/observability/public/hooks/use_fetcher.tsx
new file mode 100644
index 0000000000000..88a8ad264e737
--- /dev/null
+++ b/x-pack/plugins/observability/public/hooks/use_fetcher.tsx
@@ -0,0 +1,84 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useEffect, useState, useMemo } from 'react';
+
+export enum FETCH_STATUS {
+ LOADING = 'loading',
+ SUCCESS = 'success',
+ FAILURE = 'failure',
+ PENDING = 'pending',
+}
+
+export interface FetcherResult {
+ data?: Data;
+ status: FETCH_STATUS;
+ error?: Error;
+}
+
+// fetcher functions can return undefined OR a promise. Previously we had a more simple type
+// but it led to issues when using object destructuring with default values
+type InferResponseType = Exclude extends Promise
+ ? TResponseType
+ : unknown;
+
+export function useFetcher(
+ fn: () => TReturn,
+ fnDeps: any[],
+ options: {
+ preservePreviousData?: boolean;
+ } = {}
+): FetcherResult> & { refetch: () => void } {
+ const { preservePreviousData = true } = options;
+
+ const [result, setResult] = useState>>({
+ data: undefined,
+ status: FETCH_STATUS.PENDING,
+ });
+ const [counter, setCounter] = useState(0);
+ useEffect(() => {
+ async function doFetch() {
+ const promise = fn();
+ if (!promise) {
+ return;
+ }
+
+ setResult((prevResult) => ({
+ data: preservePreviousData ? prevResult.data : undefined,
+ status: FETCH_STATUS.LOADING,
+ error: undefined,
+ }));
+
+ try {
+ const data = await promise;
+ setResult({
+ data,
+ status: FETCH_STATUS.SUCCESS,
+ error: undefined,
+ } as FetcherResult>);
+ } catch (e) {
+ setResult((prevResult) => ({
+ data: preservePreviousData ? prevResult.data : undefined,
+ status: FETCH_STATUS.FAILURE,
+ error: e,
+ }));
+ }
+ }
+
+ doFetch();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [counter, ...fnDeps]);
+
+ return useMemo(() => {
+ return {
+ ...result,
+ refetch: () => {
+ // this will invalidate the deps to `useEffect` and will result in a new request
+ setCounter((count) => count + 1);
+ },
+ };
+ }, [result]);
+}
diff --git a/x-pack/plugins/observability/public/hooks/use_kibana_ui_settings.tsx b/x-pack/plugins/observability/public/hooks/use_kibana_ui_settings.tsx
new file mode 100644
index 0000000000000..884d74db391ee
--- /dev/null
+++ b/x-pack/plugins/observability/public/hooks/use_kibana_ui_settings.tsx
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { usePluginContext } from './use_plugin_context';
+import { UI_SETTINGS } from '../../../../../src/plugins/data/public';
+
+export { UI_SETTINGS };
+
+type SettingKeys = keyof typeof UI_SETTINGS;
+type SettingValues = typeof UI_SETTINGS[SettingKeys];
+
+export function useKibanaUISettings(key: SettingValues): T {
+ const { core } = usePluginContext();
+ return core.uiSettings.get(key);
+}
diff --git a/x-pack/plugins/observability/public/hooks/use_url_params.tsx b/x-pack/plugins/observability/public/hooks/use_url_params.tsx
new file mode 100644
index 0000000000000..680a32fb49677
--- /dev/null
+++ b/x-pack/plugins/observability/public/hooks/use_url_params.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import * as t from 'io-ts';
+import { useLocation, useParams } from 'react-router-dom';
+import { isLeft } from 'fp-ts/lib/Either';
+import { PathReporter } from 'io-ts/lib/PathReporter';
+import { Params } from '../routes';
+
+function getQueryParams(location: ReturnType) {
+ const urlSearchParms = new URLSearchParams(location.search);
+ const queryParams: Record = {};
+ urlSearchParms.forEach((value, key) => {
+ queryParams[key] = value;
+ });
+ return queryParams;
+}
+
+/**
+ * Extracts query and path params from the url and validate it against the type defined in the route file.
+ * It removes any aditional item which is not declared in the type.
+ * @param params
+ */
+export function useUrlParams(params: Params) {
+ const location = useLocation();
+ const pathParams = useParams();
+ const queryParams = getQueryParams(location);
+
+ const rts = {
+ queryRt: params.query ? t.exact(params.query) : t.strict({}),
+ pathRt: params.path ? t.exact(params.path) : t.strict({}),
+ };
+
+ const queryResult = rts.queryRt.decode(queryParams);
+ const pathResult = rts.pathRt.decode(pathParams);
+ if (isLeft(queryResult)) {
+ // eslint-disable-next-line no-console
+ console.error(PathReporter.report(queryResult)[0]);
+ }
+
+ if (isLeft(pathResult)) {
+ // eslint-disable-next-line no-console
+ console.error(PathReporter.report(pathResult)[0]);
+ }
+
+ return {
+ query: isLeft(queryResult) ? {} : queryResult.right,
+ path: isLeft(pathResult) ? {} : pathResult.right,
+ };
+}
diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts
index d2f1d246f79ec..03939736b64ae 100644
--- a/x-pack/plugins/observability/public/index.ts
+++ b/x-pack/plugins/observability/public/index.ts
@@ -15,7 +15,7 @@ export const plugin: PluginInitializer props.theme.eui.euiColorEmptyShade};
-`;
-
-const Title = styled.div`
- background-color: ${(props) => props.theme.eui.euiPageBackgroundColor};
- border-bottom: ${(props) => props.theme.eui.euiBorderThin};
-`;
-
-const Page = styled.div`
- width: 100%;
- max-width: 1200px;
- margin: 0 auto;
- overflow: hidden;
-}
-`;
-
-const EuiCardWithoutPadding = styled(EuiCard)`
- padding: 0;
-`;
-
-export const Home = () => {
- const { core } = usePluginContext();
-
- useEffect(() => {
- core.chrome.setBreadcrumbs([
- {
- text: i18n.translate('xpack.observability.home.breadcrumb.observability', {
- defaultMessage: 'Observability',
- }),
- },
- {
- text: i18n.translate('xpack.observability.home.breadcrumb.gettingStarted', {
- defaultMessage: 'Getting started',
- }),
- },
- ]);
- }, [core]);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- {i18n.translate('xpack.observability.home.title', {
- defaultMessage: 'Observability',
- })}
-
-
-
-
-
-
-
-
-
-
- {/* title and description */}
-
-
-
- {i18n.translate('xpack.observability.home.sectionTitle', {
- defaultMessage: 'Unified visibility across your entire ecosystem',
- })}
-
-
-
-
- {i18n.translate('xpack.observability.home.sectionsubtitle', {
- defaultMessage:
- 'Monitor, analyze, and react to events happening anywhere in your environment by bringing logs, metrics, and traces together at scale in a single stack.',
- })}
-
-
-
- {/* Apps sections */}
-
-
-
-
-
- {appsSection.map((app) => (
-
- }
- title={
-
- {app.title}
-
- }
- description={app.description}
- />
-
- ))}
-
-
-
-
-
-
-
-
- {/* Get started button */}
-
-
-
-
- {i18n.translate('xpack.observability.home.getStatedButton', {
- defaultMessage: 'Get started',
- })}
-
-
-
-
-
-
-
- );
+import React from 'react';
+import { useHistory } from 'react-router-dom';
+import { fetchHasData } from '../../data_handler';
+import { useFetcher } from '../../hooks/use_fetcher';
+
+export const HomePage = () => {
+ const history = useHistory();
+ const { data = {} } = useFetcher(() => fetchHasData(), []);
+
+ const values = Object.values(data);
+ const hasSomeData = values.length ? values.some((hasData) => hasData) : null;
+
+ if (hasSomeData === true) {
+ history.push({ pathname: '/overview' });
+ }
+ if (hasSomeData === false) {
+ history.push({ pathname: '/landing' });
+ }
+
+ return <>>;
};
diff --git a/x-pack/plugins/observability/public/pages/home/section.ts b/x-pack/plugins/observability/public/pages/home/section.ts
index d33571a16ccb7..8c87f17c16b3d 100644
--- a/x-pack/plugins/observability/public/pages/home/section.ts
+++ b/x-pack/plugins/observability/public/pages/home/section.ts
@@ -4,19 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
-
-interface ISection {
- id: string;
- title: string;
- icon: string;
- description: string;
- href?: string;
- target?: '_blank';
-}
+import { ISection } from '../../typings/section';
export const appsSection: ISection[] = [
{
- id: 'logs',
+ id: 'infra_logs',
title: i18n.translate('xpack.observability.section.apps.logs.title', {
defaultMessage: 'Logs',
}),
@@ -25,6 +17,7 @@ export const appsSection: ISection[] = [
defaultMessage:
'Centralize logs from any source. Search, tail, automate anomaly detection, and visualize trends so you can take action quicker.',
}),
+ href: 'https://www.elastic.co',
},
{
id: 'apm',
@@ -36,9 +29,10 @@ export const appsSection: ISection[] = [
defaultMessage:
'Trace transactions through a distributed architecture and map your services’ interactions to easily spot performance bottlenecks.',
}),
+ href: 'https://www.elastic.co',
},
{
- id: 'metrics',
+ id: 'infra_metrics',
title: i18n.translate('xpack.observability.section.apps.metrics.title', {
defaultMessage: 'Metrics',
}),
@@ -47,6 +41,7 @@ export const appsSection: ISection[] = [
defaultMessage:
'Analyze metrics from your infrastructure, apps, and services. Discover trends, forecast behavior, get alerts on anomalies, and more.',
}),
+ href: 'https://www.elastic.co',
},
{
id: 'uptime',
@@ -58,5 +53,6 @@ export const appsSection: ISection[] = [
defaultMessage:
'Proactively monitor the availability of your sites and services. Receive alerts and resolve issues faster to optimize your users’ experience.',
}),
+ href: 'https://www.elastic.co',
},
];
diff --git a/x-pack/plugins/observability/public/pages/landing/index.tsx b/x-pack/plugins/observability/public/pages/landing/index.tsx
new file mode 100644
index 0000000000000..b614095641250
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/landing/index.tsx
@@ -0,0 +1,116 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ EuiButton,
+ EuiCard,
+ EuiFlexGrid,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiImage,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React, { useContext } from 'react';
+import styled, { ThemeContext } from 'styled-components';
+import { WithHeaderLayout } from '../../components/app/layout/with_header';
+import { usePluginContext } from '../../hooks/use_plugin_context';
+import { appsSection } from '../home/section';
+
+const EuiCardWithoutPadding = styled(EuiCard)`
+ padding: 0;
+`;
+
+export const LandingPage = () => {
+ const { core } = usePluginContext();
+ const theme = useContext(ThemeContext);
+
+ return (
+
+
+ {/* title and description */}
+
+
+
+ {i18n.translate('xpack.observability.home.sectionTitle', {
+ defaultMessage: 'Unified visibility across your entire ecosystem',
+ })}
+
+
+
+
+ {i18n.translate('xpack.observability.home.sectionsubtitle', {
+ defaultMessage:
+ 'Monitor, analyze, and react to events happening anywhere in your environment by bringing logs, metrics, and traces together at scale in a single stack.',
+ })}
+
+
+
+ {/* Apps sections */}
+
+
+
+
+
+ {appsSection.map((app) => (
+
+ }
+ title={
+
+ {app.title}
+
+ }
+ description={app.description}
+ />
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ {/* Get started button */}
+
+
+
+
+ {i18n.translate('xpack.observability.home.getStatedButton', {
+ defaultMessage: 'Get started',
+ })}
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/observability/public/pages/overview/empty_section.ts b/x-pack/plugins/observability/public/pages/overview/empty_section.ts
new file mode 100644
index 0000000000000..61456bc88bd3e
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/overview/empty_section.ts
@@ -0,0 +1,90 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { i18n } from '@kbn/i18n';
+import { AppMountContext } from 'kibana/public';
+import { ISection } from '../../typings/section';
+
+export const getEmptySections = ({ core }: { core: AppMountContext['core'] }): ISection[] => {
+ return [
+ {
+ id: 'infra_logs',
+ title: i18n.translate('xpack.observability.emptySection.apps.logs.title', {
+ defaultMessage: 'Logs',
+ }),
+ icon: 'logoLogging',
+ description: i18n.translate('xpack.observability.emptySection.apps.logs.description', {
+ defaultMessage:
+ 'Centralize logs from any source. Search, tail, automate anomaly detection, and visualize trends so you can take action quicker.',
+ }),
+ linkTitle: i18n.translate('xpack.observability.emptySection.apps.logs.link', {
+ defaultMessage: 'Install Filebeat',
+ }),
+ href: 'https://www.elastic.co',
+ },
+ {
+ id: 'apm',
+ title: i18n.translate('xpack.observability.emptySection.apps.apm.title', {
+ defaultMessage: 'APM',
+ }),
+ icon: 'logoAPM',
+ description: i18n.translate('xpack.observability.emptySection.apps.apm.description', {
+ defaultMessage:
+ 'Trace transactions through a distributed architecture and map your services’ interactions to easily spot performance bottlenecks.',
+ }),
+ linkTitle: i18n.translate('xpack.observability.emptySection.apps.apm.link', {
+ defaultMessage: 'Install agent',
+ }),
+ href: 'https://www.elastic.co',
+ },
+ {
+ id: 'infra_metrics',
+ title: i18n.translate('xpack.observability.emptySection.apps.metrics.title', {
+ defaultMessage: 'Metrics',
+ }),
+ icon: 'logoMetrics',
+ description: i18n.translate('xpack.observability.emptySection.apps.metrics.description', {
+ defaultMessage:
+ 'Analyze metrics from your infrastructure, apps, and services. Discover trends, forecast behavior, get alerts on anomalies, and more.',
+ }),
+ linkTitle: i18n.translate('xpack.observability.emptySection.apps.metrics.link', {
+ defaultMessage: 'Install metrics module',
+ }),
+ href: 'https://www.elastic.co',
+ },
+ {
+ id: 'uptime',
+ title: i18n.translate('xpack.observability.emptySection.apps.uptime.title', {
+ defaultMessage: 'Uptime',
+ }),
+ icon: 'logoUptime',
+ description: i18n.translate('xpack.observability.emptySection.apps.uptime.description', {
+ defaultMessage:
+ 'Proactively monitor the availability of your sites and services. Receive alerts and resolve issues faster to optimize your users’ experience.',
+ }),
+ linkTitle: i18n.translate('xpack.observability.emptySection.apps.uptime.link', {
+ defaultMessage: 'Install Heartbeat',
+ }),
+ href: 'https://www.elastic.co',
+ },
+ {
+ id: 'alert',
+ title: i18n.translate('xpack.observability.emptySection.apps.alert.title', {
+ defaultMessage: 'No alerts found.',
+ }),
+ icon: 'watchesApp',
+ description: i18n.translate('xpack.observability.emptySection.apps.alert.description', {
+ defaultMessage:
+ '503 errors stacking up. Applications not responding. CPU and RAM utilization jumping. See these warnings as they happen - not as part of the post-mortem.',
+ }),
+ linkTitle: i18n.translate('xpack.observability.emptySection.apps.alert.link', {
+ defaultMessage: 'Create alert',
+ }),
+ href: core.http.basePath.prepend(
+ '/app/management/insightsAndAlerting/triggersActions/alerts'
+ ),
+ },
+ ];
+};
diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx
new file mode 100644
index 0000000000000..9caac7f9d86f4
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/overview/index.tsx
@@ -0,0 +1,198 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
+import moment from 'moment';
+import React, { useContext } from 'react';
+import { ThemeContext } from 'styled-components';
+import { EmptySection } from '../../components/app/empty_section';
+import { WithHeaderLayout } from '../../components/app/layout/with_header';
+import { Resources } from '../../components/app/resources';
+import { AlertsSection } from '../../components/app/section/alerts';
+import { APMSection } from '../../components/app/section/apm';
+import { LogsSection } from '../../components/app/section/logs';
+import { MetricsSection } from '../../components/app/section/metrics';
+import { UptimeSection } from '../../components/app/section/uptime';
+import { DatePicker, TimePickerTime } from '../../components/shared/data_picker';
+import { fetchHasData } from '../../data_handler';
+import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher';
+import { UI_SETTINGS, useKibanaUISettings } from '../../hooks/use_kibana_ui_settings';
+import { usePluginContext } from '../../hooks/use_plugin_context';
+import { RouteParams } from '../../routes';
+import { getObservabilityAlerts } from '../../services/get_observability_alerts';
+import { getParsedDate } from '../../utils/date';
+import { getBucketSize } from '../../utils/get_bucket_size';
+import { getEmptySections } from './empty_section';
+import { LoadingObservability } from './loading_observability';
+
+interface Props {
+ routeParams: RouteParams<'/overview'>;
+}
+
+function calculatetBucketSize({ startTime, endTime }: { startTime?: string; endTime?: string }) {
+ if (startTime && endTime) {
+ return getBucketSize({
+ start: moment.utc(startTime).valueOf(),
+ end: moment.utc(endTime).valueOf(),
+ minInterval: '60s',
+ });
+ }
+}
+
+export const OverviewPage = ({ routeParams }: Props) => {
+ const { core } = usePluginContext();
+
+ const { data: alerts = [], status: alertStatus } = useFetcher(() => {
+ return getObservabilityAlerts({ core });
+ }, []);
+
+ const theme = useContext(ThemeContext);
+ const timePickerTime = useKibanaUISettings(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS);
+
+ const result = useFetcher(() => fetchHasData(), []);
+ const hasData = result.data;
+
+ if (!hasData) {
+ return ;
+ }
+
+ const {
+ rangeFrom = timePickerTime.from,
+ rangeTo = timePickerTime.to,
+ refreshInterval = 10000,
+ refreshPaused = true,
+ } = routeParams.query;
+
+ const startTime = getParsedDate(rangeFrom);
+ const endTime = getParsedDate(rangeTo, { roundUp: true });
+ const bucketSize = calculatetBucketSize({ startTime, endTime });
+
+ const appEmptySections = getEmptySections({ core }).filter(({ id }) => {
+ if (id === 'alert') {
+ return alertStatus !== FETCH_STATUS.FAILURE && !alerts.length;
+ }
+ return !hasData[id];
+ });
+
+ // Hides the data section when all 'hasData' is false or undefined
+ const showDataSections = Object.values(hasData).some((hasPluginData) => hasPluginData);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {/* Data sections */}
+ {showDataSections && (
+
+ {hasData.infra_logs && (
+
+
+
+ )}
+ {hasData.infra_metrics && (
+
+
+
+ )}
+ {hasData.apm && (
+
+
+
+ )}
+ {hasData.uptime && (
+
+
+
+ )}
+
+ )}
+
+ {/* Empty sections */}
+ {!!appEmptySections.length && (
+
+
+ 2 ? 2 : 1
+ }
+ gutterSize="s"
+ >
+ {appEmptySections.map((app) => {
+ return (
+
+
+
+ );
+ })}
+
+
+ )}
+
+
+ {/* Alert section */}
+ {!!alerts.length && (
+
+
+
+ )}
+
+ {/* Resources section */}
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx b/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx
new file mode 100644
index 0000000000000..90e3104443e6b
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { useContext } from 'react';
+import styled, { ThemeContext } from 'styled-components';
+import { EuiFlexItem } from '@elastic/eui';
+import { EuiPanel } from '@elastic/eui';
+import { EuiFlexGroup } from '@elastic/eui';
+import { EuiLoadingSpinner } from '@elastic/eui';
+import { EuiText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { WithHeaderLayout } from '../../components/app/layout/with_header';
+
+const CentralizedFlexGroup = styled(EuiFlexGroup)`
+ justify-content: center;
+ align-items: center;
+ // place the element in the center of the page
+ min-height: calc(100vh - ${(props) => props.theme.eui.euiHeaderChildSize});
+`;
+
+export const LoadingObservability = () => {
+ const theme = useContext(ThemeContext);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {i18n.translate('xpack.observability.overview.loadingObservability', {
+ defaultMessage: 'Loading Observability',
+ })}
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/observability/public/pages/overview/mock/alerts.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/alerts.mock.ts
new file mode 100644
index 0000000000000..759b8b5fdae4f
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/overview/mock/alerts.mock.ts
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const alertsFetchData = async () => {
+ return Promise.resolve({
+ data: [
+ {
+ id: '1',
+ consumer: 'apm',
+ name: 'Error rate | opbeans-java',
+ alertTypeId: 'apm.error_rate',
+ tags: ['apm', 'service.name:opbeans-java'],
+ updatedAt: '2020-07-03T14:27:51.488Z',
+ muteAll: true,
+ },
+ {
+ id: '2',
+ consumer: 'apm',
+ name: 'Transaction duration | opbeans-java',
+ alertTypeId: 'apm.transaction_duration',
+ tags: ['apm', 'service.name:opbeans-java'],
+ updatedAt: '2020-07-02T14:27:51.488Z',
+ muteAll: true,
+ },
+ {
+ id: '3',
+ consumer: 'logs',
+ name: 'Logs obs test',
+ alertTypeId: 'logs.alert.document.count',
+ tags: ['logs', 'observability'],
+ updatedAt: '2020-06-30T14:27:51.488Z',
+ muteAll: true,
+ },
+ {
+ id: '4',
+ consumer: 'metrics',
+ name: 'Metrics obs test',
+ alertTypeId: 'metrics.alert.inventory.threshold',
+ tags: ['metrics', 'observability'],
+ updatedAt: '2020-03-20T14:27:51.488Z',
+ muteAll: true,
+ },
+ {
+ id: '5',
+ consumer: 'uptime',
+ name: 'Uptime obs test',
+ alertTypeId: 'xpack.uptime.alerts.monitorStatus',
+ tags: ['uptime', 'observability'],
+ updatedAt: '2020-03-25T17:27:51.488Z',
+ muteAll: true,
+ },
+ ],
+ });
+};
diff --git a/x-pack/plugins/observability/public/pages/overview/mock/apm.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/apm.mock.ts
new file mode 100644
index 0000000000000..7303b78cc0132
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/overview/mock/apm.mock.ts
@@ -0,0 +1,627 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { ApmFetchDataResponse, FetchData } from '../../../typings';
+
+export const fetchApmData: FetchData = () => {
+ return Promise.resolve(response);
+};
+
+const response: ApmFetchDataResponse = {
+ title: 'APM',
+ appLink: '/app/apm',
+ stats: {
+ services: {
+ type: 'number',
+ value: 7,
+ },
+ transactions: {
+ type: 'number',
+ value: 125808,
+ },
+ },
+ series: {
+ transactions: {
+ coordinates: [
+ {
+ x: 1593295200000,
+ y: 891,
+ },
+ {
+ x: 1593297000000,
+ y: 902,
+ },
+ {
+ x: 1593298800000,
+ y: 924,
+ },
+ {
+ x: 1593300600000,
+ y: 944,
+ },
+ {
+ x: 1593302400000,
+ y: 935,
+ },
+ {
+ x: 1593304200000,
+ y: 915,
+ },
+ {
+ x: 1593306000000,
+ y: 917,
+ },
+ {
+ x: 1593307800000,
+ y: 941,
+ },
+ {
+ x: 1593309600000,
+ y: 906,
+ },
+ {
+ x: 1593311400000,
+ y: 939,
+ },
+ {
+ x: 1593313200000,
+ y: 961,
+ },
+ {
+ x: 1593315000000,
+ y: 911,
+ },
+ {
+ x: 1593316800000,
+ y: 958,
+ },
+ {
+ x: 1593318600000,
+ y: 861,
+ },
+ {
+ x: 1593320400000,
+ y: 906,
+ },
+ {
+ x: 1593322200000,
+ y: 899,
+ },
+ {
+ x: 1593324000000,
+ y: 785,
+ },
+ {
+ x: 1593325800000,
+ y: 952,
+ },
+ {
+ x: 1593327600000,
+ y: 910,
+ },
+ {
+ x: 1593329400000,
+ y: 869,
+ },
+ {
+ x: 1593331200000,
+ y: 895,
+ },
+ {
+ x: 1593333000000,
+ y: 924,
+ },
+ {
+ x: 1593334800000,
+ y: 930,
+ },
+ {
+ x: 1593336600000,
+ y: 947,
+ },
+ {
+ x: 1593338400000,
+ y: 905,
+ },
+ {
+ x: 1593340200000,
+ y: 963,
+ },
+ {
+ x: 1593342000000,
+ y: 877,
+ },
+ {
+ x: 1593343800000,
+ y: 839,
+ },
+ {
+ x: 1593345600000,
+ y: 884,
+ },
+ {
+ x: 1593347400000,
+ y: 934,
+ },
+ {
+ x: 1593349200000,
+ y: 908,
+ },
+ {
+ x: 1593351000000,
+ y: 982,
+ },
+ {
+ x: 1593352800000,
+ y: 897,
+ },
+ {
+ x: 1593354600000,
+ y: 903,
+ },
+ {
+ x: 1593356400000,
+ y: 877,
+ },
+ {
+ x: 1593358200000,
+ y: 893,
+ },
+ {
+ x: 1593360000000,
+ y: 919,
+ },
+ {
+ x: 1593361800000,
+ y: 844,
+ },
+ {
+ x: 1593363600000,
+ y: 940,
+ },
+ {
+ x: 1593365400000,
+ y: 951,
+ },
+ {
+ x: 1593367200000,
+ y: 869,
+ },
+ {
+ x: 1593369000000,
+ y: 901,
+ },
+ {
+ x: 1593370800000,
+ y: 940,
+ },
+ {
+ x: 1593372600000,
+ y: 942,
+ },
+ {
+ x: 1593374400000,
+ y: 881,
+ },
+ {
+ x: 1593376200000,
+ y: 935,
+ },
+ {
+ x: 1593378000000,
+ y: 892,
+ },
+ {
+ x: 1593379800000,
+ y: 861,
+ },
+ {
+ x: 1593381600000,
+ y: 868,
+ },
+ {
+ x: 1593383400000,
+ y: 990,
+ },
+ {
+ x: 1593385200000,
+ y: 931,
+ },
+ {
+ x: 1593387000000,
+ y: 898,
+ },
+ {
+ x: 1593388800000,
+ y: 906,
+ },
+ {
+ x: 1593390600000,
+ y: 928,
+ },
+ {
+ x: 1593392400000,
+ y: 975,
+ },
+ {
+ x: 1593394200000,
+ y: 842,
+ },
+ {
+ x: 1593396000000,
+ y: 940,
+ },
+ {
+ x: 1593397800000,
+ y: 922,
+ },
+ {
+ x: 1593399600000,
+ y: 962,
+ },
+ {
+ x: 1593401400000,
+ y: 940,
+ },
+ {
+ x: 1593403200000,
+ y: 974,
+ },
+ {
+ x: 1593405000000,
+ y: 887,
+ },
+ {
+ x: 1593406800000,
+ y: 920,
+ },
+ {
+ x: 1593408600000,
+ y: 854,
+ },
+ {
+ x: 1593410400000,
+ y: 898,
+ },
+ {
+ x: 1593412200000,
+ y: 952,
+ },
+ {
+ x: 1593414000000,
+ y: 987,
+ },
+ {
+ x: 1593415800000,
+ y: 932,
+ },
+ {
+ x: 1593417600000,
+ y: 1009,
+ },
+ {
+ x: 1593419400000,
+ y: 989,
+ },
+ {
+ x: 1593421200000,
+ y: 939,
+ },
+ {
+ x: 1593423000000,
+ y: 929,
+ },
+ {
+ x: 1593424800000,
+ y: 929,
+ },
+ {
+ x: 1593426600000,
+ y: 864,
+ },
+ {
+ x: 1593428400000,
+ y: 895,
+ },
+ {
+ x: 1593430200000,
+ y: 876,
+ },
+ {
+ x: 1593432000000,
+ y: 68,
+ },
+ {
+ x: 1593433800000,
+ y: 0,
+ },
+ {
+ x: 1593435600000,
+ y: 0,
+ },
+ {
+ x: 1593437400000,
+ y: 0,
+ },
+ {
+ x: 1593439200000,
+ y: 0,
+ },
+ {
+ x: 1593441000000,
+ y: 0,
+ },
+ {
+ x: 1593442800000,
+ y: 700,
+ },
+ {
+ x: 1593444600000,
+ y: 930,
+ },
+ {
+ x: 1593446400000,
+ y: 953,
+ },
+ {
+ x: 1593448200000,
+ y: 995,
+ },
+ {
+ x: 1593450000000,
+ y: 883,
+ },
+ {
+ x: 1593451800000,
+ y: 902,
+ },
+ {
+ x: 1593453600000,
+ y: 988,
+ },
+ {
+ x: 1593455400000,
+ y: 947,
+ },
+ {
+ x: 1593457200000,
+ y: 889,
+ },
+ {
+ x: 1593459000000,
+ y: 982,
+ },
+ {
+ x: 1593460800000,
+ y: 919,
+ },
+ {
+ x: 1593462600000,
+ y: 854,
+ },
+ {
+ x: 1593464400000,
+ y: 894,
+ },
+ {
+ x: 1593466200000,
+ y: 901,
+ },
+ {
+ x: 1593468000000,
+ y: 970,
+ },
+ {
+ x: 1593469800000,
+ y: 840,
+ },
+ {
+ x: 1593471600000,
+ y: 857,
+ },
+ {
+ x: 1593473400000,
+ y: 943,
+ },
+ {
+ x: 1593475200000,
+ y: 825,
+ },
+ {
+ x: 1593477000000,
+ y: 955,
+ },
+ {
+ x: 1593478800000,
+ y: 959,
+ },
+ {
+ x: 1593480600000,
+ y: 921,
+ },
+ {
+ x: 1593482400000,
+ y: 924,
+ },
+ {
+ x: 1593484200000,
+ y: 840,
+ },
+ {
+ x: 1593486000000,
+ y: 943,
+ },
+ {
+ x: 1593487800000,
+ y: 919,
+ },
+ {
+ x: 1593489600000,
+ y: 882,
+ },
+ {
+ x: 1593491400000,
+ y: 900,
+ },
+ {
+ x: 1593493200000,
+ y: 930,
+ },
+ {
+ x: 1593495000000,
+ y: 854,
+ },
+ {
+ x: 1593496800000,
+ y: 905,
+ },
+ {
+ x: 1593498600000,
+ y: 922,
+ },
+ {
+ x: 1593500400000,
+ y: 863,
+ },
+ {
+ x: 1593502200000,
+ y: 966,
+ },
+ {
+ x: 1593504000000,
+ y: 910,
+ },
+ {
+ x: 1593505800000,
+ y: 851,
+ },
+ {
+ x: 1593507600000,
+ y: 867,
+ },
+ {
+ x: 1593509400000,
+ y: 904,
+ },
+ {
+ x: 1593511200000,
+ y: 913,
+ },
+ {
+ x: 1593513000000,
+ y: 889,
+ },
+ {
+ x: 1593514800000,
+ y: 907,
+ },
+ {
+ x: 1593516600000,
+ y: 965,
+ },
+ {
+ x: 1593518400000,
+ y: 868,
+ },
+ {
+ x: 1593520200000,
+ y: 919,
+ },
+ {
+ x: 1593522000000,
+ y: 945,
+ },
+ {
+ x: 1593523800000,
+ y: 883,
+ },
+ {
+ x: 1593525600000,
+ y: 902,
+ },
+ {
+ x: 1593527400000,
+ y: 900,
+ },
+ {
+ x: 1593529200000,
+ y: 829,
+ },
+ {
+ x: 1593531000000,
+ y: 919,
+ },
+ {
+ x: 1593532800000,
+ y: 942,
+ },
+ {
+ x: 1593534600000,
+ y: 924,
+ },
+ {
+ x: 1593536400000,
+ y: 958,
+ },
+ {
+ x: 1593538200000,
+ y: 867,
+ },
+ {
+ x: 1593540000000,
+ y: 844,
+ },
+ {
+ x: 1593541800000,
+ y: 976,
+ },
+ {
+ x: 1593543600000,
+ y: 937,
+ },
+ {
+ x: 1593545400000,
+ y: 891,
+ },
+ {
+ x: 1593547200000,
+ y: 936,
+ },
+ {
+ x: 1593549000000,
+ y: 895,
+ },
+ {
+ x: 1593550800000,
+ y: 850,
+ },
+ {
+ x: 1593552600000,
+ y: 899,
+ },
+ ],
+ },
+ },
+};
+
+export const emptyResponse: ApmFetchDataResponse = {
+ title: 'APM',
+ appLink: '/app/apm',
+ stats: {
+ services: {
+ type: 'number',
+ value: 0,
+ },
+ transactions: {
+ type: 'number',
+ value: 0,
+ },
+ },
+ series: {
+ transactions: {
+ coordinates: [],
+ },
+ },
+};
diff --git a/x-pack/plugins/observability/public/pages/overview/mock/logs.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/logs.mock.ts
new file mode 100644
index 0000000000000..5bea1fbf19ace
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/overview/mock/logs.mock.ts
@@ -0,0 +1,2326 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { FetchData, LogsFetchDataResponse } from '../../../typings';
+
+export const fetchLogsData: FetchData = () => {
+ return Promise.resolve(response);
+};
+
+const response: LogsFetchDataResponse = {
+ title: 'Logs',
+ appLink:
+ "/app/logs/stream?logPosition=(end:'2020-06-30T21:30:00.000Z',start:'2020-06-27T22:00:00.000Z')",
+ stats: {
+ 'haproxy.log': {
+ type: 'number',
+ label: 'haproxy.log',
+ value: 145.84289044289045,
+ },
+ 'nginx.access': {
+ type: 'number',
+ label: 'nginx.access',
+ value: 94.67039627039627,
+ },
+ 'kibana.log': {
+ type: 'number',
+ label: 'kibana.log',
+ value: 11.018181818181818,
+ },
+ 'nginx.error': {
+ type: 'number',
+ label: 'nginx.error',
+ value: 8.218181818181819,
+ },
+ },
+ series: {
+ 'haproxy.log': {
+ label: 'haproxy.log',
+ coordinates: [
+ {
+ x: 1593295200000,
+ y: 146.83333333333334,
+ },
+ {
+ x: 1593297000000,
+ y: 146.96666666666667,
+ },
+ {
+ x: 1593298800000,
+ y: 146.96666666666667,
+ },
+ {
+ x: 1593300600000,
+ y: 146.86666666666667,
+ },
+ {
+ x: 1593302400000,
+ y: 146.96666666666667,
+ },
+ {
+ x: 1593304200000,
+ y: 147.03333333333333,
+ },
+ {
+ x: 1593306000000,
+ y: 147.16666666666666,
+ },
+ {
+ x: 1593307800000,
+ y: 146.96666666666667,
+ },
+ {
+ x: 1593309600000,
+ y: 146.96666666666667,
+ },
+ {
+ x: 1593311400000,
+ y: 146.96666666666667,
+ },
+ {
+ x: 1593313200000,
+ y: 147.03333333333333,
+ },
+ {
+ x: 1593315000000,
+ y: 147.13333333333333,
+ },
+ {
+ x: 1593316800000,
+ y: 146.96666666666667,
+ },
+ {
+ x: 1593318600000,
+ y: 146.96666666666667,
+ },
+ {
+ x: 1593320400000,
+ y: 146.93333333333334,
+ },
+ {
+ x: 1593322200000,
+ y: 147.06666666666666,
+ },
+ {
+ x: 1593324000000,
+ y: 146.9,
+ },
+ {
+ x: 1593325800000,
+ y: 147.06666666666666,
+ },
+ {
+ x: 1593327600000,
+ y: 147.06666666666666,
+ },
+ {
+ x: 1593329400000,
+ y: 146.93333333333334,
+ },
+ {
+ x: 1593331200000,
+ y: 146.86666666666667,
+ },
+ {
+ x: 1593333000000,
+ y: 146.86666666666667,
+ },
+ {
+ x: 1593334800000,
+ y: 147,
+ },
+ {
+ x: 1593336600000,
+ y: 146.66666666666666,
+ },
+ {
+ x: 1593338400000,
+ y: 146.83333333333334,
+ },
+ {
+ x: 1593340200000,
+ y: 146.9,
+ },
+ {
+ x: 1593342000000,
+ y: 146.96666666666667,
+ },
+ {
+ x: 1593343800000,
+ y: 146.86666666666667,
+ },
+ {
+ x: 1593345600000,
+ y: 146.83333333333334,
+ },
+ {
+ x: 1593347400000,
+ y: 146.86666666666667,
+ },
+ {
+ x: 1593349200000,
+ y: 146.93333333333334,
+ },
+ {
+ x: 1593351000000,
+ y: 146.8,
+ },
+ {
+ x: 1593352800000,
+ y: 146.83333333333334,
+ },
+ {
+ x: 1593354600000,
+ y: 146.83333333333334,
+ },
+ {
+ x: 1593356400000,
+ y: 146.73333333333332,
+ },
+ {
+ x: 1593358200000,
+ y: 146.9,
+ },
+ {
+ x: 1593360000000,
+ y: 146.73333333333332,
+ },
+ {
+ x: 1593361800000,
+ y: 146.63333333333333,
+ },
+ {
+ x: 1593363600000,
+ y: 146.6,
+ },
+ {
+ x: 1593365400000,
+ y: 147.06666666666666,
+ },
+ {
+ x: 1593367200000,
+ y: 147,
+ },
+ {
+ x: 1593369000000,
+ y: 146.93333333333334,
+ },
+ {
+ x: 1593370800000,
+ y: 146.73333333333332,
+ },
+ {
+ x: 1593372600000,
+ y: 147.06666666666666,
+ },
+ {
+ x: 1593374400000,
+ y: 147,
+ },
+ {
+ x: 1593376200000,
+ y: 147.06666666666666,
+ },
+ {
+ x: 1593378000000,
+ y: 147.2,
+ },
+ {
+ x: 1593379800000,
+ y: 147.1,
+ },
+ {
+ x: 1593381600000,
+ y: 147,
+ },
+ {
+ x: 1593383400000,
+ y: 147.06666666666666,
+ },
+ {
+ x: 1593385200000,
+ y: 147.13333333333333,
+ },
+ {
+ x: 1593387000000,
+ y: 147.2,
+ },
+ {
+ x: 1593388800000,
+ y: 146.96666666666667,
+ },
+ {
+ x: 1593390600000,
+ y: 146.83333333333334,
+ },
+ {
+ x: 1593392400000,
+ y: 146.8,
+ },
+ {
+ x: 1593394200000,
+ y: 144.3,
+ },
+ {
+ x: 1593396000000,
+ y: 147.3,
+ },
+ {
+ x: 1593397800000,
+ y: 147.2,
+ },
+ {
+ x: 1593399600000,
+ y: 147.33333333333334,
+ },
+ {
+ x: 1593401400000,
+ y: 147.1,
+ },
+ {
+ x: 1593403200000,
+ y: 147.13333333333333,
+ },
+ {
+ x: 1593405000000,
+ y: 147.16666666666666,
+ },
+ {
+ x: 1593406800000,
+ y: 147.1,
+ },
+ {
+ x: 1593408600000,
+ y: 147.3,
+ },
+ {
+ x: 1593410400000,
+ y: 147.26666666666668,
+ },
+ {
+ x: 1593412200000,
+ y: 147.2,
+ },
+ {
+ x: 1593414000000,
+ y: 147.03333333333333,
+ },
+ {
+ x: 1593415800000,
+ y: 146.9,
+ },
+ {
+ x: 1593417600000,
+ y: 146.96666666666667,
+ },
+ {
+ x: 1593419400000,
+ y: 147.1,
+ },
+ {
+ x: 1593421200000,
+ y: 147.13333333333333,
+ },
+ {
+ x: 1593423000000,
+ y: 147.03333333333333,
+ },
+ {
+ x: 1593424800000,
+ y: 141.36666666666667,
+ },
+ {
+ x: 1593426600000,
+ y: 144.63333333333333,
+ },
+ {
+ x: 1593428400000,
+ y: 153.66666666666666,
+ },
+ {
+ x: 1593430200000,
+ y: 136.76666666666668,
+ },
+ {
+ x: 1593432000000,
+ y: 123.43333333333334,
+ },
+ {
+ x: 1593433800000,
+ y: 123.5,
+ },
+ {
+ x: 1593435600000,
+ y: 123.26666666666667,
+ },
+ {
+ x: 1593437400000,
+ y: 123.23333333333333,
+ },
+ {
+ x: 1593439200000,
+ y: 123.13333333333334,
+ },
+ {
+ x: 1593441000000,
+ y: 123.2,
+ },
+ {
+ x: 1593442800000,
+ y: 144.23333333333332,
+ },
+ {
+ x: 1593444600000,
+ y: 147.06666666666666,
+ },
+ {
+ x: 1593446400000,
+ y: 146.9,
+ },
+ {
+ x: 1593448200000,
+ y: 146.7,
+ },
+ {
+ x: 1593450000000,
+ y: 146.8,
+ },
+ {
+ x: 1593451800000,
+ y: 146.73333333333332,
+ },
+ {
+ x: 1593453600000,
+ y: 146.7,
+ },
+ {
+ x: 1593455400000,
+ y: 146.7,
+ },
+ {
+ x: 1593457200000,
+ y: 146.56666666666666,
+ },
+ {
+ x: 1593459000000,
+ y: 146.8,
+ },
+ {
+ x: 1593460800000,
+ y: 146.8,
+ },
+ {
+ x: 1593462600000,
+ y: 146.83333333333334,
+ },
+ {
+ x: 1593464400000,
+ y: 146.7,
+ },
+ {
+ x: 1593466200000,
+ y: 146.9,
+ },
+ {
+ x: 1593468000000,
+ y: 147.03333333333333,
+ },
+ {
+ x: 1593469800000,
+ y: 146.76666666666668,
+ },
+ {
+ x: 1593471600000,
+ y: 146.7,
+ },
+ {
+ x: 1593473400000,
+ y: 146.63333333333333,
+ },
+ {
+ x: 1593475200000,
+ y: 146.93333333333334,
+ },
+ {
+ x: 1593477000000,
+ y: 146.5,
+ },
+ {
+ x: 1593478800000,
+ y: 146.76666666666668,
+ },
+ {
+ x: 1593480600000,
+ y: 144.83333333333334,
+ },
+ {
+ x: 1593482400000,
+ y: 146.96666666666667,
+ },
+ {
+ x: 1593484200000,
+ y: 147.1,
+ },
+ {
+ x: 1593486000000,
+ y: 147.1,
+ },
+ {
+ x: 1593487800000,
+ y: 147.3,
+ },
+ {
+ x: 1593489600000,
+ y: 147.1,
+ },
+ {
+ x: 1593491400000,
+ y: 147.03333333333333,
+ },
+ {
+ x: 1593493200000,
+ y: 147.2,
+ },
+ {
+ x: 1593495000000,
+ y: 147.06666666666666,
+ },
+ {
+ x: 1593496800000,
+ y: 147.1,
+ },
+ {
+ x: 1593498600000,
+ y: 147.2,
+ },
+ {
+ x: 1593500400000,
+ y: 147.06666666666666,
+ },
+ {
+ x: 1593502200000,
+ y: 147.06666666666666,
+ },
+ {
+ x: 1593504000000,
+ y: 147.06666666666666,
+ },
+ {
+ x: 1593505800000,
+ y: 147.06666666666666,
+ },
+ {
+ x: 1593507600000,
+ y: 146.96666666666667,
+ },
+ {
+ x: 1593509400000,
+ y: 147.16666666666666,
+ },
+ {
+ x: 1593511200000,
+ y: 147.03333333333333,
+ },
+ {
+ x: 1593513000000,
+ y: 147,
+ },
+ {
+ x: 1593514800000,
+ y: 147.03333333333333,
+ },
+ {
+ x: 1593516600000,
+ y: 146.96666666666667,
+ },
+ {
+ x: 1593518400000,
+ y: 146.63333333333333,
+ },
+ {
+ x: 1593520200000,
+ y: 146.43333333333334,
+ },
+ {
+ x: 1593522000000,
+ y: 147.13333333333333,
+ },
+ {
+ x: 1593523800000,
+ y: 147.13333333333333,
+ },
+ {
+ x: 1593525600000,
+ y: 146.93333333333334,
+ },
+ {
+ x: 1593527400000,
+ y: 147,
+ },
+ {
+ x: 1593529200000,
+ y: 147.03333333333333,
+ },
+ {
+ x: 1593531000000,
+ y: 147.2,
+ },
+ {
+ x: 1593532800000,
+ y: 147.13333333333333,
+ },
+ {
+ x: 1593534600000,
+ y: 147.13333333333333,
+ },
+ {
+ x: 1593536400000,
+ y: 147.13333333333333,
+ },
+ {
+ x: 1593538200000,
+ y: 147.1,
+ },
+ {
+ x: 1593540000000,
+ y: 147,
+ },
+ {
+ x: 1593541800000,
+ y: 147.26666666666668,
+ },
+ {
+ x: 1593543600000,
+ y: 146.73333333333332,
+ },
+ {
+ x: 1593545400000,
+ y: 147.03333333333333,
+ },
+ {
+ x: 1593547200000,
+ y: 147,
+ },
+ {
+ x: 1593549000000,
+ y: 146.9,
+ },
+ {
+ x: 1593550800000,
+ y: 147.03333333333333,
+ },
+ ],
+ },
+ 'nginx.access': {
+ label: 'nginx.access',
+ coordinates: [
+ {
+ x: 1593295200000,
+ y: 94.06666666666666,
+ },
+ {
+ x: 1593297000000,
+ y: 91.4,
+ },
+ {
+ x: 1593298800000,
+ y: 95.03333333333333,
+ },
+ {
+ x: 1593300600000,
+ y: 94.5,
+ },
+ {
+ x: 1593302400000,
+ y: 94.06666666666666,
+ },
+ {
+ x: 1593304200000,
+ y: 93.3,
+ },
+ {
+ x: 1593306000000,
+ y: 91.16666666666667,
+ },
+ {
+ x: 1593307800000,
+ y: 94.5,
+ },
+ {
+ x: 1593309600000,
+ y: 93.53333333333333,
+ },
+ {
+ x: 1593311400000,
+ y: 118.9,
+ },
+ {
+ x: 1593313200000,
+ y: 110.66666666666667,
+ },
+ {
+ x: 1593315000000,
+ y: 95.66666666666667,
+ },
+ {
+ x: 1593316800000,
+ y: 99.53333333333333,
+ },
+ {
+ x: 1593318600000,
+ y: 123.36666666666666,
+ },
+ {
+ x: 1593320400000,
+ y: 94.13333333333334,
+ },
+ {
+ x: 1593322200000,
+ y: 95.53333333333333,
+ },
+ {
+ x: 1593324000000,
+ y: 93.93333333333334,
+ },
+ {
+ x: 1593325800000,
+ y: 94.06666666666666,
+ },
+ {
+ x: 1593327600000,
+ y: 118.16666666666667,
+ },
+ {
+ x: 1593329400000,
+ y: 108.6,
+ },
+ {
+ x: 1593331200000,
+ y: 93.53333333333333,
+ },
+ {
+ x: 1593333000000,
+ y: 93.06666666666666,
+ },
+ {
+ x: 1593334800000,
+ y: 93.76666666666667,
+ },
+ {
+ x: 1593336600000,
+ y: 95.3,
+ },
+ {
+ x: 1593338400000,
+ y: 96.4,
+ },
+ {
+ x: 1593340200000,
+ y: 121.93333333333334,
+ },
+ {
+ x: 1593342000000,
+ y: 134.43333333333334,
+ },
+ {
+ x: 1593343800000,
+ y: 160.4,
+ },
+ {
+ x: 1593345600000,
+ y: 129.7,
+ },
+ {
+ x: 1593347400000,
+ y: 119.16666666666667,
+ },
+ {
+ x: 1593349200000,
+ y: 133.06666666666666,
+ },
+ {
+ x: 1593351000000,
+ y: 212.4,
+ },
+ {
+ x: 1593352800000,
+ y: 95.36666666666666,
+ },
+ {
+ x: 1593354600000,
+ y: 93.6,
+ },
+ {
+ x: 1593356400000,
+ y: 93.4,
+ },
+ {
+ x: 1593358200000,
+ y: 95.1,
+ },
+ {
+ x: 1593360000000,
+ y: 94.36666666666666,
+ },
+ {
+ x: 1593361800000,
+ y: 97.23333333333333,
+ },
+ {
+ x: 1593363600000,
+ y: 94.03333333333333,
+ },
+ {
+ x: 1593365400000,
+ y: 94.53333333333333,
+ },
+ {
+ x: 1593367200000,
+ y: 93.56666666666666,
+ },
+ {
+ x: 1593369000000,
+ y: 98.43333333333334,
+ },
+ {
+ x: 1593370800000,
+ y: 92.3,
+ },
+ {
+ x: 1593372600000,
+ y: 93.13333333333334,
+ },
+ {
+ x: 1593374400000,
+ y: 93.16666666666667,
+ },
+ {
+ x: 1593376200000,
+ y: 93.7,
+ },
+ {
+ x: 1593378000000,
+ y: 94.46666666666667,
+ },
+ {
+ x: 1593379800000,
+ y: 97.16666666666667,
+ },
+ {
+ x: 1593381600000,
+ y: 94.36666666666666,
+ },
+ {
+ x: 1593383400000,
+ y: 93.7,
+ },
+ {
+ x: 1593385200000,
+ y: 93.4,
+ },
+ {
+ x: 1593387000000,
+ y: 91.3,
+ },
+ {
+ x: 1593388800000,
+ y: 92.66666666666667,
+ },
+ {
+ x: 1593390600000,
+ y: 93.73333333333333,
+ },
+ {
+ x: 1593392400000,
+ y: 94.33333333333333,
+ },
+ {
+ x: 1593394200000,
+ y: 93.23333333333333,
+ },
+ {
+ x: 1593396000000,
+ y: 93.9,
+ },
+ {
+ x: 1593397800000,
+ y: 92.83333333333333,
+ },
+ {
+ x: 1593399600000,
+ y: 93,
+ },
+ {
+ x: 1593401400000,
+ y: 91.2,
+ },
+ {
+ x: 1593403200000,
+ y: 91.96666666666667,
+ },
+ {
+ x: 1593405000000,
+ y: 93.83333333333333,
+ },
+ {
+ x: 1593406800000,
+ y: 93.16666666666667,
+ },
+ {
+ x: 1593408600000,
+ y: 95.36666666666666,
+ },
+ {
+ x: 1593410400000,
+ y: 92.5,
+ },
+ {
+ x: 1593412200000,
+ y: 93.16666666666667,
+ },
+ {
+ x: 1593414000000,
+ y: 92.8,
+ },
+ {
+ x: 1593415800000,
+ y: 95.83333333333333,
+ },
+ {
+ x: 1593417600000,
+ y: 96.96666666666667,
+ },
+ {
+ x: 1593419400000,
+ y: 94.63333333333334,
+ },
+ {
+ x: 1593421200000,
+ y: 98.7,
+ },
+ {
+ x: 1593423000000,
+ y: 100.03333333333333,
+ },
+ {
+ x: 1593424800000,
+ y: 108.66666666666667,
+ },
+ {
+ x: 1593426600000,
+ y: 110.9,
+ },
+ {
+ x: 1593428400000,
+ y: 88.56666666666666,
+ },
+ {
+ x: 1593430200000,
+ y: 1,
+ },
+ {
+ x: 1593442800000,
+ y: 74.53333333333333,
+ },
+ {
+ x: 1593444600000,
+ y: 99.03333333333333,
+ },
+ {
+ x: 1593446400000,
+ y: 98.03333333333333,
+ },
+ {
+ x: 1593448200000,
+ y: 91.26666666666667,
+ },
+ {
+ x: 1593450000000,
+ y: 107.76666666666667,
+ },
+ {
+ x: 1593451800000,
+ y: 98.26666666666667,
+ },
+ {
+ x: 1593453600000,
+ y: 99.46666666666667,
+ },
+ {
+ x: 1593455400000,
+ y: 102.33333333333333,
+ },
+ {
+ x: 1593457200000,
+ y: 108.13333333333334,
+ },
+ {
+ x: 1593459000000,
+ y: 95.36666666666666,
+ },
+ {
+ x: 1593460800000,
+ y: 98.23333333333333,
+ },
+ {
+ x: 1593462600000,
+ y: 91.46666666666667,
+ },
+ {
+ x: 1593464400000,
+ y: 115.63333333333334,
+ },
+ {
+ x: 1593466200000,
+ y: 116.23333333333333,
+ },
+ {
+ x: 1593468000000,
+ y: 91.66666666666667,
+ },
+ {
+ x: 1593469800000,
+ y: 94.33333333333333,
+ },
+ {
+ x: 1593471600000,
+ y: 96.43333333333334,
+ },
+ {
+ x: 1593473400000,
+ y: 94.7,
+ },
+ {
+ x: 1593475200000,
+ y: 93.76666666666667,
+ },
+ {
+ x: 1593477000000,
+ y: 91.5,
+ },
+ {
+ x: 1593478800000,
+ y: 91.9,
+ },
+ {
+ x: 1593480600000,
+ y: 91.3,
+ },
+ {
+ x: 1593482400000,
+ y: 98.3,
+ },
+ {
+ x: 1593484200000,
+ y: 95.53333333333333,
+ },
+ {
+ x: 1593486000000,
+ y: 95.66666666666667,
+ },
+ {
+ x: 1593487800000,
+ y: 92.73333333333333,
+ },
+ {
+ x: 1593489600000,
+ y: 93.6,
+ },
+ {
+ x: 1593491400000,
+ y: 94.3,
+ },
+ {
+ x: 1593493200000,
+ y: 93.13333333333334,
+ },
+ {
+ x: 1593495000000,
+ y: 104.36666666666666,
+ },
+ {
+ x: 1593496800000,
+ y: 107.26666666666667,
+ },
+ {
+ x: 1593498600000,
+ y: 101.83333333333333,
+ },
+ {
+ x: 1593500400000,
+ y: 105.46666666666667,
+ },
+ {
+ x: 1593502200000,
+ y: 111.86666666666666,
+ },
+ {
+ x: 1593504000000,
+ y: 111.56666666666666,
+ },
+ {
+ x: 1593505800000,
+ y: 103.76666666666667,
+ },
+ {
+ x: 1593507600000,
+ y: 93.9,
+ },
+ {
+ x: 1593509400000,
+ y: 97.16666666666667,
+ },
+ {
+ x: 1593511200000,
+ y: 93.03333333333333,
+ },
+ {
+ x: 1593513000000,
+ y: 94.4,
+ },
+ {
+ x: 1593514800000,
+ y: 94.76666666666667,
+ },
+ {
+ x: 1593516600000,
+ y: 94.96666666666667,
+ },
+ {
+ x: 1593518400000,
+ y: 101.3,
+ },
+ {
+ x: 1593520200000,
+ y: 98.63333333333334,
+ },
+ {
+ x: 1593522000000,
+ y: 94.8,
+ },
+ {
+ x: 1593523800000,
+ y: 97.46666666666667,
+ },
+ {
+ x: 1593525600000,
+ y: 95.86666666666666,
+ },
+ {
+ x: 1593527400000,
+ y: 97.3,
+ },
+ {
+ x: 1593529200000,
+ y: 96.1,
+ },
+ {
+ x: 1593531000000,
+ y: 97.1,
+ },
+ {
+ x: 1593532800000,
+ y: 97.56666666666666,
+ },
+ {
+ x: 1593534600000,
+ y: 107.6,
+ },
+ {
+ x: 1593536400000,
+ y: 97.46666666666667,
+ },
+ {
+ x: 1593538200000,
+ y: 96.46666666666667,
+ },
+ {
+ x: 1593540000000,
+ y: 93.83333333333333,
+ },
+ {
+ x: 1593541800000,
+ y: 98.73333333333333,
+ },
+ {
+ x: 1593543600000,
+ y: 99.86666666666666,
+ },
+ {
+ x: 1593545400000,
+ y: 98.66666666666667,
+ },
+ {
+ x: 1593547200000,
+ y: 102.8,
+ },
+ {
+ x: 1593549000000,
+ y: 96.13333333333334,
+ },
+ {
+ x: 1593550800000,
+ y: 94.53333333333333,
+ },
+ ],
+ },
+ 'kibana.log': {
+ label: 'kibana.log',
+ coordinates: [
+ {
+ x: 1593295200000,
+ y: 11.8,
+ },
+ {
+ x: 1593297000000,
+ y: 11.833333333333334,
+ },
+ {
+ x: 1593298800000,
+ y: 12.1,
+ },
+ {
+ x: 1593300600000,
+ y: 12.133333333333333,
+ },
+ {
+ x: 1593302400000,
+ y: 11.2,
+ },
+ {
+ x: 1593304200000,
+ y: 11.933333333333334,
+ },
+ {
+ x: 1593306000000,
+ y: 11.466666666666667,
+ },
+ {
+ x: 1593307800000,
+ y: 12.066666666666666,
+ },
+ {
+ x: 1593309600000,
+ y: 11.9,
+ },
+ {
+ x: 1593311400000,
+ y: 11.766666666666667,
+ },
+ {
+ x: 1593313200000,
+ y: 12.066666666666666,
+ },
+ {
+ x: 1593315000000,
+ y: 11.7,
+ },
+ {
+ x: 1593316800000,
+ y: 11.6,
+ },
+ {
+ x: 1593318600000,
+ y: 11.766666666666667,
+ },
+ {
+ x: 1593320400000,
+ y: 11.633333333333333,
+ },
+ {
+ x: 1593322200000,
+ y: 11.833333333333334,
+ },
+ {
+ x: 1593324000000,
+ y: 11.8,
+ },
+ {
+ x: 1593325800000,
+ y: 11.7,
+ },
+ {
+ x: 1593327600000,
+ y: 11.666666666666666,
+ },
+ {
+ x: 1593329400000,
+ y: 11.8,
+ },
+ {
+ x: 1593331200000,
+ y: 11.966666666666667,
+ },
+ {
+ x: 1593333000000,
+ y: 11.766666666666667,
+ },
+ {
+ x: 1593334800000,
+ y: 11.766666666666667,
+ },
+ {
+ x: 1593336600000,
+ y: 11.866666666666667,
+ },
+ {
+ x: 1593338400000,
+ y: 11.433333333333334,
+ },
+ {
+ x: 1593340200000,
+ y: 12.033333333333333,
+ },
+ {
+ x: 1593342000000,
+ y: 12.1,
+ },
+ {
+ x: 1593343800000,
+ y: 12.1,
+ },
+ {
+ x: 1593345600000,
+ y: 11.8,
+ },
+ {
+ x: 1593347400000,
+ y: 12.366666666666667,
+ },
+ {
+ x: 1593349200000,
+ y: 12.033333333333333,
+ },
+ {
+ x: 1593351000000,
+ y: 12,
+ },
+ {
+ x: 1593352800000,
+ y: 11.8,
+ },
+ {
+ x: 1593354600000,
+ y: 11.5,
+ },
+ {
+ x: 1593356400000,
+ y: 12.1,
+ },
+ {
+ x: 1593358200000,
+ y: 11.966666666666667,
+ },
+ {
+ x: 1593360000000,
+ y: 11.9,
+ },
+ {
+ x: 1593361800000,
+ y: 12.233333333333333,
+ },
+ {
+ x: 1593363600000,
+ y: 11.533333333333333,
+ },
+ {
+ x: 1593365400000,
+ y: 11.633333333333333,
+ },
+ {
+ x: 1593367200000,
+ y: 11.866666666666667,
+ },
+ {
+ x: 1593369000000,
+ y: 12,
+ },
+ {
+ x: 1593370800000,
+ y: 11.7,
+ },
+ {
+ x: 1593372600000,
+ y: 11.8,
+ },
+ {
+ x: 1593374400000,
+ y: 11.4,
+ },
+ {
+ x: 1593376200000,
+ y: 11.766666666666667,
+ },
+ {
+ x: 1593378000000,
+ y: 12.033333333333333,
+ },
+ {
+ x: 1593379800000,
+ y: 11.833333333333334,
+ },
+ {
+ x: 1593381600000,
+ y: 11.9,
+ },
+ {
+ x: 1593383400000,
+ y: 11.966666666666667,
+ },
+ {
+ x: 1593385200000,
+ y: 11.8,
+ },
+ {
+ x: 1593387000000,
+ y: 12,
+ },
+ {
+ x: 1593388800000,
+ y: 11.933333333333334,
+ },
+ {
+ x: 1593390600000,
+ y: 12.033333333333333,
+ },
+ {
+ x: 1593392400000,
+ y: 12,
+ },
+ {
+ x: 1593394200000,
+ y: 11.533333333333333,
+ },
+ {
+ x: 1593396000000,
+ y: 11.4,
+ },
+ {
+ x: 1593397800000,
+ y: 11.666666666666666,
+ },
+ {
+ x: 1593399600000,
+ y: 11.633333333333333,
+ },
+ {
+ x: 1593401400000,
+ y: 11.166666666666666,
+ },
+ {
+ x: 1593403200000,
+ y: 11.3,
+ },
+ {
+ x: 1593405000000,
+ y: 11.2,
+ },
+ {
+ x: 1593406800000,
+ y: 10.966666666666667,
+ },
+ {
+ x: 1593408600000,
+ y: 11.5,
+ },
+ {
+ x: 1593410400000,
+ y: 11.1,
+ },
+ {
+ x: 1593412200000,
+ y: 11.2,
+ },
+ {
+ x: 1593414000000,
+ y: 11.4,
+ },
+ {
+ x: 1593415800000,
+ y: 10.8,
+ },
+ {
+ x: 1593417600000,
+ y: 11.066666666666666,
+ },
+ {
+ x: 1593419400000,
+ y: 11.8,
+ },
+ {
+ x: 1593421200000,
+ y: 11.266666666666667,
+ },
+ {
+ x: 1593423000000,
+ y: 11.333333333333334,
+ },
+ {
+ x: 1593424800000,
+ y: 11.233333333333333,
+ },
+ {
+ x: 1593426600000,
+ y: 11.5,
+ },
+ {
+ x: 1593428400000,
+ y: 8.2,
+ },
+ {
+ x: 1593442800000,
+ y: 8.2,
+ },
+ {
+ x: 1593444600000,
+ y: 11.4,
+ },
+ {
+ x: 1593446400000,
+ y: 10.733333333333333,
+ },
+ {
+ x: 1593448200000,
+ y: 10.833333333333334,
+ },
+ {
+ x: 1593450000000,
+ y: 11.3,
+ },
+ {
+ x: 1593451800000,
+ y: 11.633333333333333,
+ },
+ {
+ x: 1593453600000,
+ y: 11.266666666666667,
+ },
+ {
+ x: 1593455400000,
+ y: 11.3,
+ },
+ {
+ x: 1593457200000,
+ y: 11.333333333333334,
+ },
+ {
+ x: 1593459000000,
+ y: 11.133333333333333,
+ },
+ {
+ x: 1593460800000,
+ y: 10.933333333333334,
+ },
+ {
+ x: 1593462600000,
+ y: 11.2,
+ },
+ {
+ x: 1593464400000,
+ y: 11.166666666666666,
+ },
+ {
+ x: 1593466200000,
+ y: 11.766666666666667,
+ },
+ {
+ x: 1593468000000,
+ y: 11.433333333333334,
+ },
+ {
+ x: 1593469800000,
+ y: 10.8,
+ },
+ {
+ x: 1593471600000,
+ y: 11.266666666666667,
+ },
+ {
+ x: 1593473400000,
+ y: 11.333333333333334,
+ },
+ {
+ x: 1593475200000,
+ y: 11.133333333333333,
+ },
+ {
+ x: 1593477000000,
+ y: 11.133333333333333,
+ },
+ {
+ x: 1593478800000,
+ y: 10.9,
+ },
+ {
+ x: 1593480600000,
+ y: 11.3,
+ },
+ {
+ x: 1593482400000,
+ y: 12.166666666666666,
+ },
+ {
+ x: 1593484200000,
+ y: 11.433333333333334,
+ },
+ {
+ x: 1593486000000,
+ y: 12.133333333333333,
+ },
+ {
+ x: 1593487800000,
+ y: 11.666666666666666,
+ },
+ {
+ x: 1593489600000,
+ y: 11.533333333333333,
+ },
+ {
+ x: 1593491400000,
+ y: 11.833333333333334,
+ },
+ {
+ x: 1593493200000,
+ y: 11.766666666666667,
+ },
+ {
+ x: 1593495000000,
+ y: 11.9,
+ },
+ {
+ x: 1593496800000,
+ y: 11.433333333333334,
+ },
+ {
+ x: 1593498600000,
+ y: 12,
+ },
+ {
+ x: 1593500400000,
+ y: 12.1,
+ },
+ {
+ x: 1593502200000,
+ y: 11.6,
+ },
+ {
+ x: 1593504000000,
+ y: 12,
+ },
+ {
+ x: 1593505800000,
+ y: 12.233333333333333,
+ },
+ {
+ x: 1593507600000,
+ y: 11.633333333333333,
+ },
+ {
+ x: 1593509400000,
+ y: 11.2,
+ },
+ {
+ x: 1593511200000,
+ y: 11.766666666666667,
+ },
+ {
+ x: 1593513000000,
+ y: 11.9,
+ },
+ {
+ x: 1593514800000,
+ y: 11.366666666666667,
+ },
+ {
+ x: 1593516600000,
+ y: 11.833333333333334,
+ },
+ {
+ x: 1593518400000,
+ y: 11.5,
+ },
+ {
+ x: 1593520200000,
+ y: 12,
+ },
+ {
+ x: 1593522000000,
+ y: 12.033333333333333,
+ },
+ {
+ x: 1593523800000,
+ y: 11.733333333333333,
+ },
+ {
+ x: 1593525600000,
+ y: 11.566666666666666,
+ },
+ {
+ x: 1593527400000,
+ y: 11.6,
+ },
+ {
+ x: 1593529200000,
+ y: 11.333333333333334,
+ },
+ {
+ x: 1593531000000,
+ y: 11.833333333333334,
+ },
+ {
+ x: 1593532800000,
+ y: 11.233333333333333,
+ },
+ {
+ x: 1593534600000,
+ y: 11.833333333333334,
+ },
+ {
+ x: 1593536400000,
+ y: 11.266666666666667,
+ },
+ {
+ x: 1593538200000,
+ y: 12,
+ },
+ {
+ x: 1593540000000,
+ y: 11.633333333333333,
+ },
+ {
+ x: 1593541800000,
+ y: 11.9,
+ },
+ {
+ x: 1593543600000,
+ y: 11.966666666666667,
+ },
+ {
+ x: 1593545400000,
+ y: 11.5,
+ },
+ {
+ x: 1593547200000,
+ y: 11.466666666666667,
+ },
+ {
+ x: 1593549000000,
+ y: 11.4,
+ },
+ {
+ x: 1593550800000,
+ y: 11.833333333333334,
+ },
+ ],
+ },
+ 'nginx.error': {
+ label: 'nginx.error',
+ coordinates: [
+ {
+ x: 1593295200000,
+ y: 9.266666666666667,
+ },
+ {
+ x: 1593297000000,
+ y: 8.833333333333334,
+ },
+ {
+ x: 1593298800000,
+ y: 9.033333333333333,
+ },
+ {
+ x: 1593300600000,
+ y: 8.933333333333334,
+ },
+ {
+ x: 1593302400000,
+ y: 8.9,
+ },
+ {
+ x: 1593304200000,
+ y: 9.6,
+ },
+ {
+ x: 1593306000000,
+ y: 9.066666666666666,
+ },
+ {
+ x: 1593307800000,
+ y: 8.966666666666667,
+ },
+ {
+ x: 1593309600000,
+ y: 8.933333333333334,
+ },
+ {
+ x: 1593311400000,
+ y: 8.5,
+ },
+ {
+ x: 1593313200000,
+ y: 8.133333333333333,
+ },
+ {
+ x: 1593315000000,
+ y: 8.233333333333333,
+ },
+ {
+ x: 1593316800000,
+ y: 8.433333333333334,
+ },
+ {
+ x: 1593318600000,
+ y: 8.4,
+ },
+ {
+ x: 1593320400000,
+ y: 9.266666666666667,
+ },
+ {
+ x: 1593322200000,
+ y: 8.566666666666666,
+ },
+ {
+ x: 1593324000000,
+ y: 8.966666666666667,
+ },
+ {
+ x: 1593325800000,
+ y: 8.833333333333334,
+ },
+ {
+ x: 1593327600000,
+ y: 7.5,
+ },
+ {
+ x: 1593329400000,
+ y: 8.033333333333333,
+ },
+ {
+ x: 1593331200000,
+ y: 8.633333333333333,
+ },
+ {
+ x: 1593333000000,
+ y: 8.5,
+ },
+ {
+ x: 1593334800000,
+ y: 8.866666666666667,
+ },
+ {
+ x: 1593336600000,
+ y: 8.3,
+ },
+ {
+ x: 1593338400000,
+ y: 8.966666666666667,
+ },
+ {
+ x: 1593340200000,
+ y: 8.2,
+ },
+ {
+ x: 1593342000000,
+ y: 7.566666666666666,
+ },
+ {
+ x: 1593343800000,
+ y: 7.5,
+ },
+ {
+ x: 1593345600000,
+ y: 7.933333333333334,
+ },
+ {
+ x: 1593347400000,
+ y: 7.866666666666666,
+ },
+ {
+ x: 1593349200000,
+ y: 7.566666666666666,
+ },
+ {
+ x: 1593351000000,
+ y: 7.533333333333333,
+ },
+ {
+ x: 1593352800000,
+ y: 8.866666666666667,
+ },
+ {
+ x: 1593354600000,
+ y: 8.566666666666666,
+ },
+ {
+ x: 1593356400000,
+ y: 8.233333333333333,
+ },
+ {
+ x: 1593358200000,
+ y: 8.9,
+ },
+ {
+ x: 1593360000000,
+ y: 8.533333333333333,
+ },
+ {
+ x: 1593361800000,
+ y: 8.733333333333333,
+ },
+ {
+ x: 1593363600000,
+ y: 9.333333333333334,
+ },
+ {
+ x: 1593365400000,
+ y: 9.133333333333333,
+ },
+ {
+ x: 1593367200000,
+ y: 9.166666666666666,
+ },
+ {
+ x: 1593369000000,
+ y: 9.266666666666667,
+ },
+ {
+ x: 1593370800000,
+ y: 8.966666666666667,
+ },
+ {
+ x: 1593372600000,
+ y: 9.2,
+ },
+ {
+ x: 1593374400000,
+ y: 9.433333333333334,
+ },
+ {
+ x: 1593376200000,
+ y: 9.166666666666666,
+ },
+ {
+ x: 1593378000000,
+ y: 9.266666666666667,
+ },
+ {
+ x: 1593379800000,
+ y: 9.5,
+ },
+ {
+ x: 1593381600000,
+ y: 9.333333333333334,
+ },
+ {
+ x: 1593383400000,
+ y: 8.8,
+ },
+ {
+ x: 1593385200000,
+ y: 8.733333333333333,
+ },
+ {
+ x: 1593387000000,
+ y: 8.633333333333333,
+ },
+ {
+ x: 1593388800000,
+ y: 8.9,
+ },
+ {
+ x: 1593390600000,
+ y: 8.533333333333333,
+ },
+ {
+ x: 1593392400000,
+ y: 9.3,
+ },
+ {
+ x: 1593394200000,
+ y: 9.266666666666667,
+ },
+ {
+ x: 1593396000000,
+ y: 8.966666666666667,
+ },
+ {
+ x: 1593397800000,
+ y: 8.666666666666666,
+ },
+ {
+ x: 1593399600000,
+ y: 9.166666666666666,
+ },
+ {
+ x: 1593401400000,
+ y: 8.733333333333333,
+ },
+ {
+ x: 1593403200000,
+ y: 8.866666666666667,
+ },
+ {
+ x: 1593405000000,
+ y: 8.633333333333333,
+ },
+ {
+ x: 1593406800000,
+ y: 8.8,
+ },
+ {
+ x: 1593408600000,
+ y: 8.466666666666667,
+ },
+ {
+ x: 1593410400000,
+ y: 8.966666666666667,
+ },
+ {
+ x: 1593412200000,
+ y: 8.166666666666666,
+ },
+ {
+ x: 1593414000000,
+ y: 8.7,
+ },
+ {
+ x: 1593415800000,
+ y: 8.333333333333334,
+ },
+ {
+ x: 1593417600000,
+ y: 8.666666666666666,
+ },
+ {
+ x: 1593419400000,
+ y: 8.533333333333333,
+ },
+ {
+ x: 1593421200000,
+ y: 8.233333333333333,
+ },
+ {
+ x: 1593423000000,
+ y: 8.3,
+ },
+ {
+ x: 1593424800000,
+ y: 7.7,
+ },
+ {
+ x: 1593426600000,
+ y: 7.7,
+ },
+ {
+ x: 1593428400000,
+ y: 6.133333333333334,
+ },
+ {
+ x: 1593430200000,
+ y: 0.4666666666666667,
+ },
+ {
+ x: 1593442800000,
+ y: 7.233333333333333,
+ },
+ {
+ x: 1593444600000,
+ y: 8.333333333333334,
+ },
+ {
+ x: 1593446400000,
+ y: 8.666666666666666,
+ },
+ {
+ x: 1593448200000,
+ y: 8.466666666666667,
+ },
+ {
+ x: 1593450000000,
+ y: 8.666666666666666,
+ },
+ {
+ x: 1593451800000,
+ y: 8.5,
+ },
+ {
+ x: 1593453600000,
+ y: 8.6,
+ },
+ {
+ x: 1593455400000,
+ y: 8.5,
+ },
+ {
+ x: 1593457200000,
+ y: 8.6,
+ },
+ {
+ x: 1593459000000,
+ y: 8.866666666666667,
+ },
+ {
+ x: 1593460800000,
+ y: 9.166666666666666,
+ },
+ {
+ x: 1593462600000,
+ y: 8.4,
+ },
+ {
+ x: 1593464400000,
+ y: 8.533333333333333,
+ },
+ {
+ x: 1593466200000,
+ y: 8.066666666666666,
+ },
+ {
+ x: 1593468000000,
+ y: 8.666666666666666,
+ },
+ {
+ x: 1593469800000,
+ y: 8.966666666666667,
+ },
+ {
+ x: 1593471600000,
+ y: 8.4,
+ },
+ {
+ x: 1593473400000,
+ y: 8.833333333333334,
+ },
+ {
+ x: 1593475200000,
+ y: 8.533333333333333,
+ },
+ {
+ x: 1593477000000,
+ y: 8.066666666666666,
+ },
+ {
+ x: 1593478800000,
+ y: 8.533333333333333,
+ },
+ {
+ x: 1593480600000,
+ y: 8.633333333333333,
+ },
+ {
+ x: 1593482400000,
+ y: 8.933333333333334,
+ },
+ {
+ x: 1593484200000,
+ y: 8.833333333333334,
+ },
+ {
+ x: 1593486000000,
+ y: 8.4,
+ },
+ {
+ x: 1593487800000,
+ y: 8.633333333333333,
+ },
+ {
+ x: 1593489600000,
+ y: 9.333333333333334,
+ },
+ {
+ x: 1593491400000,
+ y: 9.366666666666667,
+ },
+ {
+ x: 1593493200000,
+ y: 8.333333333333334,
+ },
+ {
+ x: 1593495000000,
+ y: 9.266666666666667,
+ },
+ {
+ x: 1593496800000,
+ y: 8.2,
+ },
+ {
+ x: 1593498600000,
+ y: 8.4,
+ },
+ {
+ x: 1593500400000,
+ y: 8.433333333333334,
+ },
+ {
+ x: 1593502200000,
+ y: 7.633333333333334,
+ },
+ {
+ x: 1593504000000,
+ y: 7.766666666666667,
+ },
+ {
+ x: 1593505800000,
+ y: 8.4,
+ },
+ {
+ x: 1593507600000,
+ y: 8.3,
+ },
+ {
+ x: 1593509400000,
+ y: 8.833333333333334,
+ },
+ {
+ x: 1593511200000,
+ y: 8.433333333333334,
+ },
+ {
+ x: 1593513000000,
+ y: 8.766666666666667,
+ },
+ {
+ x: 1593514800000,
+ y: 9.066666666666666,
+ },
+ {
+ x: 1593516600000,
+ y: 8.4,
+ },
+ {
+ x: 1593518400000,
+ y: 8.4,
+ },
+ {
+ x: 1593520200000,
+ y: 8.8,
+ },
+ {
+ x: 1593522000000,
+ y: 8.466666666666667,
+ },
+ {
+ x: 1593523800000,
+ y: 8.633333333333333,
+ },
+ {
+ x: 1593525600000,
+ y: 9.133333333333333,
+ },
+ {
+ x: 1593527400000,
+ y: 8.7,
+ },
+ {
+ x: 1593529200000,
+ y: 8.566666666666666,
+ },
+ {
+ x: 1593531000000,
+ y: 9.033333333333333,
+ },
+ {
+ x: 1593532800000,
+ y: 8.9,
+ },
+ {
+ x: 1593534600000,
+ y: 8.7,
+ },
+ {
+ x: 1593536400000,
+ y: 8.7,
+ },
+ {
+ x: 1593538200000,
+ y: 8.8,
+ },
+ {
+ x: 1593540000000,
+ y: 9.166666666666666,
+ },
+ {
+ x: 1593541800000,
+ y: 9.033333333333333,
+ },
+ {
+ x: 1593543600000,
+ y: 8.733333333333333,
+ },
+ {
+ x: 1593545400000,
+ y: 9.2,
+ },
+ {
+ x: 1593547200000,
+ y: 8.933333333333334,
+ },
+ {
+ x: 1593549000000,
+ y: 9.2,
+ },
+ {
+ x: 1593550800000,
+ y: 9.333333333333334,
+ },
+ ],
+ },
+ sample_web_logs: {
+ label: 'sample_web_logs',
+ coordinates: [
+ {
+ x: 1593430200000,
+ y: 0.5666666666666667,
+ },
+ {
+ x: 1593432000000,
+ y: 0.36666666666666664,
+ },
+ {
+ x: 1593433800000,
+ y: 0.5666666666666667,
+ },
+ {
+ x: 1593435600000,
+ y: 0.4666666666666667,
+ },
+ {
+ x: 1593437400000,
+ y: 0.36666666666666664,
+ },
+ {
+ x: 1593439200000,
+ y: 0.3,
+ },
+ {
+ x: 1593441000000,
+ y: 0.13333333333333333,
+ },
+ ],
+ },
+ 'postgresql.log': {
+ label: 'postgresql.log',
+ coordinates: [
+ {
+ x: 1593439200000,
+ y: 0.1,
+ },
+ {
+ x: 1593441000000,
+ y: 0.1,
+ },
+ ],
+ },
+ },
+};
+
+export const emptyResponse: LogsFetchDataResponse = {
+ title: 'Logs',
+ appLink: '/app/logs',
+ stats: {},
+ series: {},
+};
diff --git a/x-pack/plugins/observability/public/pages/overview/mock/metrics.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/metrics.mock.ts
new file mode 100644
index 0000000000000..37233b4f6342c
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/overview/mock/metrics.mock.ts
@@ -0,0 +1,133 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { MetricsFetchDataResponse, FetchData } from '../../../typings';
+
+export const fetchMetricsData: FetchData = () => {
+ return Promise.resolve(response);
+};
+
+const response: MetricsFetchDataResponse = {
+ title: 'Metrics',
+ appLink: '/app/apm',
+ stats: {
+ hosts: { value: 11, type: 'number' },
+ cpu: { value: 0.8, type: 'percent' },
+ memory: { value: 0.362, type: 'percent' },
+ inboundTraffic: { value: 1024, type: 'bytesPerSecond' },
+ outboundTraffic: { value: 1024, type: 'bytesPerSecond' },
+ },
+ series: {
+ outboundTraffic: {
+ coordinates: [
+ {
+ x: 1589805437549,
+ y: 331514,
+ },
+ {
+ x: 1590047357549,
+ y: 319208,
+ },
+ {
+ x: 1590289277549,
+ y: 309648,
+ },
+ {
+ x: 1590531197549,
+ y: 280568,
+ },
+ {
+ x: 1590773117549,
+ y: 337180,
+ },
+ {
+ x: 1591015037549,
+ y: 122468,
+ },
+ {
+ x: 1591256957549,
+ y: 184164,
+ },
+ {
+ x: 1591498877549,
+ y: 316323,
+ },
+ {
+ x: 1591740797549,
+ y: 307351,
+ },
+ {
+ x: 1591982717549,
+ y: 290262,
+ },
+ ],
+ },
+ inboundTraffic: {
+ coordinates: [
+ {
+ x: 1589805437549,
+ y: 331514,
+ },
+ {
+ x: 1590047357549,
+ y: 319208,
+ },
+ {
+ x: 1590289277549,
+ y: 309648,
+ },
+ {
+ x: 1590531197549,
+ y: 280568,
+ },
+ {
+ x: 1590773117549,
+ y: 337180,
+ },
+ {
+ x: 1591015037549,
+ y: 122468,
+ },
+ {
+ x: 1591256957549,
+ y: 184164,
+ },
+ {
+ x: 1591498877549,
+ y: 316323,
+ },
+ {
+ x: 1591740797549,
+ y: 307351,
+ },
+ {
+ x: 1591982717549,
+ y: 290262,
+ },
+ ],
+ },
+ },
+};
+
+export const emptyResponse: MetricsFetchDataResponse = {
+ title: 'Metrics',
+ appLink: '/app/apm',
+ stats: {
+ hosts: { value: 0, type: 'number' },
+ cpu: { value: 0, type: 'percent' },
+ memory: { value: 0, type: 'percent' },
+ inboundTraffic: { value: 0, type: 'bytesPerSecond' },
+ outboundTraffic: { value: 0, type: 'bytesPerSecond' },
+ },
+ series: {
+ outboundTraffic: {
+ coordinates: [],
+ },
+ inboundTraffic: {
+ coordinates: [],
+ },
+ },
+};
diff --git a/x-pack/plugins/observability/public/pages/overview/mock/uptime.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/uptime.mock.ts
new file mode 100644
index 0000000000000..ab5874f8bfcd4
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/overview/mock/uptime.mock.ts
@@ -0,0 +1,1218 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { UptimeFetchDataResponse, FetchData } from '../../../typings';
+
+export const fetchUptimeData: FetchData = () => {
+ return Promise.resolve(response);
+};
+
+const response: UptimeFetchDataResponse = {
+ title: 'Uptime',
+ appLink: '/app/uptime#/',
+ stats: {
+ monitors: {
+ type: 'number',
+ value: 26,
+ },
+ up: {
+ type: 'number',
+ value: 20,
+ },
+ down: {
+ type: 'number',
+ value: 6,
+ },
+ },
+ series: {
+ up: {
+ coordinates: [
+ {
+ x: 1593295200000,
+ y: 1170,
+ },
+ {
+ x: 1593297000000,
+ y: 1170,
+ },
+ {
+ x: 1593298800000,
+ y: 1170,
+ },
+ {
+ x: 1593300600000,
+ y: 1170,
+ },
+ {
+ x: 1593302400000,
+ y: 1170,
+ },
+ {
+ x: 1593304200000,
+ y: 1170,
+ },
+ {
+ x: 1593306000000,
+ y: 1170,
+ },
+ {
+ x: 1593307800000,
+ y: 1170,
+ },
+ {
+ x: 1593309600000,
+ y: 1170,
+ },
+ {
+ x: 1593311400000,
+ y: 1170,
+ },
+ {
+ x: 1593313200000,
+ y: 1170,
+ },
+ {
+ x: 1593315000000,
+ y: 1170,
+ },
+ {
+ x: 1593316800000,
+ y: 1170,
+ },
+ {
+ x: 1593318600000,
+ y: 1170,
+ },
+ {
+ x: 1593320400000,
+ y: 1170,
+ },
+ {
+ x: 1593322200000,
+ y: 1170,
+ },
+ {
+ x: 1593324000000,
+ y: 1170,
+ },
+ {
+ x: 1593325800000,
+ y: 1170,
+ },
+ {
+ x: 1593327600000,
+ y: 1170,
+ },
+ {
+ x: 1593329400000,
+ y: 1170,
+ },
+ {
+ x: 1593331200000,
+ y: 1170,
+ },
+ {
+ x: 1593333000000,
+ y: 1170,
+ },
+ {
+ x: 1593334800000,
+ y: 1170,
+ },
+ {
+ x: 1593336600000,
+ y: 1170,
+ },
+ {
+ x: 1593338400000,
+ y: 1170,
+ },
+ {
+ x: 1593340200000,
+ y: 1170,
+ },
+ {
+ x: 1593342000000,
+ y: 1170,
+ },
+ {
+ x: 1593343800000,
+ y: 1170,
+ },
+ {
+ x: 1593345600000,
+ y: 1170,
+ },
+ {
+ x: 1593347400000,
+ y: 1170,
+ },
+ {
+ x: 1593349200000,
+ y: 1170,
+ },
+ {
+ x: 1593351000000,
+ y: 1170,
+ },
+ {
+ x: 1593352800000,
+ y: 1170,
+ },
+ {
+ x: 1593354600000,
+ y: 1170,
+ },
+ {
+ x: 1593356400000,
+ y: 1170,
+ },
+ {
+ x: 1593358200000,
+ y: 1170,
+ },
+ {
+ x: 1593360000000,
+ y: 1170,
+ },
+ {
+ x: 1593361800000,
+ y: 1170,
+ },
+ {
+ x: 1593363600000,
+ y: 1170,
+ },
+ {
+ x: 1593365400000,
+ y: 1170,
+ },
+ {
+ x: 1593367200000,
+ y: 1170,
+ },
+ {
+ x: 1593369000000,
+ y: 1170,
+ },
+ {
+ x: 1593370800000,
+ y: 1170,
+ },
+ {
+ x: 1593372600000,
+ y: 1170,
+ },
+ {
+ x: 1593374400000,
+ y: 1169,
+ },
+ {
+ x: 1593376200000,
+ y: 1170,
+ },
+ {
+ x: 1593378000000,
+ y: 1170,
+ },
+ {
+ x: 1593379800000,
+ y: 1170,
+ },
+ {
+ x: 1593381600000,
+ y: 1170,
+ },
+ {
+ x: 1593383400000,
+ y: 1170,
+ },
+ {
+ x: 1593385200000,
+ y: 1170,
+ },
+ {
+ x: 1593387000000,
+ y: 1170,
+ },
+ {
+ x: 1593388800000,
+ y: 1170,
+ },
+ {
+ x: 1593390600000,
+ y: 1170,
+ },
+ {
+ x: 1593392400000,
+ y: 1170,
+ },
+ {
+ x: 1593394200000,
+ y: 1239,
+ },
+ {
+ x: 1593396000000,
+ y: 1170,
+ },
+ {
+ x: 1593397800000,
+ y: 1170,
+ },
+ {
+ x: 1593399600000,
+ y: 1170,
+ },
+ {
+ x: 1593401400000,
+ y: 1170,
+ },
+ {
+ x: 1593403200000,
+ y: 1170,
+ },
+ {
+ x: 1593405000000,
+ y: 1170,
+ },
+ {
+ x: 1593406800000,
+ y: 1170,
+ },
+ {
+ x: 1593408600000,
+ y: 1170,
+ },
+ {
+ x: 1593410400000,
+ y: 1170,
+ },
+ {
+ x: 1593412200000,
+ y: 1170,
+ },
+ {
+ x: 1593414000000,
+ y: 1170,
+ },
+ {
+ x: 1593415800000,
+ y: 1170,
+ },
+ {
+ x: 1593417600000,
+ y: 1170,
+ },
+ {
+ x: 1593419400000,
+ y: 1170,
+ },
+ {
+ x: 1593421200000,
+ y: 1170,
+ },
+ {
+ x: 1593423000000,
+ y: 1170,
+ },
+ {
+ x: 1593424800000,
+ y: 1166,
+ },
+ {
+ x: 1593426600000,
+ y: 1206,
+ },
+ {
+ x: 1593428400000,
+ y: 1143,
+ },
+ {
+ x: 1593430200000,
+ y: 1170,
+ },
+ {
+ x: 1593432000000,
+ y: 1170,
+ },
+ {
+ x: 1593433800000,
+ y: 1170,
+ },
+ {
+ x: 1593435600000,
+ y: 1170,
+ },
+ {
+ x: 1593437400000,
+ y: 1170,
+ },
+ {
+ x: 1593439200000,
+ y: 1170,
+ },
+ {
+ x: 1593441000000,
+ y: 1170,
+ },
+ {
+ x: 1593442800000,
+ y: 1170,
+ },
+ {
+ x: 1593444600000,
+ y: 1170,
+ },
+ {
+ x: 1593446400000,
+ y: 1170,
+ },
+ {
+ x: 1593448200000,
+ y: 1170,
+ },
+ {
+ x: 1593450000000,
+ y: 1170,
+ },
+ {
+ x: 1593451800000,
+ y: 1170,
+ },
+ {
+ x: 1593453600000,
+ y: 1170,
+ },
+ {
+ x: 1593455400000,
+ y: 1170,
+ },
+ {
+ x: 1593457200000,
+ y: 1170,
+ },
+ {
+ x: 1593459000000,
+ y: 1170,
+ },
+ {
+ x: 1593460800000,
+ y: 1170,
+ },
+ {
+ x: 1593462600000,
+ y: 1170,
+ },
+ {
+ x: 1593464400000,
+ y: 1170,
+ },
+ {
+ x: 1593466200000,
+ y: 1170,
+ },
+ {
+ x: 1593468000000,
+ y: 1170,
+ },
+ {
+ x: 1593469800000,
+ y: 1170,
+ },
+ {
+ x: 1593471600000,
+ y: 1170,
+ },
+ {
+ x: 1593473400000,
+ y: 1170,
+ },
+ {
+ x: 1593475200000,
+ y: 1170,
+ },
+ {
+ x: 1593477000000,
+ y: 1170,
+ },
+ {
+ x: 1593478800000,
+ y: 1170,
+ },
+ {
+ x: 1593480600000,
+ y: 1201,
+ },
+ {
+ x: 1593482400000,
+ y: 1139,
+ },
+ {
+ x: 1593484200000,
+ y: 1140,
+ },
+ {
+ x: 1593486000000,
+ y: 1140,
+ },
+ {
+ x: 1593487800000,
+ y: 1140,
+ },
+ {
+ x: 1593489600000,
+ y: 1140,
+ },
+ {
+ x: 1593491400000,
+ y: 1140,
+ },
+ {
+ x: 1593493200000,
+ y: 1140,
+ },
+ {
+ x: 1593495000000,
+ y: 1140,
+ },
+ {
+ x: 1593496800000,
+ y: 1140,
+ },
+ {
+ x: 1593498600000,
+ y: 1140,
+ },
+ {
+ x: 1593500400000,
+ y: 1140,
+ },
+ {
+ x: 1593502200000,
+ y: 1140,
+ },
+ {
+ x: 1593504000000,
+ y: 1140,
+ },
+ {
+ x: 1593505800000,
+ y: 1140,
+ },
+ {
+ x: 1593507600000,
+ y: 1140,
+ },
+ {
+ x: 1593509400000,
+ y: 1140,
+ },
+ {
+ x: 1593511200000,
+ y: 1140,
+ },
+ {
+ x: 1593513000000,
+ y: 1140,
+ },
+ {
+ x: 1593514800000,
+ y: 1140,
+ },
+ {
+ x: 1593516600000,
+ y: 1140,
+ },
+ {
+ x: 1593518400000,
+ y: 1140,
+ },
+ {
+ x: 1593520200000,
+ y: 1140,
+ },
+ {
+ x: 1593522000000,
+ y: 1140,
+ },
+ {
+ x: 1593523800000,
+ y: 1140,
+ },
+ {
+ x: 1593525600000,
+ y: 1140,
+ },
+ {
+ x: 1593527400000,
+ y: 1140,
+ },
+ {
+ x: 1593529200000,
+ y: 1140,
+ },
+ {
+ x: 1593531000000,
+ y: 1140,
+ },
+ {
+ x: 1593532800000,
+ y: 1140,
+ },
+ {
+ x: 1593534600000,
+ y: 1140,
+ },
+ {
+ x: 1593536400000,
+ y: 1140,
+ },
+ {
+ x: 1593538200000,
+ y: 1140,
+ },
+ {
+ x: 1593540000000,
+ y: 1140,
+ },
+ {
+ x: 1593541800000,
+ y: 1139,
+ },
+ {
+ x: 1593543600000,
+ y: 1140,
+ },
+ {
+ x: 1593545400000,
+ y: 1140,
+ },
+ {
+ x: 1593547200000,
+ y: 1140,
+ },
+ {
+ x: 1593549000000,
+ y: 1140,
+ },
+ {
+ x: 1593550800000,
+ y: 1140,
+ },
+ {
+ x: 1593552600000,
+ y: 1140,
+ },
+ ],
+ },
+ down: {
+ coordinates: [
+ {
+ x: 1593295200000,
+ y: 234,
+ },
+ {
+ x: 1593297000000,
+ y: 234,
+ },
+ {
+ x: 1593298800000,
+ y: 234,
+ },
+ {
+ x: 1593300600000,
+ y: 234,
+ },
+ {
+ x: 1593302400000,
+ y: 234,
+ },
+ {
+ x: 1593304200000,
+ y: 234,
+ },
+ {
+ x: 1593306000000,
+ y: 234,
+ },
+ {
+ x: 1593307800000,
+ y: 234,
+ },
+ {
+ x: 1593309600000,
+ y: 234,
+ },
+ {
+ x: 1593311400000,
+ y: 234,
+ },
+ {
+ x: 1593313200000,
+ y: 234,
+ },
+ {
+ x: 1593315000000,
+ y: 234,
+ },
+ {
+ x: 1593316800000,
+ y: 234,
+ },
+ {
+ x: 1593318600000,
+ y: 234,
+ },
+ {
+ x: 1593320400000,
+ y: 234,
+ },
+ {
+ x: 1593322200000,
+ y: 234,
+ },
+ {
+ x: 1593324000000,
+ y: 234,
+ },
+ {
+ x: 1593325800000,
+ y: 234,
+ },
+ {
+ x: 1593327600000,
+ y: 234,
+ },
+ {
+ x: 1593329400000,
+ y: 234,
+ },
+ {
+ x: 1593331200000,
+ y: 234,
+ },
+ {
+ x: 1593333000000,
+ y: 234,
+ },
+ {
+ x: 1593334800000,
+ y: 234,
+ },
+ {
+ x: 1593336600000,
+ y: 234,
+ },
+ {
+ x: 1593338400000,
+ y: 234,
+ },
+ {
+ x: 1593340200000,
+ y: 234,
+ },
+ {
+ x: 1593342000000,
+ y: 234,
+ },
+ {
+ x: 1593343800000,
+ y: 234,
+ },
+ {
+ x: 1593345600000,
+ y: 234,
+ },
+ {
+ x: 1593347400000,
+ y: 234,
+ },
+ {
+ x: 1593349200000,
+ y: 234,
+ },
+ {
+ x: 1593351000000,
+ y: 234,
+ },
+ {
+ x: 1593352800000,
+ y: 234,
+ },
+ {
+ x: 1593354600000,
+ y: 234,
+ },
+ {
+ x: 1593356400000,
+ y: 234,
+ },
+ {
+ x: 1593358200000,
+ y: 234,
+ },
+ {
+ x: 1593360000000,
+ y: 234,
+ },
+ {
+ x: 1593361800000,
+ y: 234,
+ },
+ {
+ x: 1593363600000,
+ y: 234,
+ },
+ {
+ x: 1593365400000,
+ y: 234,
+ },
+ {
+ x: 1593367200000,
+ y: 234,
+ },
+ {
+ x: 1593369000000,
+ y: 234,
+ },
+ {
+ x: 1593370800000,
+ y: 234,
+ },
+ {
+ x: 1593372600000,
+ y: 234,
+ },
+ {
+ x: 1593374400000,
+ y: 235,
+ },
+ {
+ x: 1593376200000,
+ y: 234,
+ },
+ {
+ x: 1593378000000,
+ y: 234,
+ },
+ {
+ x: 1593379800000,
+ y: 234,
+ },
+ {
+ x: 1593381600000,
+ y: 234,
+ },
+ {
+ x: 1593383400000,
+ y: 234,
+ },
+ {
+ x: 1593385200000,
+ y: 234,
+ },
+ {
+ x: 1593387000000,
+ y: 234,
+ },
+ {
+ x: 1593388800000,
+ y: 234,
+ },
+ {
+ x: 1593390600000,
+ y: 234,
+ },
+ {
+ x: 1593392400000,
+ y: 234,
+ },
+ {
+ x: 1593394200000,
+ y: 246,
+ },
+ {
+ x: 1593396000000,
+ y: 234,
+ },
+ {
+ x: 1593397800000,
+ y: 234,
+ },
+ {
+ x: 1593399600000,
+ y: 234,
+ },
+ {
+ x: 1593401400000,
+ y: 234,
+ },
+ {
+ x: 1593403200000,
+ y: 234,
+ },
+ {
+ x: 1593405000000,
+ y: 234,
+ },
+ {
+ x: 1593406800000,
+ y: 234,
+ },
+ {
+ x: 1593408600000,
+ y: 234,
+ },
+ {
+ x: 1593410400000,
+ y: 234,
+ },
+ {
+ x: 1593412200000,
+ y: 234,
+ },
+ {
+ x: 1593414000000,
+ y: 234,
+ },
+ {
+ x: 1593415800000,
+ y: 234,
+ },
+ {
+ x: 1593417600000,
+ y: 234,
+ },
+ {
+ x: 1593419400000,
+ y: 234,
+ },
+ {
+ x: 1593421200000,
+ y: 234,
+ },
+ {
+ x: 1593423000000,
+ y: 234,
+ },
+ {
+ x: 1593424800000,
+ y: 240,
+ },
+ {
+ x: 1593426600000,
+ y: 254,
+ },
+ {
+ x: 1593428400000,
+ y: 231,
+ },
+ {
+ x: 1593430200000,
+ y: 234,
+ },
+ {
+ x: 1593432000000,
+ y: 234,
+ },
+ {
+ x: 1593433800000,
+ y: 234,
+ },
+ {
+ x: 1593435600000,
+ y: 234,
+ },
+ {
+ x: 1593437400000,
+ y: 234,
+ },
+ {
+ x: 1593439200000,
+ y: 234,
+ },
+ {
+ x: 1593441000000,
+ y: 234,
+ },
+ {
+ x: 1593442800000,
+ y: 234,
+ },
+ {
+ x: 1593444600000,
+ y: 234,
+ },
+ {
+ x: 1593446400000,
+ y: 234,
+ },
+ {
+ x: 1593448200000,
+ y: 234,
+ },
+ {
+ x: 1593450000000,
+ y: 234,
+ },
+ {
+ x: 1593451800000,
+ y: 234,
+ },
+ {
+ x: 1593453600000,
+ y: 234,
+ },
+ {
+ x: 1593455400000,
+ y: 234,
+ },
+ {
+ x: 1593457200000,
+ y: 234,
+ },
+ {
+ x: 1593459000000,
+ y: 234,
+ },
+ {
+ x: 1593460800000,
+ y: 234,
+ },
+ {
+ x: 1593462600000,
+ y: 234,
+ },
+ {
+ x: 1593464400000,
+ y: 234,
+ },
+ {
+ x: 1593466200000,
+ y: 234,
+ },
+ {
+ x: 1593468000000,
+ y: 234,
+ },
+ {
+ x: 1593469800000,
+ y: 234,
+ },
+ {
+ x: 1593471600000,
+ y: 234,
+ },
+ {
+ x: 1593473400000,
+ y: 234,
+ },
+ {
+ x: 1593475200000,
+ y: 234,
+ },
+ {
+ x: 1593477000000,
+ y: 234,
+ },
+ {
+ x: 1593478800000,
+ y: 234,
+ },
+ {
+ x: 1593480600000,
+ y: 254,
+ },
+ {
+ x: 1593482400000,
+ y: 265,
+ },
+ {
+ x: 1593484200000,
+ y: 264,
+ },
+ {
+ x: 1593486000000,
+ y: 264,
+ },
+ {
+ x: 1593487800000,
+ y: 264,
+ },
+ {
+ x: 1593489600000,
+ y: 264,
+ },
+ {
+ x: 1593491400000,
+ y: 264,
+ },
+ {
+ x: 1593493200000,
+ y: 264,
+ },
+ {
+ x: 1593495000000,
+ y: 264,
+ },
+ {
+ x: 1593496800000,
+ y: 264,
+ },
+ {
+ x: 1593498600000,
+ y: 264,
+ },
+ {
+ x: 1593500400000,
+ y: 264,
+ },
+ {
+ x: 1593502200000,
+ y: 264,
+ },
+ {
+ x: 1593504000000,
+ y: 264,
+ },
+ {
+ x: 1593505800000,
+ y: 264,
+ },
+ {
+ x: 1593507600000,
+ y: 264,
+ },
+ {
+ x: 1593509400000,
+ y: 264,
+ },
+ {
+ x: 1593511200000,
+ y: 264,
+ },
+ {
+ x: 1593513000000,
+ y: 264,
+ },
+ {
+ x: 1593514800000,
+ y: 264,
+ },
+ {
+ x: 1593516600000,
+ y: 264,
+ },
+ {
+ x: 1593518400000,
+ y: 264,
+ },
+ {
+ x: 1593520200000,
+ y: 264,
+ },
+ {
+ x: 1593522000000,
+ y: 264,
+ },
+ {
+ x: 1593523800000,
+ y: 264,
+ },
+ {
+ x: 1593525600000,
+ y: 264,
+ },
+ {
+ x: 1593527400000,
+ y: 264,
+ },
+ {
+ x: 1593529200000,
+ y: 264,
+ },
+ {
+ x: 1593531000000,
+ y: 264,
+ },
+ {
+ x: 1593532800000,
+ y: 264,
+ },
+ {
+ x: 1593534600000,
+ y: 264,
+ },
+ {
+ x: 1593536400000,
+ y: 264,
+ },
+ {
+ x: 1593538200000,
+ y: 264,
+ },
+ {
+ x: 1593540000000,
+ y: 264,
+ },
+ {
+ x: 1593541800000,
+ y: 265,
+ },
+ {
+ x: 1593543600000,
+ y: 264,
+ },
+ {
+ x: 1593545400000,
+ y: 264,
+ },
+ {
+ x: 1593547200000,
+ y: 264,
+ },
+ {
+ x: 1593549000000,
+ y: 264,
+ },
+ {
+ x: 1593550800000,
+ y: 264,
+ },
+ {
+ x: 1593552600000,
+ y: 264,
+ },
+ ],
+ },
+ },
+};
+
+export const emptyResponse: UptimeFetchDataResponse = {
+ title: 'Uptime',
+ appLink: '/app/uptime#/',
+ stats: {
+ monitors: {
+ type: 'number',
+ value: 0,
+ },
+ up: {
+ type: 'number',
+ value: 0,
+ },
+ down: {
+ type: 'number',
+ value: 0,
+ },
+ },
+ series: {
+ up: {
+ coordinates: [],
+ },
+ down: {
+ coordinates: [],
+ },
+ },
+};
diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx
new file mode 100644
index 0000000000000..b88614b22e81a
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx
@@ -0,0 +1,534 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { storiesOf } from '@storybook/react';
+import { AppMountContext } from 'kibana/public';
+import React from 'react';
+import { MemoryRouter } from 'react-router-dom';
+import { UI_SETTINGS } from '../../../../../../src/plugins/data/public';
+import { PluginContext } from '../../context/plugin_context';
+import { registerDataHandler, unregisterDataHandler } from '../../data_handler';
+import { emptyResponse as emptyAPMResponse, fetchApmData } from './mock/apm.mock';
+import { fetchLogsData, emptyResponse as emptyLogsResponse } from './mock/logs.mock';
+import { fetchMetricsData, emptyResponse as emptyMetricsResponse } from './mock/metrics.mock';
+import { fetchUptimeData, emptyResponse as emptyUptimeResponse } from './mock/uptime.mock';
+import { EuiThemeProvider } from '../../typings';
+import { OverviewPage } from './';
+import { alertsFetchData } from './mock/alerts.mock';
+
+const core = {
+ http: {
+ basePath: {
+ prepend: (link) => `http://localhost:5601${link}`,
+ },
+ },
+ uiSettings: {
+ get: (key: string) => {
+ const euiSettings = {
+ [UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: {
+ from: 'now-15m',
+ to: 'now',
+ },
+ [UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: {
+ pause: true,
+ value: 1000,
+ },
+ [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [
+ {
+ from: 'now/d',
+ to: 'now/d',
+ display: 'Today',
+ },
+ {
+ from: 'now/w',
+ to: 'now/w',
+ display: 'This week',
+ },
+ {
+ from: 'now-15m',
+ to: 'now',
+ display: 'Last 15 minutes',
+ },
+ {
+ from: 'now-30m',
+ to: 'now',
+ display: 'Last 30 minutes',
+ },
+ {
+ from: 'now-1h',
+ to: 'now',
+ display: 'Last 1 hour',
+ },
+ {
+ from: 'now-24h',
+ to: 'now',
+ display: 'Last 24 hours',
+ },
+ {
+ from: 'now-7d',
+ to: 'now',
+ display: 'Last 7 days',
+ },
+ {
+ from: 'now-30d',
+ to: 'now',
+ display: 'Last 30 days',
+ },
+ {
+ from: 'now-90d',
+ to: 'now',
+ display: 'Last 90 days',
+ },
+ {
+ from: 'now-1y',
+ to: 'now',
+ display: 'Last 1 year',
+ },
+ ],
+ };
+ // @ts-expect-error
+ return euiSettings[key];
+ },
+ },
+} as AppMountContext['core'];
+
+const coreWithAlerts = ({
+ ...core,
+ http: {
+ ...core.http,
+ get: alertsFetchData,
+ },
+} as unknown) as AppMountContext['core'];
+
+function unregisterAll() {
+ unregisterDataHandler({ appName: 'apm' });
+ unregisterDataHandler({ appName: 'infra_logs' });
+ unregisterDataHandler({ appName: 'infra_metrics' });
+ unregisterDataHandler({ appName: 'uptime' });
+}
+
+storiesOf('app/Overview', module)
+ .addDecorator((storyFn) => (
+
+
+ {storyFn()} )
+
+
+ ))
+ .add('Empty state', () => {
+ unregisterAll();
+ registerDataHandler({
+ appName: 'apm',
+ fetchData: fetchApmData,
+ hasData: async () => false,
+ });
+ registerDataHandler({
+ appName: 'infra_logs',
+ fetchData: fetchLogsData,
+ hasData: async () => false,
+ });
+ registerDataHandler({
+ appName: 'infra_metrics',
+ fetchData: fetchMetricsData,
+ hasData: async () => false,
+ });
+ registerDataHandler({
+ appName: 'uptime',
+ fetchData: fetchUptimeData,
+ hasData: async () => false,
+ });
+
+ return ;
+ });
+
+storiesOf('app/Overview', module)
+ .addDecorator((storyFn) => (
+
+
+ {storyFn()} )
+
+
+ ))
+ .add('single panel', () => {
+ unregisterAll();
+ registerDataHandler({
+ appName: 'infra_logs',
+ fetchData: fetchLogsData,
+ hasData: async () => true,
+ });
+ return (
+
+ );
+ });
+
+storiesOf('app/Overview', module)
+ .addDecorator((storyFn) => (
+
+
+ {storyFn()} )
+
+
+ ))
+ .add('logs and metrics', () => {
+ unregisterAll();
+ registerDataHandler({
+ appName: 'infra_logs',
+ fetchData: fetchLogsData,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'infra_metrics',
+ fetchData: fetchMetricsData,
+ hasData: async () => true,
+ });
+ return (
+
+ );
+ });
+
+storiesOf('app/Overview', module)
+ .addDecorator((storyFn) => (
+
+
+ {storyFn()} )
+
+
+ ))
+ .add('logs, metrics and alerts', () => {
+ unregisterAll();
+ registerDataHandler({
+ appName: 'infra_logs',
+ fetchData: fetchLogsData,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'infra_metrics',
+ fetchData: fetchMetricsData,
+ hasData: async () => true,
+ });
+ return (
+
+ );
+ });
+
+storiesOf('app/Overview', module)
+ .addDecorator((storyFn) => (
+
+
+ {storyFn()} )
+
+
+ ))
+ .add('logs, metrics, APM and alerts', () => {
+ unregisterAll();
+ registerDataHandler({
+ appName: 'infra_logs',
+ fetchData: fetchLogsData,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'infra_metrics',
+ fetchData: fetchMetricsData,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'apm',
+ fetchData: fetchApmData,
+ hasData: async () => true,
+ });
+ return (
+
+ );
+ });
+
+storiesOf('app/Overview', module)
+ .addDecorator((storyFn) => (
+
+
+ {storyFn()} )
+
+
+ ))
+ .add('logs, metrics, APM and Uptime', () => {
+ unregisterAll();
+ registerDataHandler({
+ appName: 'apm',
+ fetchData: fetchApmData,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'infra_logs',
+ fetchData: fetchLogsData,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'infra_metrics',
+ fetchData: fetchMetricsData,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'uptime',
+ fetchData: fetchUptimeData,
+ hasData: async () => true,
+ });
+ return (
+
+ );
+ });
+
+storiesOf('app/Overview', module)
+ .addDecorator((storyFn) => (
+
+
+ {storyFn()} )
+
+
+ ))
+ .add('logs, metrics, APM, Uptime and Alerts', () => {
+ unregisterAll();
+ registerDataHandler({
+ appName: 'apm',
+ fetchData: fetchApmData,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'infra_logs',
+ fetchData: fetchLogsData,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'infra_metrics',
+ fetchData: fetchMetricsData,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'uptime',
+ fetchData: fetchUptimeData,
+ hasData: async () => true,
+ });
+ return (
+
+ );
+ });
+
+storiesOf('app/Overview', module)
+ .addDecorator((storyFn) => (
+
+
+ {storyFn()} )
+
+
+ ))
+ .add('no data', () => {
+ unregisterAll();
+ registerDataHandler({
+ appName: 'apm',
+ fetchData: async () => emptyAPMResponse,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'infra_logs',
+ fetchData: async () => emptyLogsResponse,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'infra_metrics',
+ fetchData: async () => emptyMetricsResponse,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'uptime',
+ fetchData: async () => emptyUptimeResponse,
+ hasData: async () => true,
+ });
+ return (
+
+ );
+ });
+
+const coreAlertsThrowsError = ({
+ ...core,
+ http: {
+ ...core.http,
+ get: async () => {
+ throw new Error('Error fetching Alerts data');
+ },
+ },
+} as unknown) as AppMountContext['core'];
+storiesOf('app/Overview', module)
+ .addDecorator((storyFn) => (
+
+
+ {storyFn()} )
+
+
+ ))
+ .add('fetch data with error', () => {
+ unregisterAll();
+ registerDataHandler({
+ appName: 'apm',
+ fetchData: async () => {
+ throw new Error('Error fetching APM data');
+ },
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'infra_logs',
+ fetchData: async () => {
+ throw new Error('Error fetching Logs data');
+ },
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'infra_metrics',
+ fetchData: async () => {
+ throw new Error('Error fetching Metric data');
+ },
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'uptime',
+ fetchData: async () => {
+ throw new Error('Error fetching Uptime data');
+ },
+ hasData: async () => true,
+ });
+ return (
+
+ );
+ });
+
+storiesOf('app/Overview', module)
+ .addDecorator((storyFn) => (
+
+
+ {storyFn()} )
+
+
+ ))
+ .add('hasData with error and alerts', () => {
+ unregisterAll();
+ registerDataHandler({
+ appName: 'apm',
+ fetchData: fetchApmData,
+ // @ts-ignore thows an error instead
+ hasData: async () => {
+ new Error('Error has data');
+ },
+ });
+ registerDataHandler({
+ appName: 'infra_logs',
+ fetchData: fetchLogsData,
+ // @ts-ignore thows an error instead
+ hasData: async () => {
+ new Error('Error has data');
+ },
+ });
+ registerDataHandler({
+ appName: 'infra_metrics',
+ fetchData: fetchMetricsData,
+ // @ts-ignore thows an error instead
+ hasData: async () => {
+ new Error('Error has data');
+ },
+ });
+ registerDataHandler({
+ appName: 'uptime',
+ fetchData: fetchUptimeData,
+ // @ts-ignore thows an error instead
+ hasData: async () => {
+ new Error('Error has data');
+ },
+ });
+ return (
+
+ );
+ });
+storiesOf('app/Overview', module)
+ .addDecorator((storyFn) => (
+
+
+ {storyFn()} )
+
+
+ ))
+ .add('hasData with error', () => {
+ unregisterAll();
+ registerDataHandler({
+ appName: 'apm',
+ fetchData: fetchApmData,
+ // @ts-ignore thows an error instead
+ hasData: async () => {
+ new Error('Error has data');
+ },
+ });
+ registerDataHandler({
+ appName: 'infra_logs',
+ fetchData: fetchLogsData,
+ // @ts-ignore thows an error instead
+ hasData: async () => {
+ new Error('Error has data');
+ },
+ });
+ registerDataHandler({
+ appName: 'infra_metrics',
+ fetchData: fetchMetricsData,
+ // @ts-ignore thows an error instead
+ hasData: async () => {
+ new Error('Error has data');
+ },
+ });
+ registerDataHandler({
+ appName: 'uptime',
+ fetchData: fetchUptimeData,
+ // @ts-ignore thows an error instead
+ hasData: async () => {
+ new Error('Error has data');
+ },
+ });
+ return (
+
+ );
+ });
diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx
new file mode 100644
index 0000000000000..10f9b4dc42723
--- /dev/null
+++ b/x-pack/plugins/observability/public/routes/index.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import * as t from 'io-ts';
+import { i18n } from '@kbn/i18n';
+import { HomePage } from '../pages/home';
+import { LandingPage } from '../pages/landing';
+import { OverviewPage } from '../pages/overview';
+import { jsonRt } from './json_rt';
+
+export type RouteParams = DecodeParams;
+
+type DecodeParams = {
+ [key in keyof TParams]: TParams[key] extends t.Any ? t.TypeOf : never;
+};
+
+export interface Params {
+ query?: t.HasProps;
+ path?: t.HasProps;
+}
+export const routes = {
+ '/': {
+ handler: () => {
+ return ;
+ },
+ params: {},
+ breadcrumb: [
+ {
+ text: i18n.translate('xpack.observability.home.breadcrumb', {
+ defaultMessage: 'Overview',
+ }),
+ },
+ ],
+ },
+ '/landing': {
+ handler: () => {
+ return ;
+ },
+ params: {},
+ breadcrumb: [
+ {
+ text: i18n.translate('xpack.observability.landing.breadcrumb', {
+ defaultMessage: 'Getting started',
+ }),
+ },
+ ],
+ },
+ '/overview': {
+ handler: ({ query }: any) => {
+ return ;
+ },
+ params: {
+ query: t.partial({
+ rangeFrom: t.string,
+ rangeTo: t.string,
+ refreshPaused: jsonRt.pipe(t.boolean),
+ refreshInterval: jsonRt.pipe(t.number),
+ }),
+ },
+ breadcrumb: [
+ {
+ text: i18n.translate('xpack.observability.overview.breadcrumb', {
+ defaultMessage: 'Overview',
+ }),
+ },
+ ],
+ },
+};
diff --git a/x-pack/plugins/observability/public/routes/json_rt.ts b/x-pack/plugins/observability/public/routes/json_rt.ts
new file mode 100644
index 0000000000000..fcc73547a686b
--- /dev/null
+++ b/x-pack/plugins/observability/public/routes/json_rt.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import * as t from 'io-ts';
+import { either } from 'fp-ts/lib/Either';
+
+export const jsonRt = new t.Type(
+ 'JSON',
+ t.any.is,
+ (input, context) =>
+ either.chain(t.string.validate(input, context), (str) => {
+ try {
+ return t.success(JSON.parse(str));
+ } catch (e) {
+ return t.failure(input, context);
+ }
+ }),
+ (a) => JSON.stringify(a)
+);
diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts
new file mode 100644
index 0000000000000..dd3f476fe7d53
--- /dev/null
+++ b/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts
@@ -0,0 +1,91 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { AppMountContext } from 'kibana/public';
+import { getObservabilityAlerts } from './get_observability_alerts';
+
+describe('getObservabilityAlerts', () => {
+ it('Returns empty array when api throws exception', async () => {
+ const core = ({
+ http: {
+ get: async () => {
+ throw new Error('Boom');
+ },
+ },
+ } as unknown) as AppMountContext['core'];
+
+ const alerts = await getObservabilityAlerts({ core });
+ expect(alerts).toEqual([]);
+ });
+
+ it('Returns empty array when api return undefined', async () => {
+ const core = ({
+ http: {
+ get: async () => {
+ return {
+ data: undefined,
+ };
+ },
+ },
+ } as unknown) as AppMountContext['core'];
+
+ const alerts = await getObservabilityAlerts({ core });
+ expect(alerts).toEqual([]);
+ });
+
+ it('Shows alerts from Observability', async () => {
+ const core = ({
+ http: {
+ get: async () => {
+ return {
+ data: [
+ {
+ id: 1,
+ consumer: 'siem',
+ },
+ {
+ id: 2,
+ consumer: 'apm',
+ },
+ {
+ id: 3,
+ consumer: 'uptime',
+ },
+ {
+ id: 4,
+ consumer: 'logs',
+ },
+ {
+ id: 5,
+ consumer: 'metrics',
+ },
+ ],
+ };
+ },
+ },
+ } as unknown) as AppMountContext['core'];
+
+ const alerts = await getObservabilityAlerts({ core });
+ expect(alerts).toEqual([
+ {
+ id: 2,
+ consumer: 'apm',
+ },
+ {
+ id: 3,
+ consumer: 'uptime',
+ },
+ {
+ id: 4,
+ consumer: 'logs',
+ },
+ {
+ id: 5,
+ consumer: 'metrics',
+ },
+ ]);
+ });
+});
diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.ts
new file mode 100644
index 0000000000000..1bbabbad2834a
--- /dev/null
+++ b/x-pack/plugins/observability/public/services/get_observability_alerts.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { AppMountContext } from 'kibana/public';
+import { Alert } from '../../../alerts/common';
+
+export async function getObservabilityAlerts({ core }: { core: AppMountContext['core'] }) {
+ try {
+ const { data = [] }: { data: Alert[] } = await core.http.get('/api/alerts/_find', {
+ query: {
+ page: 1,
+ per_page: 20,
+ },
+ });
+
+ return data.filter(({ consumer }) => {
+ return (
+ consumer === 'apm' || consumer === 'uptime' || consumer === 'logs' || consumer === 'metrics'
+ );
+ });
+ } catch (e) {
+ return [];
+ }
+}
diff --git a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts
index e57dfebb36419..2dafd70896cc5 100644
--- a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts
+++ b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts
@@ -6,11 +6,9 @@
import { ObservabilityApp } from '../../../typings/common';
-interface Stat {
+export interface Stat {
type: 'number' | 'percent' | 'bytesPerSecond';
- label: string;
value: number;
- color?: string;
}
export interface Coordinates {
@@ -18,10 +16,8 @@ export interface Coordinates {
y?: number;
}
-interface Series {
- label: string;
+export interface Series {
coordinates: Coordinates[];
- color?: string;
}
export interface FetchDataParams {
@@ -50,8 +46,8 @@ export interface FetchDataResponse {
}
export interface LogsFetchDataResponse extends FetchDataResponse {
- stats: Record;
- series: Record;
+ stats: Record;
+ series: Record;
}
export interface MetricsFetchDataResponse extends FetchDataResponse {
diff --git a/x-pack/plugins/observability/public/typings/section/index.ts b/x-pack/plugins/observability/public/typings/section/index.ts
new file mode 100644
index 0000000000000..f336b6b981687
--- /dev/null
+++ b/x-pack/plugins/observability/public/typings/section/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ObservabilityApp } from '../../../typings/common';
+
+export interface ISection {
+ id: ObservabilityApp | 'alert';
+ title: string;
+ icon: string;
+ description: string;
+ href?: string;
+ linkTitle?: string;
+ target?: '_blank';
+}
diff --git a/x-pack/plugins/observability/public/utils/date.ts b/x-pack/plugins/observability/public/utils/date.ts
new file mode 100644
index 0000000000000..fc0bbdae20cb9
--- /dev/null
+++ b/x-pack/plugins/observability/public/utils/date.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import datemath from '@elastic/datemath';
+
+export function getParsedDate(range?: string, opts = {}) {
+ if (range) {
+ const parsed = datemath.parse(range, opts);
+ if (parsed) {
+ return parsed.toISOString();
+ }
+ }
+}
diff --git a/x-pack/plugins/observability/public/utils/format_stat_value.test.ts b/x-pack/plugins/observability/public/utils/format_stat_value.test.ts
new file mode 100644
index 0000000000000..6643692e02dd4
--- /dev/null
+++ b/x-pack/plugins/observability/public/utils/format_stat_value.test.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { formatStatValue } from './format_stat_value';
+import { Stat } from '../typings';
+
+describe('formatStatValue', () => {
+ it('formats value as number', () => {
+ const stat = {
+ type: 'number',
+ label: 'numeral stat',
+ value: 1000,
+ } as Stat;
+ expect(formatStatValue(stat)).toEqual('1k');
+ });
+ it('formats value as bytes', () => {
+ expect(
+ formatStatValue({
+ type: 'bytesPerSecond',
+ label: 'bytes stat',
+ value: 1,
+ } as Stat)
+ ).toEqual('1.0B/s');
+ expect(
+ formatStatValue({
+ type: 'bytesPerSecond',
+ label: 'bytes stat',
+ value: 1048576,
+ } as Stat)
+ ).toEqual('1.0MB/s');
+ expect(
+ formatStatValue({
+ type: 'bytesPerSecond',
+ label: 'bytes stat',
+ value: 1073741824,
+ } as Stat)
+ ).toEqual('1.0GB/s');
+ });
+ it('formats value as percent', () => {
+ const stat = {
+ type: 'percent',
+ label: 'percent stat',
+ value: 0.841,
+ } as Stat;
+ expect(formatStatValue(stat)).toEqual('84.1%');
+ });
+});
diff --git a/x-pack/plugins/observability/public/utils/format_stat_value.ts b/x-pack/plugins/observability/public/utils/format_stat_value.ts
new file mode 100644
index 0000000000000..c200d94d5699e
--- /dev/null
+++ b/x-pack/plugins/observability/public/utils/format_stat_value.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import numeral from '@elastic/numeral';
+import { Stat } from '../typings';
+
+export function formatStatValue(stat: Stat) {
+ const { value, type } = stat;
+ switch (type) {
+ case 'bytesPerSecond':
+ return `${numeral(value).format('0.0b')}/s`;
+ case 'number':
+ return numeral(value).format('0a');
+ case 'percent':
+ return numeral(value).format('0.0%');
+ }
+}
diff --git a/x-pack/plugins/observability/public/utils/get_bucket_size/calculate_auto.js b/x-pack/plugins/observability/public/utils/get_bucket_size/calculate_auto.js
new file mode 100644
index 0000000000000..1608003641596
--- /dev/null
+++ b/x-pack/plugins/observability/public/utils/get_bucket_size/calculate_auto.js
@@ -0,0 +1,77 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import moment from 'moment';
+const d = moment.duration;
+
+const roundingRules = [
+ [d(500, 'ms'), d(100, 'ms')],
+ [d(5, 'second'), d(1, 'second')],
+ [d(7.5, 'second'), d(5, 'second')],
+ [d(15, 'second'), d(10, 'second')],
+ [d(45, 'second'), d(30, 'second')],
+ [d(3, 'minute'), d(1, 'minute')],
+ [d(9, 'minute'), d(5, 'minute')],
+ [d(20, 'minute'), d(10, 'minute')],
+ [d(45, 'minute'), d(30, 'minute')],
+ [d(2, 'hour'), d(1, 'hour')],
+ [d(6, 'hour'), d(3, 'hour')],
+ [d(24, 'hour'), d(12, 'hour')],
+ [d(1, 'week'), d(1, 'd')],
+ [d(3, 'week'), d(1, 'week')],
+ [d(1, 'year'), d(1, 'month')],
+ [Infinity, d(1, 'year')],
+];
+
+const revRoundingRules = roundingRules.slice(0).reverse();
+
+function find(rules, check, last) {
+ function pick(buckets, duration) {
+ const target = duration / buckets;
+ let lastResp = null;
+
+ for (let i = 0; i < rules.length; i++) {
+ const rule = rules[i];
+ const resp = check(rule[0], rule[1], target);
+
+ if (resp == null) {
+ if (!last) continue;
+ if (lastResp) return lastResp;
+ break;
+ }
+
+ if (!last) return resp;
+ lastResp = resp;
+ }
+
+ // fallback to just a number of milliseconds, ensure ms is >= 1
+ const ms = Math.max(Math.floor(target), 1);
+ return moment.duration(ms, 'ms');
+ }
+
+ return (buckets, duration) => {
+ const interval = pick(buckets, duration);
+ if (interval) return moment.duration(interval._data);
+ };
+}
+
+export const calculateAuto = {
+ near: find(
+ revRoundingRules,
+ function near(bound, interval, target) {
+ if (bound > target) return interval;
+ },
+ true
+ ),
+
+ lessThan: find(revRoundingRules, function lessThan(_bound, interval, target) {
+ if (interval < target) return interval;
+ }),
+
+ atLeast: find(revRoundingRules, function atLeast(_bound, interval, target) {
+ if (interval <= target) return interval;
+ }),
+};
diff --git a/x-pack/plugins/observability/public/utils/get_bucket_size/index.test.ts b/x-pack/plugins/observability/public/utils/get_bucket_size/index.test.ts
new file mode 100644
index 0000000000000..39c4aedaa6013
--- /dev/null
+++ b/x-pack/plugins/observability/public/utils/get_bucket_size/index.test.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { getBucketSize } from './index';
+import moment from 'moment';
+
+describe('getBuckets', () => {
+ describe("minInterval 'auto'", () => {
+ it('last 15 minutes', () => {
+ const start = moment().subtract(15, 'minutes').valueOf();
+ const end = moment.now();
+ expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({
+ bucketSize: 10,
+ intervalString: '10s',
+ });
+ });
+ it('last 1 hour', () => {
+ const start = moment().subtract(1, 'hour').valueOf();
+ const end = moment.now();
+ expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({
+ bucketSize: 30,
+ intervalString: '30s',
+ });
+ });
+ it('last 1 week', () => {
+ const start = moment().subtract(1, 'week').valueOf();
+ const end = moment.now();
+ expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({
+ bucketSize: 3600,
+ intervalString: '3600s',
+ });
+ });
+ it('last 30 days', () => {
+ const start = moment().subtract(30, 'days').valueOf();
+ const end = moment.now();
+ expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({
+ bucketSize: 43200,
+ intervalString: '43200s',
+ });
+ });
+ it('last 1 year', () => {
+ const start = moment().subtract(1, 'year').valueOf();
+ const end = moment.now();
+ expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({
+ bucketSize: 86400,
+ intervalString: '86400s',
+ });
+ });
+ });
+ describe("minInterval '30s'", () => {
+ it('last 15 minutes', () => {
+ const start = moment().subtract(15, 'minutes').valueOf();
+ const end = moment.now();
+ expect(getBucketSize({ start, end, minInterval: '30s' })).toEqual({
+ bucketSize: 30,
+ intervalString: '30s',
+ });
+ });
+ it('last 1 year', () => {
+ const start = moment().subtract(1, 'year').valueOf();
+ const end = moment.now();
+ expect(getBucketSize({ start, end, minInterval: '30s' })).toEqual({
+ bucketSize: 86400,
+ intervalString: '86400s',
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/observability/public/utils/get_bucket_size/index.ts b/x-pack/plugins/observability/public/utils/get_bucket_size/index.ts
new file mode 100644
index 0000000000000..5673b890adf33
--- /dev/null
+++ b/x-pack/plugins/observability/public/utils/get_bucket_size/index.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import moment from 'moment';
+// @ts-ignore
+import { calculateAuto } from './calculate_auto';
+import { unitToSeconds } from './unit_to_seconds';
+
+export function getBucketSize({
+ start,
+ end,
+ minInterval,
+}: {
+ start: number;
+ end: number;
+ minInterval: string;
+}) {
+ const duration = moment.duration(end - start, 'ms');
+ const bucketSize = Math.max(calculateAuto.near(100, duration).asSeconds(), 1);
+ const intervalString = `${bucketSize}s`;
+ const matches = minInterval && minInterval.match(/^([\d]+)([shmdwMy]|ms)$/);
+ const minBucketSize = matches ? Number(matches[1]) * unitToSeconds(matches[2]) : 0;
+
+ if (bucketSize < minBucketSize) {
+ return {
+ bucketSize: minBucketSize,
+ intervalString: minInterval,
+ };
+ }
+
+ return { bucketSize, intervalString };
+}
diff --git a/x-pack/plugins/observability/public/utils/get_bucket_size/unit_to_seconds.ts b/x-pack/plugins/observability/public/utils/get_bucket_size/unit_to_seconds.ts
new file mode 100644
index 0000000000000..657726d988495
--- /dev/null
+++ b/x-pack/plugins/observability/public/utils/get_bucket_size/unit_to_seconds.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import moment, { unitOfTime as UnitOfTIme } from 'moment';
+
+function getDurationAsSeconds(value: number, unitOfTime: UnitOfTIme.Base) {
+ return moment.duration(value, unitOfTime).asSeconds();
+}
+
+const units = {
+ ms: getDurationAsSeconds(1, 'millisecond'),
+ s: getDurationAsSeconds(1, 'second'),
+ m: getDurationAsSeconds(1, 'minute'),
+ h: getDurationAsSeconds(1, 'hour'),
+ d: getDurationAsSeconds(1, 'day'),
+ w: getDurationAsSeconds(1, 'week'),
+ M: getDurationAsSeconds(1, 'month'),
+ y: getDurationAsSeconds(1, 'year'),
+};
+
+export function unitToSeconds(unit: string) {
+ return units[unit as keyof typeof units];
+}
diff --git a/x-pack/plugins/observability/public/utils/test_helper.tsx b/x-pack/plugins/observability/public/utils/test_helper.tsx
new file mode 100644
index 0000000000000..2a290f2b24d6b
--- /dev/null
+++ b/x-pack/plugins/observability/public/utils/test_helper.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { render as testLibRender } from '@testing-library/react';
+import { AppMountContext } from 'kibana/public';
+import { PluginContext } from '../context/plugin_context';
+import { EuiThemeProvider } from '../typings';
+
+export const core = ({
+ http: {
+ basePath: {
+ prepend: jest.fn(),
+ },
+ },
+} as unknown) as AppMountContext['core'];
+
+export const render = (component: React.ReactNode) => {
+ return testLibRender(
+
+ {component}
+
+ );
+};
diff --git a/x-pack/plugins/observability/public/utils/url.ts b/x-pack/plugins/observability/public/utils/url.ts
new file mode 100644
index 0000000000000..962ab8233a8f5
--- /dev/null
+++ b/x-pack/plugins/observability/public/utils/url.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { parse, stringify } from 'query-string';
+import { url } from '../../../../../src/plugins/kibana_utils/public';
+
+export function toQuery(search?: string) {
+ return search ? parse(search.slice(1), { sort: false }) : {};
+}
+
+export function fromQuery(query: Record) {
+ const encodedQuery = url.encodeQuery(query, (value) =>
+ encodeURIComponent(value).replace(/%3A/g, ':')
+ );
+
+ return stringify(encodedQuery, { sort: false, encode: false });
+}
diff --git a/x-pack/plugins/observability/scripts/storybook.js b/x-pack/plugins/observability/scripts/storybook.js
new file mode 100644
index 0000000000000..e9db98e2adf6b
--- /dev/null
+++ b/x-pack/plugins/observability/scripts/storybook.js
@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { join } from 'path';
+
+// eslint-disable-next-line
+require('@kbn/storybook').runStorybookCli({
+ name: 'observability',
+ storyGlobs: [
+ join(__dirname, '..', 'public', 'components', '**', '*.stories.tsx'),
+ join(__dirname, '..', 'public', 'pages', '**', '*.stories.tsx'),
+ ],
+});
diff --git a/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js b/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js
index 4ff189d8f1be0..643cc3efb0136 100644
--- a/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js
+++ b/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js
@@ -100,6 +100,7 @@ export class RollupIndexPatternCreationConfig extends IndexPatternCreationConfig
{
key: this.type,
name: rollupIndexPatternIndexLabel,
+ color: 'primary',
},
]
: [];
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index a34a76361f799..bf9cf7486810d 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -51,7 +51,7 @@ export const APP_HOSTS_PATH = `${APP_PATH}/hosts`;
export const APP_NETWORK_PATH = `${APP_PATH}/network`;
export const APP_TIMELINES_PATH = `${APP_PATH}/timelines`;
export const APP_CASES_PATH = `${APP_PATH}/cases`;
-export const APP_MANAGEMENT_PATH = `${APP_PATH}/management`;
+export const APP_MANAGEMENT_PATH = `${APP_PATH}/administration`;
/** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */
export const DEFAULT_INDEX_PATTERN = [
diff --git a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts
index efd9ece8aec56..9438c28f05fef 100644
--- a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts
@@ -99,6 +99,6 @@ describe('Cases', () => {
cy.get(TIMELINE_TITLE).should('have.attr', 'value', case1.timeline.title);
cy.get(TIMELINE_DESCRIPTION).should('have.attr', 'value', case1.timeline.description);
- cy.get(TIMELINE_QUERY).should('have.attr', 'value', case1.timeline.query);
+ cy.get(TIMELINE_QUERY).invoke('text').should('eq', case1.timeline.query);
});
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts
index 0c3424576e4cf..6b3fc9e751ea4 100644
--- a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts
@@ -27,74 +27,67 @@ import {
describe('ml conditional links', () => {
it('sets the KQL from a single IP with a value for the query', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpKqlQuery);
- cy.get(KQL_INPUT).should(
- 'have.attr',
- 'value',
- '(process.name: "conhost.exe" or process.name: "sc.exe")'
- );
+ cy.get(KQL_INPUT)
+ .invoke('text')
+ .should('eq', '(process.name: "conhost.exe" or process.name: "sc.exe")');
});
it('sets the KQL from a multiple IPs with a null for the query', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpNullKqlQuery);
- cy.get(KQL_INPUT).should(
- 'have.attr',
- 'value',
- '((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2"))'
- );
+ cy.get(KQL_INPUT)
+ .invoke('text')
+ .should(
+ 'eq',
+ '((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2"))'
+ );
});
it('sets the KQL from a multiple IPs with a value for the query', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpKqlQuery);
- cy.get(KQL_INPUT).should(
- 'have.attr',
- 'value',
- '((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2")) and ((process.name: "conhost.exe" or process.name: "sc.exe"))'
- );
+ cy.get(KQL_INPUT)
+ .invoke('text')
+ .should(
+ 'eq',
+ '((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2")) and ((process.name: "conhost.exe" or process.name: "sc.exe"))'
+ );
});
it('sets the KQL from a $ip$ with a value for the query', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkKqlQuery);
- cy.get(KQL_INPUT).should(
- 'have.attr',
- 'value',
- '(process.name: "conhost.exe" or process.name: "sc.exe")'
- );
+ cy.get(KQL_INPUT)
+ .invoke('text')
+ .should('eq', '(process.name: "conhost.exe" or process.name: "sc.exe")');
});
it('sets the KQL from a single host name with a value for query', () => {
loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQuery);
- cy.get(KQL_INPUT).should(
- 'have.attr',
- 'value',
- '(process.name: "conhost.exe" or process.name: "sc.exe")'
- );
+ cy.get(KQL_INPUT)
+ .invoke('text')
+ .should('eq', '(process.name: "conhost.exe" or process.name: "sc.exe")');
});
it('sets the KQL from a multiple host names with null for query', () => {
loginAndWaitForPageWithoutDateRange(mlHostMultiHostNullKqlQuery);
- cy.get(KQL_INPUT).should(
- 'have.attr',
- 'value',
- '(host.name: "siem-windows" or host.name: "siem-suricata")'
- );
+ cy.get(KQL_INPUT)
+ .invoke('text')
+ .should('eq', '(host.name: "siem-windows" or host.name: "siem-suricata")');
});
it('sets the KQL from a multiple host names with a value for query', () => {
loginAndWaitForPageWithoutDateRange(mlHostMultiHostKqlQuery);
- cy.get(KQL_INPUT).should(
- 'have.attr',
- 'value',
- '(host.name: "siem-windows" or host.name: "siem-suricata") and ((process.name: "conhost.exe" or process.name: "sc.exe"))'
- );
+ cy.get(KQL_INPUT)
+ .invoke('text')
+ .should(
+ 'eq',
+ '(host.name: "siem-windows" or host.name: "siem-suricata") and ((process.name: "conhost.exe" or process.name: "sc.exe"))'
+ );
});
it('sets the KQL from a undefined/null host name but with a value for query', () => {
loginAndWaitForPageWithoutDateRange(mlHostVariableHostKqlQuery);
- cy.get(KQL_INPUT).should(
- 'have.attr',
- 'value',
- '(process.name: "conhost.exe" or process.name: "sc.exe")'
- );
+ cy.get(KQL_INPUT)
+ .invoke('text')
+ .should('eq', '(process.name: "conhost.exe" or process.name: "sc.exe")');
});
it('redirects from a single IP with a null for the query', () => {
diff --git a/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts
index ea3a78c77152a..7864160d5bca0 100644
--- a/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts
@@ -21,7 +21,7 @@ import {
CASES_URL,
HOSTS_URL,
KIBANA_HOME,
- MANAGEMENT_URL,
+ ADMINISTRATION_URL,
NETWORK_URL,
OVERVIEW_URL,
TIMELINES_URL,
@@ -31,7 +31,7 @@ import {
ALERTS_PAGE,
CASES_PAGE,
HOSTS_PAGE,
- MANAGEMENT_PAGE,
+ ADMINISTRATION_PAGE,
NETWORK_PAGE,
OVERVIEW_PAGE,
TIMELINES_PAGE,
@@ -72,9 +72,9 @@ describe('top-level navigation common to all pages in the Security app', () => {
cy.url().should('include', CASES_URL);
});
- it('navigates to the Management page', () => {
+ it('navigates to the Administration page', () => {
navigateFromHeaderTo(MANAGEMENT);
- cy.url().should('include', MANAGEMENT_URL);
+ cy.url().should('include', ADMINISTRATION_URL);
});
});
@@ -115,8 +115,8 @@ describe('Kibana navigation to all pages in the Security app ', () => {
cy.url().should('include', CASES_URL);
});
- it('navigates to the Management page', () => {
- navigateFromKibanaCollapsibleTo(MANAGEMENT_PAGE);
- cy.url().should('include', MANAGEMENT_URL);
+ it('navigates to the Administration page', () => {
+ navigateFromKibanaCollapsibleTo(ADMINISTRATION_PAGE);
+ cy.url().should('include', ADMINISTRATION_URL);
});
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts
index a3a927cbea7d4..81af9ece9ed45 100644
--- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts
@@ -154,12 +154,12 @@ describe('url state', () => {
it('sets kql on network page', () => {
loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.urlKqlNetworkNetwork);
- cy.get(KQL_INPUT).should('have.attr', 'value', 'source.ip: "10.142.0.9"');
+ cy.get(KQL_INPUT).invoke('text').should('eq', 'source.ip: "10.142.0.9"');
});
it('sets kql on hosts page', () => {
loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.urlKqlHostsHosts);
- cy.get(KQL_INPUT).should('have.attr', 'value', 'source.ip: "10.142.0.9"');
+ cy.get(KQL_INPUT).invoke('text').should('eq', 'source.ip: "10.142.0.9"');
});
it('sets the url state when kql is set', () => {
@@ -230,8 +230,7 @@ describe('url state', () => {
it('Do not clears kql when navigating to a new page', () => {
loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.urlKqlHostsHosts);
navigateFromHeaderTo(NETWORK);
-
- cy.get(KQL_INPUT).should('have.attr', 'value', 'source.ip: "10.142.0.9"');
+ cy.get(KQL_INPUT).invoke('text').should('eq', 'source.ip: "10.142.0.9"');
});
it.skip('sets and reads the url state for timeline by id', () => {
diff --git a/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts b/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts
index 2f7956ce370bc..eeec19fa3dd1e 100644
--- a/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts
@@ -12,8 +12,8 @@ export const HOSTS_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [titl
export const KIBANA_NAVIGATION_TOGGLE = '[data-test-subj="toggleNavButton"]';
-export const MANAGEMENT_PAGE =
- '[data-test-subj="collapsibleNavGroup-security"] [title="Management"]';
+export const ADMINISTRATION_PAGE =
+ '[data-test-subj="collapsibleNavGroup-security"] [title="Administration"]';
export const NETWORK_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [title="Network"]';
diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts
index eca5885e7b3d9..88ae582b58891 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts
@@ -82,7 +82,7 @@ export const fillAboutRuleAndContinue = (rule: CustomRule | MachineLearningRule)
export const fillDefineCustomRuleAndContinue = (rule: CustomRule) => {
cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery);
- cy.get(CUSTOM_QUERY_INPUT).should('have.attr', 'value', rule.customQuery);
+ cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery);
cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true });
cy.get(CUSTOM_QUERY_INPUT).should('not.exist');
@@ -91,7 +91,7 @@ export const fillDefineCustomRuleAndContinue = (rule: CustomRule) => {
export const fillDefineCustomRuleWithImportedQueryAndContinue = (rule: CustomRule) => {
cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click();
cy.get(TIMELINE(rule.timelineId)).click();
- cy.get(CUSTOM_QUERY_INPUT).should('have.attr', 'value', rule.customQuery);
+ cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery);
cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true });
cy.get(CUSTOM_QUERY_INPUT).should('not.exist');
diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
index 9e17433090c2b..761fd2c1e6a0b 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
@@ -58,7 +58,7 @@ export const createNewTimeline = () => {
};
export const executeTimelineKQL = (query: string) => {
- cy.get(`${SEARCH_OR_FILTER_CONTAINER} input`).type(`${query} {enter}`);
+ cy.get(`${SEARCH_OR_FILTER_CONTAINER} textarea`).type(`${query} {enter}`);
};
export const expandFirstTimelineEventDetails = () => {
diff --git a/x-pack/plugins/security_solution/cypress/urls/navigation.ts b/x-pack/plugins/security_solution/cypress/urls/navigation.ts
index 9da9abf388e4d..e53dac157eed7 100644
--- a/x-pack/plugins/security_solution/cypress/urls/navigation.ts
+++ b/x-pack/plugins/security_solution/cypress/urls/navigation.ts
@@ -16,7 +16,7 @@ export const HOSTS_PAGE_TAB_URLS = {
uncommonProcesses: '/app/security/hosts/uncommonProcesses',
};
export const KIBANA_HOME = '/app/home#/';
-export const MANAGEMENT_URL = '/app/security/management';
+export const ADMINISTRATION_URL = '/app/security/administration';
export const NETWORK_URL = '/app/security/network';
export const OVERVIEW_URL = '/app/security/overview';
export const TIMELINES_URL = '/app/security/timelines';
diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx
index 88e9d4179a971..d7acda4988570 100644
--- a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx
@@ -27,7 +27,7 @@ export const navTabs: SiemNavTab = {
},
[SecurityPageName.alerts]: {
id: SecurityPageName.alerts,
- name: i18n.Alerts,
+ name: i18n.ALERTS,
href: APP_ALERTS_PATH,
disabled: false,
urlKey: 'alerts',
@@ -63,7 +63,7 @@ export const navTabs: SiemNavTab = {
},
[SecurityPageName.management]: {
id: SecurityPageName.management,
- name: i18n.MANAGEMENT,
+ name: i18n.ADMINISTRATION,
href: APP_MANAGEMENT_PATH,
disabled: false,
urlKey: SecurityPageName.management,
diff --git a/x-pack/plugins/security_solution/public/app/home/translations.ts b/x-pack/plugins/security_solution/public/app/home/translations.ts
index f5a08e6395f1f..bee1dfe333851 100644
--- a/x-pack/plugins/security_solution/public/app/home/translations.ts
+++ b/x-pack/plugins/security_solution/public/app/home/translations.ts
@@ -25,7 +25,7 @@ export const DETECTION_ENGINE = i18n.translate(
}
);
-export const Alerts = i18n.translate('xpack.securitySolution.navigation.alerts', {
+export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts', {
defaultMessage: 'Alerts',
});
@@ -37,6 +37,6 @@ export const CASE = i18n.translate('xpack.securitySolution.navigation.case', {
defaultMessage: 'Cases',
});
-export const MANAGEMENT = i18n.translate('xpack.securitySolution.navigation.management', {
- defaultMessage: 'Management',
+export const ADMINISTRATION = i18n.translate('xpack.securitySolution.navigation.administration', {
+ defaultMessage: 'Administration',
});
diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts
index 888c881f45ce4..483ca5d6d332e 100644
--- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts
@@ -7,8 +7,8 @@
import dateMath from '@elastic/datemath';
import { EuiComboBoxOptionOption } from '@elastic/eui';
-import { IFieldType } from '../../../../../../../src/plugins/data/common';
-import { Ipv4Address } from '../../../../../../../src/plugins/kibana_utils/public';
+import { IFieldType, Ipv4Address } from '../../../../../../../src/plugins/data/common';
+
import {
EXCEPTION_OPERATORS,
isOperator,
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx
index 10f8b11b4d9c5..2ad83d37576b1 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx
@@ -108,9 +108,9 @@ describe('SIEM Navigation', () => {
},
management: {
disabled: false,
- href: '/app/security/management',
+ href: '/app/security/administration',
id: 'management',
- name: 'Management',
+ name: 'Administration',
urlKey: 'management',
},
hosts: {
@@ -220,9 +220,9 @@ describe('SIEM Navigation', () => {
},
management: {
disabled: false,
- href: '/app/security/management',
+ href: '/app/security/administration',
id: 'management',
- name: 'Management',
+ name: 'Administration',
urlKey: 'management',
},
network: {
diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx
index a3cab1cfabd71..aac83ce650d86 100644
--- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx
@@ -214,15 +214,18 @@ describe('QueryBar ', () => {
/>
);
- const queryInput = wrapper.find(QueryBar).find('input[data-test-subj="queryInput"]');
+ let queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]');
queryInput.simulate('change', { target: { value: 'host.name:*' } });
- expect(queryInput.html()).toContain('value="host.name:*"');
+ wrapper.update();
+ queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]');
+ expect(queryInput.props().children).toBe('host.name:*');
wrapper.setProps({ filterQueryDraft: null });
wrapper.update();
+ queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]');
- expect(queryInput.html()).toContain('value=""');
+ expect(queryInput.props().children).toBe('');
});
});
@@ -258,7 +261,7 @@ describe('QueryBar ', () => {
const onSubmitQueryRef = searchBarProps.onQuerySubmit;
const onSavedQueryRef = searchBarProps.onSavedQueryUpdated;
- const queryInput = wrapper.find(QueryBar).find('input[data-test-subj="queryInput"]');
+ const queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]');
queryInput.simulate('change', { target: { value: 'hello: world' } });
wrapper.update();
diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts
index 0fad1273c7279..4bc586bdee8a9 100644
--- a/x-pack/plugins/security_solution/public/management/common/constants.ts
+++ b/x-pack/plugins/security_solution/public/management/common/constants.ts
@@ -10,7 +10,7 @@ import { SecurityPageName } from '../../app/types';
// --[ ROUTING ]---------------------------------------------------------------------------
export const MANAGEMENT_APP_ID = `${APP_ID}:${SecurityPageName.management}`;
export const MANAGEMENT_ROUTING_ROOT_PATH = '';
-export const MANAGEMENT_ROUTING_ENDPOINTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.endpoints})`;
+export const MANAGEMENT_ROUTING_HOSTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.hosts})`;
export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})`;
export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})/:policyId`;
@@ -21,5 +21,5 @@ export const MANAGEMENT_STORE_GLOBAL_NAMESPACE: ManagementStoreGlobalNamespace =
export const MANAGEMENT_STORE_POLICY_LIST_NAMESPACE = 'policyList';
/** Namespace within the Management state where policy details state is maintained */
export const MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE = 'policyDetails';
-/** Namespace within the Management state where endpoints state is maintained */
-export const MANAGEMENT_STORE_ENDPOINTS_NAMESPACE = 'endpoints';
+/** Namespace within the Management state where hosts state is maintained */
+export const MANAGEMENT_STORE_HOSTS_NAMESPACE = 'hosts';
diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts
index 92eb7717318d3..5add6b753a7a9 100644
--- a/x-pack/plugins/security_solution/public/management/common/routing.ts
+++ b/x-pack/plugins/security_solution/public/management/common/routing.ts
@@ -10,7 +10,7 @@ import { generatePath } from 'react-router-dom';
import querystring from 'querystring';
import {
- MANAGEMENT_ROUTING_ENDPOINTS_PATH,
+ MANAGEMENT_ROUTING_HOSTS_PATH,
MANAGEMENT_ROUTING_POLICIES_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_PATH,
} from './constants';
@@ -32,11 +32,11 @@ const querystringStringify: (
) => string = querystring.stringify;
/** Make `selected_host` required */
-type EndpointDetailsUrlProps = Omit &
+type HostDetailsUrlProps = Omit &
Required>;
-export const getEndpointListPath = (
- props: { name: 'default' | 'endpointList' } & HostIndexUIQueryParams,
+export const getHostListPath = (
+ props: { name: 'default' | 'hostList' } & HostIndexUIQueryParams,
search?: string
) => {
const { name, ...queryParams } = props;
@@ -45,29 +45,27 @@ export const getEndpointListPath = (
);
const urlSearch = `${urlQueryParams && !isEmpty(search) ? '&' : ''}${search ?? ''}`;
- if (name === 'endpointList') {
- return `${generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, {
- tabName: ManagementSubTab.endpoints,
+ if (name === 'hostList') {
+ return `${generatePath(MANAGEMENT_ROUTING_HOSTS_PATH, {
+ tabName: ManagementSubTab.hosts,
})}${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`;
}
return `${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`;
};
-export const getEndpointDetailsPath = (
- props: { name: 'endpointDetails' | 'endpointPolicyResponse' } & EndpointDetailsUrlProps,
+export const getHostDetailsPath = (
+ props: { name: 'hostDetails' | 'hostPolicyResponse' } & HostDetailsUrlProps,
search?: string
) => {
const { name, ...queryParams } = props;
- queryParams.show = (props.name === 'endpointPolicyResponse'
+ queryParams.show = (props.name === 'hostPolicyResponse'
? 'policy_response'
: '') as HostIndexUIQueryParams['show'];
- const urlQueryParams = querystringStringify(
- queryParams
- );
+ const urlQueryParams = querystringStringify(queryParams);
const urlSearch = `${urlQueryParams && !isEmpty(search) ? '&' : ''}${search ?? ''}`;
- return `${generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, {
- tabName: ManagementSubTab.endpoints,
+ return `${generatePath(MANAGEMENT_ROUTING_HOSTS_PATH, {
+ tabName: ManagementSubTab.hosts,
})}${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`;
};
diff --git a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx
index 5dd47d4e88028..c3d6cb48e4dae 100644
--- a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx
@@ -103,7 +103,7 @@ const PolicyEmptyState = React.memo<{
);
});
-const EndpointsEmptyState = React.memo<{
+const HostsEmptyState = React.memo<{
loading: boolean;
onActionClick: (event: MouseEvent) => void;
actionDisabled: boolean;
@@ -113,14 +113,14 @@ const EndpointsEmptyState = React.memo<{
const policySteps = useMemo(
() => [
{
- title: i18n.translate('xpack.securitySolution.endpoint.endpointList.stepOneTitle', {
+ title: i18n.translate('xpack.securitySolution.endpoint.hostList.stepOneTitle', {
defaultMessage: 'Select a policy you created from the list below.',
}),
children: (
<>
@@ -138,7 +138,7 @@ const EndpointsEmptyState = React.memo<{
return loading ? (
@@ -146,7 +146,7 @@ const EndpointsEmptyState = React.memo<{
list
) : (
);
@@ -156,14 +156,14 @@ const EndpointsEmptyState = React.memo<{
),
},
{
- title: i18n.translate('xpack.securitySolution.endpoint.endpointList.stepTwoTitle', {
+ title: i18n.translate('xpack.securitySolution.endpoint.hostList.stepTwoTitle', {
defaultMessage:
'Head over to Ingest to deploy your Agent with Endpoint Security enabled.',
}),
children: (
@@ -178,18 +178,18 @@ const EndpointsEmptyState = React.memo<{
loading={loading}
onActionClick={onActionClick}
actionDisabled={actionDisabled}
- dataTestSubj="emptyEndpointsTable"
+ dataTestSubj="emptyHostsTable"
steps={policySteps}
headerComponent={
}
bodyComponent={
}
/>
@@ -271,7 +271,7 @@ const ManagementEmptyState = React.memo<{
);
PolicyEmptyState.displayName = 'PolicyEmptyState';
-EndpointsEmptyState.displayName = 'EndpointsEmptyState';
+HostsEmptyState.displayName = 'HostsEmptyState';
ManagementEmptyState.displayName = 'ManagementEmptyState';
-export { PolicyEmptyState, EndpointsEmptyState, ManagementEmptyState };
+export { PolicyEmptyState, HostsEmptyState, ManagementEmptyState };
diff --git a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx
index c3dbb93b369a9..8495628709d2a 100644
--- a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx
@@ -11,7 +11,7 @@ import { PageView, PageViewProps } from '../../common/components/endpoint/page_v
import { ManagementSubTab } from '../types';
import { SecurityPageName } from '../../app/types';
import { useFormatUrl } from '../../common/components/link_to';
-import { getEndpointListPath, getPoliciesPath } from '../common/routing';
+import { getHostListPath, getPoliciesPath } from '../common/routing';
import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler';
export const ManagementPageView = memo>((options) => {
@@ -19,7 +19,7 @@ export const ManagementPageView = memo>((options) =>
const { tabName } = useParams<{ tabName: ManagementSubTab }>();
const goToEndpoint = useNavigateByRouterEventHandler(
- getEndpointListPath({ name: 'endpointList' }, search)
+ getHostListPath({ name: 'hostList' }, search)
);
const goToPolicies = useNavigateByRouterEventHandler(getPoliciesPath(search));
@@ -31,11 +31,11 @@ export const ManagementPageView = memo>((options) =>
return [
{
name: i18n.translate('xpack.securitySolution.managementTabs.endpoints', {
- defaultMessage: 'Endpoints',
+ defaultMessage: 'Hosts',
}),
- id: ManagementSubTab.endpoints,
- isSelected: tabName === ManagementSubTab.endpoints,
- href: formatUrl(getEndpointListPath({ name: 'endpointList' })),
+ id: ManagementSubTab.hosts,
+ isSelected: tabName === ManagementSubTab.hosts,
+ href: formatUrl(getHostListPath({ name: 'hostList' })),
onClick: goToEndpoint,
},
{
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx
index ff7f522b9bc52..a970edd4d30f4 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx
@@ -7,19 +7,19 @@
import { Switch, Route } from 'react-router-dom';
import React, { memo } from 'react';
import { HostList } from './view';
-import { MANAGEMENT_ROUTING_ENDPOINTS_PATH } from '../../common/constants';
+import { MANAGEMENT_ROUTING_HOSTS_PATH } from '../../common/constants';
import { NotFoundPage } from '../../../app/404';
/**
- * Provides the routing container for the endpoints related views
+ * Provides the routing container for the hosts related views
*/
-export const EndpointsContainer = memo(() => {
+export const HostsContainer = memo(() => {
return (
-
+
);
});
-EndpointsContainer.displayName = 'EndpointsContainer';
+HostsContainer.displayName = 'HostsContainer';
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts
index ae2ce9facc837..533b14e50f3dd 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts
@@ -24,7 +24,7 @@ import {
MiddlewareActionSpyHelper,
createSpyMiddleware,
} from '../../../../common/store/test_utils';
-import { getEndpointListPath } from '../../../common/routing';
+import { getHostListPath } from '../../../common/routing';
describe('host list pagination: ', () => {
let fakeCoreStart: jest.Mocked;
@@ -56,7 +56,7 @@ describe('host list pagination: ', () => {
queryParams = () => uiQueryParams(store.getState());
historyPush = (nextQueryParams: HostIndexUIQueryParams): void => {
- return history.push(getEndpointListPath({ name: 'endpointList', ...nextQueryParams }));
+ return history.push(getHostListPath({ name: 'hostList', ...nextQueryParams }));
};
});
@@ -70,7 +70,7 @@ describe('host list pagination: ', () => {
type: 'userChangedUrl',
payload: {
...history.location,
- pathname: getEndpointListPath({ name: 'endpointList' }),
+ pathname: getHostListPath({ name: 'hostList' }),
},
});
await waitForAction('serverReturnedHostList');
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
index e62c53e061a33..1c5c4fbac51ba 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
@@ -21,7 +21,7 @@ import { listData } from './selectors';
import { HostState } from '../types';
import { hostListReducer } from './reducer';
import { hostMiddlewareFactory } from './middleware';
-import { getEndpointListPath } from '../../../common/routing';
+import { getHostListPath } from '../../../common/routing';
describe('host list middleware', () => {
let fakeCoreStart: jest.Mocked;
@@ -60,7 +60,7 @@ describe('host list middleware', () => {
type: 'userChangedUrl',
payload: {
...history.location,
- pathname: getEndpointListPath({ name: 'endpointList' }),
+ pathname: getHostListPath({ name: 'hostList' }),
},
});
await waitForAction('serverReturnedHostList');
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts
index e75d2129f61a5..4f47eaf565d8c 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts
@@ -15,7 +15,7 @@ import {
HostPolicyResponseActionStatus,
} from '../../../../../common/endpoint/types';
import { HostState, HostIndexUIQueryParams } from '../types';
-import { MANAGEMENT_ROUTING_ENDPOINTS_PATH } from '../../../common/constants';
+import { MANAGEMENT_ROUTING_HOSTS_PATH } from '../../../common/constants';
const PAGE_SIZES = Object.freeze([10, 20, 50]);
@@ -114,7 +114,7 @@ export const policyResponseError = (state: Immutable) => state.policy
export const isOnHostPage = (state: Immutable) => {
return (
matchPath(state.location?.pathname ?? '', {
- path: MANAGEMENT_ROUTING_ENDPOINTS_PATH,
+ path: MANAGEMENT_ROUTING_HOSTS_PATH,
exact: true,
}) !== null
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx
index 66abf993770a7..10ea271139e49 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx
@@ -26,7 +26,7 @@ import { POLICY_STATUS_TO_HEALTH_COLOR } from '../host_constants';
import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time';
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app';
-import { getEndpointDetailsPath, getPolicyDetailPath } from '../../../../common/routing';
+import { getHostDetailsPath, getPolicyDetailPath } from '../../../../common/routing';
import { SecurityPageName } from '../../../../../app/types';
import { useFormatUrl } from '../../../../../common/components/link_to';
import { AgentDetailsReassignConfigAction } from '../../../../../../../ingest_manager/public';
@@ -84,14 +84,14 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
const { selected_host, show, ...currentUrlParams } = queryParams;
return [
formatUrl(
- getEndpointDetailsPath({
- name: 'endpointPolicyResponse',
+ getHostDetailsPath({
+ name: 'hostPolicyResponse',
...currentUrlParams,
selected_host: details.host.id,
})
),
- getEndpointDetailsPath({
- name: 'endpointPolicyResponse',
+ getHostDetailsPath({
+ name: 'hostPolicyResponse',
...currentUrlParams,
selected_host: details.host.id,
}),
@@ -108,7 +108,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
onDoneNavigateTo: [
'securitySolution:management',
{
- path: getEndpointDetailsPath({ name: 'endpointDetails', selected_host: details.host.id }),
+ path: getHostDetailsPath({ name: 'hostDetails', selected_host: details.host.id }),
},
],
},
@@ -200,8 +200,8 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
description: details.host.hostname,
},
{
- title: i18n.translate('xpack.securitySolution.endpoint.host.details.sensorVersion', {
- defaultMessage: 'Sensor Version',
+ title: i18n.translate('xpack.securitySolution.endpoint.host.details.endpointVersion', {
+ defaultMessage: 'Endpoint Version',
}),
description: details.agent.version,
},
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
index 3d44b73858e90..e29d796325bd6 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
@@ -38,7 +38,7 @@ import { PolicyResponse } from './policy_response';
import { HostMetadata } from '../../../../../../common/endpoint/types';
import { FlyoutSubHeader, FlyoutSubHeaderProps } from './components/flyout_sub_header';
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
-import { getEndpointListPath } from '../../../../common/routing';
+import { getHostListPath } from '../../../../common/routing';
import { SecurityPageName } from '../../../../../app/types';
import { useFormatUrl } from '../../../../../common/components/link_to';
@@ -122,14 +122,14 @@ const PolicyResponseFlyoutPanel = memo<{
const [detailsUri, detailsRoutePath] = useMemo(
() => [
formatUrl(
- getEndpointListPath({
- name: 'endpointList',
+ getHostListPath({
+ name: 'hostList',
...queryParams,
selected_host: hostMeta.host.id,
})
),
- getEndpointListPath({
- name: 'endpointList',
+ getHostListPath({
+ name: 'hostList',
...queryParams,
selected_host: hostMeta.host.id,
}),
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts
index b048a8f69b5d2..d11335df875e9 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts
@@ -9,14 +9,14 @@ import { useMemo } from 'react';
import { useKibana } from '../../../../common/lib/kibana';
import { HostState } from '../types';
import {
- MANAGEMENT_STORE_ENDPOINTS_NAMESPACE,
+ MANAGEMENT_STORE_HOSTS_NAMESPACE,
MANAGEMENT_STORE_GLOBAL_NAMESPACE,
} from '../../../common/constants';
import { State } from '../../../../common/store';
export function useHostSelector(selector: (state: HostState) => TSelected) {
return useSelector(function (state: State) {
return selector(
- state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_ENDPOINTS_NAMESPACE] as HostState
+ state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_HOSTS_NAMESPACE] as HostState
);
});
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
index 9766cd6abd2b1..996b987ea2be3 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
@@ -44,7 +44,7 @@ describe('when on the hosts page', () => {
it('should show the empty state when there are no hosts or polices', async () => {
const renderResult = render();
- // Initially, there are no endpoints or policies, so we prompt to add policies first.
+ // Initially, there are no hosts or policies, so we prompt to add policies first.
const table = await renderResult.findByTestId('emptyPolicyTable');
expect(table).not.toBeNull();
});
@@ -79,8 +79,8 @@ describe('when on the hosts page', () => {
it('should show the no hosts empty state', async () => {
const renderResult = render();
- const emptyEndpointsTable = await renderResult.findByTestId('emptyEndpointsTable');
- expect(emptyEndpointsTable).not.toBeNull();
+ const emptyHostsTable = await renderResult.findByTestId('emptyHostsTable');
+ expect(emptyHostsTable).not.toBeNull();
});
it('should display the onboarding steps', async () => {
@@ -335,7 +335,7 @@ describe('when on the hosts page', () => {
const policyStatusLink = await renderResult.findByTestId('policyStatusValue');
expect(policyStatusLink).not.toBeNull();
expect(policyStatusLink.getAttribute('href')).toEqual(
- '/endpoints?page_index=0&page_size=10&selected_host=1&show=policy_response'
+ '/hosts?page_index=0&page_size=10&selected_host=1&show=policy_response'
);
});
@@ -549,7 +549,7 @@ describe('when on the hosts page', () => {
const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton');
expect(subHeaderBackLink.textContent).toBe('Endpoint Details');
expect(subHeaderBackLink.getAttribute('href')).toBe(
- '/endpoints?page_index=0&page_size=10&selected_host=1'
+ '/hosts?page_index=0&page_size=10&selected_host=1'
);
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
index d49335ca8de2c..492c75607a255 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
@@ -10,6 +10,8 @@ import {
EuiBasicTable,
EuiBasicTableColumn,
EuiText,
+ EuiTitle,
+ EuiSpacer,
EuiLink,
EuiHealth,
EuiToolTip,
@@ -33,7 +35,7 @@ import { CreateStructuredSelector } from '../../../../common/store';
import { Immutable, HostInfo } from '../../../../../common/endpoint/types';
import { SpyRoute } from '../../../../common/utils/route/spy_routes';
import { ManagementPageView } from '../../../components/management_page_view';
-import { PolicyEmptyState, EndpointsEmptyState } from '../../../components/management_empty_state';
+import { PolicyEmptyState, HostsEmptyState } from '../../../components/management_empty_state';
import { FormattedDate } from '../../../../common/components/formatted_date';
import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
import {
@@ -41,11 +43,7 @@ import {
AgentConfigDetailsDeployAgentAction,
} from '../../../../../../ingest_manager/public';
import { SecurityPageName } from '../../../../app/types';
-import {
- getEndpointListPath,
- getEndpointDetailsPath,
- getPolicyDetailPath,
-} from '../../../common/routing';
+import { getHostListPath, getHostDetailsPath, getPolicyDetailPath } from '../../../common/routing';
import { useFormatUrl } from '../../../../common/components/link_to';
import { HostAction } from '../store/action';
@@ -107,8 +105,8 @@ export const HostList = () => {
const { index, size } = page;
// FIXME: PT: if host details is open, table is not displaying correct number of rows
history.push(
- getEndpointListPath({
- name: 'endpointList',
+ getHostListPath({
+ name: 'hostList',
...queryParams,
page_index: JSON.stringify(index),
page_size: JSON.stringify(size),
@@ -127,12 +125,12 @@ export const HostList = () => {
state: {
onCancelNavigateTo: [
'securitySolution:management',
- { path: getEndpointListPath({ name: 'endpointList' }) },
+ { path: getHostListPath({ name: 'hostList' }) },
],
- onCancelUrl: formatUrl(getEndpointListPath({ name: 'endpointList' })),
+ onCancelUrl: formatUrl(getHostListPath({ name: 'hostList' })),
onSaveNavigateTo: [
'securitySolution:management',
- { path: getEndpointListPath({ name: 'endpointList' }) },
+ { path: getHostListPath({ name: 'hostList' }) },
],
},
}
@@ -145,7 +143,7 @@ export const HostList = () => {
state: {
onDoneNavigateTo: [
'securitySolution:management',
- { path: getEndpointListPath({ name: 'endpointList' }) },
+ { path: getHostListPath({ name: 'hostList' }) },
],
},
});
@@ -191,10 +189,10 @@ export const HostList = () => {
defaultMessage: 'Hostname',
}),
render: ({ hostname, id }: HostInfo['metadata']['host']) => {
- const toRoutePath = getEndpointDetailsPath(
+ const toRoutePath = getHostDetailsPath(
{
...queryParams,
- name: 'endpointDetails',
+ name: 'hostDetails',
selected_host: id,
},
search
@@ -259,8 +257,8 @@ export const HostList = () => {
}),
// eslint-disable-next-line react/display-name
render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => {
- const toRoutePath = getEndpointDetailsPath({
- name: 'endpointPolicyResponse',
+ const toRoutePath = getHostDetailsPath({
+ name: 'hostPolicyResponse',
selected_host: item.metadata.host.id,
});
const toRouteUrl = formatUrl(toRoutePath);
@@ -341,7 +339,7 @@ export const HostList = () => {
);
} else if (!policyItemsLoading && policyItems && policyItems.length > 0) {
return (
- {
+
+
+
+
+
+
+
+
+
+
+
+ >
+ }
>
{hasSelectedHost && }
{listData && listData.length > 0 && (
diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx
index 0e81b75d651ba..2cf07b9b4382e 100644
--- a/x-pack/plugins/security_solution/public/management/pages/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx
@@ -9,25 +9,25 @@ import { useHistory, Route, Switch } from 'react-router-dom';
import { PolicyContainer } from './policy';
import {
- MANAGEMENT_ROUTING_ENDPOINTS_PATH,
+ MANAGEMENT_ROUTING_HOSTS_PATH,
MANAGEMENT_ROUTING_POLICIES_PATH,
MANAGEMENT_ROUTING_ROOT_PATH,
} from '../common/constants';
import { NotFoundPage } from '../../app/404';
-import { EndpointsContainer } from './endpoint_hosts';
-import { getEndpointListPath } from '../common/routing';
+import { HostsContainer } from './endpoint_hosts';
+import { getHostListPath } from '../common/routing';
export const ManagementContainer = memo(() => {
const history = useHistory();
return (
-
+
{
- history.replace(getEndpointListPath({ name: 'endpointList' }));
+ history.replace(getHostListPath({ name: 'hostList' }));
return null;
}}
/>
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
index 447a70ef998a9..aa7e867e89d6a 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
@@ -8,6 +8,8 @@ import React, { useCallback, useEffect, useMemo, CSSProperties, useState } from
import {
EuiBasicTable,
EuiText,
+ EuiTitle,
+ EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiTableFieldDataColumnType,
@@ -20,7 +22,6 @@ import {
EuiOverlayMask,
EuiConfirmModal,
EuiCallOut,
- EuiSpacer,
EuiButton,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@@ -391,9 +392,27 @@ export const PolicyList = React.memo(() => {
+
+
+
+
+
+
+
+
+
+
+
+ >
+ }
headerRight={
const policyDetailsSelector = (state: State) =>
state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE];
const endpointsSelector = (state: State) =>
- state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_ENDPOINTS_NAMESPACE];
+ state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_HOSTS_NAMESPACE];
export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = (
coreStart,
diff --git a/x-pack/plugins/security_solution/public/management/store/reducer.ts b/x-pack/plugins/security_solution/public/management/store/reducer.ts
index 2ed3dfe86d2f8..f3c470fb1e8a3 100644
--- a/x-pack/plugins/security_solution/public/management/store/reducer.ts
+++ b/x-pack/plugins/security_solution/public/management/store/reducer.ts
@@ -14,7 +14,7 @@ import {
initialPolicyListState,
} from '../pages/policy/store/policy_list/reducer';
import {
- MANAGEMENT_STORE_ENDPOINTS_NAMESPACE,
+ MANAGEMENT_STORE_HOSTS_NAMESPACE,
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
MANAGEMENT_STORE_POLICY_LIST_NAMESPACE,
} from '../common/constants';
@@ -31,7 +31,7 @@ const immutableCombineReducers: ImmutableCombineReducers = combineReducers;
export const mockManagementState: Immutable = {
[MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: initialPolicyListState(),
[MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(),
- [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialHostListState,
+ [MANAGEMENT_STORE_HOSTS_NAMESPACE]: initialHostListState,
};
/**
@@ -40,5 +40,5 @@ export const mockManagementState: Immutable = {
export const managementReducer = immutableCombineReducers({
[MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: policyListReducer,
[MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer,
- [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: hostListReducer,
+ [MANAGEMENT_STORE_HOSTS_NAMESPACE]: hostListReducer,
});
diff --git a/x-pack/plugins/security_solution/public/management/types.ts b/x-pack/plugins/security_solution/public/management/types.ts
index 854e9faa0204d..cb21a236ddd7e 100644
--- a/x-pack/plugins/security_solution/public/management/types.ts
+++ b/x-pack/plugins/security_solution/public/management/types.ts
@@ -18,14 +18,14 @@ export type ManagementStoreGlobalNamespace = 'management';
export type ManagementState = CombinedState<{
policyList: PolicyListState;
policyDetails: PolicyDetailsState;
- endpoints: HostState;
+ hosts: HostState;
}>;
/**
* The management list of sub-tabs. Changes to these will impact the Router routes.
*/
export enum ManagementSubTab {
- endpoints = 'endpoints',
+ hosts = 'hosts',
policies = 'policy',
}
diff --git a/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx b/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx
index ee048f0d61212..3758bd10bfc8f 100644
--- a/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx
@@ -7,13 +7,13 @@
import React, { memo } from 'react';
import { EuiCallOut, EuiButton, EuiButtonEmpty } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import { getEndpointListPath } from '../../../management/common/routing';
+import { getHostListPath } from '../../../management/common/routing';
import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
import { useManagementFormatUrl } from '../../../management/components/hooks/use_management_format_url';
import { MANAGEMENT_APP_ID } from '../../../management/common/constants';
export const EndpointNotice = memo<{ onDismiss: () => void }>(({ onDismiss }) => {
- const endpointsPath = getEndpointListPath({ name: 'endpointList' });
+ const endpointsPath = getHostListPath({ name: 'hostList' });
const endpointsLink = useManagementFormatUrl(endpointsPath);
const handleGetStartedClick = useNavigateToAppEventHandler(MANAGEMENT_APP_ID, {
path: endpointsPath,
diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx
index 18072c25e6dde..2b7fc160110f5 100644
--- a/x-pack/plugins/security_solution/public/plugin.tsx
+++ b/x-pack/plugins/security_solution/public/plugin.tsx
@@ -48,6 +48,15 @@ import { ConfigureEndpointPackageConfig } from './management/pages/policy/view/i
import { State, createStore, createInitialState } from './common/store';
import { SecurityPageName } from './app/types';
import { manageOldSiemRoutes } from './helpers';
+import {
+ OVERVIEW,
+ HOSTS,
+ NETWORK,
+ TIMELINES,
+ ALERTS,
+ CASE,
+ ADMINISTRATION,
+} from './app/home/translations';
export class Plugin implements IPlugin {
private kibanaVersion: string;
@@ -95,10 +104,12 @@ export class Plugin implements IPlugin {
+ mount: async () => {
const [{ application }] = await core.getStartServices();
application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { replace: true });
return () => true;
@@ -107,9 +118,7 @@ export class Plugin implements IPlugin = (
activeDescendantId: null,
selectedDescendantId: null,
processEntityIdOfSelectedDescendant: null,
- panelToDisplay: null,
},
action
) => {
@@ -39,11 +38,6 @@ const uiReducer: Reducer = (
selectedDescendantId: action.payload.nodeId,
processEntityIdOfSelectedDescendant: action.payload.selectedProcessId,
};
- } else if (action.type === 'appDisplayedDifferentPanel') {
- return {
- ...uiState,
- panelToDisplay: action.payload,
- };
} else if (
action.type === 'userBroughtProcessIntoView' ||
action.type === 'appDetectedNewIdFromQueryParams'
diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
index e54193ab394a5..2bc254d118d33 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
@@ -127,11 +127,6 @@ export const uiSelectedDescendantProcessId = composeSelectors(
uiSelectors.selectedDescendantProcessId
);
-/**
- * The current panel to display
- */
-export const currentPanelView = composeSelectors(uiStateSelector, uiSelectors.currentPanelView);
-
/**
* Returns the camera state from within ResolverState
*/
diff --git a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts
index bddc7d34abf1c..494d8884329c6 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts
@@ -39,8 +39,3 @@ export const selectedDescendantProcessId = createSelector(
return processEntityIdOfSelectedDescendant;
}
);
-
-// Select the current panel to be displayed
-export const currentPanelView = (uiState: ResolverUIState) => {
- return uiState.panelToDisplay;
-};
diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts
index 5dd9a944b88ea..2025762a0605c 100644
--- a/x-pack/plugins/security_solution/public/resolver/types.ts
+++ b/x-pack/plugins/security_solution/public/resolver/types.ts
@@ -45,10 +45,6 @@ export interface ResolverUIState {
* The entity_id of the process for the resolver's currently selected descendant.
*/
readonly processEntityIdOfSelectedDescendant: string | null;
- /**
- * Which panel the ui should display
- */
- readonly panelToDisplay: string | null;
}
/**
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx
index 2a2e7e87394a9..f4fe4fe520c92 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx
@@ -4,17 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, {
- memo,
- useCallback,
- useMemo,
- useContext,
- useLayoutEffect,
- useState,
- useEffect,
-} from 'react';
+import React, { memo, useCallback, useMemo, useContext, useLayoutEffect, useState } from 'react';
import { useSelector } from 'react-redux';
-import { useHistory } from 'react-router-dom';
+import { useHistory, useLocation } from 'react-router-dom';
// eslint-disable-next-line import/no-nodejs-modules
import querystring from 'querystring';
import { EuiPanel } from '@elastic/eui';
@@ -48,7 +40,7 @@ import { CrumbInfo } from './panels/panel_content_utilities';
*/
const PanelContent = memo(function PanelContent() {
const history = useHistory();
- const urlSearch = history.location.search;
+ const urlSearch = useLocation().search;
const dispatch = useResolverDispatch();
const { timestamp } = useContext(SideEffectContext);
@@ -205,21 +197,12 @@ const PanelContent = memo(function PanelContent() {
return 'processListWithCounts';
}, [uiSelectedEvent, crumbEvent, crumbId, graphableProcessEntityIds]);
- useEffect(() => {
- // dispatch `appDisplayedDifferentPanel` to sync state with which panel gets displayed
- dispatch({
- type: 'appDisplayedDifferentPanel',
- payload: panelToShow,
- });
- }, [panelToShow, dispatch]);
-
- const currentPanelView = useSelector(selectors.currentPanelView);
const terminatedProcesses = useSelector(selectors.terminatedProcesses);
const processEntityId = uiSelectedEvent ? event.entityId(uiSelectedEvent) : undefined;
const isProcessTerminated = processEntityId ? terminatedProcesses.has(processEntityId) : false;
const panelInstance = useMemo(() => {
- if (currentPanelView === 'processDetails') {
+ if (panelToShow === 'processDetails') {
return (
sum + val, 0);
@@ -278,7 +261,7 @@ const PanelContent = memo(function PanelContent() {
crumbId,
pushToQueryParams,
relatedStatsForIdFromParams,
- currentPanelView,
+ panelToShow,
isProcessTerminated,
]);
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx
index 56f88ccb13115..517b847855647 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx
@@ -82,10 +82,13 @@ const invalidDateText = i18n.translate(
}
);
/**
- * @param {ConstructorParameters[0]} timestamp To be passed through Date->Intl.DateTimeFormat
* @returns {string} A nicely formatted string for a date
*/
-export function formatDate(timestamp: ConstructorParameters[0]) {
+export function formatDate(
+ /** To be passed through Date->Intl.DateTimeFormat */ timestamp: ConstructorParameters<
+ typeof Date
+ >[0]
+): string {
const date = new Date(timestamp);
if (isFinite(date.getTime())) {
return formatter.format(date);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts
index 7b07548af67ae..8d55d00b50e13 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts
@@ -298,6 +298,7 @@ export const IMPORT_FAILED_DETAILED = (id: string, statusCode: number, message:
export const TEMPLATE_CALL_OUT_MESSAGE = i18n.translate(
'xpack.securitySolution.timelines.components.templateCallOutMessageTitle',
{
- defaultMessage: 'Now you can add timeline templates and link it to rules.',
+ defaultMessage:
+ 'Prebuit detection rules are now packaged with Timeline templates. You can also create your own Timeline templates and associate them with any rule.',
}
);
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts
index f5345c3dce222..84a18cb1573dd 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts
@@ -28,7 +28,7 @@ import {
import {
CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
CREATE_TIMELINE_ERROR_MESSAGE,
-} from './utils/create_timelines';
+} from './utils/failure_cases';
describe('create timelines', () => {
let server: ReturnType;
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts
index fb4991d7d1e7d..0f4e8f3204e2b 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts
@@ -46,7 +46,7 @@ import { createTimelines } from './utils/create_timelines';
import { TimelineStatus } from '../../../../common/types/timeline';
const CHUNK_PARSED_OBJECT_SIZE = 10;
-const DEFAULT_IMPORT_ERROR = `Something went wrong, there's something we didn't handle properly, please help us improve by providing the file you try to import on https://discuss.elastic.co/c/security/siem`;
+const DEFAULT_IMPORT_ERROR = `Something has gone wrong. We didn't handle something properly. To help us fix this, please upload your file to https://discuss.elastic.co/c/security/siem.`;
export const importTimelinesRoute = (
router: IRouter,
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts
index abe298566341c..67965469e1a9f 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts
@@ -13,11 +13,6 @@ import { SavedTimeline, TimelineSavedObject } from '../../../../../common/types/
import { SavedNote } from '../../../../../common/types/timeline/note';
import { NoteResult, ResponseTimeline } from '../../../../graphql/types';
-export const CREATE_TIMELINE_ERROR_MESSAGE =
- 'UPDATE timeline with POST is not allowed, please use PATCH instead';
-export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE =
- 'UPDATE template timeline with POST is not allowed, please use PATCH instead';
-
export const saveTimelines = (
frameworkRequest: FrameworkRequest,
timeline: SavedTimeline,
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts
index 60ba5389280c4..5e7a73ca18d0e 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts
@@ -11,27 +11,31 @@ import {
} from '../../../../../common/types/timeline';
export const UPDATE_TIMELINE_ERROR_MESSAGE =
- 'CREATE timeline with PATCH is not allowed, please use POST instead';
+ 'You cannot create new timelines with PATCH. Use POST instead.';
export const UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE =
- "CREATE template timeline with PATCH is not allowed, please use POST instead (Given template timeline doesn't exist)";
+ 'You cannot create new Timeline templates with PATCH. Use POST instead (templateTimelineId does not exist).';
export const NO_MATCH_VERSION_ERROR_MESSAGE =
- 'TimelineVersion conflict: The given version doesn not match with existing timeline';
+ 'Timeline template version conflict. The provided templateTimelineVersion does not match the current template.';
export const NO_MATCH_ID_ERROR_MESSAGE =
- "Timeline id doesn't match with existing template timeline";
-export const TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE = 'Template timelineVersion conflict';
+ 'There are no Timeline templates that match the provided templateTimelineId.';
+export const TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE =
+ 'To update existing Timeline templates, you must increment the templateTimelineVersion value.';
export const CREATE_TIMELINE_ERROR_MESSAGE =
- 'UPDATE timeline with POST is not allowed, please use PATCH instead';
+ 'You cannot update timelines with POST. Use PATCH instead.';
export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE =
- 'UPDATE template timeline with POST is not allowed, please use PATCH instead';
-export const EMPTY_TITLE_ERROR_MESSAGE = 'Title cannot be empty';
-export const UPDATE_STATUS_ERROR_MESSAGE = 'Update an immutable timeline is is not allowed';
+ 'You cannot update Timeline templates with POST. Use PATCH instead.';
+export const EMPTY_TITLE_ERROR_MESSAGE = 'The title field cannot be empty.';
+export const UPDATE_STATUS_ERROR_MESSAGE =
+ 'You are not allowed to set the status field value to immutable.';
export const CREATE_TEMPLATE_TIMELINE_WITHOUT_VERSION_ERROR_MESSAGE =
- 'Create template timeline without a valid templateTimelineVersion is not allowed. Please start from 1 to create a new template timeline';
-export const CREATE_WITH_INVALID_STATUS_ERROR_MESSAGE = 'Cannot create a draft timeline';
-export const NOT_ALLOW_UPDATE_STATUS_ERROR_MESSAGE = 'Update status is not allowed';
-export const NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE = 'Update timelineType is not allowed';
+ 'You must provide a valid templateTimelineVersion value. Use 1 for new Timeline templates.';
+export const CREATE_WITH_INVALID_STATUS_ERROR_MESSAGE =
+ 'You are not allowed to set the status field value to draft.';
+export const NOT_ALLOW_UPDATE_STATUS_ERROR_MESSAGE = 'You are not allowed to set the status field.';
+export const NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE =
+ 'You cannot convert a Timeline template to a timeline, or a timeline to a Timeline template.';
export const UPDAT_TIMELINE_VIA_IMPORT_NOT_ALLOWED_ERROR_MESSAGE =
- 'Update timeline via import is not allowed';
+ 'You cannot update a timeline via imports. Use the UI to modify existing timelines.';
const isUpdatingStatus = (
isHandlingTemplateTimeline: boolean,
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_button.tsx
similarity index 80%
rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx
rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_button.tsx
index aa78dfb4315f9..4686ede7bc2c2 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_button.tsx
@@ -7,7 +7,7 @@
import React, { FC, useContext } from 'react';
import { useHistory } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
-import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
+import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui';
import {
createCapabilityFailureMessage,
@@ -20,7 +20,7 @@ interface CloneActionProps {
itemId: string;
}
-export const CloneAction: FC = ({ itemId }) => {
+export const CloneButton: FC = ({ itemId }) => {
const history = useHistory();
const { canCreateTransform } = useContext(AuthorizationContext).capabilities;
@@ -34,17 +34,15 @@ export const CloneAction: FC