({
);
}, [pageSize, fieldGroupsToShow, accordionState]);
+ const hasSpecialFields = Boolean(fieldGroupsToCollapse[0]?.[1]?.fields?.length);
+
return (
{
if (el && !el.dataset.dynamicScroll) {
el.dataset.dynamicScroll = 'true';
@@ -114,9 +127,7 @@ function InnerFieldListGrouped({
>
{hasSyncedExistingFields
? [
- fieldGroups.SelectedFields &&
- (!fieldGroups.SelectedFields?.hideIfEmpty ||
- fieldGroups.SelectedFields?.fields?.length > 0) &&
+ shouldIncludeGroupDescriptionInAria(fieldGroups.SelectedFields) &&
i18n.translate(
'unifiedFieldList.fieldListGrouped.fieldSearchForSelectedFieldsLiveRegion',
{
@@ -127,6 +138,17 @@ function InnerFieldListGrouped({
},
}
),
+ shouldIncludeGroupDescriptionInAria(fieldGroups.PopularFields) &&
+ i18n.translate(
+ 'unifiedFieldList.fieldListGrouped.fieldSearchForPopularFieldsLiveRegion',
+ {
+ defaultMessage:
+ '{popularFields} popular {popularFields, plural, one {field} other {fields}}.',
+ values: {
+ popularFields: fieldGroups.PopularFields?.fields?.length || 0,
+ },
+ }
+ ),
fieldGroups.AvailableFields?.fields &&
i18n.translate(
'unifiedFieldList.fieldListGrouped.fieldSearchForAvailableFieldsLiveRegion',
@@ -138,9 +160,18 @@ function InnerFieldListGrouped({
},
}
),
- fieldGroups.EmptyFields &&
- (!fieldGroups.EmptyFields?.hideIfEmpty ||
- fieldGroups.EmptyFields?.fields?.length > 0) &&
+ shouldIncludeGroupDescriptionInAria(fieldGroups.UnmappedFields) &&
+ i18n.translate(
+ 'unifiedFieldList.fieldListGrouped.fieldSearchForUnmappedFieldsLiveRegion',
+ {
+ defaultMessage:
+ '{unmappedFields} unmapped {unmappedFields, plural, one {field} other {fields}}.',
+ values: {
+ unmappedFields: fieldGroups.UnmappedFields?.fields?.length || 0,
+ },
+ }
+ ),
+ shouldIncludeGroupDescriptionInAria(fieldGroups.EmptyFields) &&
i18n.translate(
'unifiedFieldList.fieldListGrouped.fieldSearchForEmptyFieldsLiveRegion',
{
@@ -151,9 +182,7 @@ function InnerFieldListGrouped({
},
}
),
- fieldGroups.MetaFields &&
- (!fieldGroups.MetaFields?.hideIfEmpty ||
- fieldGroups.MetaFields?.fields?.length > 0) &&
+ shouldIncludeGroupDescriptionInAria(fieldGroups.MetaFields) &&
i18n.translate(
'unifiedFieldList.fieldListGrouped.fieldSearchForMetaFieldsLiveRegion',
{
@@ -171,16 +200,26 @@ function InnerFieldListGrouped({
)}
-
- {fieldGroupsToCollapse.flatMap(([, { fields }]) =>
- fields.map((field, index) => (
-
- {renderFieldItem({ field, itemIndex: index, groupIndex: 0, hideDetails: true })}
-
- ))
- )}
-
-
+ {hasSpecialFields && (
+ <>
+
+ {fieldGroupsToCollapse.flatMap(([key, { fields }]) =>
+ fields.map((field, index) => (
+
+ {renderFieldItem({
+ field,
+ itemIndex: index,
+ groupIndex: 0,
+ groupName: key as FieldsGroupNames,
+ hideDetails: true,
+ })}
+
+ ))
+ )}
+
+
+ >
+ )}
{fieldGroupsToShow.map(([key, fieldGroup], index) => {
const hidden = Boolean(fieldGroup.hideIfEmpty) && !fieldGroup.fields.length;
if (hidden) {
@@ -199,6 +238,7 @@ function InnerFieldListGrouped({
isFiltered={fieldGroup.fieldCount !== fieldGroup.fields.length}
paginatedFields={paginatedFields[key]}
groupIndex={index + 1}
+ groupName={key as FieldsGroupNames}
onToggle={(open) => {
setAccordionState((s) => ({
...s,
@@ -224,6 +264,7 @@ function InnerFieldListGrouped({
isAffectedByFieldFilter={fieldGroup.fieldCount !== fieldGroup.fields.length}
fieldsExistInIndex={!!fieldsExistInIndex}
defaultNoFieldsMessage={fieldGroup.defaultNoFieldsMessage}
+ data-test-subj={`${dataTestSubject}${key}NoFieldsCallout`}
/>
)}
renderFieldItem={renderFieldItem}
@@ -243,3 +284,13 @@ const FieldListGrouped = React.memo(InnerFieldListGrouped) as GenericFieldListGr
// Necessary for React.lazy
// eslint-disable-next-line import/no-default-export
export default FieldListGrouped;
+
+function shouldIncludeGroupDescriptionInAria(
+ group: FieldsGroup | undefined
+): boolean {
+ if (!group) {
+ return false;
+ }
+ // has some fields or an empty list should be still shown
+ return group.fields?.length > 0 || !group.hideIfEmpty;
+}
diff --git a/src/plugins/unified_field_list/public/components/field_list/fields_accordion.test.tsx b/src/plugins/unified_field_list/public/components/field_list/fields_accordion.test.tsx
index 2804c1bbe5ee1..6c94f8a8e8335 100644
--- a/src/plugins/unified_field_list/public/components/field_list/fields_accordion.test.tsx
+++ b/src/plugins/unified_field_list/public/components/field_list/fields_accordion.test.tsx
@@ -11,7 +11,7 @@ import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/
import { EuiLoadingSpinner, EuiNotificationBadge, EuiText } from '@elastic/eui';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { FieldsAccordion, FieldsAccordionProps } from './fields_accordion';
-import { FieldListItem } from '../../types';
+import { FieldListItem, FieldsGroupNames } from '../../types';
describe('UnifiedFieldList ', () => {
let defaultProps: FieldsAccordionProps;
@@ -21,7 +21,8 @@ describe('UnifiedFieldList ', () => {
defaultProps = {
initialIsOpen: true,
onToggle: jest.fn(),
- groupIndex: 0,
+ groupIndex: 1,
+ groupName: FieldsGroupNames.AvailableFields,
id: 'id',
label: 'label-test',
hasLoaded: true,
diff --git a/src/plugins/unified_field_list/public/components/field_list/fields_accordion.tsx b/src/plugins/unified_field_list/public/components/field_list/fields_accordion.tsx
index 5222cf1b0e678..8b7ca22bff676 100644
--- a/src/plugins/unified_field_list/public/components/field_list/fields_accordion.tsx
+++ b/src/plugins/unified_field_list/public/components/field_list/fields_accordion.tsx
@@ -18,7 +18,7 @@ import {
} from '@elastic/eui';
import classNames from 'classnames';
import { type DataViewField } from '@kbn/data-views-plugin/common';
-import type { FieldListItem } from '../../types';
+import { type FieldListItem, FieldsGroupNames } from '../../types';
import './fields_accordion.scss';
export interface FieldsAccordionProps {
@@ -32,12 +32,14 @@ export interface FieldsAccordionProps {
hideDetails?: boolean;
isFiltered: boolean;
groupIndex: number;
+ groupName: FieldsGroupNames;
paginatedFields: T[];
renderFieldItem: (params: {
field: T;
hideDetails?: boolean;
itemIndex: number;
groupIndex: number;
+ groupName: FieldsGroupNames;
}) => JSX.Element;
renderCallout: () => JSX.Element;
showExistenceFetchError?: boolean;
@@ -55,6 +57,7 @@ function InnerFieldsAccordion({
hideDetails,
isFiltered,
groupIndex,
+ groupName,
paginatedFields,
renderFieldItem,
renderCallout,
@@ -99,6 +102,9 @@ function InnerFieldsAccordion({
content={i18n.translate('unifiedFieldList.fieldsAccordion.existenceErrorLabel', {
defaultMessage: "Field information can't be loaded",
})}
+ iconProps={{
+ 'data-test-subj': `${id}-fetchWarning`,
+ }}
/>
);
}
@@ -128,7 +134,7 @@ function InnerFieldsAccordion({
);
}
- return ;
+ return ;
}, [showExistenceFetchError, showExistenceFetchTimeout, hasLoaded, isFiltered, id, fieldsCount]);
return (
@@ -146,8 +152,8 @@ function InnerFieldsAccordion({
{paginatedFields &&
paginatedFields.map((field, index) => (
-
- {renderFieldItem({ field, itemIndex: index, groupIndex, hideDetails })}
+
+ {renderFieldItem({ field, itemIndex: index, groupIndex, groupName, hideDetails })}
))}
@@ -159,3 +165,6 @@ function InnerFieldsAccordion({
}
export const FieldsAccordion = React.memo(InnerFieldsAccordion) as typeof InnerFieldsAccordion;
+
+export const getFieldKey = (field: FieldListItem): string =>
+ `${field.name}-${field.displayName}-${field.type}`;
diff --git a/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.test.tsx b/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.test.tsx
index 03936a89877ba..5a18a261d136d 100644
--- a/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.test.tsx
+++ b/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.test.tsx
@@ -16,6 +16,7 @@ describe('UnifiedFieldList ', () => {
expect(component).toMatchInlineSnapshot(`
@@ -26,6 +27,7 @@ describe('UnifiedFieldList ', () => {
expect(component).toMatchInlineSnapshot(`
@@ -38,6 +40,7 @@ describe('UnifiedFieldList ', () => {
expect(component).toMatchInlineSnapshot(`
@@ -51,6 +54,7 @@ describe('UnifiedFieldList ', () => {
expect(component).toMatchInlineSnapshot(`
@@ -78,6 +82,7 @@ describe('UnifiedFieldList ', () => {
expect(component).toMatchInlineSnapshot(`
@@ -108,6 +113,7 @@ describe('UnifiedFieldList ', () => {
expect(component).toMatchInlineSnapshot(`
@@ -139,6 +145,7 @@ describe('UnifiedFieldList ', () => {
expect(component).toMatchInlineSnapshot(`
diff --git a/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.tsx b/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.tsx
index 3d24b400da3cb..3eca7573d9110 100644
--- a/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.tsx
+++ b/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.tsx
@@ -23,12 +23,14 @@ export const NoFieldsCallout = ({
isAffectedByFieldFilter = false,
isAffectedByTimerange = false,
isAffectedByGlobalFilter = false,
+ 'data-test-subj': dataTestSubject = 'noFieldsCallout',
}: {
fieldsExistInIndex: boolean;
isAffectedByFieldFilter?: boolean;
defaultNoFieldsMessage?: string;
isAffectedByTimerange?: boolean;
isAffectedByGlobalFilter?: boolean;
+ 'data-test-subj'?: string;
}) => {
if (!fieldsExistInIndex) {
return (
@@ -38,6 +40,7 @@ export const NoFieldsCallout = ({
title={i18n.translate('unifiedFieldList.fieldList.noFieldsCallout.noFieldsLabel', {
defaultMessage: 'No fields exist in this data view.',
})}
+ data-test-subj={`${dataTestSubject}-noFieldsExist`}
/>
);
}
@@ -53,6 +56,7 @@ export const NoFieldsCallout = ({
})
: defaultNoFieldsMessage
}
+ data-test-subj={`${dataTestSubject}-noFieldsMatch`}
>
{(isAffectedByTimerange || isAffectedByFieldFilter || isAffectedByGlobalFilter) && (
<>
diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx
index 312abc2bb323f..317fe9082d28e 100644
--- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx
+++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx
@@ -8,6 +8,7 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
+import { ReactWrapper } from 'enzyme';
import { EuiLoadingSpinner, EuiProgress } from '@elastic/eui';
import { coreMock } from '@kbn/core/public/mocks';
import { mountWithIntl } from '@kbn/test-jest-helpers';
@@ -120,6 +121,18 @@ describe('UnifiedFieldList ', () => {
});
});
+ async function mountComponent(component: React.ReactElement): Promise {
+ let wrapper: ReactWrapper;
+ await act(async () => {
+ wrapper = await mountWithIntl(component);
+ // wait for lazy modules if any
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ await wrapper.update();
+ });
+
+ return wrapper!;
+ }
+
beforeEach(() => {
(loadFieldStats as jest.Mock).mockReset();
(loadFieldStats as jest.Mock).mockImplementation(() => Promise.resolve({}));
@@ -134,7 +147,7 @@ describe('UnifiedFieldList ', () => {
});
});
- const wrapper = mountWithIntl(
+ const wrapper = await mountComponent(
', () => {
/>
);
- await wrapper.update();
-
expect(loadFieldStats).toHaveBeenCalledWith({
abortController: new AbortController(),
services: { data: mockedServices.data },
@@ -260,33 +271,27 @@ describe('UnifiedFieldList ', () => {
});
it('should not request field stats for range fields', async () => {
- const wrapper = await mountWithIntl(
+ const wrapper = await mountComponent(
f.name === 'ip_range')!} />
);
- await wrapper.update();
-
expect(loadFieldStats).toHaveBeenCalled();
expect(wrapper.text()).toBe('Analysis is not available for this field.');
});
it('should not request field stats for geo fields', async () => {
- const wrapper = await mountWithIntl(
+ const wrapper = await mountComponent(
f.name === 'geo_shape')!} />
);
- await wrapper.update();
-
expect(loadFieldStats).toHaveBeenCalled();
expect(wrapper.text()).toBe('Analysis is not available for this field.');
});
it('should render a message if no data is found', async () => {
- const wrapper = await mountWithIntl();
-
- await wrapper.update();
+ const wrapper = await mountComponent();
expect(loadFieldStats).toHaveBeenCalled();
@@ -302,9 +307,7 @@ describe('UnifiedFieldList ', () => {
});
});
- const wrapper = mountWithIntl();
-
- await wrapper.update();
+ const wrapper = await mountComponent();
await act(async () => {
resolveFunction!({
@@ -330,7 +333,7 @@ describe('UnifiedFieldList ', () => {
});
});
- const wrapper = mountWithIntl(
+ const wrapper = await mountComponent(
', () => {
/>
);
- await wrapper.update();
-
expect(loadFieldStats).toHaveBeenCalledWith({
abortController: new AbortController(),
services: { data: mockedServices.data },
@@ -433,7 +434,7 @@ describe('UnifiedFieldList ', () => {
});
});
- const wrapper = mountWithIntl(
+ const wrapper = await mountComponent(
', () => {
/>
);
- await wrapper.update();
-
expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1);
await act(async () => {
@@ -507,7 +506,7 @@ describe('UnifiedFieldList ', () => {
});
});
- const wrapper = mountWithIntl(
+ const wrapper = await mountComponent(
', () => {
/>
);
- await wrapper.update();
-
expect(loadFieldStats).toHaveBeenCalledWith({
abortController: new AbortController(),
services: { data: mockedServices.data },
@@ -615,7 +612,7 @@ describe('UnifiedFieldList ', () => {
const field = dataView.fields.find((f) => f.name === 'machine.ram')!;
- const wrapper = mountWithIntl(
+ const wrapper = await mountComponent(
', () => {
/>
);
- await wrapper.update();
-
expect(loadFieldStats).toHaveBeenCalledWith({
abortController: new AbortController(),
services: { data: mockedServices.data },
diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx
index eafdcc0dab69a..99c31fc9f64e1 100755
--- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx
+++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx
@@ -378,33 +378,36 @@ const FieldStatsComponent: React.FC = ({
if (histogram && histogram.buckets.length && topValues && topValues.buckets.length) {
title = (
- {
- setShowingHistogram(optionId === 'histogram');
- }}
- idSelected={showingHistogram ? 'histogram' : 'topValues'}
- />
+ <>
+ {
+ setShowingHistogram(optionId === 'histogram');
+ }}
+ idSelected={showingHistogram ? 'histogram' : 'topValues'}
+ />
+
+ >
);
} else if (field.type === 'date') {
title = (
diff --git a/src/plugins/unified_field_list/public/hooks/__snapshots__/use_grouped_fields.test.tsx.snap b/src/plugins/unified_field_list/public/hooks/__snapshots__/use_grouped_fields.test.tsx.snap
new file mode 100644
index 0000000000000..d3c95c363695d
--- /dev/null
+++ b/src/plugins/unified_field_list/public/hooks/__snapshots__/use_grouped_fields.test.tsx.snap
@@ -0,0 +1,259 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`UnifiedFieldList useGroupedFields() should work correctly for no data 1`] = `
+Object {
+ "AvailableFields": Object {
+ "defaultNoFieldsMessage": "There are no available fields that contain data.",
+ "fieldCount": 0,
+ "fields": Array [],
+ "hideDetails": false,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": true,
+ "isInitiallyOpen": true,
+ "showInAccordion": true,
+ "title": "Available fields",
+ },
+ "EmptyFields": Object {
+ "defaultNoFieldsMessage": "There are no empty fields.",
+ "fieldCount": 0,
+ "fields": Array [],
+ "helpText": "Fields that don't have any values based on your filters.",
+ "hideDetails": false,
+ "hideIfEmpty": false,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": false,
+ "isInitiallyOpen": false,
+ "showInAccordion": true,
+ "title": "Empty fields",
+ },
+ "MetaFields": Object {
+ "defaultNoFieldsMessage": "There are no meta fields.",
+ "fieldCount": 0,
+ "fields": Array [],
+ "hideDetails": false,
+ "hideIfEmpty": false,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": false,
+ "isInitiallyOpen": false,
+ "showInAccordion": true,
+ "title": "Meta fields",
+ },
+ "PopularFields": Object {
+ "fieldCount": 0,
+ "fields": Array [],
+ "helpText": "Fields that your organization frequently uses, from most to least popular.",
+ "hideDetails": false,
+ "hideIfEmpty": true,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": true,
+ "isInitiallyOpen": true,
+ "showInAccordion": true,
+ "title": "Popular fields",
+ },
+ "SelectedFields": Object {
+ "fieldCount": 0,
+ "fields": Array [],
+ "hideDetails": false,
+ "hideIfEmpty": true,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": true,
+ "isInitiallyOpen": true,
+ "showInAccordion": true,
+ "title": "Selected fields",
+ },
+ "SpecialFields": Object {
+ "fieldCount": 0,
+ "fields": Array [],
+ "hideDetails": true,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": false,
+ "isInitiallyOpen": false,
+ "showInAccordion": false,
+ "title": "",
+ },
+ "UnmappedFields": Object {
+ "fieldCount": 0,
+ "fields": Array [],
+ "helpText": "Fields that aren't explicitly mapped to a field data type.",
+ "hideDetails": false,
+ "hideIfEmpty": true,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": true,
+ "isInitiallyOpen": false,
+ "showInAccordion": true,
+ "title": "Unmapped fields",
+ },
+}
+`;
+
+exports[`UnifiedFieldList useGroupedFields() should work correctly in loading state 1`] = `
+Object {
+ "AvailableFields": Object {
+ "defaultNoFieldsMessage": "There are no available fields that contain data.",
+ "fieldCount": 0,
+ "fields": Array [],
+ "hideDetails": false,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": true,
+ "isInitiallyOpen": true,
+ "showInAccordion": true,
+ "title": "Available fields",
+ },
+ "EmptyFields": Object {
+ "defaultNoFieldsMessage": "There are no empty fields.",
+ "fieldCount": 0,
+ "fields": Array [],
+ "helpText": "Fields that don't have any values based on your filters.",
+ "hideDetails": false,
+ "hideIfEmpty": false,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": false,
+ "isInitiallyOpen": false,
+ "showInAccordion": true,
+ "title": "Empty fields",
+ },
+ "MetaFields": Object {
+ "defaultNoFieldsMessage": "There are no meta fields.",
+ "fieldCount": 0,
+ "fields": Array [],
+ "hideDetails": false,
+ "hideIfEmpty": false,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": false,
+ "isInitiallyOpen": false,
+ "showInAccordion": true,
+ "title": "Meta fields",
+ },
+ "PopularFields": Object {
+ "fieldCount": 0,
+ "fields": Array [],
+ "helpText": "Fields that your organization frequently uses, from most to least popular.",
+ "hideDetails": false,
+ "hideIfEmpty": true,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": true,
+ "isInitiallyOpen": true,
+ "showInAccordion": true,
+ "title": "Popular fields",
+ },
+ "SelectedFields": Object {
+ "fieldCount": 0,
+ "fields": Array [],
+ "hideDetails": false,
+ "hideIfEmpty": true,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": true,
+ "isInitiallyOpen": true,
+ "showInAccordion": true,
+ "title": "Selected fields",
+ },
+ "SpecialFields": Object {
+ "fieldCount": 0,
+ "fields": Array [],
+ "hideDetails": true,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": false,
+ "isInitiallyOpen": false,
+ "showInAccordion": false,
+ "title": "",
+ },
+ "UnmappedFields": Object {
+ "fieldCount": 0,
+ "fields": Array [],
+ "helpText": "Fields that aren't explicitly mapped to a field data type.",
+ "hideDetails": false,
+ "hideIfEmpty": true,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": true,
+ "isInitiallyOpen": false,
+ "showInAccordion": true,
+ "title": "Unmapped fields",
+ },
+}
+`;
+
+exports[`UnifiedFieldList useGroupedFields() should work correctly when global filters are set 1`] = `
+Object {
+ "AvailableFields": Object {
+ "defaultNoFieldsMessage": "There are no available fields that contain data.",
+ "fieldCount": 0,
+ "fields": Array [],
+ "hideDetails": false,
+ "isAffectedByGlobalFilter": true,
+ "isAffectedByTimeFilter": true,
+ "isInitiallyOpen": true,
+ "showInAccordion": true,
+ "title": "Available fields",
+ },
+ "EmptyFields": Object {
+ "defaultNoFieldsMessage": "There are no empty fields.",
+ "fieldCount": 0,
+ "fields": Array [],
+ "helpText": "Fields that don't have any values based on your filters.",
+ "hideDetails": false,
+ "hideIfEmpty": false,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": false,
+ "isInitiallyOpen": false,
+ "showInAccordion": true,
+ "title": "Empty fields",
+ },
+ "MetaFields": Object {
+ "defaultNoFieldsMessage": "There are no meta fields.",
+ "fieldCount": 0,
+ "fields": Array [],
+ "hideDetails": false,
+ "hideIfEmpty": false,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": false,
+ "isInitiallyOpen": false,
+ "showInAccordion": true,
+ "title": "Meta fields",
+ },
+ "PopularFields": Object {
+ "fieldCount": 0,
+ "fields": Array [],
+ "helpText": "Fields that your organization frequently uses, from most to least popular.",
+ "hideDetails": false,
+ "hideIfEmpty": true,
+ "isAffectedByGlobalFilter": true,
+ "isAffectedByTimeFilter": true,
+ "isInitiallyOpen": true,
+ "showInAccordion": true,
+ "title": "Popular fields",
+ },
+ "SelectedFields": Object {
+ "fieldCount": 0,
+ "fields": Array [],
+ "hideDetails": false,
+ "hideIfEmpty": true,
+ "isAffectedByGlobalFilter": true,
+ "isAffectedByTimeFilter": true,
+ "isInitiallyOpen": true,
+ "showInAccordion": true,
+ "title": "Selected fields",
+ },
+ "SpecialFields": Object {
+ "fieldCount": 0,
+ "fields": Array [],
+ "hideDetails": true,
+ "isAffectedByGlobalFilter": false,
+ "isAffectedByTimeFilter": false,
+ "isInitiallyOpen": false,
+ "showInAccordion": false,
+ "title": "",
+ },
+ "UnmappedFields": Object {
+ "fieldCount": 0,
+ "fields": Array [],
+ "helpText": "Fields that aren't explicitly mapped to a field data type.",
+ "hideDetails": false,
+ "hideIfEmpty": true,
+ "isAffectedByGlobalFilter": true,
+ "isAffectedByTimeFilter": true,
+ "isInitiallyOpen": false,
+ "showInAccordion": true,
+ "title": "Unmapped fields",
+ },
+}
+`;
diff --git a/src/plugins/unified_field_list/public/hooks/use_existing_fields.ts b/src/plugins/unified_field_list/public/hooks/use_existing_fields.ts
index ebf12d4609500..583ca32ce7508 100644
--- a/src/plugins/unified_field_list/public/hooks/use_existing_fields.ts
+++ b/src/plugins/unified_field_list/public/hooks/use_existing_fields.ts
@@ -15,7 +15,6 @@ import {
DataPublicPluginStart,
DataViewsContract,
getEsQueryConfig,
- UI_SETTINGS,
} from '@kbn/data-plugin/public';
import { type DataView } from '@kbn/data-plugin/common';
import { loadFieldExisting } from '../services/field_existing';
@@ -32,11 +31,12 @@ export interface ExistingFieldsInfo {
}
export interface ExistingFieldsFetcherParams {
+ disableAutoFetching?: boolean;
dataViews: DataView[];
- fromDate: string;
- toDate: string;
- query: Query | AggregateQuery;
- filters: Filter[];
+ fromDate: string | undefined; // fetching will be skipped if `undefined`
+ toDate: string | undefined;
+ query: Query | AggregateQuery | undefined;
+ filters: Filter[] | undefined;
services: {
core: Pick;
data: DataPublicPluginStart;
@@ -89,7 +89,7 @@ export const useExistingFieldsFetcher = (
dataViewId: string | undefined;
fetchId: string;
}): Promise => {
- if (!dataViewId) {
+ if (!dataViewId || !query || !fromDate || !toDate) {
return;
}
@@ -123,7 +123,7 @@ export const useExistingFieldsFetcher = (
dslQuery: await buildSafeEsQuery(
dataView,
query,
- filters,
+ filters || [],
getEsQueryConfig(core.uiSettings)
),
fromDate,
@@ -137,11 +137,11 @@ export const useExistingFieldsFetcher = (
const existingFieldNames = result?.existingFieldNames || [];
- const metaFields = core.uiSettings.get(UI_SETTINGS.META_FIELDS) || [];
if (
- !existingFieldNames.filter((fieldName) => !metaFields.includes?.(fieldName)).length &&
+ onNoData &&
numberOfFetches === 1 &&
- onNoData
+ !existingFieldNames.filter((fieldName) => !dataView?.metaFields?.includes(fieldName))
+ .length
) {
onNoData(dataViewId);
}
@@ -173,12 +173,17 @@ export const useExistingFieldsFetcher = (
async (dataViewId?: string) => {
const fetchId = generateId();
lastFetchId = fetchId;
+
+ const options = {
+ fetchId,
+ dataViewId,
+ ...params,
+ };
// refetch only for the specified data view
if (dataViewId) {
await fetchFieldsExistenceInfo({
- fetchId,
+ ...options,
dataViewId,
- ...params,
});
return;
}
@@ -186,9 +191,8 @@ export const useExistingFieldsFetcher = (
await Promise.all(
params.dataViews.map((dataView) =>
fetchFieldsExistenceInfo({
- fetchId,
+ ...options,
dataViewId: dataView.id,
- ...params,
})
)
);
@@ -205,8 +209,10 @@ export const useExistingFieldsFetcher = (
);
useEffect(() => {
- refetchFieldsExistenceInfo();
- }, [refetchFieldsExistenceInfo]);
+ if (!params.disableAutoFetching) {
+ refetchFieldsExistenceInfo();
+ }
+ }, [refetchFieldsExistenceInfo, params.disableAutoFetching]);
useEffect(() => {
return () => {
diff --git a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx
index d4d6d3cdc906f..df4b3f684647f 100644
--- a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx
+++ b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx
@@ -20,6 +20,12 @@ import { ExistenceFetchStatus, FieldListGroups, FieldsGroupNames } from '../type
describe('UnifiedFieldList useGroupedFields()', () => {
let mockedServices: GroupedFieldsParams['services'];
const allFields = dataView.fields;
+ // Added fields will be treated as Unmapped as they are not a part of the data view.
+ const allFieldsIncludingUnmapped = [...new Array(2)].flatMap((_, index) =>
+ allFields.map((field) => {
+ return new DataViewField({ ...field.toSpec(), name: `${field.name}${index || ''}` });
+ })
+ );
const anotherDataView = createStubDataView({
spec: {
id: 'another-data-view',
@@ -39,14 +45,43 @@ describe('UnifiedFieldList useGroupedFields()', () => {
});
});
+ it('should work correctly in loading state', async () => {
+ const props: GroupedFieldsParams = {
+ dataViewId: dataView.id!,
+ allFields: null,
+ services: mockedServices,
+ };
+ const { result, waitForNextUpdate, rerender } = renderHook(useGroupedFields, {
+ initialProps: props,
+ });
+
+ await waitForNextUpdate();
+
+ expect(result.current.fieldGroups).toMatchSnapshot();
+ expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.unknown);
+ expect(result.current.fieldsExistInIndex).toBe(false);
+ expect(result.current.scrollToTopResetCounter).toBeTruthy();
+
+ rerender({
+ ...props,
+ dataViewId: null, // for text-based queries
+ allFields: null,
+ });
+
+ expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.unknown);
+ expect(result.current.fieldsExistInIndex).toBe(true);
+ expect(result.current.scrollToTopResetCounter).toBeTruthy();
+ });
+
it('should work correctly for no data', async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useGroupedFields({
- dataViewId: dataView.id!,
- allFields: [],
- services: mockedServices,
- })
- );
+ const props: GroupedFieldsParams = {
+ dataViewId: dataView.id!,
+ allFields: [],
+ services: mockedServices,
+ };
+ const { result, waitForNextUpdate, rerender } = renderHook(useGroupedFields, {
+ initialProps: props,
+ });
await waitForNextUpdate();
@@ -59,20 +94,36 @@ describe('UnifiedFieldList useGroupedFields()', () => {
).toStrictEqual([
'SpecialFields-0',
'SelectedFields-0',
+ 'PopularFields-0',
'AvailableFields-0',
+ 'UnmappedFields-0',
'EmptyFields-0',
'MetaFields-0',
]);
+
+ expect(fieldGroups).toMatchSnapshot();
+ expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded);
+ expect(result.current.fieldsExistInIndex).toBe(false);
+
+ rerender({
+ ...props,
+ dataViewId: null, // for text-based queries
+ allFields: [],
+ });
+
+ expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded);
+ expect(result.current.fieldsExistInIndex).toBe(true);
});
it('should work correctly with fields', async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useGroupedFields({
- dataViewId: dataView.id!,
- allFields,
- services: mockedServices,
- })
- );
+ const props: GroupedFieldsParams = {
+ dataViewId: dataView.id!,
+ allFields,
+ services: mockedServices,
+ };
+ const { result, waitForNextUpdate, rerender } = renderHook(useGroupedFields, {
+ initialProps: props,
+ });
await waitForNextUpdate();
@@ -85,48 +136,116 @@ describe('UnifiedFieldList useGroupedFields()', () => {
).toStrictEqual([
'SpecialFields-0',
'SelectedFields-0',
+ 'PopularFields-0',
'AvailableFields-25',
+ 'UnmappedFields-0',
'EmptyFields-0',
'MetaFields-3',
]);
+
+ expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded);
+ expect(result.current.fieldsExistInIndex).toBe(true);
+
+ rerender({
+ ...props,
+ dataViewId: null, // for text-based queries
+ allFields,
+ });
+
+ expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded);
+ expect(result.current.fieldsExistInIndex).toBe(true);
});
it('should work correctly when filtered', async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useGroupedFields({
- dataViewId: dataView.id!,
- allFields,
- services: mockedServices,
- onFilterField: (field: DataViewField) => field.name.startsWith('@'),
- })
- );
+ const props: GroupedFieldsParams = {
+ dataViewId: dataView.id!,
+ allFields: allFieldsIncludingUnmapped,
+ services: mockedServices,
+ };
+ const { result, waitForNextUpdate, rerender } = renderHook(useGroupedFields, {
+ initialProps: props,
+ });
await waitForNextUpdate();
- const fieldGroups = result.current.fieldGroups;
+ let fieldGroups = result.current.fieldGroups;
+ const scrollToTopResetCounter1 = result.current.scrollToTopResetCounter;
expect(
Object.keys(fieldGroups!).map(
- (key) => `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}`
+ (key) =>
+ `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}-${
+ fieldGroups![key as FieldsGroupNames]?.fieldCount
+ }`
)
).toStrictEqual([
- 'SpecialFields-0',
- 'SelectedFields-0',
- 'AvailableFields-2',
- 'EmptyFields-0',
- 'MetaFields-0',
+ 'SpecialFields-0-0',
+ 'SelectedFields-0-0',
+ 'PopularFields-0-0',
+ 'AvailableFields-25-25',
+ 'UnmappedFields-28-28',
+ 'EmptyFields-0-0',
+ 'MetaFields-3-3',
+ ]);
+
+ rerender({
+ ...props,
+ onFilterField: (field: DataViewField) => field.name.startsWith('@'),
+ });
+
+ fieldGroups = result.current.fieldGroups;
+
+ expect(
+ Object.keys(fieldGroups!).map(
+ (key) =>
+ `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}-${
+ fieldGroups![key as FieldsGroupNames]?.fieldCount
+ }`
+ )
+ ).toStrictEqual([
+ 'SpecialFields-0-0',
+ 'SelectedFields-0-0',
+ 'PopularFields-0-0',
+ 'AvailableFields-2-25',
+ 'UnmappedFields-2-28',
+ 'EmptyFields-0-0',
+ 'MetaFields-0-3',
]);
+
+ expect(result.current.scrollToTopResetCounter).not.toBe(scrollToTopResetCounter1);
+ });
+
+ it('should not change the scroll position if fields list is extended', async () => {
+ const props: GroupedFieldsParams = {
+ dataViewId: dataView.id!,
+ allFields,
+ services: mockedServices,
+ };
+ const { result, waitForNextUpdate, rerender } = renderHook(useGroupedFields, {
+ initialProps: props,
+ });
+
+ await waitForNextUpdate();
+
+ const scrollToTopResetCounter1 = result.current.scrollToTopResetCounter;
+
+ rerender({
+ ...props,
+ allFields: allFieldsIncludingUnmapped,
+ });
+
+ expect(result.current.scrollToTopResetCounter).toBe(scrollToTopResetCounter1);
});
it('should work correctly when custom unsupported fields are skipped', async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useGroupedFields({
+ const { result, waitForNextUpdate } = renderHook(useGroupedFields, {
+ initialProps: {
dataViewId: dataView.id!,
allFields,
services: mockedServices,
onSupportedFieldFilter: (field: DataViewField) => field.aggregatable,
- })
- );
+ },
+ });
await waitForNextUpdate();
@@ -139,22 +258,24 @@ describe('UnifiedFieldList useGroupedFields()', () => {
).toStrictEqual([
'SpecialFields-0',
'SelectedFields-0',
+ 'PopularFields-0',
'AvailableFields-23',
+ 'UnmappedFields-0',
'EmptyFields-0',
'MetaFields-3',
]);
});
it('should work correctly when selected fields are present', async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useGroupedFields({
+ const { result, waitForNextUpdate } = renderHook(useGroupedFields, {
+ initialProps: {
dataViewId: dataView.id!,
allFields,
services: mockedServices,
onSelectedFieldFilter: (field: DataViewField) =>
['bytes', 'extension', '_id', '@timestamp'].includes(field.name),
- })
- );
+ },
+ });
await waitForNextUpdate();
@@ -167,20 +288,22 @@ describe('UnifiedFieldList useGroupedFields()', () => {
).toStrictEqual([
'SpecialFields-0',
'SelectedFields-4',
+ 'PopularFields-0',
'AvailableFields-25',
+ 'UnmappedFields-0',
'EmptyFields-0',
'MetaFields-3',
]);
});
it('should work correctly for text-based queries (no data view)', async () => {
- const { result } = renderHook(() =>
- useGroupedFields({
+ const { result } = renderHook(useGroupedFields, {
+ initialProps: {
dataViewId: null,
- allFields,
+ allFields: allFieldsIncludingUnmapped,
services: mockedServices,
- })
- );
+ },
+ });
const fieldGroups = result.current.fieldGroups;
@@ -188,24 +311,36 @@ describe('UnifiedFieldList useGroupedFields()', () => {
Object.keys(fieldGroups!).map(
(key) => `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}`
)
- ).toStrictEqual(['SpecialFields-0', 'SelectedFields-0', 'AvailableFields-28', 'MetaFields-0']);
+ ).toStrictEqual([
+ 'SpecialFields-0',
+ 'SelectedFields-0',
+ 'PopularFields-0',
+ 'AvailableFields-56', // even unmapped fields fall into Available
+ 'UnmappedFields-0',
+ 'MetaFields-0',
+ ]);
+
+ expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded);
+ expect(result.current.fieldsExistInIndex).toBe(true);
});
it('should work correctly when details are overwritten', async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useGroupedFields({
+ const onOverrideFieldGroupDetails: GroupedFieldsParams['onOverrideFieldGroupDetails'] =
+ jest.fn((groupName) => {
+ if (groupName === FieldsGroupNames.SelectedFields) {
+ return {
+ helpText: 'test',
+ };
+ }
+ });
+ const { result, waitForNextUpdate } = renderHook(useGroupedFields, {
+ initialProps: {
dataViewId: dataView.id!,
allFields,
services: mockedServices,
- onOverrideFieldGroupDetails: (groupName) => {
- if (groupName === FieldsGroupNames.SelectedFields) {
- return {
- helpText: 'test',
- };
- }
- },
- })
- );
+ onOverrideFieldGroupDetails,
+ },
+ });
await waitForNextUpdate();
@@ -213,6 +348,7 @@ describe('UnifiedFieldList useGroupedFields()', () => {
expect(fieldGroups[FieldsGroupNames.SelectedFields]?.helpText).toBe('test');
expect(fieldGroups[FieldsGroupNames.AvailableFields]?.helpText).not.toBe('test');
+ expect(onOverrideFieldGroupDetails).toHaveBeenCalled();
});
it('should work correctly when changing a data view and existence info is available only for one of them', async () => {
@@ -248,11 +384,16 @@ describe('UnifiedFieldList useGroupedFields()', () => {
).toStrictEqual([
'SpecialFields-0',
'SelectedFields-0',
+ 'PopularFields-0',
'AvailableFields-2',
+ 'UnmappedFields-0',
'EmptyFields-23',
'MetaFields-3',
]);
+ expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded);
+ expect(result.current.fieldsExistInIndex).toBe(true);
+
rerender({
...props,
dataViewId: anotherDataView.id!,
@@ -267,6 +408,133 @@ describe('UnifiedFieldList useGroupedFields()', () => {
Object.keys(fieldGroups!).map(
(key) => `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}`
)
- ).toStrictEqual(['SpecialFields-0', 'SelectedFields-0', 'AvailableFields-8', 'MetaFields-0']);
+ ).toStrictEqual([
+ 'SpecialFields-0',
+ 'SelectedFields-0',
+ 'PopularFields-0',
+ 'AvailableFields-8',
+ 'UnmappedFields-0',
+ 'MetaFields-0',
+ ]);
+
+ expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.unknown);
+ expect(result.current.fieldsExistInIndex).toBe(true);
+ });
+
+ it('should work correctly when popular fields limit is present', async () => {
+ // `bytes` is popular, but we are skipping it here to test that it would not be shown under Popular and Available
+ const onSupportedFieldFilter = jest.fn((field) => field.name !== 'bytes');
+
+ const { result, waitForNextUpdate } = renderHook(useGroupedFields, {
+ initialProps: {
+ dataViewId: dataView.id!,
+ allFields,
+ popularFieldsLimit: 10,
+ services: mockedServices,
+ onSupportedFieldFilter,
+ },
+ });
+
+ await waitForNextUpdate();
+
+ const fieldGroups = result.current.fieldGroups;
+
+ expect(
+ Object.keys(fieldGroups!).map(
+ (key) => `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}`
+ )
+ ).toStrictEqual([
+ 'SpecialFields-0',
+ 'SelectedFields-0',
+ 'PopularFields-3',
+ 'AvailableFields-24',
+ 'UnmappedFields-0',
+ 'EmptyFields-0',
+ 'MetaFields-3',
+ ]);
+
+ expect(fieldGroups.PopularFields?.fields.map((field) => field.name).join(',')).toBe(
+ '@timestamp,time,ssl'
+ );
+ });
+
+ it('should work correctly when global filters are set', async () => {
+ const { result, waitForNextUpdate } = renderHook(useGroupedFields, {
+ initialProps: {
+ dataViewId: dataView.id!,
+ allFields: [],
+ isAffectedByGlobalFilter: true,
+ services: mockedServices,
+ },
+ });
+
+ await waitForNextUpdate();
+
+ const fieldGroups = result.current.fieldGroups;
+ expect(fieldGroups).toMatchSnapshot();
+ });
+
+ it('should work correctly and show unmapped fields separately', async () => {
+ const { result, waitForNextUpdate } = renderHook(useGroupedFields, {
+ initialProps: {
+ dataViewId: dataView.id!,
+ allFields: allFieldsIncludingUnmapped,
+ services: mockedServices,
+ },
+ });
+
+ await waitForNextUpdate();
+
+ const fieldGroups = result.current.fieldGroups;
+
+ expect(
+ Object.keys(fieldGroups!).map(
+ (key) => `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}`
+ )
+ ).toStrictEqual([
+ 'SpecialFields-0',
+ 'SelectedFields-0',
+ 'PopularFields-0',
+ 'AvailableFields-25',
+ 'UnmappedFields-28',
+ 'EmptyFields-0',
+ 'MetaFields-3',
+ ]);
+ });
+
+ it('should work correctly when custom selected fields are provided', async () => {
+ const customSortedFields = [
+ allFieldsIncludingUnmapped[allFieldsIncludingUnmapped.length - 1],
+ allFields[2],
+ allFields[0],
+ ];
+ const { result, waitForNextUpdate } = renderHook(useGroupedFields, {
+ initialProps: {
+ dataViewId: dataView.id!,
+ allFields,
+ sortedSelectedFields: customSortedFields,
+ services: mockedServices,
+ },
+ });
+
+ await waitForNextUpdate();
+
+ const fieldGroups = result.current.fieldGroups;
+
+ expect(
+ Object.keys(fieldGroups!).map(
+ (key) => `${key}-${fieldGroups![key as FieldsGroupNames]?.fields.length}`
+ )
+ ).toStrictEqual([
+ 'SpecialFields-0',
+ 'SelectedFields-3',
+ 'PopularFields-0',
+ 'AvailableFields-25',
+ 'UnmappedFields-0',
+ 'EmptyFields-0',
+ 'MetaFields-3',
+ ]);
+
+ expect(fieldGroups.SelectedFields?.fields).toBe(customSortedFields);
});
});
diff --git a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts
index cfa5407a238cc..39d1258ee62d8 100644
--- a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts
+++ b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts
@@ -17,16 +17,20 @@ import {
type FieldsGroup,
type FieldListItem,
FieldsGroupNames,
+ ExistenceFetchStatus,
} from '../types';
import { type ExistingFieldsReader } from './use_existing_fields';
export interface GroupedFieldsParams {
dataViewId: string | null; // `null` is for text-based queries
- allFields: T[];
+ allFields: T[] | null; // `null` is for loading indicator
services: {
dataViews: DataViewsContract;
};
- fieldsExistenceReader?: ExistingFieldsReader;
+ fieldsExistenceReader?: ExistingFieldsReader; // use `undefined` for text-based queries
+ isAffectedByGlobalFilter?: boolean;
+ popularFieldsLimit?: number;
+ sortedSelectedFields?: T[];
onOverrideFieldGroupDetails?: (
groupName: FieldsGroupNames
) => Partial | undefined | null;
@@ -37,6 +41,9 @@ export interface GroupedFieldsParams {
export interface GroupedFieldsResult {
fieldGroups: FieldListGroups;
+ scrollToTopResetCounter: number;
+ fieldsExistenceStatus: ExistenceFetchStatus;
+ fieldsExistInIndex: boolean;
}
export function useGroupedFields({
@@ -44,12 +51,16 @@ export function useGroupedFields({
allFields,
services,
fieldsExistenceReader,
+ isAffectedByGlobalFilter = false,
+ popularFieldsLimit,
+ sortedSelectedFields,
onOverrideFieldGroupDetails,
onSupportedFieldFilter,
onSelectedFieldFilter,
onFilterField,
}: GroupedFieldsParams): GroupedFieldsResult {
const [dataView, setDataView] = useState(null);
+ const isAffectedByTimeFilter = Boolean(dataView?.timeFieldName);
const fieldsExistenceInfoUnavailable: boolean = dataViewId
? fieldsExistenceReader?.isFieldsExistenceInfoUnavailable(dataViewId) ?? false
: true;
@@ -68,33 +79,59 @@ export function useGroupedFields({
// if field existence information changed, reload the data view too
}, [dataViewId, services.dataViews, setDataView, hasFieldDataHandler]);
+ // important when switching from a known dataViewId to no data view (like in text-based queries)
+ useEffect(() => {
+ if (dataView && !dataViewId) {
+ setDataView(null);
+ }
+ }, [dataView, setDataView, dataViewId]);
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const scrollToTopResetCounter: number = useMemo(() => Date.now(), [dataViewId, onFilterField]);
+
const unfilteredFieldGroups: FieldListGroups = useMemo(() => {
const containsData = (field: T) => {
- if (!dataViewId || !dataView) {
- return true;
- }
- const overallField = dataView.getFieldByName?.(field.name);
- return Boolean(overallField && hasFieldDataHandler(dataViewId, overallField.name));
+ return dataViewId ? hasFieldDataHandler(dataViewId, field.name) : true;
};
- const fields = allFields || [];
- const allSupportedTypesFields = onSupportedFieldFilter
- ? fields.filter(onSupportedFieldFilter)
- : fields;
- const sortedFields = [...allSupportedTypesFields].sort(sortFields);
+ const selectedFields = sortedSelectedFields || [];
+ const sortedFields = [...(allFields || [])].sort(sortFields);
const groupedFields = {
...getDefaultFieldGroups(),
...groupBy(sortedFields, (field) => {
+ if (!sortedSelectedFields && onSelectedFieldFilter && onSelectedFieldFilter(field)) {
+ selectedFields.push(field);
+ }
+ if (onSupportedFieldFilter && !onSupportedFieldFilter(field)) {
+ return 'skippedFields';
+ }
if (field.type === 'document') {
return 'specialFields';
- } else if (dataView?.metaFields?.includes(field.name)) {
+ }
+ if (dataView?.metaFields?.includes(field.name)) {
return 'metaFields';
- } else if (containsData(field)) {
+ }
+ if (dataView?.getFieldByName && !dataView.getFieldByName(field.name)) {
+ return 'unmappedFields';
+ }
+ if (containsData(field) || fieldsExistenceInfoUnavailable) {
return 'availableFields';
- } else return 'emptyFields';
+ }
+ return 'emptyFields';
}),
};
- const selectedFields = onSelectedFieldFilter ? sortedFields.filter(onSelectedFieldFilter) : [];
+
+ const popularFields = popularFieldsLimit
+ ? sortedFields
+ .filter(
+ (field) =>
+ field.count &&
+ field.type !== '_source' &&
+ (!onSupportedFieldFilter || onSupportedFieldFilter(field))
+ )
+ .sort((a: T, b: T) => (b.count || 0) - (a.count || 0)) // sort by popularity score
+ .slice(0, popularFieldsLimit)
+ : [];
let fieldGroupDefinitions: FieldListGroups = {
SpecialFields: {
@@ -115,8 +152,25 @@ export function useGroupedFields({
title: i18n.translate('unifiedFieldList.useGroupedFields.selectedFieldsLabel', {
defaultMessage: 'Selected fields',
}),
- isAffectedByGlobalFilter: false,
- isAffectedByTimeFilter: true,
+ isAffectedByGlobalFilter,
+ isAffectedByTimeFilter,
+ hideDetails: false,
+ hideIfEmpty: true,
+ },
+ PopularFields: {
+ fields: popularFields,
+ fieldCount: popularFields.length,
+ isInitiallyOpen: true,
+ showInAccordion: true,
+ title: i18n.translate('unifiedFieldList.useGroupedFields.popularFieldsLabel', {
+ defaultMessage: 'Popular fields',
+ }),
+ helpText: i18n.translate('unifiedFieldList.useGroupedFields.popularFieldsLabelHelp', {
+ defaultMessage:
+ 'Fields that your organization frequently uses, from most to least popular.',
+ }),
+ isAffectedByGlobalFilter,
+ isAffectedByTimeFilter,
hideDetails: false,
hideIfEmpty: true,
},
@@ -133,8 +187,8 @@ export function useGroupedFields({
: i18n.translate('unifiedFieldList.useGroupedFields.availableFieldsLabel', {
defaultMessage: 'Available fields',
}),
- isAffectedByGlobalFilter: false,
- isAffectedByTimeFilter: true,
+ isAffectedByGlobalFilter,
+ isAffectedByTimeFilter,
// Show details on timeout but not failure
// hideDetails: fieldsExistenceInfoUnavailable && !existenceFetchTimeout, // TODO: is this check still necessary?
hideDetails: fieldsExistenceInfoUnavailable,
@@ -145,6 +199,22 @@ export function useGroupedFields({
}
),
},
+ UnmappedFields: {
+ fields: groupedFields.unmappedFields,
+ fieldCount: groupedFields.unmappedFields.length,
+ isAffectedByGlobalFilter,
+ isAffectedByTimeFilter,
+ isInitiallyOpen: false,
+ showInAccordion: true,
+ hideDetails: false,
+ hideIfEmpty: true,
+ title: i18n.translate('unifiedFieldList.useGroupedFields.unmappedFieldsLabel', {
+ defaultMessage: 'Unmapped fields',
+ }),
+ helpText: i18n.translate('unifiedFieldList.useGroupedFields.unmappedFieldsLabelHelp', {
+ defaultMessage: "Fields that aren't explicitly mapped to a field data type.",
+ }),
+ },
EmptyFields: {
fields: groupedFields.emptyFields,
fieldCount: groupedFields.emptyFields.length,
@@ -157,15 +227,15 @@ export function useGroupedFields({
title: i18n.translate('unifiedFieldList.useGroupedFields.emptyFieldsLabel', {
defaultMessage: 'Empty fields',
}),
+ helpText: i18n.translate('unifiedFieldList.useGroupedFields.emptyFieldsLabelHelp', {
+ defaultMessage: "Fields that don't have any values based on your filters.",
+ }),
defaultNoFieldsMessage: i18n.translate(
'unifiedFieldList.useGroupedFields.noEmptyDataLabel',
{
defaultMessage: `There are no empty fields.`,
}
),
- helpText: i18n.translate('unifiedFieldList.useGroupedFields.emptyFieldsLabelHelp', {
- defaultMessage: 'Empty fields did not contain any values based on your filters.',
- }),
},
MetaFields: {
fields: groupedFields.metaFields,
@@ -220,6 +290,10 @@ export function useGroupedFields({
dataViewId,
hasFieldDataHandler,
fieldsExistenceInfoUnavailable,
+ isAffectedByGlobalFilter,
+ isAffectedByTimeFilter,
+ popularFieldsLimit,
+ sortedSelectedFields,
]);
const fieldGroups: FieldListGroups = useMemo(() => {
@@ -235,22 +309,39 @@ export function useGroupedFields({
) as FieldListGroups;
}, [unfilteredFieldGroups, onFilterField]);
- return useMemo(
- () => ({
+ const hasDataLoaded = Boolean(allFields);
+ const allFieldsLength = allFields?.length;
+
+ const fieldsExistInIndex = useMemo(() => {
+ return dataViewId ? Boolean(allFieldsLength) : true;
+ }, [dataViewId, allFieldsLength]);
+
+ const fieldsExistenceStatus = useMemo(() => {
+ if (!hasDataLoaded) {
+ return ExistenceFetchStatus.unknown; // to show loading indicator in the list
+ }
+ if (!dataViewId || !fieldsExistenceReader) {
+ // ex. for text-based queries
+ return ExistenceFetchStatus.succeeded;
+ }
+ return fieldsExistenceReader.getFieldsExistenceStatus(dataViewId);
+ }, [dataViewId, hasDataLoaded, fieldsExistenceReader]);
+
+ return useMemo(() => {
+ return {
fieldGroups,
- }),
- [fieldGroups]
- );
+ scrollToTopResetCounter,
+ fieldsExistInIndex,
+ fieldsExistenceStatus,
+ };
+ }, [fieldGroups, scrollToTopResetCounter, fieldsExistInIndex, fieldsExistenceStatus]);
}
+const collator = new Intl.Collator(undefined, {
+ sensitivity: 'base',
+});
function sortFields(fieldA: T, fieldB: T) {
- return (fieldA.displayName || fieldA.name).localeCompare(
- fieldB.displayName || fieldB.name,
- undefined,
- {
- sensitivity: 'base',
- }
- );
+ return collator.compare(fieldA.displayName || fieldA.name, fieldB.displayName || fieldB.name);
}
function hasFieldDataByDefault(): boolean {
@@ -263,5 +354,7 @@ function getDefaultFieldGroups() {
availableFields: [],
emptyFields: [],
metaFields: [],
+ unmappedFields: [],
+ skippedFields: [],
};
}
diff --git a/src/plugins/unified_field_list/public/hooks/use_query_subscriber.ts b/src/plugins/unified_field_list/public/hooks/use_query_subscriber.ts
index 9b42db6301f8f..44101d206a2de 100644
--- a/src/plugins/unified_field_list/public/hooks/use_query_subscriber.ts
+++ b/src/plugins/unified_field_list/public/hooks/use_query_subscriber.ts
@@ -9,6 +9,7 @@
import { useEffect, useState } from 'react';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { AggregateQuery, Query, Filter } from '@kbn/es-query';
+import { getResolvedDateRange } from '../utils/get_resolved_date_range';
/**
* Hook params
@@ -23,32 +24,68 @@ export interface QuerySubscriberParams {
export interface QuerySubscriberResult {
query: Query | AggregateQuery | undefined;
filters: Filter[] | undefined;
+ fromDate: string | undefined;
+ toDate: string | undefined;
}
/**
- * Memorizes current query and filters
+ * Memorizes current query, filters and absolute date range
* @param data
+ * @public
*/
export const useQuerySubscriber = ({ data }: QuerySubscriberParams) => {
+ const timefilter = data.query.timefilter.timefilter;
const [result, setResult] = useState(() => {
const state = data.query.getState();
+ const dateRange = getResolvedDateRange(timefilter);
return {
query: state?.query,
filters: state?.filters,
+ fromDate: dateRange.fromDate,
+ toDate: dateRange.toDate,
};
});
useEffect(() => {
- const subscription = data.query.state$.subscribe(({ state }) => {
+ const subscription = data.search.session.state$.subscribe((sessionState) => {
+ const dateRange = getResolvedDateRange(timefilter);
setResult((prevState) => ({
...prevState,
- query: state.query,
- filters: state.filters,
+ fromDate: dateRange.fromDate,
+ toDate: dateRange.toDate,
}));
});
+ return () => subscription.unsubscribe();
+ }, [setResult, timefilter, data.search.session.state$]);
+
+ useEffect(() => {
+ const subscription = data.query.state$.subscribe(({ state, changes }) => {
+ if (changes.query || changes.filters) {
+ setResult((prevState) => ({
+ ...prevState,
+ query: state.query,
+ filters: state.filters,
+ }));
+ }
+ });
+
return () => subscription.unsubscribe();
}, [setResult, data.query.state$]);
return result;
};
+
+/**
+ * Checks if query result is ready to be used
+ * @param result
+ * @public
+ */
+export const hasQuerySubscriberData = (
+ result: QuerySubscriberResult
+): result is {
+ query: Query | AggregateQuery;
+ filters: Filter[];
+ fromDate: string;
+ toDate: string;
+} => Boolean(result.query && result.filters && result.fromDate && result.toDate);
diff --git a/src/plugins/unified_field_list/public/index.ts b/src/plugins/unified_field_list/public/index.ts
index e1a315401e0bc..68fddef0ffc16 100755
--- a/src/plugins/unified_field_list/public/index.ts
+++ b/src/plugins/unified_field_list/public/index.ts
@@ -76,6 +76,7 @@ export {
export {
useQuerySubscriber,
+ hasQuerySubscriberData,
type QuerySubscriberResult,
type QuerySubscriberParams,
} from './hooks/use_query_subscriber';
diff --git a/src/plugins/unified_field_list/public/types.ts b/src/plugins/unified_field_list/public/types.ts
index d2c80286f8dea..c28452ebc6f25 100755
--- a/src/plugins/unified_field_list/public/types.ts
+++ b/src/plugins/unified_field_list/public/types.ts
@@ -29,14 +29,17 @@ export interface FieldListItem {
name: DataViewField['name'];
type?: DataViewField['type'];
displayName?: DataViewField['displayName'];
+ count?: DataViewField['count'];
}
export enum FieldsGroupNames {
SpecialFields = 'SpecialFields',
SelectedFields = 'SelectedFields',
+ PopularFields = 'PopularFields',
AvailableFields = 'AvailableFields',
EmptyFields = 'EmptyFields',
MetaFields = 'MetaFields',
+ UnmappedFields = 'UnmappedFields',
}
export interface FieldsGroupDetails {
diff --git a/src/plugins/unified_field_list/public/utils/get_resolved_date_range.ts b/src/plugins/unified_field_list/public/utils/get_resolved_date_range.ts
new file mode 100644
index 0000000000000..3939c49d7f514
--- /dev/null
+++ b/src/plugins/unified_field_list/public/utils/get_resolved_date_range.ts
@@ -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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { type TimefilterContract } from '@kbn/data-plugin/public';
+
+/**
+ * Get resolved time range by using now provider
+ * @param timefilter
+ */
+export const getResolvedDateRange = (timefilter: TimefilterContract) => {
+ const { from, to } = timefilter.getTime();
+ const { min, max } = timefilter.calculateBounds({
+ from,
+ to,
+ });
+ return { fromDate: min?.toISOString() || from, toDate: max?.toISOString() || to };
+};
diff --git a/src/plugins/vis_types/timeseries/public/application/components/lib/replace_vars.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/replace_vars.ts
index d3a63f9f2421d..77ac817d53d28 100644
--- a/src/plugins/vis_types/timeseries/public/application/components/lib/replace_vars.ts
+++ b/src/plugins/vis_types/timeseries/public/application/components/lib/replace_vars.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { encode, RisonValue } from 'rison-node';
+import { encode } from '@kbn/rison';
import Handlebars, { ExtendedCompileOptions, compileFnName } from '@kbn/handlebars';
import { i18n } from '@kbn/i18n';
import { emptyLabel } from '../../../../common/empty_label';
@@ -41,7 +41,7 @@ function createSerializationHelper(
handlebars.registerHelper(
'rison',
- createSerializationHelper('rison', (v) => encode(v as RisonValue))
+ createSerializationHelper('rison', (v) => encode(v))
);
handlebars.registerHelper('encodeURIComponent', (component: unknown) => {
diff --git a/src/plugins/visualizations/common/locator_location.ts b/src/plugins/visualizations/common/locator_location.ts
index c4c86007fd124..e43fc9fcbd5e5 100644
--- a/src/plugins/visualizations/common/locator_location.ts
+++ b/src/plugins/visualizations/common/locator_location.ts
@@ -10,7 +10,7 @@ import type { Serializable } from '@kbn/utility-types';
import { omitBy } from 'lodash';
import type { ParsedQuery } from 'query-string';
import { stringify } from 'query-string';
-import rison from 'rison-node';
+import rison from '@kbn/rison';
import { isFilterPinned } from '@kbn/es-query';
import { url } from '@kbn/kibana-utils-plugin/common';
import { GLOBAL_STATE_STORAGE_KEY, STATE_STORAGE_KEY, VisualizeConstants } from './constants';
diff --git a/test/api_integration/apis/guided_onboarding/get_config.ts b/test/api_integration/apis/guided_onboarding/get_config.ts
new file mode 100644
index 0000000000000..fc96cb81c3816
--- /dev/null
+++ b/test/api_integration/apis/guided_onboarding/get_config.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import expect from '@kbn/expect';
+import type { FtrProviderContext } from '../../ftr_provider_context';
+
+const getConfigsPath = '/api/guided_onboarding/configs';
+export default function testGetGuidesState({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+
+ describe('GET /api/guided_onboarding/configs', () => {
+ // check that all guides are present
+ ['testGuide', 'security', 'search', 'observability'].map((guideId) => {
+ it(`returns config for ${guideId}`, async () => {
+ const response = await supertest.get(`${getConfigsPath}/${guideId}`).expect(200);
+ expect(response.body).not.to.be.empty();
+ const { config } = response.body;
+ expect(config).to.not.be.empty();
+ });
+ });
+ });
+}
diff --git a/test/api_integration/apis/guided_onboarding/index.ts b/test/api_integration/apis/guided_onboarding/index.ts
index c924eafe6bdb1..b2b3c23705763 100644
--- a/test/api_integration/apis/guided_onboarding/index.ts
+++ b/test/api_integration/apis/guided_onboarding/index.ts
@@ -13,5 +13,6 @@ export default function apiIntegrationTests({ loadTestFile }: FtrProviderContext
loadTestFile(require.resolve('./get_state'));
loadTestFile(require.resolve('./put_state'));
loadTestFile(require.resolve('./get_guides'));
+ loadTestFile(require.resolve('./get_config'));
});
}
diff --git a/test/functional/apps/discover/group1/_shared_links.ts b/test/functional/apps/discover/group1/_shared_links.ts
index 9235cd1160db7..edad2010db7ed 100644
--- a/test/functional/apps/discover/group1/_shared_links.ts
+++ b/test/functional/apps/discover/group1/_shared_links.ts
@@ -76,7 +76,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const expectedUrl =
baseUrl +
'/app/discover?_t=1453775307251#' +
- '/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time' +
+ '/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time' +
":(from:'2015-09-19T06:31:44.000Z',to:'2015-09" +
"-23T18:31:44.000Z'))&_a=(columns:!(),filters:!(),index:'logstash-" +
"*',interval:auto,query:(language:kuery,query:'')" +
@@ -102,7 +102,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
baseUrl +
'/app/discover#' +
'/view/ab12e3c0-f231-11e6-9486-733b1ac9221a' +
- '?_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!t%2Cvalue%3A0)' +
+ '?_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!t%2Cvalue%3A60000)' +
"%2Ctime%3A(from%3A'2015-09-19T06%3A31%3A44.000Z'%2C" +
"to%3A'2015-09-23T18%3A31%3A44.000Z'))";
await PageObjects.discover.loadSavedSearch('A Saved Search');
diff --git a/test/functional/apps/discover/group1/_sidebar.ts b/test/functional/apps/discover/group1/_sidebar.ts
index 585aae36196e6..109e8aa37cd38 100644
--- a/test/functional/apps/discover/group1/_sidebar.ts
+++ b/test/functional/apps/discover/group1/_sidebar.ts
@@ -20,22 +20,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'unifiedSearch',
]);
const testSubjects = getService('testSubjects');
+ const find = getService('find');
const browser = getService('browser');
+ const monacoEditor = getService('monacoEditor');
const filterBar = getService('filterBar');
+ const fieldEditor = getService('fieldEditor');
describe('discover sidebar', function describeIndexTests() {
before(async function () {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
+ });
+
+ beforeEach(async () => {
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
await kibanaServer.uiSettings.replace({
defaultIndex: 'logstash-*',
});
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
await PageObjects.common.navigateToApp('discover');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
});
- after(async () => {
+ afterEach(async () => {
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
+ await kibanaServer.savedObjects.cleanStandardList();
+ await kibanaServer.uiSettings.replace({});
});
describe('field filtering', function () {
@@ -107,5 +116,449 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.existOrFail('discover-sidebar');
});
});
+
+ describe('renders field groups', function () {
+ it('should show field list groups excluding subfields', async function () {
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+ expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true);
+
+ // Initial Available fields
+ const expectedInitialAvailableFields =
+ '@message, @tags, @timestamp, agent, bytes, clientip, extension, geo.coordinates, geo.dest, geo.src, geo.srcdest, headings, host, id, index, ip, links, machine.os, machine.ram, machine.ram_range, memory, meta.char, meta.related, meta.user.firstname, meta.user.lastname, nestedField.child, phpmemory, referer, relatedContent.article:modified_time, relatedContent.article:published_time, relatedContent.article:section, relatedContent.article:tag, relatedContent.og:description, relatedContent.og:image, relatedContent.og:image:height, relatedContent.og:image:width, relatedContent.og:site_name, relatedContent.og:title, relatedContent.og:type, relatedContent.og:url, relatedContent.twitter:card, relatedContent.twitter:description, relatedContent.twitter:image, relatedContent.twitter:site, relatedContent.twitter:title, relatedContent.url, request, response, spaces, type';
+ let availableFields = await PageObjects.discover.getSidebarSectionFieldNames('available');
+ expect(availableFields.length).to.be(50);
+ expect(availableFields.join(', ')).to.be(expectedInitialAvailableFields);
+
+ // Available fields after scrolling down
+ const emptySectionButton = await find.byCssSelector(
+ PageObjects.discover.getSidebarSectionSelector('empty', true)
+ );
+ await emptySectionButton.scrollIntoViewIfNecessary();
+ availableFields = await PageObjects.discover.getSidebarSectionFieldNames('available');
+ expect(availableFields.length).to.be(53);
+ expect(availableFields.join(', ')).to.be(
+ `${expectedInitialAvailableFields}, url, utc_time, xss`
+ );
+
+ // Expand Empty section
+ await PageObjects.discover.toggleSidebarSection('empty');
+ expect((await PageObjects.discover.getSidebarSectionFieldNames('empty')).join(', ')).to.be(
+ ''
+ );
+
+ // Expand Meta section
+ await PageObjects.discover.toggleSidebarSection('meta');
+ expect((await PageObjects.discover.getSidebarSectionFieldNames('meta')).join(', ')).to.be(
+ '_id, _index, _score'
+ );
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '53 available fields. 0 empty fields. 3 meta fields.'
+ );
+ });
+
+ it('should show field list groups excluding subfields when searched from source', async function () {
+ await kibanaServer.uiSettings.update({ 'discover:searchFieldsFromSource': true });
+ await browser.refresh();
+
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+ expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true);
+
+ // Initial Available fields
+ let availableFields = await PageObjects.discover.getSidebarSectionFieldNames('available');
+ expect(availableFields.length).to.be(50);
+ expect(
+ availableFields
+ .join(', ')
+ .startsWith(
+ '@message, @tags, @timestamp, agent, bytes, clientip, extension, geo.coordinates'
+ )
+ ).to.be(true);
+
+ // Available fields after scrolling down
+ const emptySectionButton = await find.byCssSelector(
+ PageObjects.discover.getSidebarSectionSelector('empty', true)
+ );
+ await emptySectionButton.scrollIntoViewIfNecessary();
+ availableFields = await PageObjects.discover.getSidebarSectionFieldNames('available');
+ expect(availableFields.length).to.be(53);
+
+ // Expand Empty section
+ await PageObjects.discover.toggleSidebarSection('empty');
+ expect((await PageObjects.discover.getSidebarSectionFieldNames('empty')).join(', ')).to.be(
+ ''
+ );
+
+ // Expand Meta section
+ await PageObjects.discover.toggleSidebarSection('meta');
+ expect((await PageObjects.discover.getSidebarSectionFieldNames('meta')).join(', ')).to.be(
+ '_id, _index, _score'
+ );
+
+ // Expand Unmapped section
+ await PageObjects.discover.toggleSidebarSection('unmapped');
+ expect(
+ (await PageObjects.discover.getSidebarSectionFieldNames('unmapped')).join(', ')
+ ).to.be('relatedContent');
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '53 available fields. 1 unmapped field. 0 empty fields. 3 meta fields.'
+ );
+ });
+
+ it('should show selected and popular fields', async function () {
+ await PageObjects.discover.clickFieldListItemAdd('extension');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ await PageObjects.discover.clickFieldListItemAdd('@message');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ expect(
+ (await PageObjects.discover.getSidebarSectionFieldNames('selected')).join(', ')
+ ).to.be('extension, @message');
+
+ const availableFields = await PageObjects.discover.getSidebarSectionFieldNames('available');
+ expect(availableFields.includes('extension')).to.be(true);
+ expect(availableFields.includes('@message')).to.be(true);
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '2 selected fields. 2 popular fields. 53 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ await PageObjects.discover.clickFieldListItemRemove('@message');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ await PageObjects.discover.clickFieldListItemAdd('_id');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ await PageObjects.discover.clickFieldListItemAdd('@message');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ expect(
+ (await PageObjects.discover.getSidebarSectionFieldNames('selected')).join(', ')
+ ).to.be('extension, _id, @message');
+
+ expect(
+ (await PageObjects.discover.getSidebarSectionFieldNames('popular')).join(', ')
+ ).to.be('@message, _id, extension');
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '3 selected fields. 3 popular fields. 53 available fields. 0 empty fields. 3 meta fields.'
+ );
+ });
+
+ it('should show selected and available fields in text-based mode', async function () {
+ await kibanaServer.uiSettings.update({ 'discover:enableSql': true });
+ await browser.refresh();
+
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '53 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ await PageObjects.discover.selectTextBaseLang('SQL');
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '50 selected fields. 51 available fields.'
+ );
+
+ await PageObjects.discover.clickFieldListItemRemove('extension');
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '49 selected fields. 51 available fields.'
+ );
+
+ const testQuery = `SELECT "@tags", geo.dest, count(*) occurred FROM "logstash-*"
+ GROUP BY "@tags", geo.dest
+ HAVING occurred > 20
+ ORDER BY occurred DESC`;
+
+ await monacoEditor.setCodeEditorValue(testQuery);
+ await testSubjects.click('querySubmitButton');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '3 selected fields. 3 available fields.'
+ );
+ expect(
+ (await PageObjects.discover.getSidebarSectionFieldNames('selected')).join(', ')
+ ).to.be('@tags, geo.dest, occurred');
+
+ await PageObjects.unifiedSearch.switchDataView(
+ 'discover-dataView-switch-link',
+ 'logstash-*',
+ true
+ );
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '1 popular field. 53 available fields. 0 empty fields. 3 meta fields.'
+ );
+ });
+
+ it('should work correctly for a data view for a missing index', async function () {
+ // but we are skipping importing the index itself
+ await kibanaServer.importExport.load(
+ 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield'
+ );
+ await browser.refresh();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '53 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ await PageObjects.discover.selectIndexPattern('with-timefield');
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '0 available fields. 0 meta fields.'
+ );
+ await testSubjects.existOrFail(
+ `${PageObjects.discover.getSidebarSectionSelector('available')}-fetchWarning`
+ );
+ await testSubjects.existOrFail(
+ `${PageObjects.discover.getSidebarSectionSelector(
+ 'available'
+ )}NoFieldsCallout-noFieldsExist`
+ );
+
+ await PageObjects.discover.selectIndexPattern('logstash-*');
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '53 available fields. 0 empty fields. 3 meta fields.'
+ );
+ await kibanaServer.importExport.unload(
+ 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield'
+ );
+ });
+
+ it('should work correctly when switching data views', async function () {
+ await esArchiver.loadIfNeeded(
+ 'test/functional/fixtures/es_archiver/index_pattern_without_timefield'
+ );
+ await kibanaServer.importExport.load(
+ 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield'
+ );
+
+ await browser.refresh();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '53 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ await PageObjects.discover.selectIndexPattern('without-timefield');
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '6 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ await PageObjects.discover.selectIndexPattern('with-timefield');
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '0 available fields. 7 empty fields. 3 meta fields.'
+ );
+ await testSubjects.existOrFail(
+ `${PageObjects.discover.getSidebarSectionSelector(
+ 'available'
+ )}NoFieldsCallout-noFieldsMatch`
+ );
+
+ await PageObjects.discover.selectIndexPattern('logstash-*');
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '53 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ await kibanaServer.importExport.unload(
+ 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield'
+ );
+
+ await esArchiver.unload(
+ 'test/functional/fixtures/es_archiver/index_pattern_without_timefield'
+ );
+ });
+
+ it('should work when filters change', async () => {
+ await PageObjects.header.waitUntilLoadingHasFinished();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '53 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ await PageObjects.discover.clickFieldListItem('extension');
+ expect(await testSubjects.getVisibleText('dscFieldStats-topValues')).to.be(
+ 'jpg\n65.0%\ncss\n15.4%\npng\n9.8%\ngif\n6.6%\nphp\n3.2%'
+ );
+
+ await filterBar.addFilter('extension', 'is', 'jpg');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '53 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ // check that the filter was passed down to the sidebar
+ await PageObjects.discover.clickFieldListItem('extension');
+ expect(await testSubjects.getVisibleText('dscFieldStats-topValues')).to.be('jpg\n100%');
+ });
+
+ it('should work for many fields', async () => {
+ await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/many_fields');
+ await kibanaServer.importExport.load(
+ 'test/functional/fixtures/kbn_archiver/many_fields_data_view'
+ );
+
+ await browser.refresh();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '53 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ await PageObjects.discover.selectIndexPattern('indices-stats*');
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '6873 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ await PageObjects.discover.selectIndexPattern('logstash-*');
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '53 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ await kibanaServer.importExport.unload(
+ 'test/functional/fixtures/kbn_archiver/many_fields_data_view'
+ );
+ await esArchiver.unload('test/functional/fixtures/es_archiver/many_fields');
+ });
+
+ it('should work with ad-hoc data views and runtime fields', async () => {
+ await PageObjects.discover.createAdHocDataView('logstash', true);
+ await PageObjects.header.waitUntilLoadingHasFinished();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '53 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ await PageObjects.discover.addRuntimeField(
+ '_bytes-runtimefield',
+ `emit((doc["bytes"].value * 2).toString())`
+ );
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '54 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ let allFields = await PageObjects.discover.getAllFieldNames();
+ expect(allFields.includes('_bytes-runtimefield')).to.be(true);
+
+ await PageObjects.discover.editField('_bytes-runtimefield');
+ await fieldEditor.enableCustomLabel();
+ await fieldEditor.setCustomLabel('_bytes-runtimefield2');
+ await fieldEditor.save();
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '54 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ allFields = await PageObjects.discover.getAllFieldNames();
+ expect(allFields.includes('_bytes-runtimefield2')).to.be(true);
+ expect(allFields.includes('_bytes-runtimefield')).to.be(false);
+
+ await PageObjects.discover.removeField('_bytes-runtimefield');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '53 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ allFields = await PageObjects.discover.getAllFieldNames();
+ expect(allFields.includes('_bytes-runtimefield2')).to.be(false);
+ expect(allFields.includes('_bytes-runtimefield')).to.be(false);
+ });
+
+ it('should work correctly when time range is updated', async function () {
+ await esArchiver.loadIfNeeded(
+ 'test/functional/fixtures/es_archiver/index_pattern_without_timefield'
+ );
+ await kibanaServer.importExport.load(
+ 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield'
+ );
+
+ await browser.refresh();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '53 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ await PageObjects.discover.selectIndexPattern('with-timefield');
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '0 available fields. 7 empty fields. 3 meta fields.'
+ );
+ await testSubjects.existOrFail(
+ `${PageObjects.discover.getSidebarSectionSelector(
+ 'available'
+ )}NoFieldsCallout-noFieldsMatch`
+ );
+
+ await PageObjects.timePicker.setAbsoluteRange(
+ 'Sep 21, 2019 @ 00:00:00.000',
+ 'Sep 23, 2019 @ 00:00:00.000'
+ );
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSidebarHasLoaded();
+
+ expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
+ '7 available fields. 0 empty fields. 3 meta fields.'
+ );
+
+ await kibanaServer.importExport.unload(
+ 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield'
+ );
+
+ await esArchiver.unload(
+ 'test/functional/fixtures/es_archiver/index_pattern_without_timefield'
+ );
+ });
+ });
});
}
diff --git a/test/functional/apps/discover/group2/_adhoc_data_views.ts b/test/functional/apps/discover/group2/_adhoc_data_views.ts
index 773471994237f..50eb3be5f07d1 100644
--- a/test/functional/apps/discover/group2/_adhoc_data_views.ts
+++ b/test/functional/apps/discover/group2/_adhoc_data_views.ts
@@ -51,6 +51,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
after(async () => {
+ await kibanaServer.savedObjects.cleanStandardList();
await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional');
});
diff --git a/test/functional/apps/discover/group2/_indexpattern_with_unmapped_fields.ts b/test/functional/apps/discover/group2/_indexpattern_with_unmapped_fields.ts
index d5d45d227d685..a17e6c0798a78 100644
--- a/test/functional/apps/discover/group2/_indexpattern_with_unmapped_fields.ts
+++ b/test/functional/apps/discover/group2/_indexpattern_with_unmapped_fields.ts
@@ -22,8 +22,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] });
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/unmapped_fields');
await security.testUser.setRoles(['kibana_admin', 'test-index-unmapped-fields']);
- const fromTime = 'Jan 20, 2021 @ 00:00:00.000';
- const toTime = 'Jan 25, 2021 @ 00:00:00.000';
+ const fromTime = '2021-01-20T00:00:00.000Z';
+ const toTime = '2021-01-25T00:00:00.000Z';
await kibanaServer.uiSettings.replace({
defaultIndex: 'test-index-unmapped-fields',
@@ -48,11 +48,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async function () {
expect(await PageObjects.discover.getHitCount()).to.be(expectedHitCount);
});
- const allFields = await PageObjects.discover.getAllFieldNames();
+ let allFields = await PageObjects.discover.getAllFieldNames();
// message is a mapped field
expect(allFields.includes('message')).to.be(true);
// sender is not a mapped field
- expect(allFields.includes('sender')).to.be(true);
+ expect(allFields.includes('sender')).to.be(false);
+
+ await PageObjects.discover.toggleSidebarSection('unmapped');
+
+ allFields = await PageObjects.discover.getAllFieldNames();
+ expect(allFields.includes('sender')).to.be(true); // now visible under Unmapped section
+
+ await PageObjects.discover.toggleSidebarSection('unmapped');
});
it('unmapped fields exist on an existing saved search', async () => {
@@ -61,10 +68,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async function () {
expect(await PageObjects.discover.getHitCount()).to.be(expectedHitCount);
});
- const allFields = await PageObjects.discover.getAllFieldNames();
+ let allFields = await PageObjects.discover.getAllFieldNames();
expect(allFields.includes('message')).to.be(true);
+ expect(allFields.includes('sender')).to.be(false);
+ expect(allFields.includes('receiver')).to.be(false);
+
+ await PageObjects.discover.toggleSidebarSection('unmapped');
+
+ allFields = await PageObjects.discover.getAllFieldNames();
+
+ // now visible under Unmapped section
expect(allFields.includes('sender')).to.be(true);
expect(allFields.includes('receiver')).to.be(true);
+
+ await PageObjects.discover.toggleSidebarSection('unmapped');
});
});
}
diff --git a/test/functional/apps/discover/group2/_search_on_page_load.ts b/test/functional/apps/discover/group2/_search_on_page_load.ts
index be738c3708854..2adeb9606d5f6 100644
--- a/test/functional/apps/discover/group2/_search_on_page_load.ts
+++ b/test/functional/apps/discover/group2/_search_on_page_load.ts
@@ -25,6 +25,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
defaultIndex: 'logstash-*',
};
+ const savedSearchName = 'saved-search-with-on-page-load';
+
const initSearchOnPageLoad = async (searchOnPageLoad: boolean) => {
await kibanaServer.uiSettings.replace({ 'discover:searchOnPageLoad': searchOnPageLoad });
await PageObjects.common.navigateToApp('discover');
@@ -60,6 +62,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await esArchiver.unload('test/functional/fixtures/es_archiver/date_nested');
await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.uiSettings.replace(defaultSettings);
+ await kibanaServer.savedObjects.cleanStandardList();
});
describe(`when it's false`, () => {
@@ -68,6 +71,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should not fetch data from ES initially', async function () {
expect(await testSubjects.exists(refreshButtonSelector)).to.be(true);
await retry.waitFor('number of fetches to be 0', waitForFetches(0));
+ expect(await PageObjects.discover.doesSidebarShowFields()).to.be(false);
});
it('should not fetch on indexPattern change', async function () {
@@ -78,43 +82,77 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await testSubjects.exists(refreshButtonSelector)).to.be(true);
await retry.waitFor('number of fetches to be 0', waitForFetches(0));
+ expect(await PageObjects.discover.doesSidebarShowFields()).to.be(false);
});
it('should fetch data from ES after refreshDataButton click', async function () {
expect(await testSubjects.exists(refreshButtonSelector)).to.be(true);
await retry.waitFor('number of fetches to be 0', waitForFetches(0));
+ expect(await PageObjects.discover.doesSidebarShowFields()).to.be(false);
await testSubjects.click(refreshButtonSelector);
await testSubjects.missingOrFail(refreshButtonSelector);
await retry.waitFor('number of fetches to be 1', waitForFetches(1));
+ expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true);
});
it('should fetch data from ES after submit query', async function () {
expect(await testSubjects.exists(refreshButtonSelector)).to.be(true);
await retry.waitFor('number of fetches to be 0', waitForFetches(0));
+ expect(await PageObjects.discover.doesSidebarShowFields()).to.be(false);
await queryBar.submitQuery();
await testSubjects.missingOrFail(refreshButtonSelector);
await retry.waitFor('number of fetches to be 1', waitForFetches(1));
+ expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true);
});
it('should fetch data from ES after choosing commonly used time range', async function () {
await PageObjects.discover.selectIndexPattern('logstash-*');
expect(await testSubjects.exists(refreshButtonSelector)).to.be(true);
await retry.waitFor('number of fetches to be 0', waitForFetches(0));
+ expect(await PageObjects.discover.doesSidebarShowFields()).to.be(false);
await PageObjects.timePicker.setCommonlyUsedTime('This_week');
await testSubjects.missingOrFail(refreshButtonSelector);
await retry.waitFor('number of fetches to be 1', waitForFetches(1));
+ expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true);
+ });
+
+ it('should fetch data when a search is saved', async function () {
+ await PageObjects.discover.selectIndexPattern('logstash-*');
+
+ await retry.waitFor('number of fetches to be 0', waitForFetches(0));
+ expect(await PageObjects.discover.doesSidebarShowFields()).to.be(false);
+
+ await PageObjects.discover.saveSearch(savedSearchName);
+
+ await retry.waitFor('number of fetches to be 1', waitForFetches(1));
+ expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true);
+ });
+
+ it('should reset state after opening a saved search and pressing New', async function () {
+ await PageObjects.discover.loadSavedSearch(savedSearchName);
+ await PageObjects.header.waitUntilLoadingHasFinished();
+
+ await retry.waitFor('number of fetches to be 1', waitForFetches(1));
+ expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true);
+
+ await testSubjects.click('discoverNewButton');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+
+ await retry.waitFor('number of fetches to be 0', waitForFetches(1));
+ expect(await PageObjects.discover.doesSidebarShowFields()).to.be(false);
});
});
it(`when it's true should fetch data from ES initially`, async function () {
await initSearchOnPageLoad(true);
await retry.waitFor('number of fetches to be 1', waitForFetches(1));
+ expect(await PageObjects.discover.doesSidebarShowFields()).to.be(true);
});
});
}
diff --git a/test/functional/page_objects/context_page.ts b/test/functional/page_objects/context_page.ts
index 05ea89cb65b3d..2bb3d7fb84a2a 100644
--- a/test/functional/page_objects/context_page.ts
+++ b/test/functional/page_objects/context_page.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import rison from 'rison-node';
+import rison from '@kbn/rison';
import { getUrl } from '@kbn/test';
import { FtrService } from '../ftr_provider_context';
diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index 79a30dba288d3..0b22917be5e49 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -9,6 +9,8 @@
import expect from '@kbn/expect';
import { FtrService } from '../ftr_provider_context';
+type SidebarSectionName = 'meta' | 'empty' | 'available' | 'unmapped' | 'popular' | 'selected';
+
export class DiscoverPageObject extends FtrService {
private readonly retry = this.ctx.getService('retry');
private readonly testSubjects = this.ctx.getService('testSubjects');
@@ -437,8 +439,61 @@ export class DiscoverPageObject extends FtrService {
return await this.testSubjects.exists('discoverNoResultsTimefilter');
}
+ public async getSidebarAriaDescription(): Promise {
+ return await (
+ await this.testSubjects.find('fieldListGrouped__ariaDescription')
+ ).getAttribute('innerText');
+ }
+
+ public async waitUntilSidebarHasLoaded() {
+ await this.retry.waitFor('sidebar is loaded', async () => {
+ return (await this.getSidebarAriaDescription()).length > 0;
+ });
+ }
+
+ public async doesSidebarShowFields() {
+ return await this.testSubjects.exists('fieldListGroupedFieldGroups');
+ }
+
+ public getSidebarSectionSelector(
+ sectionName: SidebarSectionName,
+ asCSSSelector: boolean = false
+ ) {
+ const testSubj = `fieldListGrouped${sectionName[0].toUpperCase()}${sectionName.substring(
+ 1
+ )}Fields`;
+ if (!asCSSSelector) {
+ return testSubj;
+ }
+ return `[data-test-subj="${testSubj}"]`;
+ }
+
+ public async getSidebarSectionFieldNames(sectionName: SidebarSectionName): Promise {
+ const elements = await this.find.allByCssSelector(
+ `${this.getSidebarSectionSelector(sectionName, true)} li`
+ );
+
+ if (!elements?.length) {
+ return [];
+ }
+
+ return Promise.all(
+ elements.map(async (element) => await element.getAttribute('data-attr-field'))
+ );
+ }
+
+ public async toggleSidebarSection(sectionName: SidebarSectionName) {
+ return await this.find.clickByCssSelector(
+ `${this.getSidebarSectionSelector(sectionName, true)} .euiAccordion__iconButton`
+ );
+ }
+
public async clickFieldListItem(field: string) {
- return await this.testSubjects.click(`field-${field}`);
+ await this.testSubjects.click(`field-${field}`);
+
+ await this.retry.waitFor('popover is open', async () => {
+ return Boolean(await this.find.byCssSelector('[data-popover-open="true"]'));
+ });
}
public async clickFieldSort(field: string, text = 'Sort New-Old') {
@@ -455,11 +510,16 @@ export class DiscoverPageObject extends FtrService {
}
public async clickFieldListItemAdd(field: string) {
+ await this.waitUntilSidebarHasLoaded();
+
// a filter check may make sense here, but it should be properly handled to make
// it work with the _score and _source fields as well
if (await this.isFieldSelected(field)) {
return;
}
+ if (['_score', '_id', '_index'].includes(field)) {
+ await this.toggleSidebarSection('meta'); // expand Meta section
+ }
await this.clickFieldListItemToggle(field);
const isLegacyDefault = await this.useLegacyTable();
if (isLegacyDefault) {
@@ -474,16 +534,18 @@ export class DiscoverPageObject extends FtrService {
}
public async isFieldSelected(field: string) {
- if (!(await this.testSubjects.exists('fieldList-selected'))) {
+ if (!(await this.testSubjects.exists('fieldListGroupedSelectedFields'))) {
return false;
}
- const selectedList = await this.testSubjects.find('fieldList-selected');
+ const selectedList = await this.testSubjects.find('fieldListGroupedSelectedFields');
return await this.testSubjects.descendantExists(`field-${field}`, selectedList);
}
public async clickFieldListItemRemove(field: string) {
+ await this.waitUntilSidebarHasLoaded();
+
if (
- !(await this.testSubjects.exists('fieldList-selected')) ||
+ !(await this.testSubjects.exists('fieldListGroupedSelectedFields')) ||
!(await this.isFieldSelected(field))
) {
return;
@@ -493,6 +555,8 @@ export class DiscoverPageObject extends FtrService {
}
public async clickFieldListItemVisualize(fieldName: string) {
+ await this.waitUntilSidebarHasLoaded();
+
const field = await this.testSubjects.find(`field-${fieldName}-showDetails`);
const isActive = await field.elementHasClass('kbnFieldButton-isActive');
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 6a236d215a6d9..787992f6e2133 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -540,6 +540,8 @@
"@kbn/repo-source-classifier/*": ["packages/kbn-repo-source-classifier/*"],
"@kbn/repo-source-classifier-cli": ["packages/kbn-repo-source-classifier-cli"],
"@kbn/repo-source-classifier-cli/*": ["packages/kbn-repo-source-classifier-cli/*"],
+ "@kbn/rison": ["packages/kbn-rison"],
+ "@kbn/rison/*": ["packages/kbn-rison/*"],
"@kbn/rule-data-utils": ["packages/kbn-rule-data-utils"],
"@kbn/rule-data-utils/*": ["packages/kbn-rule-data-utils/*"],
"@kbn/safer-lodash-set": ["packages/kbn-safer-lodash-set"],
@@ -720,6 +722,8 @@
"@kbn/shared-ux-prompt-no-data-views-mocks/*": ["packages/shared-ux/prompt/no_data_views/mocks/*"],
"@kbn/shared-ux-prompt-no-data-views-types": ["packages/shared-ux/prompt/no_data_views/types"],
"@kbn/shared-ux-prompt-no-data-views-types/*": ["packages/shared-ux/prompt/no_data_views/types/*"],
+ "@kbn/shared-ux-prompt-not-found": ["packages/shared-ux/prompt/not_found"],
+ "@kbn/shared-ux-prompt-not-found/*": ["packages/shared-ux/prompt/not_found/*"],
"@kbn/shared-ux-router": ["packages/shared-ux/router/impl"],
"@kbn/shared-ux-router/*": ["packages/shared-ux/router/impl/*"],
"@kbn/shared-ux-router-mocks": ["packages/shared-ux/router/mocks"],
diff --git a/typings/rison_node.d.ts b/typings/rison_node.d.ts
deleted file mode 100644
index dacb2524907be..0000000000000
--- a/typings/rison_node.d.ts
+++ /dev/null
@@ -1,28 +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
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-declare module 'rison-node' {
- export type RisonValue = undefined | null | boolean | number | string | RisonObject | RisonArray;
-
- // eslint-disable-next-line @typescript-eslint/no-empty-interface
- export interface RisonArray extends Array {}
-
- export interface RisonObject {
- [key: string]: RisonValue;
- }
-
- export const decode: (input: string) => RisonValue;
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- export const decode_object: (input: string) => RisonObject;
-
- export const encode: (input: Input) => string;
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- export const encode_object: (input: Input) => string;
-}
diff --git a/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts b/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts
index 8a1c438199878..a56d5c0cdfc6e 100644
--- a/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts
+++ b/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import rison from 'rison-node';
+import rison from '@kbn/rison';
import moment from 'moment';
import type { TimeRangeBounds } from '@kbn/data-plugin/common';
diff --git a/x-pack/plugins/aiops/public/hooks/use_url_state.tsx b/x-pack/plugins/aiops/public/hooks/use_url_state.tsx
index c4d84163f887e..94273a204f5cc 100644
--- a/x-pack/plugins/aiops/public/hooks/use_url_state.tsx
+++ b/x-pack/plugins/aiops/public/hooks/use_url_state.tsx
@@ -8,7 +8,7 @@
import React, { FC } from 'react';
import { parse, stringify } from 'query-string';
import { createContext, useCallback, useContext, useMemo } from 'react';
-import { decode, encode } from 'rison-node';
+import { decode, encode } from '@kbn/rison';
import { useHistory, useLocation } from 'react-router-dom';
import { isEqual } from 'lodash';
diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts b/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts
index 4b4cc42e08089..ed9971307bf64 100644
--- a/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts
+++ b/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts
@@ -33,7 +33,8 @@ export function registerApmRuleTypes(
return {
reason: fields[ALERT_REASON]!,
link: getAlertUrlErrorCount(
- String(fields[SERVICE_NAME][0]!),
+ // TODO:fix SERVICE_NAME when we move it to initializeIndex
+ String(fields[SERVICE_NAME]![0]),
fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0])
),
};
@@ -46,6 +47,12 @@ export function registerApmRuleTypes(
validate: () => ({
errors: [],
}),
+ alertDetailsAppSection: lazy(
+ () =>
+ import(
+ '../ui_components/alert_details_app_section/alert_details_app_section'
+ )
+ ),
requiresAppContext: false,
defaultActionMessage: i18n.translate(
'xpack.apm.alertTypes.errorCount.defaultActionMessage',
@@ -73,9 +80,10 @@ export function registerApmRuleTypes(
return {
reason: fields[ALERT_REASON]!,
link: getAlertUrlTransaction(
- String(fields[SERVICE_NAME][0]!),
+ // TODO:fix SERVICE_NAME when we move it to initializeIndex
+ String(fields[SERVICE_NAME]![0]),
fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0]),
- String(fields[TRANSACTION_TYPE][0]!)
+ String(fields[TRANSACTION_TYPE]![0])
),
};
},
@@ -89,6 +97,12 @@ export function registerApmRuleTypes(
validate: () => ({
errors: [],
}),
+ alertDetailsAppSection: lazy(
+ () =>
+ import(
+ '../ui_components/alert_details_app_section/alert_details_app_section'
+ )
+ ),
requiresAppContext: false,
defaultActionMessage: i18n.translate(
'xpack.apm.alertTypes.transactionDuration.defaultActionMessage',
@@ -116,9 +130,10 @@ export function registerApmRuleTypes(
format: ({ fields, formatters: { asPercent } }) => ({
reason: fields[ALERT_REASON]!,
link: getAlertUrlTransaction(
- String(fields[SERVICE_NAME][0]!),
+ // TODO:fix SERVICE_NAME when we move it to initializeIndex
+ String(fields[SERVICE_NAME]![0]),
fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0]),
- String(fields[TRANSACTION_TYPE][0]!)
+ String(fields[TRANSACTION_TYPE]![0])
),
}),
iconClass: 'bell',
@@ -131,6 +146,12 @@ export function registerApmRuleTypes(
validate: () => ({
errors: [],
}),
+ alertDetailsAppSection: lazy(
+ () =>
+ import(
+ '../ui_components/alert_details_app_section/alert_details_app_section'
+ )
+ ),
requiresAppContext: false,
defaultActionMessage: i18n.translate(
'xpack.apm.alertTypes.transactionErrorRate.defaultActionMessage',
@@ -155,9 +176,10 @@ export function registerApmRuleTypes(
format: ({ fields }) => ({
reason: fields[ALERT_REASON]!,
link: getAlertUrlTransaction(
- String(fields[SERVICE_NAME][0]!),
+ // TODO:fix SERVICE_NAME when we move it to initializeIndex
+ String(fields[SERVICE_NAME]![0]),
fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0]),
- String(fields[TRANSACTION_TYPE][0]!)
+ String(fields[TRANSACTION_TYPE]![0])
),
}),
iconClass: 'bell',
@@ -170,6 +192,12 @@ export function registerApmRuleTypes(
validate: () => ({
errors: [],
}),
+ alertDetailsAppSection: lazy(
+ () =>
+ import(
+ '../ui_components/alert_details_app_section/alert_details_app_section'
+ )
+ ),
requiresAppContext: false,
defaultActionMessage: i18n.translate(
'xpack.apm.alertTypes.transactionDurationAnomaly.defaultActionMessage',
diff --git a/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/alert_details_app_section.tsx b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/alert_details_app_section.tsx
new file mode 100644
index 0000000000000..92cd229333062
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/alert_details_app_section.tsx
@@ -0,0 +1,419 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo } from 'react';
+import { EuiFlexGroup } from '@elastic/eui';
+import { EuiFlexItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { EuiPanel } from '@elastic/eui';
+import { EuiTitle } from '@elastic/eui';
+import { EuiIconTip } from '@elastic/eui';
+import { ALERT_DURATION, ALERT_END } from '@kbn/rule-data-utils';
+import moment from 'moment';
+import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values';
+import { getTransactionType } from '../../../../context/apm_service/apm_service_context';
+import { useServiceAgentFetcher } from '../../../../context/apm_service/use_service_agent_fetcher';
+import { useServiceTransactionTypesFetcher } from '../../../../context/apm_service/use_service_transaction_types_fetcher';
+import { asPercent } from '../../../../../common/utils/formatters';
+import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
+import { getDurationFormatter } from '../../../../../common/utils/formatters/duration';
+import { useFetcher } from '../../../../hooks/use_fetcher';
+import { useTimeRange } from '../../../../hooks/use_time_range';
+import { isTimeComparison } from '../../../shared/time_comparison/get_comparison_options';
+import { getComparisonChartTheme } from '../../../shared/time_comparison/get_comparison_chart_theme';
+import { getLatencyChartSelector } from '../../../../selectors/latency_chart_selectors';
+import { TimeseriesChart } from '../../../shared/charts/timeseries_chart';
+import {
+ getMaxY,
+ getResponseTimeTickFormatter,
+} from '../../../shared/charts/transaction_charts/helper';
+import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event/chart_pointer_event_context';
+import {
+ ChartType,
+ getTimeSeriesColor,
+} from '../../../shared/charts/helper/get_timeseries_color';
+import {
+ AlertDetailsAppSectionProps,
+ SERVICE_NAME,
+ TRANSACTION_TYPE,
+} from './types';
+import { getAggsTypeFromRule } from './helpers';
+import { filterNil } from '../../../shared/charts/latency_chart';
+import { errorRateI18n } from '../../../shared/charts/failed_transaction_rate_chart';
+
+export function AlertDetailsAppSection({
+ rule,
+ alert,
+ timeZone,
+}: AlertDetailsAppSectionProps) {
+ const params = rule.params;
+ const environment = String(params.environment) || ENVIRONMENT_ALL.value;
+ const latencyAggregationType = getAggsTypeFromRule(
+ params.aggregationType as string
+ );
+
+ // duration is us, convert it to MS
+ const alertDurationMS = alert.fields[ALERT_DURATION]! / 1000;
+
+ const serviceName = String(alert.fields[SERVICE_NAME]);
+
+ // Currently, we don't use comparisonEnabled nor offset.
+ // But providing them as they are required for the chart.
+ const comparisonEnabled = false;
+ const offset = '1d';
+ const ruleWindowSizeMS = moment
+ .duration(rule.params.windowSize, rule.params.windowUnit)
+ .asMilliseconds();
+
+ const TWENTY_TIMES_RULE_WINDOW_MS = 20 * ruleWindowSizeMS;
+ /**
+ * This is part or the requirements (RFC).
+ * If the alert is less than 20 units of `FOR THE LAST ` then we should draw a time range of 20 units.
+ * IE. The user set "FOR THE LAST 5 minutes" at a minimum we should show 100 minutes.
+ */
+ const rangeFrom =
+ alertDurationMS < TWENTY_TIMES_RULE_WINDOW_MS
+ ? moment(alert.start)
+ .subtract(TWENTY_TIMES_RULE_WINDOW_MS, 'millisecond')
+ .toISOString()
+ : moment(alert.start)
+ .subtract(ruleWindowSizeMS, 'millisecond')
+ .toISOString();
+
+ const rangeTo = alert.active
+ ? 'now'
+ : moment(alert.fields[ALERT_END])
+ .add(ruleWindowSizeMS, 'millisecond')
+ .toISOString();
+
+ const { start, end } = useTimeRange({ rangeFrom, rangeTo });
+ const { agentName } = useServiceAgentFetcher({
+ serviceName,
+ start,
+ end,
+ });
+ const transactionTypes = useServiceTransactionTypesFetcher({
+ serviceName,
+ start,
+ end,
+ });
+
+ const transactionType = getTransactionType({
+ transactionType: String(alert.fields[TRANSACTION_TYPE]),
+ transactionTypes,
+ agentName,
+ });
+
+ const comparisonChartTheme = getComparisonChartTheme();
+ const INITIAL_STATE = {
+ currentPeriod: [],
+ previousPeriod: [],
+ };
+
+ /* Latency Chart */
+ const { data, status } = useFetcher(
+ (callApmApi) => {
+ if (
+ serviceName &&
+ start &&
+ end &&
+ transactionType &&
+ latencyAggregationType
+ ) {
+ return callApmApi(
+ `GET /internal/apm/services/{serviceName}/transactions/charts/latency`,
+ {
+ params: {
+ path: { serviceName },
+ query: {
+ environment,
+ kuery: '',
+ start,
+ end,
+ transactionType,
+ transactionName: undefined,
+ latencyAggregationType,
+ },
+ },
+ }
+ );
+ }
+ },
+ [
+ end,
+ environment,
+ latencyAggregationType,
+ serviceName,
+ start,
+ transactionType,
+ ]
+ );
+
+ const memoizedData = useMemo(
+ () =>
+ getLatencyChartSelector({
+ latencyChart: data,
+ latencyAggregationType,
+ previousPeriodLabel: '',
+ }),
+ // It should only update when the data has changed
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [data]
+ );
+ const { currentPeriod, previousPeriod } = memoizedData;
+
+ const timeseriesLatency = [
+ currentPeriod,
+ comparisonEnabled && isTimeComparison(offset) ? previousPeriod : undefined,
+ ].filter(filterNil);
+
+ const latencyMaxY = getMaxY(timeseriesLatency);
+ const latencyFormatter = getDurationFormatter(latencyMaxY);
+
+ /* Latency Chart */
+
+ /* Throughput Chart */
+ const { data: dataThroughput = INITIAL_STATE, status: statusThroughput } =
+ useFetcher(
+ (callApmApi) => {
+ if (serviceName && transactionType && start && end) {
+ return callApmApi(
+ 'GET /internal/apm/services/{serviceName}/throughput',
+ {
+ params: {
+ path: {
+ serviceName,
+ },
+ query: {
+ environment,
+ kuery: '',
+ start,
+ end,
+ transactionType,
+ transactionName: undefined,
+ },
+ },
+ }
+ );
+ }
+ },
+ [environment, serviceName, start, end, transactionType]
+ );
+ const { currentPeriodColor, previousPeriodColor } = getTimeSeriesColor(
+ ChartType.THROUGHPUT
+ );
+ const timeseriesThroughput = [
+ {
+ data: dataThroughput.currentPeriod,
+ type: 'linemark',
+ color: currentPeriodColor,
+ title: i18n.translate('xpack.apm.serviceOverview.throughtputChartTitle', {
+ defaultMessage: 'Throughput',
+ }),
+ },
+ ...(comparisonEnabled
+ ? [
+ {
+ data: dataThroughput.previousPeriod,
+ type: 'area',
+ color: previousPeriodColor,
+ title: '',
+ },
+ ]
+ : []),
+ ];
+
+ /* Throughput Chart */
+
+ /* Error Rate */
+ type ErrorRate =
+ APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/charts/error_rate'>;
+
+ const INITIAL_STATE_ERROR_RATE: ErrorRate = {
+ currentPeriod: {
+ timeseries: [],
+ average: null,
+ },
+ previousPeriod: {
+ timeseries: [],
+ average: null,
+ },
+ };
+ function yLabelFormat(y?: number | null) {
+ return asPercent(y || 0, 1);
+ }
+
+ const {
+ data: dataErrorRate = INITIAL_STATE_ERROR_RATE,
+ status: statusErrorRate,
+ } = useFetcher(
+ (callApmApi) => {
+ if (transactionType && serviceName && start && end) {
+ return callApmApi(
+ 'GET /internal/apm/services/{serviceName}/transactions/charts/error_rate',
+ {
+ params: {
+ path: {
+ serviceName,
+ },
+ query: {
+ environment,
+ kuery: '',
+ start,
+ end,
+ transactionType,
+ transactionName: undefined,
+ },
+ },
+ }
+ );
+ }
+ },
+ [environment, serviceName, start, end, transactionType]
+ );
+
+ const { currentPeriodColor: currentPeriodColorErrorRate } =
+ getTimeSeriesColor(ChartType.FAILED_TRANSACTION_RATE);
+
+ const timeseriesErrorRate = [
+ {
+ data: dataErrorRate.currentPeriod.timeseries,
+ type: 'linemark',
+ color: currentPeriodColorErrorRate,
+ title: i18n.translate('xpack.apm.errorRate.chart.errorRate', {
+ defaultMessage: 'Failed transaction rate (avg.)',
+ }),
+ },
+ ];
+
+ /* Error Rate */
+
+ return (
+
+
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.apm.dependencyLatencyChart.chartTitle',
+ {
+ defaultMessage: 'Latency',
+ }
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.apm.serviceOverview.throughtputChartTitle',
+ { defaultMessage: 'Throughput' }
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {i18n.translate('xpack.apm.errorRate', {
+ defaultMessage: 'Failed transaction rate',
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// eslint-disable-next-line import/no-default-export
+export default AlertDetailsAppSection;
diff --git a/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/helpers.ts b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/helpers.ts
new file mode 100644
index 0000000000000..a095f8caa4574
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/helpers.ts
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
+
+export const getAggsTypeFromRule = (
+ ruleAggType: string
+): LatencyAggregationType => {
+ if (ruleAggType === '95th') return LatencyAggregationType.p95;
+ if (ruleAggType === '99th') return LatencyAggregationType.p99;
+ return LatencyAggregationType.avg;
+};
diff --git a/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts
new file mode 100644
index 0000000000000..0094d9332009a
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Rule } from '@kbn/alerting-plugin/common';
+import { TopAlert } from '@kbn/observability-plugin/public/pages/alerts';
+import { TIME_UNITS } from '@kbn/triggers-actions-ui-plugin/public';
+import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
+
+export const SERVICE_NAME = 'service.name' as const;
+export const TRANSACTION_TYPE = 'transaction.type' as const;
+export interface AlertDetailsAppSectionProps {
+ rule: Rule<{
+ environment: string;
+ aggregationType: LatencyAggregationType;
+ windowSize: number;
+ windowUnit: TIME_UNITS;
+ }>;
+ alert: TopAlert<{ [SERVICE_NAME]: string; [TRANSACTION_TYPE]: string }>;
+ timeZone: string;
+}
diff --git a/x-pack/plugins/apm/public/components/app/metrics/serverless_metrics/serverless_active_instances.tsx b/x-pack/plugins/apm/public/components/app/metrics/serverless_metrics/serverless_active_instances.tsx
index 9c44d472c5b70..a0a72526e3632 100644
--- a/x-pack/plugins/apm/public/components/app/metrics/serverless_metrics/serverless_active_instances.tsx
+++ b/x-pack/plugins/apm/public/components/app/metrics/serverless_metrics/serverless_active_instances.tsx
@@ -27,7 +27,7 @@ import { useApmParams } from '../../../../hooks/use_apm_params';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
-import { TimeseriesChart } from '../../../shared/charts/timeseries_chart';
+import { TimeseriesChartWithContext } from '../../../shared/charts/timeseries_chart_with_context';
import { ListMetric } from '../../../shared/list_metric';
import { ServerlessFunctionNameLink } from './serverless_function_name_link';
@@ -201,7 +201,7 @@ export function ServerlessActiveInstances({ serverlessId }: Props) {
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/apm/public/components/shared/charts/failed_transaction_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/failed_transaction_rate_chart/index.tsx
index dacb295a011dd..508d3d0f0ed68 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/failed_transaction_rate_chart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/failed_transaction_rate_chart/index.tsx
@@ -15,7 +15,7 @@ import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { asPercent } from '../../../../../common/utils/formatters';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params';
-import { TimeseriesChart } from '../timeseries_chart';
+import { TimeseriesChartWithContext } from '../timeseries_chart_with_context';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { getComparisonChartTheme } from '../../time_comparison/get_comparison_chart_theme';
import { useApmParams } from '../../../../hooks/use_apm_params';
@@ -49,6 +49,10 @@ const INITIAL_STATE: ErrorRate = {
},
};
+export const errorRateI18n = i18n.translate('xpack.apm.errorRate.tip', {
+ defaultMessage:
+ "The percentage of failed transactions for the selected service. HTTP server transactions with a 4xx status code (client error) aren't considered failures because the caller, not the server, caused the failure.",
+});
export function FailedTransactionRateChart({
height,
showAnnotations = true,
@@ -154,17 +158,11 @@ export function FailedTransactionRateChart({
-
+
- = [
{ value: LatencyAggregationType.p99, text: '99th percentile' },
];
-function filterNil(value: T | null | undefined): value is T {
+export function filterNil(value: T | null | undefined): value is T {
return value != null;
}
@@ -126,7 +126,7 @@ export function LatencyChart({ height, kuery }: Props) {
-
)}
- >;
- /**
- * Formatter for y-axis tick values
- */
- yLabelFormat: (y: number) => string;
- /**
- * Formatter for legend and tooltip values
- */
- yTickFormat?: (y: number) => string;
- showAnnotations?: boolean;
- yDomain?: YDomainRange;
- anomalyTimeseries?: AnomalyTimeseries;
- customTheme?: Record;
- anomalyTimeseriesColor?: string;
-}
+import { TimeseriesChartWithContextProps } from './timeseries_chart_with_context';
const END_ZONE_LABEL = i18n.translate('xpack.apm.timeseries.endzone', {
defaultMessage:
'The selected time range does not include this entire bucket. It might contain partial data.',
});
-
+interface TimeseriesChartProps extends TimeseriesChartWithContextProps {
+ comparisonEnabled: boolean;
+ offset?: string;
+ timeZone: string;
+}
export function TimeseriesChart({
id,
height = unit * 16,
@@ -91,30 +64,22 @@ export function TimeseriesChart({
yDomain,
anomalyTimeseries,
customTheme = {},
-}: Props) {
+ comparisonEnabled,
+ offset,
+ timeZone,
+}: TimeseriesChartProps) {
const history = useHistory();
- const { core } = useApmPluginContext();
const { annotations } = useAnnotationsContext();
const { chartRef, updatePointerEvent } = useChartPointerEventContext();
const theme = useTheme();
const chartTheme = useChartTheme();
- const {
- query: { comparisonEnabled, offset },
- } = useAnyOfApmParams(
- '/services',
- '/dependencies/*',
- '/services/{serviceName}'
- );
-
const anomalyChartTimeseries = getChartAnomalyTimeseries({
anomalyTimeseries,
theme,
anomalyTimeseriesColor: anomalyTimeseries?.color,
});
-
const isEmpty = isTimeseriesEmpty(timeseries);
const annotationColor = theme.eui.euiColorSuccess;
-
const isComparingExpectedBounds =
comparisonEnabled && isExpectedBoundsComparison(offset);
const allSeries = [
@@ -134,20 +99,14 @@ export function TimeseriesChart({
);
const xValues = timeseries.flatMap(({ data }) => data.map(({ x }) => x));
-
const xValuesExpectedBounds =
anomalyChartTimeseries?.boundaries?.flatMap(({ data }) =>
data.map(({ x }) => x)
) ?? [];
-
- const timeZone = getTimeZone(core.uiSettings);
-
const min = Math.min(...xValues);
const max = Math.max(...xValues, ...xValuesExpectedBounds);
const xFormatter = niceTimeFormatter([min, max]);
-
const xDomain = isEmpty ? { min: 0, max: 1 } : { min, max };
-
// Using custom legendSort here when comparing expected bounds
// because by default elastic-charts will show legends for expected bounds first
// but for consistency, we are making `Expected bounds` last
diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart_with_context.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart_with_context.tsx
new file mode 100644
index 0000000000000..5c9aac5d28bdf
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart_with_context.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { LegendItemListener, YDomainRange } from '@elastic/charts';
+import React from 'react';
+import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
+import { ServiceAnomalyTimeseries } from '../../../../common/anomaly_detection/service_anomaly_timeseries';
+import { Coordinate, TimeSeries } from '../../../../typings/timeseries';
+import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
+import { FETCH_STATUS } from '../../../hooks/use_fetcher';
+import { unit } from '../../../utils/style';
+import { getTimeZone } from './helper/timezone';
+import { TimeseriesChart } from './timeseries_chart';
+
+interface AnomalyTimeseries extends ServiceAnomalyTimeseries {
+ color?: string;
+}
+export interface TimeseriesChartWithContextProps {
+ id: string;
+ fetchStatus: FETCH_STATUS;
+ height?: number;
+ onToggleLegend?: LegendItemListener;
+ timeseries: Array>;
+ /**
+ * Formatter for y-axis tick values
+ */
+ yLabelFormat: (y: number) => string;
+ /**
+ * Formatter for legend and tooltip values
+ */
+ yTickFormat?: (y: number) => string;
+ showAnnotations?: boolean;
+ yDomain?: YDomainRange;
+ anomalyTimeseries?: AnomalyTimeseries;
+ customTheme?: Record;
+ anomalyTimeseriesColor?: string;
+}
+
+export function TimeseriesChartWithContext({
+ id,
+ height = unit * 16,
+ fetchStatus,
+ onToggleLegend,
+ timeseries,
+ yLabelFormat,
+ yTickFormat,
+ showAnnotations = true,
+ yDomain,
+ anomalyTimeseries,
+ customTheme = {},
+}: TimeseriesChartWithContextProps) {
+ const {
+ query: { comparisonEnabled, offset },
+ } = useAnyOfApmParams(
+ '/services',
+ '/dependencies/*',
+ '/services/{serviceName}'
+ );
+ const { core } = useApmPluginContext();
+ const timeZone = getTimeZone(core.uiSettings);
+
+ return (
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx
index a3143bb7b6849..3dfc22a1c2809 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx
@@ -20,7 +20,7 @@ import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { asPercent } from '../../../../../common/utils/formatters';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { useTheme } from '../../../../hooks/use_theme';
-import { TimeseriesChart } from '../timeseries_chart';
+import { TimeseriesChartWithContext } from '../timeseries_chart_with_context';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { getComparisonChartTheme } from '../../time_comparison/get_comparison_chart_theme';
import { useApmParams } from '../../../../hooks/use_apm_params';
@@ -161,7 +161,7 @@ export function TransactionColdstartRateChart({
/>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/cosmos_db.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/cosmos_db.svg
new file mode 100644
index 0000000000000..26205c2292a1f
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/cosmos_db.svg
@@ -0,0 +1,19 @@
+
diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/dynamo_db.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/dynamo_db.svg
new file mode 100644
index 0000000000000..a8f80e39c6ca3
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/dynamo_db.svg
@@ -0,0 +1,15 @@
+
diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/file_share_storage.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/file_share_storage.svg
new file mode 100644
index 0000000000000..9c8b135e945f3
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/file_share_storage.svg
@@ -0,0 +1,15 @@
+
diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/s3.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/s3.svg
new file mode 100644
index 0000000000000..1dfa8fccf7765
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/s3.svg
@@ -0,0 +1,15 @@
+
diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/service_bus.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/service_bus.svg
new file mode 100644
index 0000000000000..76fc82312752f
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/service_bus.svg
@@ -0,0 +1,25 @@
+
diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/sns.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/sns.svg
new file mode 100644
index 0000000000000..f668c7019baa0
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/sns.svg
@@ -0,0 +1,15 @@
+
diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/sqs.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/sqs.svg
new file mode 100644
index 0000000000000..21fe40a46c9c2
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/sqs.svg
@@ -0,0 +1,15 @@
+
diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/storage_queue.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/storage_queue.svg
new file mode 100644
index 0000000000000..3b540975b17d1
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/storage_queue.svg
@@ -0,0 +1,15 @@
+
diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/icons/table_storage.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/table_storage.svg
new file mode 100644
index 0000000000000..9c3ba79e0371e
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/span_icon/icons/table_storage.svg
@@ -0,0 +1,19 @@
+
diff --git a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx
index afbad8543059a..ce74feab48102 100644
--- a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx
+++ b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx
@@ -69,7 +69,7 @@ export function ApmServiceContextProvider({
end,
});
- const transactionType = getOrRedirectToTransactionType({
+ const currentTransactionType = getOrRedirectToTransactionType({
transactionType: query.transactionType,
transactionTypes,
agentName,
@@ -85,7 +85,7 @@ export function ApmServiceContextProvider({
value={{
serviceName,
agentName,
- transactionType,
+ transactionType: currentTransactionType,
transactionTypes,
runtimeName,
fallbackToTransactions,
@@ -96,24 +96,44 @@ export function ApmServiceContextProvider({
);
}
-export function getOrRedirectToTransactionType({
+const isTypeExistsInTransactionTypesList = ({
+ transactionType,
+ transactionTypes,
+}: {
+ transactionType?: string;
+ transactionTypes: string[];
+}): boolean => !!transactionType && transactionTypes.includes(transactionType);
+
+const isNoAgentAndNoTransactionTypes = ({
+ transactionTypes,
+ agentName,
+}: {
+ transactionTypes: string[];
+ agentName?: string;
+}): boolean => !agentName || transactionTypes.length === 0;
+
+export function getTransactionType({
transactionType,
transactionTypes,
agentName,
- history,
}: {
transactionType?: string;
transactionTypes: string[];
agentName?: string;
- history: History;
-}) {
- if (transactionType && transactionTypes.includes(transactionType)) {
- return transactionType;
- }
+}): string | undefined {
+ const isTransactionTypeExists = isTypeExistsInTransactionTypesList({
+ transactionType,
+ transactionTypes,
+ });
- if (!agentName || transactionTypes.length === 0) {
- return;
- }
+ if (isTransactionTypeExists) return transactionType;
+
+ const isNoAgentAndNoTransactionTypesExists = isNoAgentAndNoTransactionTypes({
+ transactionTypes,
+ agentName,
+ });
+
+ if (isNoAgentAndNoTransactionTypesExists) return undefined;
// The default transaction type is "page-load" for RUM agents and "request" for all others
const defaultTransactionType = isRumAgentName(agentName)
@@ -127,7 +147,42 @@ export function getOrRedirectToTransactionType({
? defaultTransactionType
: transactionTypes[0];
+ return currentTransactionType;
+}
+
+export function getOrRedirectToTransactionType({
+ transactionType,
+ transactionTypes,
+ agentName,
+ history,
+}: {
+ transactionType?: string;
+ transactionTypes: string[];
+ agentName?: string;
+ history: History;
+}) {
+ const isTransactionTypeExists = isTypeExistsInTransactionTypesList({
+ transactionType,
+ transactionTypes,
+ });
+
+ if (isTransactionTypeExists) return transactionType;
+
+ const isNoAgentAndNoTransactionTypesExists = isNoAgentAndNoTransactionTypes({
+ transactionTypes,
+ agentName,
+ });
+
+ if (isNoAgentAndNoTransactionTypesExists) return undefined;
+
+ const currentTransactionType = getTransactionType({
+ transactionTypes,
+ transactionType,
+ agentName,
+ });
+
// Replace transactionType in the URL in case it is not one of the types returned by the API
- replace(history, { query: { transactionType: currentTransactionType } });
+ replace(history, { query: { transactionType: currentTransactionType! } });
+
return currentTransactionType;
}
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/editor.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/editor.tsx
index c96024658048c..1a09116ba1196 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/editor.tsx
+++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/editor.tsx
@@ -52,7 +52,7 @@ const EditorArg: FC = ({ argValue, typeInstance, onValueChange,
const { language } = typeInstance?.options ?? {};
return (
-
+
{
return (
-
+
{
logger = new AuthorizationAuditLogger(mockLogger);
});
- it('does not throw an error when the underlying audit logger is undefined', () => {
- const authLogger = new AuthorizationAuditLogger();
- jest.spyOn(authLogger, 'log');
-
- expect(() => {
- authLogger.log({
- operation: Operations.createCase,
- entity: {
- owner: 'a',
- id: '1',
- },
- });
- }).not.toThrow();
-
- expect(authLogger.log).toHaveBeenCalledTimes(1);
- });
-
it('logs a message with a saved object ID in the message field', () => {
logger.log({
operation: Operations.createCase,
diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.ts b/x-pack/plugins/cases/server/authorization/audit_logger.ts
index 8a415e1b69559..88293689446f8 100644
--- a/x-pack/plugins/cases/server/authorization/audit_logger.ts
+++ b/x-pack/plugins/cases/server/authorization/audit_logger.ts
@@ -21,9 +21,9 @@ interface CreateAuditMsgParams {
* Audit logger for authorization operations
*/
export class AuthorizationAuditLogger {
- private readonly auditLogger?: AuditLogger;
+ private readonly auditLogger: AuditLogger;
- constructor(logger?: AuditLogger) {
+ constructor(logger: AuditLogger) {
this.auditLogger = logger;
}
@@ -97,6 +97,6 @@ export class AuthorizationAuditLogger {
* Logs an audit event based on the status of an operation.
*/
public log(auditMsgParams: CreateAuditMsgParams) {
- this.auditLogger?.log(AuthorizationAuditLogger.createAuditMsg(auditMsgParams));
+ this.auditLogger.log(AuthorizationAuditLogger.createAuditMsg(auditMsgParams));
}
}
diff --git a/x-pack/plugins/cases/server/authorization/authorization.test.ts b/x-pack/plugins/cases/server/authorization/authorization.test.ts
index 0483489d6c8a2..e7bbcb0abbb9b 100644
--- a/x-pack/plugins/cases/server/authorization/authorization.test.ts
+++ b/x-pack/plugins/cases/server/authorization/authorization.test.ts
@@ -61,7 +61,7 @@ describe('authorization', () => {
securityAuth: securityStart.authz,
spaces: spacesStart,
features: featuresStart,
- auditLogger: new AuthorizationAuditLogger(),
+ auditLogger: new AuthorizationAuditLogger(mockLogger),
logger: loggingSystemMock.createLogger(),
});
@@ -81,7 +81,7 @@ describe('authorization', () => {
securityAuth: securityStart.authz,
spaces: spacesStart,
features: featuresStart,
- auditLogger: new AuthorizationAuditLogger(),
+ auditLogger: new AuthorizationAuditLogger(mockLogger),
logger: loggingSystemMock.createLogger(),
});
@@ -140,7 +140,7 @@ describe('authorization', () => {
request,
spaces: spacesStart,
features: featuresStart,
- auditLogger: new AuthorizationAuditLogger(),
+ auditLogger: new AuthorizationAuditLogger(mockLogger),
logger: loggingSystemMock.createLogger(),
});
@@ -266,7 +266,7 @@ describe('authorization', () => {
securityAuth: securityStart.authz,
spaces: spacesStart,
features: featuresStart,
- auditLogger: new AuthorizationAuditLogger(),
+ auditLogger: new AuthorizationAuditLogger(mockLogger),
logger: loggingSystemMock.createLogger(),
});
@@ -295,7 +295,7 @@ describe('authorization', () => {
securityAuth: securityStart.authz,
spaces: spacesStart,
features: featuresStart,
- auditLogger: new AuthorizationAuditLogger(),
+ auditLogger: new AuthorizationAuditLogger(mockLogger),
logger: loggingSystemMock.createLogger(),
});
@@ -322,7 +322,7 @@ describe('authorization', () => {
securityAuth: securityStart.authz,
spaces: spacesStart,
features: featuresStart,
- auditLogger: new AuthorizationAuditLogger(),
+ auditLogger: new AuthorizationAuditLogger(mockLogger),
logger: loggingSystemMock.createLogger(),
});
diff --git a/x-pack/plugins/cloud_integrations/cloud_full_story/.i18nrc.json b/x-pack/plugins/cloud_integrations/cloud_full_story/.i18nrc.json
index aa690dec41fc1..e86db74eed77a 100755
--- a/x-pack/plugins/cloud_integrations/cloud_full_story/.i18nrc.json
+++ b/x-pack/plugins/cloud_integrations/cloud_full_story/.i18nrc.json
@@ -3,5 +3,5 @@
"paths": {
"cloudFullStory": "."
},
- "translations": ["translations/ja-JP.json"]
+ "translations": []
}
diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_url_query.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_url_query.ts
index 0573d77e6f9c8..8bc6ff96b4c4f 100644
--- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_url_query.ts
+++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_url_query.ts
@@ -6,7 +6,6 @@
*/
import { useEffect, useCallback, useMemo } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
-import type { RisonObject } from 'rison-node';
import { decodeQuery, encodeQuery } from '../navigation/query_utils';
/**
@@ -35,7 +34,7 @@ export const useUrlQuery = (getDefaultQuery: () => T) => {
useEffect(() => {
if (search) return;
- replace({ search: encodeQuery(getDefaultQuery() as RisonObject) });
+ replace({ search: encodeQuery(getDefaultQuery()) });
}, [getDefaultQuery, search, replace]);
return {
diff --git a/x-pack/plugins/cloud_security_posture/public/common/navigation/query_utils.ts b/x-pack/plugins/cloud_security_posture/public/common/navigation/query_utils.ts
index 601ad3097b7a8..3a051456733a6 100644
--- a/x-pack/plugins/cloud_security_posture/public/common/navigation/query_utils.ts
+++ b/x-pack/plugins/cloud_security_posture/public/common/navigation/query_utils.ts
@@ -4,10 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { encode, decode, type RisonObject } from 'rison-node';
+import { encode, decode } from '@kbn/rison';
import type { LocationDescriptorObject } from 'history';
-const encodeRison = (v: RisonObject): string | undefined => {
+const encodeRison = (v: any): string | undefined => {
try {
return encode(v);
} catch (e) {
@@ -27,7 +27,7 @@ const decodeRison = (query: string): T | undefined => {
const QUERY_PARAM_KEY = 'cspq';
-export const encodeQuery = (query: RisonObject): LocationDescriptorObject['search'] => {
+export const encodeQuery = (query: any): LocationDescriptorObject['search'] => {
const risonQuery = encodeRison(query);
if (!risonQuery) return;
return `${QUERY_PARAM_KEY}=${risonQuery}`;
diff --git a/x-pack/plugins/cloud_security_posture/public/components/csp_evaluation_badge.tsx b/x-pack/plugins/cloud_security_posture/public/components/csp_evaluation_badge.tsx
index 0d838daa1e660..24ca4cd4fe4eb 100644
--- a/x-pack/plugins/cloud_security_posture/public/components/csp_evaluation_badge.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/components/csp_evaluation_badge.tsx
@@ -24,7 +24,11 @@ const getColor = (type: Props['type']): EuiBadgeProps['color'] => {
};
export const CspEvaluationBadge = ({ type }: Props) => (
-
+
{type === 'failed' ? (
) : (
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/__mocks__/findings.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/__mocks__/findings.ts
new file mode 100644
index 0000000000000..48956856bf31c
--- /dev/null
+++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/__mocks__/findings.ts
@@ -0,0 +1,117 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { CspFinding } from '../../../../common/schemas/csp_finding';
+
+export const mockFindingsHit: CspFinding = {
+ result: {
+ evaluation: 'passed',
+ evidence: {
+ serviceAccounts: [],
+ serviceAccount: [],
+ },
+ // TODO: wrong type
+ // expected: null,
+ },
+ orchestrator: {
+ cluster: {
+ name: 'kind-multi',
+ },
+ },
+ agent: {
+ name: 'kind-multi-worker',
+ id: '41b2ba39-fd4e-474d-8c61-d79c9204e793',
+ // TODO: missing
+ // ephemeral_id: '20964f94-a4fe-48c1-8bf3-4b7140baf03c',
+ type: 'cloudbeat',
+ version: '8.6.0',
+ },
+ cluster_id: '087606d6-c71a-4892-9b27-67ab937770ce',
+ '@timestamp': '2022-11-24T22:27:19.515Z',
+ ecs: {
+ version: '8.0.0',
+ },
+ resource: {
+ sub_type: 'ServiceAccount',
+ name: 'certificate-controller',
+ raw: {
+ metadata: {
+ uid: '597cd43e-90a5-4aea-95aa-35f177429794',
+ resourceVersion: '277',
+ creationTimestamp: '2022-11-15T16:08:49Z',
+ name: 'certificate-controller',
+ namespace: 'kube-system',
+ },
+ apiVersion: 'v1',
+ kind: 'ServiceAccount',
+ secrets: [
+ {
+ name: 'certificate-controller-token-ql8wn',
+ },
+ ],
+ },
+ id: '597cd43e-90a5-4aea-95aa-35f177429794',
+ type: 'k8s_object',
+ },
+ host: {
+ id: '', // TODO: missing
+ hostname: 'kind-multi-worker',
+ os: {
+ kernel: '5.10.76-linuxkit',
+ codename: 'bullseye',
+ name: 'Debian GNU/Linux',
+ type: 'linux',
+ family: 'debian',
+ version: '11 (bullseye)',
+ platform: 'debian',
+ },
+ containerized: false,
+ ip: ['172.19.0.3', 'fc00:f853:ccd:e793::3', 'fe80::42:acff:fe13:3'],
+ name: 'kind-multi-worker',
+ mac: ['02-42-AC-13-00-03'],
+ architecture: 'x86_64',
+ },
+ rule: {
+ references:
+ '1. [https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)\n',
+ impact:
+ 'All workloads which require access to the Kubernetes API will require an explicit service account to be created.\n',
+ description:
+ 'The `default` service account should not be used to ensure that rights granted to applications can be more easily audited and reviewed.\n',
+ default_value:
+ 'By default the `default` service account allows for its service account token\nto be mounted\nin pods in its namespace.\n',
+ section: 'RBAC and Service Accounts',
+ rationale:
+ 'Kubernetes provides a `default` service account which is used by cluster workloads where no specific service account is assigned to the pod. Where access to the Kubernetes API from a pod is required, a specific service account should be created for that pod, and rights granted to that service account. The default service account should be configured such that it does not provide a service account token and does not have any explicit rights assignments.\n',
+ version: '1.0',
+ benchmark: {
+ name: 'CIS Kubernetes V1.23',
+ id: 'cis_k8s',
+ version: 'v1.0.0',
+ },
+ tags: ['CIS', 'Kubernetes', 'CIS 5.1.5', 'RBAC and Service Accounts'],
+ remediation:
+ 'Create explicit service accounts wherever a Kubernetes workload requires\nspecific access\nto the Kubernetes API server.\nModify the configuration of each default service account to include this value\n```\nautomountServiceAccountToken: false\n```\n',
+ audit:
+ 'For each namespace in the cluster, review the rights assigned to the default service account and ensure that it has no roles or cluster roles bound to it apart from the defaults. Additionally ensure that the `automountServiceAccountToken: false` setting is in place for each default service account.\n',
+ name: 'Ensure that default service accounts are not actively used. (Manual)',
+ id: '2b399496-f79d-5533-8a86-4ea00b95e3bd',
+ profile_applicability: '* Level 1 - Master Node\n',
+ rego_rule_id: '',
+ },
+ event: {
+ agent_id_status: 'auth_metadata_missing',
+ sequence: 1669328831,
+ ingested: '2022-11-24T22:28:25Z',
+ created: '2022-11-24T22:27:19.514650003Z',
+ kind: 'state',
+ id: 'ce5c1501-90a3-4543-bf28-cd6c9e4d73e8',
+ type: ['info'],
+ category: ['configuration'],
+ outcome: 'success',
+ },
+};
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.test.tsx
new file mode 100644
index 0000000000000..40b87da1245ef
--- /dev/null
+++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.test.tsx
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import userEvent from '@testing-library/user-event';
+import { FindingsRuleFlyout } from './findings_flyout';
+import { render, screen } from '@testing-library/react';
+import { TestProvider } from '../../../test/test_provider';
+import { mockFindingsHit } from '../__mocks__/findings';
+import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '../../../../common/constants';
+
+const TestComponent = () => (
+
+
+
+);
+
+describe('', () => {
+ describe('Overview Tab', () => {
+ it('details and remediation accordions are open', () => {
+ const { getAllByRole } = render();
+
+ getAllByRole('button', { expanded: true, name: 'Details' });
+ getAllByRole('button', { expanded: true, name: 'Remediation' });
+ });
+
+ it('displays text details summary info', () => {
+ const { getAllByText, getByText } = render();
+
+ getAllByText(mockFindingsHit.rule.name);
+ getByText(mockFindingsHit.resource.id);
+ getByText(mockFindingsHit.resource.name);
+ getAllByText(mockFindingsHit.rule.section);
+ getByText(LATEST_FINDINGS_INDEX_DEFAULT_NS);
+ mockFindingsHit.rule.tags.forEach((tag) => {
+ getAllByText(tag);
+ });
+ });
+ });
+
+ describe('Rule Tab', () => {
+ it('displays rule text details', () => {
+ const { getByText, getAllByText } = render();
+
+ userEvent.click(screen.getByTestId('findings_flyout_tab_rule'));
+
+ getAllByText(mockFindingsHit.rule.name);
+ getByText(mockFindingsHit.rule.benchmark.name);
+ getAllByText(mockFindingsHit.rule.section);
+ mockFindingsHit.rule.tags.forEach((tag) => {
+ getAllByText(tag);
+ });
+ });
+ });
+
+ describe('Resource Tab', () => {
+ it('displays resource name and id', () => {
+ const { getAllByText } = render();
+
+ userEvent.click(screen.getByTestId('findings_flyout_tab_resource'));
+
+ getAllByText(mockFindingsHit.resource.name);
+ getAllByText(mockFindingsHit.resource.id);
+ });
+ });
+});
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.tsx
index 0b00136b165c5..8229084c10dd9 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.tsx
@@ -129,7 +129,12 @@ export const FindingsRuleFlyout = ({ onClose, findings }: FindingFlyoutProps) =>
{tabs.map((v) => (
- setTab(v)}>
+ setTab(v)}
+ data-test-subj={`findings_flyout_tab_${v.id}`}
+ >
{v.title}
))}
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/overview_tab.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/overview_tab.tsx
index 5dba83b9019b8..a0c5d330d22a3 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/overview_tab.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/overview_tab.tsx
@@ -46,7 +46,7 @@ const getDetailsList = (data: CspFinding, discoverIndexLink: string | undefined)
description: (
<>
{data.rule.tags.map((tag) => (
- {tag}
+ {tag}
))}
>
),
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/rule_tab.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/rule_tab.tsx
index 74904041888a4..e51abb0bd3e9d 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/rule_tab.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/rule_tab.tsx
@@ -31,7 +31,7 @@ export const getRuleList = (rule: CspFinding['rule']) => [
description: (
<>
{rule.tags.map((tag) => (
- {tag}
+ {tag}
))}
>
),
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx
index 2d709433e7fc5..3c6b51f881989 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx
@@ -16,7 +16,6 @@ import { TestProvider } from '../../../test/test_provider';
import { getFindingsQuery } from './use_latest_findings';
import { encodeQuery } from '../../../common/navigation/query_utils';
import { useLocation } from 'react-router-dom';
-import { RisonObject } from 'rison-node';
import { buildEsQuery } from '@kbn/es-query';
import { getPaginationQuery } from '../utils/utils';
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
@@ -52,7 +51,7 @@ describe('', () => {
});
(useLocation as jest.Mock).mockReturnValue({
- search: encodeQuery(query as unknown as RisonObject),
+ search: encodeQuery(query),
});
render(
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_distribution_bar.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_distribution_bar.tsx
index f293b82341a61..9db41a7786174 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_distribution_bar.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_distribution_bar.tsx
@@ -126,6 +126,7 @@ const DistributionBar: React.FC> = ({
distributionOnClick={() => {
distributionOnClick(RULE_PASSED);
}}
+ data-test-subj="distribution_bar_passed"
/>
> = ({
distributionOnClick={() => {
distributionOnClick(RULE_FAILED);
}}
+ data-test-subj="distribution_bar_failed"
/>
);
@@ -142,12 +144,15 @@ const DistributionBarPart = ({
value,
color,
distributionOnClick,
+ ...rest
}: {
value: number;
color: string;
distributionOnClick: () => void;
+ ['data-test-subj']: string;
}) => (