Reorganize Observability Plugin (#157970)

This commit is contained in:
Coen Warmer 2023-05-18 19:38:36 +02:00 committed by GitHub
parent b754a2df75
commit ab27caff47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
208 changed files with 355 additions and 5015 deletions

23
.github/CODEOWNERS vendored
View file

@ -487,7 +487,7 @@ x-pack/packages/observability/alert_details @elastic/actionable-observability
x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops
x-pack/plugins/observability_onboarding @elastic/apm-ui
x-pack/plugins/observability @elastic/actionable-observability
x-pack/plugins/observability_shared @elastic/actionable-observability
x-pack/plugins/observability_shared @elastic/observability-ui
x-pack/test/security_api_integration/plugins/oidc_provider @elastic/kibana-security
test/common/plugins/otel_metrics @elastic/infra-monitoring-ui
packages/kbn-optimizer @elastic/kibana-operations
@ -792,16 +792,7 @@ packages/kbn-yarn-lock-validator @elastic/kibana-operations
#CC# /x-pack/plugins/reporting/ @elastic/appex-sharedux
#CC# /x-pack/plugins/serverless_security/ @elastic/appex-sharedux
### Observability Plugins
# Observability Shared App
x-pack/plugins/observability_shared @elastic/observability-ui
# Observability App
x-pack/plugins/observability @elastic/actionable-observability
# Observability App > Overview page
x-pack/plugins/observability/public/pages/overview @elastic/observability-ui
### Observability packages
# Observability App > Alert Details
x-pack/packages/observability/alert_details @elastic/actionable-observability
@ -846,13 +837,6 @@ x-pack/test/observability_functional @elastic/actionable-observability
/x-pack/test/functional/services/uptime @elastic/uptime
/x-pack/test/api_integration/apis/uptime @elastic/uptime
/x-pack/test/api_integration/apis/synthetics @elastic/uptime
/x-pack/plugins/observability/public/components/shared/exploratory_view @elastic/uptime
/x-pack/plugins/observability/public/components/shared/field_value_suggestions @elastic/uptime
/x-pack/plugins/observability/public/components/shared/core_web_vitals @elastic/uptime
/x-pack/plugins/observability/public/components/shared/load_when_in_view @elastic/uptime
/x-pack/plugins/observability/public/components/shared/filter_value_label @elastic/uptime
/x-pack/plugins/observability/public/utils/observability_data_views @elastic/uptime
/x-pack/plugins/observability/e2e @elastic/uptime
# Client Side Monitoring / Uptime (lives in APM directories but owned by Uptime)
/x-pack/plugins/apm/public/application/uxApp.tsx @elastic/uptime
@ -860,7 +844,7 @@ x-pack/test/observability_functional @elastic/actionable-observability
/x-pack/test/apm_api_integration/tests/csm/ @elastic/uptime
# Observability onboarding tour
/x-pack/plugins/observability/public/components/shared/tour @elastic/platform-onboarding
/x-pack/plugins/observability_shared/public/components/tour @elastic/platform-onboarding
/x-pack/test/functional/apps/infra/tour.ts @elastic/platform-onboarding
### END Observability Plugins
@ -1230,7 +1214,6 @@ x-pack/test/threat_intelligence_cypress @elastic/protections-experience
/x-pack/plugins/apm/**/*.scss @elastic/observability-design
/x-pack/plugins/infra/**/*.scss @elastic/observability-design
/x-pack/plugins/fleet/**/*.scss @elastic/observability-design
/x-pack/plugins/observability/**/*.scss @elastic/observability-design
/x-pack/plugins/monitoring/**/*.scss @elastic/observability-design
# Ent. Search design

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { LinkDescriptor } from '@kbn/observability-plugin/public';
import { LinkDescriptor } from '@kbn/observability-shared-plugin/public';
import { useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { InventoryItemType } from '../../../common/inventory_models/types';

View file

@ -22,8 +22,8 @@ import {
} from '@kbn/kibana-react-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { HasDataContextProvider } from '../context/has_data_context';
import { PluginContext } from '../context/plugin_context';
import { HasDataContextProvider } from '../context/has_data_context/has_data_context';
import { PluginContext } from '../context/plugin_context/plugin_context';
import { ConfigSchema, ObservabilityPublicPluginsStart } from '../plugin';
import { routes } from '../routes';
import { ObservabilityRuleTypeRegistry } from '../rules/create_observability_rule_type_registry';

View file

@ -10,8 +10,8 @@ import { waitFor } from '@testing-library/react';
import { timefilterServiceMock } from '@kbn/data-plugin/public/query/timefilter/timefilter_service.mock';
import { ObservabilityAlertSearchBarProps } from './types';
import { ObservabilityAlertSearchBar } from './alert_search_bar';
import { observabilityAlertFeatureIds } from '../../../config/alert_feature_ids';
import { render } from '../../../utils/test_helper';
import { observabilityAlertFeatureIds } from '../../config/alert_feature_ids';
import { render } from '../../utils/test_helper';
const getAlertsSearchBarMock = jest.fn();
const ALERT_SEARCH_BAR_DATA_TEST_SUBJ = 'alerts-search-bar';

View file

@ -11,11 +11,11 @@ import React, { useCallback, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { Query } from '@kbn/es-query';
import { AlertsStatusFilter } from './components';
import { observabilityAlertFeatureIds } from '../../../config/alert_feature_ids';
import { observabilityAlertFeatureIds } from '../../config/alert_feature_ids';
import { ALERT_STATUS_QUERY, DEFAULT_QUERIES, DEFAULT_QUERY_STRING } from './constants';
import { ObservabilityAlertSearchBarProps } from './types';
import { buildEsQuery } from '../../../utils/build_es_query';
import { AlertStatus } from '../../../../common/typings';
import { buildEsQuery } from '../../utils/build_es_query';
import { AlertStatus } from '../../../common/typings';
const getAlertStatusQuery = (status: string): Query[] => {
return ALERT_STATUS_QUERY[status]

View file

@ -13,9 +13,9 @@ import {
} from './containers';
import { ObservabilityAlertSearchBar } from './alert_search_bar';
import { AlertSearchBarWithUrlSyncProps } from './types';
import { useKibana } from '../../../utils/kibana_react';
import { ObservabilityAppServices } from '../../../application/types';
import { useToasts } from '../../../hooks/use_toast';
import { useKibana } from '../../utils/kibana_react';
import { ObservabilityAppServices } from '../../application/types';
import { useToasts } from '../../hooks/use_toast';
function AlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) {
const { urlStorageKey, ...searchBarProps } = props;

View file

@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { ALL_ALERTS, ACTIVE_ALERTS, RECOVERED_ALERTS } from '../constants';
import { AlertStatusFilterProps } from '../types';
import { AlertStatus } from '../../../../../common/typings';
import { AlertStatus } from '../../../../common/typings';
const options: EuiButtonGroupOptionProps[] = [
{

View file

@ -8,8 +8,8 @@
import { Query } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED, ALERT_STATUS } from '@kbn/rule-data-utils';
import { AlertStatusFilter } from '../../../../common/typings';
import { ALERT_STATUS_ALL } from '../../../../common/constants';
import { AlertStatusFilter } from '../../../common/typings';
import { ALERT_STATUS_ALL } from '../../../common/constants';
export const DEFAULT_QUERIES: Query[] = [];
export const DEFAULT_QUERY_STRING = '';

View file

@ -9,7 +9,7 @@ import {
createStateContainer,
createStateContainerReactHelpers,
} from '@kbn/kibana-utils-plugin/public';
import { AlertStatus } from '../../../../../common/typings';
import { AlertStatus } from '../../../../common/typings';
import { ALL_ALERTS } from '../constants';
interface AlertSearchBarContainerState {

View file

@ -18,9 +18,9 @@ import {
IKbnUrlStateStorage,
useContainerSelector,
} from '@kbn/kibana-utils-plugin/public';
import { datemathStringRT } from '../../../../utils/datemath';
import { ALERT_STATUS_ALL } from '../../../../../common/constants';
import { useTimefilterService } from '../../../../hooks/use_timefilter_service';
import { datemathStringRT } from '../../../utils/datemath';
import { ALERT_STATUS_ALL } from '../../../../common/constants';
import { useTimefilterService } from '../../../hooks/use_timefilter_service';
import {
useContainer,

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { ObservabilityAlertSearchBarProps } from './types';
const ObservabilityAlertSearchBarLazy = lazy(() => import('./alert_search_bar'));
export function ObservabilityAlertSearchBar(props: ObservabilityAlertSearchBarProps) {
return (
<Suspense fallback={<EuiLoadingSpinner />}>
<ObservabilityAlertSearchBarLazy {...props} />
</Suspense>
);
}

View file

@ -10,7 +10,7 @@ import { ToastsStart } from '@kbn/core-notifications-browser';
import { TimefilterContract } from '@kbn/data-plugin/public';
import { AlertsSearchBarProps } from '@kbn/triggers-actions-ui-plugin/public/application/sections/alerts_search_bar';
import { BoolQuery, Query } from '@kbn/es-query';
import { AlertStatus } from '../../../../common/typings';
import { AlertStatus } from '../../../common/typings';
export interface AlertStatusFilterProps {
status: AlertStatus;

View file

@ -8,8 +8,8 @@
import React, { ComponentType } from 'react';
import { ALERT_UUID } from '@kbn/rule-data-utils';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { PluginContext, PluginContextValue } from '../context/plugin_context';
import { createObservabilityRuleTypeRegistryMock } from '../rules/observability_rule_type_registry_mock';
import { PluginContext, PluginContextValue } from '../../context/plugin_context/plugin_context';
import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock';
import { apmAlertResponseExample } from './alerts_flyout.mock';
import { AlertsFlyout } from './alerts_flyout';

View file

@ -7,10 +7,10 @@
import React from 'react';
import * as useUiSettingHook from '@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting';
import { createObservabilityRuleTypeRegistryMock } from '../rules/observability_rule_type_registry_mock';
import { render } from '../utils/test_helper';
import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock';
import { render } from '../../utils/test_helper';
import { AlertsFlyout } from './alerts_flyout';
import type { TopAlert } from '../typings/alerts';
import type { TopAlert } from '../../typings/alerts';
describe('AlertsFlyout', () => {
jest

View file

@ -12,9 +12,9 @@ import { ALERT_UUID } from '@kbn/rule-data-utils';
import { AlertsFlyoutHeader } from './alerts_flyout_header';
import { AlertsFlyoutBody } from './alerts_flyout_body';
import { AlertsFlyoutFooter } from './alerts_flyout_footer';
import { parseAlert } from '../pages/alerts/helpers/parse_alert';
import type { ObservabilityRuleTypeRegistry } from '../rules/create_observability_rule_type_registry';
import type { TopAlert } from '../typings/alerts';
import { parseAlert } from '../../pages/alerts/helpers/parse_alert';
import type { ObservabilityRuleTypeRegistry } from '../../rules/create_observability_rule_type_registry';
import type { TopAlert } from '../../typings/alerts';
type AlertsFlyoutProps = {
alert?: TopAlert;

View file

@ -5,13 +5,13 @@
* 2.0.
*/
import React from 'react';
import { render } from '../utils/test_helper';
import { render } from '../../utils/test_helper';
import * as useUiSettingHook from '@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting';
import { createObservabilityRuleTypeRegistryMock } from '../rules/observability_rule_type_registry_mock';
import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock';
import { AlertsFlyoutBody } from './alerts_flyout_body';
import { inventoryThresholdAlert } from '../rules/fixtures/example_alerts';
import { parseAlert } from '../pages/alerts/helpers/parse_alert';
import { RULE_DETAILS_PAGE_ID } from '../pages/rule_details/constants';
import { inventoryThresholdAlert } from '../../rules/fixtures/example_alerts';
import { parseAlert } from '../../pages/alerts/helpers/parse_alert';
import { RULE_DETAILS_PAGE_ID } from '../../pages/rule_details/constants';
describe('AlertsFlyoutBody', () => {
jest

View file

@ -29,13 +29,13 @@ import {
import { AlertLifecycleStatusBadge } from '@kbn/alerts-ui-shared';
import moment from 'moment-timezone';
import { useUiSetting } from '@kbn/kibana-react-plugin/public';
import { useKibana } from '../utils/kibana_react';
import { asDuration, toMicroseconds } from '../../common/utils/formatters';
import { paths } from '../config/paths';
import { translations } from '../config/translations';
import { formatAlertEvaluationValue } from '../utils/format_alert_evaluation_value';
import { RULE_DETAILS_PAGE_ID } from '../pages/rule_details/constants';
import type { TopAlert } from '../typings/alerts';
import { useKibana } from '../../utils/kibana_react';
import { asDuration, toMicroseconds } from '../../../common/utils/formatters';
import { paths } from '../../config/paths';
import { translations } from '../../config/translations';
import { formatAlertEvaluationValue } from '../../utils/format_alert_evaluation_value';
import { RULE_DETAILS_PAGE_ID } from '../../pages/rule_details/constants';
import type { TopAlert } from '../../typings/alerts';
interface FlyoutProps {
alert: TopAlert;

View file

@ -6,12 +6,12 @@
*/
import React from 'react';
import { EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import { useKibana } from '../utils/kibana_react';
import { usePluginContext } from '../hooks/use_plugin_context';
import { isAlertDetailsEnabledPerApp } from '../utils/is_alert_details_enabled';
import { translations } from '../config/translations';
import { paths } from '../config/paths';
import type { TopAlert } from '../typings/alerts';
import { useKibana } from '../../utils/kibana_react';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { isAlertDetailsEnabledPerApp } from '../../utils/is_alert_details_enabled';
import { translations } from '../../config/translations';
import { paths } from '../../config/paths';
import type { TopAlert } from '../../typings/alerts';
interface FlyoutProps {
alert: TopAlert;

View file

@ -7,7 +7,7 @@
import React from 'react';
import { ALERT_RULE_NAME } from '@kbn/rule-data-utils';
import { EuiSpacer, EuiTitle } from '@elastic/eui';
import type { TopAlert } from '../typings/alerts';
import type { TopAlert } from '../../typings/alerts';
interface FlyoutProps {
alert: TopAlert;

View file

@ -8,11 +8,11 @@
import React, { useCallback, useMemo } from 'react';
import { AlertsTableFlyoutBaseProps } from '@kbn/triggers-actions-ui-plugin/public';
import type { ObservabilityRuleTypeRegistry } from '../rules/create_observability_rule_type_registry';
import type { ObservabilityRuleTypeRegistry } from '../../rules/create_observability_rule_type_registry';
import { AlertsFlyoutHeader } from './alerts_flyout_header';
import { AlertsFlyoutBody } from './alerts_flyout_body';
import { AlertsFlyoutFooter } from './alerts_flyout_footer';
import { parseAlert } from '../pages/alerts/helpers/parse_alert';
import { parseAlert } from '../../pages/alerts/helpers/parse_alert';
export { AlertsFlyout } from './alerts_flyout';

View file

@ -20,7 +20,7 @@ import {
AlertActions,
Props as AlertActionsProps,
} from '../../pages/alerts/components/alert_actions';
import { useGetAlertFlyoutComponents } from '../use_get_alert_flyout_components';
import { useGetAlertFlyoutComponents } from '../alerts_flyout/use_get_alert_flyout_components';
import type { ObservabilityRuleTypeRegistry } from '../../rules/create_observability_rule_type_registry';
import type { ConfigSchema } from '../../plugin';
import type { TopAlert } from '../../typings/alerts';

View file

@ -20,8 +20,8 @@ import type { CellValueElementProps, TimelineNonEcsData } from '@kbn/timelines-p
import { asDuration } from '../../../common/utils/formatters';
import { AlertSeverityBadge } from '../alert_severity_badge';
import { AlertStatusIndicator } from '../shared/alert_status_indicator';
import { TimestampTooltip } from '../shared/timestamp_tooltip';
import { AlertStatusIndicator } from '../alert_status_indicator';
import { TimestampTooltip } from './timestamp_tooltip';
import { parseAlert } from '../../pages/alerts/helpers/parse_alert';
import type { ObservabilityRuleTypeRegistry } from '../../rules/create_observability_rule_type_registry';
import type { TopAlert } from '../../typings/alerts';

View file

@ -8,7 +8,7 @@
import { shallow } from 'enzyme';
import React from 'react';
import moment from 'moment-timezone';
import { TimestampTooltip } from '.';
import { TimestampTooltip } from './timestamp_tooltip';
function mockNow(date: string | number | Date) {
const fakeNow = new Date(date).getTime();

View file

@ -7,7 +7,7 @@
import React from 'react';
import { EuiToolTip } from '@elastic/eui';
import { asAbsoluteDateTime, TimeUnit } from '../../../../common/utils/formatters/datetime';
import { asAbsoluteDateTime, TimeUnit } from '../../../common/utils/formatters/datetime';
interface Props {
/**

View file

@ -1,39 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { onBrushEnd } from './helper';
import { History } from 'history';
describe('Chart helper', () => {
describe('onBrushEnd', () => {
const history = {
push: jest.fn(),
location: {
search: '',
},
} as unknown as History;
it("doesn't push a new history when x is not defined", () => {
onBrushEnd({ x: undefined, history });
expect(history.push).not.toBeCalled();
});
it('pushes a new history with time range converted to ISO', () => {
onBrushEnd({ x: [1593409448167, 1593415727797], history });
expect(history.push).toBeCalledWith({
search: 'rangeFrom=2020-06-29T05:44:08.167Z&rangeTo=2020-06-29T07:28:47.797Z',
});
});
it('pushes a new history keeping current search', () => {
history.location.search = '?foo=bar';
onBrushEnd({ x: [1593409448167, 1593415727797], history });
expect(history.push).toBeCalledWith({
search: 'foo=bar&rangeFrom=2020-06-29T05:44:08.167Z&rangeTo=2020-06-29T07:28:47.797Z',
});
});
});
});

View file

@ -1,30 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { XYBrushEvent } from '@elastic/charts';
import { History } from 'history';
import { fromQuery, toQuery } from '../../../utils/url';
export const onBrushEnd = ({ x, history }: { x: XYBrushEvent['x']; history: History }) => {
if (x) {
const start = x[0];
const end = x[1];
const currentSearch = toQuery(history.location.search);
const nextSearch = {
rangeFrom: new Date(start).toISOString(),
rangeTo: new Date(end).toISOString(),
};
history.push({
...history.location,
search: fromQuery({
...currentSearch,
...nextSearch,
}),
});
}
};

View file

@ -8,8 +8,8 @@
import React from 'react';
import { ComponentStory } from '@storybook/react';
import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator';
import { BurnRateRuleParams } from '../../../typings';
import { KibanaReactStorybookDecorator } from '../../utils/kibana_react.storybook_decorator';
import { BurnRateRuleParams } from '../../typings';
import { BurnRateRuleEditor as Component } from './burn_rate_rule_editor';
export default {

View file

@ -12,9 +12,9 @@ import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { SLOResponse } from '@kbn/slo-schema';
import { toDuration, toMinutes } from '../../../utils/slo/duration';
import { useFetchSloDetails } from '../../../hooks/slo/use_fetch_slo_details';
import { BurnRateRuleParams, Duration, DurationUnit } from '../../../typings';
import { toDuration, toMinutes } from '../../utils/slo/duration';
import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details';
import { BurnRateRuleParams, Duration, DurationUnit } from '../../typings';
import { SloSelector } from './slo_selector';
import { BurnRate } from './burn_rate';
import { LongWindowDuration } from './long_window_duration';

View file

@ -9,8 +9,8 @@ import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIconTip } fro
import { i18n } from '@kbn/i18n';
import React, { ChangeEvent, useState } from 'react';
import { toMinutes } from '../../../utils/slo/duration';
import { Duration } from '../../../typings';
import { toMinutes } from '../../utils/slo/duration';
import { Duration } from '../../typings';
interface Props {
shortWindowDuration: Duration;

View file

@ -9,7 +9,7 @@ import React from 'react';
import { ComponentStory } from '@storybook/react';
import { SLOResponse } from '@kbn/slo-schema';
import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator';
import { KibanaReactStorybookDecorator } from '../../utils/kibana_react.storybook_decorator';
import { SloSelector as Component } from './slo_selector';
export default {

View file

@ -10,12 +10,12 @@ import userEvent from '@testing-library/user-event';
import { wait } from '@testing-library/user-event/dist/utils';
import React from 'react';
import { emptySloList } from '../../../data/slo/slo';
import { useFetchSloList } from '../../../hooks/slo/use_fetch_slo_list';
import { render } from '../../../utils/test_helper';
import { emptySloList } from '../../data/slo/slo';
import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list';
import { render } from '../../utils/test_helper';
import { SloSelector } from './slo_selector';
jest.mock('../../../hooks/slo/use_fetch_slo_list');
jest.mock('../../hooks/slo/use_fetch_slo_list');
const useFetchSloListMock = useFetchSloList as jest.Mock;

View file

@ -11,7 +11,7 @@ import { SLOResponse } from '@kbn/slo-schema';
import { debounce } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useFetchSloList } from '../../../hooks/slo/use_fetch_slo_list';
import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list';
interface Props {
initialSlo?: SLOResponse;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { BurnRateRuleParams } from '../../../typings';
import { BurnRateRuleParams } from '../../typings';
import { validateBurnRateRule } from './validation';
const VALID_PARAMS: BurnRateRuleParams = {

View file

@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
import { ValidationResult } from '@kbn/triggers-actions-ui-plugin/public';
import { BurnRateRuleParams, Duration } from '../../../typings';
import { BurnRateRuleParams, Duration } from '../../typings';
export type ValidationBurnRateRuleResult = ValidationResult & {
errors: { sloId: string[]; longWindow: string[]; burnRateThreshold: string[] };

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { render } from '../../../utils/test_helper';
import { render } from '../../utils/test_helper';
import { CoreVitalItem } from './core_vital_item';
import {
NO_DATA,

View file

@ -18,7 +18,15 @@ import {
import { CoreVitalItem } from './core_vital_item';
import { WebCoreVitalsTitle } from './web_core_vitals_title';
import { ServiceName } from './service_name';
import { CoreVitalProps } from '../types';
export interface CoreVitalProps {
loading: boolean;
data?: UXMetrics | null;
displayServiceName?: boolean;
serviceName?: string;
totalPageViews?: number;
displayTrafficMetric?: boolean;
}
export interface UXMetrics {
cls: number | null;

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { lazy, Suspense } from 'react';
import { CoreVitalProps } from './core_vitals';
const CoreVitalsLazy = lazy(() => import('./core_vitals'));
export function getCoreVitalsComponent(props: CoreVitalProps) {
return (
<Suspense fallback={null}>
<CoreVitalsLazy {...props} />
</Suspense>
);
}

View file

@ -9,14 +9,12 @@ import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { usePluginContext } from '../hooks/use_plugin_context';
import { HeaderMenu } from '../pages/overview/components/header_menu';
export function LoadingObservability() {
const { ObservabilityPageTemplate } = usePluginContext();
return (
<ObservabilityPageTemplate pageSectionProps={{ alignment: 'center' }} showSolutionNav={false}>
<HeaderMenu />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />

View file

@ -1,76 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiPopover,
EuiText,
EuiListGroup,
EuiSpacer,
EuiHorizontalRule,
EuiListGroupItem,
EuiPopoverProps,
EuiListGroupItemProps,
} from '@elastic/eui';
import React, { HTMLAttributes, ReactNode } from 'react';
import styled from 'styled-components';
import { EuiListGroupProps } from '@elastic/eui';
type Props = EuiPopoverProps & HTMLAttributes<HTMLDivElement>;
export function SectionTitle({ children }: { children?: ReactNode }) {
return (
<>
<EuiText size={'s'} grow={false}>
<h5>{children}</h5>
</EuiText>
<EuiSpacer size={'xs'} />
</>
);
}
export function SectionSubtitle({ children }: { children?: ReactNode }) {
return (
<>
<EuiText size={'xs'} color={'subdued'} grow={false}>
<small>{children}</small>
</EuiText>
<EuiSpacer size={'s'} />
</>
);
}
export function SectionLinks({ children, ...props }: { children?: ReactNode } & EuiListGroupProps) {
return (
<EuiListGroup {...props} flush={true} bordered={false}>
{children}
</EuiListGroup>
);
}
export function SectionSpacer() {
return <EuiSpacer size={'l'} />;
}
export const Section = styled.div`
margin-bottom: 16px;
&:last-of-type {
margin-bottom: 0;
}
`;
export type SectionLinkProps = EuiListGroupItemProps;
export function SectionLink(props: SectionLinkProps) {
return <EuiListGroupItem style={{ padding: 0 }} size={'xs'} {...props} />;
}
export function ActionMenuDivider() {
return <EuiHorizontalRule margin={'s'} />;
}
export function ActionMenu(props: Props) {
return <EuiPopover {...props} ownFocus={true} />;
}

View file

@ -1,32 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiHeaderLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useKibana } from '../../../utils/kibana_react';
export function MobileAddData() {
const kibana = useKibana();
return (
<EuiHeaderLink
aria-label={i18n.translate('xpack.observability.page_header.addMobileDataLink.label', {
defaultMessage: 'Navigate to a tutorial about adding mobile APM data',
})}
href={kibana.services?.application?.getUrlForApp('/home#/tutorial/apm')}
color="primary"
iconType="indexOpen"
>
{ADD_DATA_LABEL}
</EuiHeaderLink>
);
}
const ADD_DATA_LABEL = i18n.translate('xpack.observability.mobile.addDataButtonLabel', {
defaultMessage: 'Add Mobile data',
});

View file

@ -1,32 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiHeaderLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useKibana } from '../../../utils/kibana_react';
export function SyntheticsAddData() {
const kibana = useKibana();
return (
<EuiHeaderLink
aria-label={i18n.translate('xpack.observability.page_header.addUptimeDataLink.label', {
defaultMessage: 'Navigate to a tutorial about adding Uptime data',
})}
href={kibana.services?.application?.getUrlForApp('/home#/tutorial/uptimeMonitors')}
color="primary"
iconType="indexOpen"
>
{ADD_DATA_LABEL}
</EuiHeaderLink>
);
}
const ADD_DATA_LABEL = i18n.translate('xpack.observability..synthetics.addDataButtonLabel', {
defaultMessage: 'Add synthetics data',
});

View file

@ -1,32 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiHeaderLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useKibana } from '../../../utils/kibana_react';
export function UXAddData() {
const kibana = useKibana();
return (
<EuiHeaderLink
aria-label={i18n.translate('xpack.observability.page_header.addUXDataLink.label', {
defaultMessage: 'Navigate to a tutorial about adding user experience APM data',
})}
href={kibana.services?.application?.getUrlForApp('/home#/tutorial/apm')}
color="primary"
iconType="indexOpen"
>
{ADD_DATA_LABEL}
</EuiHeaderLink>
);
}
const ADD_DATA_LABEL = i18n.translate('xpack.observability.ux.addDataButtonLabel', {
defaultMessage: 'Add UX data',
});

View file

@ -1,9 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { ObservabilityAlertSearchBar } from './alert_search_bar';
export { ObservabilityAlertSearchbarWithUrlSync } from './alert_search_bar_with_url_sync';

View file

@ -1,22 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export interface TimePickerQuickRange {
from: string;
to: string;
display: string;
}
export interface TimePickerRefreshInterval {
pause: boolean;
value: number;
}
export interface TimePickerTimeDefaults {
from: string;
to: string;
}

View file

@ -1,104 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { ComponentType, useEffect, useState } from 'react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { Observable } from 'rxjs';
import { CoreStart } from '@kbn/core/public';
import { text } from '@storybook/addon-knobs';
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import { FieldValueSelectionProps } from '../types';
import { FieldValueSelection } from '../field_value_selection';
const values = [
{ label: 'elastic co frontend', count: 1 },
{ label: 'apm server', count: 2 },
];
const KibanaReactContext = createKibanaReactContext({
uiSettings: { get: () => {}, get$: () => new Observable() },
} as unknown as Partial<CoreStart>);
export default {
title: 'app/Shared/FieldValueSuggestions',
component: FieldValueSelection,
decorators: [
(Story: ComponentType<FieldValueSelectionProps>) => (
<IntlProvider locale="en">
<KibanaReactContext.Provider>
<FieldValueSelection
label="Service name"
values={values}
onChange={() => {}}
selectedValue={[]}
loading={false}
setQuery={() => {}}
/>
</KibanaReactContext.Provider>
</IntlProvider>
),
],
};
export function ValuesLoaded() {
return (
<FieldValueSelection
label="Service name"
values={values}
onChange={() => {}}
selectedValue={[]}
loading={false}
setQuery={() => {}}
/>
);
}
export function LoadingState() {
return (
<FieldValueSelection
label="Service name"
values={values}
onChange={() => {}}
selectedValue={[]}
loading={true}
setQuery={() => {}}
/>
);
}
export function EmptyState() {
return (
<FieldValueSelection
label="Service name"
values={[]}
onChange={() => {}}
selectedValue={[]}
loading={false}
setQuery={() => {}}
/>
);
}
export function SearchState(args: FieldValueSelectionProps) {
const name = text('Query', '');
const [, setQuery] = useState('');
useEffect(() => {
setQuery(name);
}, [name]);
return (
<FieldValueSelection
label="Service name"
values={values}
onChange={() => {}}
selectedValue={[]}
loading={false}
setQuery={setQuery}
/>
);
}

View file

@ -1,116 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useState } from 'react';
import { union, isEmpty } from 'lodash';
import {
EuiComboBox,
EuiFormControlLayout,
EuiComboBoxOptionOption,
EuiFormRow,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import { FieldValueSelectionProps } from './types';
const formatOptions = (values?: string[], allowAllValuesSelection?: boolean) => {
const uniqueValues = Array.from(
new Set(
allowAllValuesSelection && (values ?? []).length > 0
? ['ALL_VALUES', ...(values ?? [])]
: values
)
);
return (uniqueValues ?? []).map((label) => ({
label,
}));
};
type ValueOption = EuiComboBoxOptionOption<string>;
export function FieldValueCombobox({
label,
selectedValue,
loading,
values,
setQuery,
usePrependLabel = true,
compressed = true,
required = true,
singleSelection = false,
allowAllValuesSelection,
onChange: onSelectionChange,
}: FieldValueSelectionProps) {
const [options, setOptions] = useState<ValueOption[]>(() =>
formatOptions(
union(values?.map(({ label: lb }) => lb) ?? [], selectedValue ?? []),
allowAllValuesSelection
)
);
useEffect(() => {
setOptions(
formatOptions(
union(values?.map(({ label: lb }) => lb) ?? [], selectedValue ?? []),
allowAllValuesSelection
)
);
}, [allowAllValuesSelection, selectedValue, values]);
const onChange = (selectedValuesN: ValueOption[]) => {
onSelectionChange(selectedValuesN.map(({ label: lbl }) => lbl));
};
const comboBox = (
<EuiComboBox
fullWidth
singleSelection={singleSelection ? { asPlainText: true } : false}
compressed={compressed}
placeholder={i18n.translate('xpack.observability.fieldValueSelection.placeholder.search', {
defaultMessage: 'Search {label}',
values: { label },
})}
isLoading={loading}
onSearchChange={(searchVal) => {
setQuery(searchVal);
}}
options={options}
selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))}
onChange={onChange}
isInvalid={required && isEmpty(selectedValue)}
/>
);
return usePrependLabel ? (
<ComboWrapper>
<EuiFormControlLayout fullWidth prepend={label} compressed>
{comboBox}
</EuiFormControlLayout>
</ComboWrapper>
) : (
<EuiFormRow label={label} display="center" fullWidth>
{comboBox}
</EuiFormRow>
);
}
const ComboWrapper = styled.div`
&&& {
.euiFormControlLayout {
height: auto;
.euiFormControlLayout__prepend {
margin: auto;
}
.euiComboBoxPill {
max-width: 250px;
}
.euiComboBox__inputWrap {
border-radius: 0;
}
}
}
`;

View file

@ -1,81 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { mount, render } from 'enzyme';
import { FieldValueSelection } from './field_value_selection';
import { EuiSelectableList } from '@elastic/eui';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
const values = [
{ label: 'elastic co frontend', count: 1 },
{ label: 'apm server', count: 2 },
];
describe('FieldValueSelection', () => {
it('renders a label for button', async () => {
const wrapper = render(
<FieldValueSelection
label="Service name"
values={values}
onChange={() => {}}
selectedValue={[]}
loading={false}
setQuery={() => {}}
/>
);
const btn = wrapper.find('[data-test-subj=fieldValueSelectionBtn]');
expect(btn.text()).toBe('Service name');
});
it('renders a list on click', async () => {
const wrapper = mount(
<EuiThemeProvider>
<FieldValueSelection
label="Service name"
values={values}
onChange={() => {}}
selectedValue={[]}
loading={false}
setQuery={() => {}}
/>
</EuiThemeProvider>
);
const btn = wrapper.find('button[data-test-subj="fieldValueSelectionBtn"]');
btn.simulate('click');
const list = wrapper.find(EuiSelectableList);
expect((list.props() as any).visibleOptions).toMatchInlineSnapshot(`
Array [
Object {
"append": <styled.div>
<EuiText
size="xs"
>
1
</EuiText>
</styled.div>,
"label": "elastic co frontend",
},
Object {
"append": <styled.div>
<EuiText
size="xs"
>
2
</EuiText>
</styled.div>,
"label": "apm server",
},
]
`);
});
});

View file

@ -1,300 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FormEvent, useEffect, useState } from 'react';
import {
EuiText,
EuiButton,
EuiSwitch,
EuiSpacer,
EuiFilterButton,
EuiPopover,
EuiPopoverFooter,
EuiPopoverTitle,
EuiSelectable,
EuiSelectableOption,
EuiLoadingSpinner,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import { isEqual, map } from 'lodash';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { FieldValueSelectionProps, ListItem } from './types';
const Counter = euiStyled.div`
border-radius: ${({ theme }) => theme.eui.euiBorderRadius};
background: ${({ theme }) => theme.eui.euiColorLightShade};
padding: 0 ${({ theme }) => theme.eui.euiSizeXS};
`;
const formatOptions = (
values?: ListItem[],
selectedValue?: string[],
excludedValues?: string[],
showCount?: boolean
): EuiSelectableOption[] => {
const uniqueValues: Record<string, number> = {};
values?.forEach(({ label, count }) => {
uniqueValues[label] = count;
});
return Object.entries(uniqueValues).map(([label, count]) => ({
label,
append: showCount ? (
<Counter>
<EuiText size="xs">{count}</EuiText>
</Counter>
) : null,
...(selectedValue?.includes(label) ? { checked: 'on' } : {}),
...(excludedValues?.includes(label) ? { checked: 'off' } : {}),
}));
};
export function FieldValueSelection({
fullWidth,
label,
loading,
query,
setQuery,
button,
width,
forceOpen,
setForceOpen,
anchorPosition,
singleSelection,
asFilterButton,
showCount = true,
values = [],
selectedValue,
excludedValue,
allowExclusions = true,
compressed = true,
useLogicalAND,
showLogicalConditionSwitch = false,
onChange: onSelectionChange,
}: FieldValueSelectionProps) {
const { euiTheme } = useEuiTheme();
const [options, setOptions] = useState<EuiSelectableOption[]>(() =>
formatOptions(values, selectedValue, excludedValue, showCount)
);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [isLogicalAND, setIsLogicalAND] = useState(useLogicalAND);
useEffect(() => {
setIsLogicalAND(useLogicalAND);
}, [useLogicalAND]);
useEffect(() => {
setOptions(formatOptions(values, selectedValue, excludedValue, showCount));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(values), JSON.stringify(selectedValue), showCount, excludedValue]);
const onButtonClick = () => {
setIsPopoverOpen(!isPopoverOpen);
};
const closePopover = () => {
setIsPopoverOpen(false);
setForceOpen?.(false);
};
const onChange = (optionsN: EuiSelectableOption[]) => {
setOptions(optionsN);
};
const onValueChange = (evt: FormEvent<HTMLInputElement>) => {
setQuery((evt.target as HTMLInputElement).value);
};
const anchorButton = (
<EuiButton
style={width ? { width } : {}}
size="m"
color="text"
iconType="arrowDown"
iconSide="right"
onClick={onButtonClick}
data-test-subj={'fieldValueSelectionBtn'}
fullWidth={fullWidth}
>
{label}
</EuiButton>
);
const numOfFilters = (selectedValue || []).length + (excludedValue || []).length;
const filterButton = (
<EuiFilterButton
aria-label={i18n.translate('xpack.observability.filterButton.label', {
defaultMessage: 'expands filter group for {label} filter',
values: { label },
})}
hasActiveFilters={numOfFilters > 0}
iconType="arrowDown"
numActiveFilters={numOfFilters}
numFilters={options.length}
onClick={onButtonClick}
>
{label}
</EuiFilterButton>
);
const applyDisabled = () => {
const currSelected = (options ?? [])
.filter((opt) => opt?.checked === 'on')
.map(({ label: labelN }) => labelN);
const currExcluded = (options ?? [])
.filter((opt) => opt?.checked === 'off')
.map(({ label: labelN }) => labelN);
const hasFilterSelected = (selectedValue ?? []).length > 0 || (excludedValue ?? []).length > 0;
return (
isEqual(selectedValue ?? [], currSelected) &&
isEqual(excludedValue ?? [], currExcluded) &&
!(isLogicalAND !== useLogicalAND && hasFilterSelected)
);
};
return (
<Wrapper>
<EuiPopover
id="popover"
panelPaddingSize="none"
button={button || (asFilterButton ? filterButton : anchorButton)}
isOpen={isPopoverOpen || forceOpen}
closePopover={closePopover}
anchorPosition={anchorPosition}
style={{ width: '100%' }}
>
<EuiSelectable
searchable
singleSelection={singleSelection}
searchProps={{
placeholder: i18n.translate('xpack.observability.fieldValueSelection.placeholder', {
defaultMessage: 'Filter {label}',
values: { label },
}),
compressed,
onInput: onValueChange,
'data-test-subj': 'suggestionInputField',
}}
listProps={{
onFocusBadge: false,
}}
options={options}
onChange={onChange}
allowExclusions={allowExclusions}
isLoading={loading && !query && options.length === 0}
>
{(list, search) => (
<div style={{ width: 240 }}>
<EuiPopoverTitle paddingSize="s">{search}</EuiPopoverTitle>
{list}
{loading && query && (
<EuiText className="eui-textCenter" color="subdued">
{i18n.translate('xpack.observability.fieldValueSelection.loading', {
defaultMessage: 'Loading',
})}{' '}
<EuiLoadingSpinner size="m" />
</EuiText>
)}
<EuiPopoverFooter paddingSize="s">
{showLogicalConditionSwitch && (
<>
<EuiSpacer size="xs" />
<div css={{ display: 'flex', justifyContent: 'flex-end' }}>
<EuiSwitch
css={{
flexDirection: 'row-reverse',
gap: euiTheme.size.s,
color: euiTheme.colors.subduedText,
}}
label={i18n.translate(
'xpack.observability.fieldValueSelection.logicalAnd',
{
defaultMessage: 'Use logical AND',
}
)}
data-test-subj="tagsLogicalOperatorSwitch"
checked={Boolean(isLogicalAND)}
compressed={true}
onChange={(e) => {
setIsLogicalAND(e.target.checked);
}}
/>
</div>
<EuiSpacer size="m" />
</>
)}
<EuiButton
data-test-subj="o11yFieldValueSelectionApplyButton"
aria-label={i18n.translate(
'xpack.observability.fieldValueSelection.apply.label',
{
defaultMessage: 'Apply the selected filters for {label}',
values: { label },
}
)}
fill
fullWidth
size="s"
isDisabled={applyDisabled()}
onClick={() => {
const selectedValuesN = options.filter((opt) => opt?.checked === 'on');
const excludedValuesN = options.filter((opt) => opt?.checked === 'off');
if (showLogicalConditionSwitch) {
onSelectionChange(
map(selectedValuesN, 'label'),
map(excludedValuesN, 'label'),
isLogicalAND
);
} else {
onSelectionChange(
map(selectedValuesN, 'label'),
map(excludedValuesN, 'label')
);
}
setIsPopoverOpen(false);
setForceOpen?.(false);
}}
>
{i18n.translate('xpack.observability.fieldValueSelection.apply', {
defaultMessage: 'Apply',
})}
</EuiButton>
</EuiPopoverFooter>
</div>
)}
</EuiSelectable>
</EuiPopover>
</Wrapper>
);
}
// eslint-disable-next-line import/no-default-export
export default FieldValueSelection;
const Wrapper = styled.div`
&&& {
div.euiPopover__anchor {
width: 100%;
.euiButton {
width: 100%;
}
}
}
`;

View file

@ -1,137 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { FieldValueSuggestions } from '.';
import { render, screen, fireEvent, waitForElementToBeRemoved } from '@testing-library/react';
import * as searchHook from '../../../hooks/use_es_search';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
jest.setTimeout(30000);
describe('FieldValueSuggestions', () => {
jest.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(1500);
jest.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(1500);
function setupSearch(data: any) {
// @ts-ignore
jest.spyOn(searchHook, 'useEsSearch').mockReturnValue({
data: {
took: 17,
timed_out: false,
_shards: { total: 35, successful: 35, skipped: 31, failed: 0 },
hits: { total: { value: 15299, relation: 'eq' }, hits: [] },
aggregations: {
values: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: data,
},
},
},
loading: false,
});
}
it('renders a list', async () => {
setupSearch([
{ key: 'US', doc_count: 14132 },
{ key: 'Pak', doc_count: 200 },
{ key: 'Japan', doc_count: 100 },
]);
render(
<EuiThemeProvider darkMode={false}>
<FieldValueSuggestions
label="Service name"
sourceField={'service'}
onChange={() => {}}
selectedValue={[]}
filters={[]}
asCombobox={false}
/>
</EuiThemeProvider>
);
fireEvent.click(screen.getByText('Service name'));
expect(await screen.findByPlaceholderText('Filter Service name')).toBeInTheDocument();
expect(await screen.findByText('Apply')).toBeInTheDocument();
expect(await screen.findByText('US')).toBeInTheDocument();
expect(await screen.findByText('Pak')).toBeInTheDocument();
expect(await screen.findByText('Japan')).toBeInTheDocument();
expect(await screen.findByText('14132')).toBeInTheDocument();
expect(await screen.findByText('200')).toBeInTheDocument();
expect(await screen.findByText('100')).toBeInTheDocument();
setupSearch([{ key: 'US', doc_count: 14132 }]);
fireEvent.input(screen.getByTestId('suggestionInputField'), {
target: { value: 'u' },
});
expect(await screen.findByDisplayValue('u')).toBeInTheDocument();
});
it('calls oncChange when applied', async () => {
setupSearch([
{ key: 'US', doc_count: 14132 },
{ key: 'Pak', doc_count: 200 },
{ key: 'Japan', doc_count: 100 },
]);
const onChange = jest.fn();
const { rerender } = render(
<EuiThemeProvider darkMode={false}>
<FieldValueSuggestions
label="Service name"
sourceField={'service'}
onChange={onChange}
selectedValue={[]}
filters={[]}
asCombobox={false}
allowExclusions={true}
/>
</EuiThemeProvider>
);
fireEvent.click(screen.getByText('Service name'));
fireEvent.click(await screen.findByText('US'));
fireEvent.click(await screen.findByText('Apply'));
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(['US'], []);
await waitForElementToBeRemoved(() => screen.queryByText('Apply'));
rerender(
<EuiThemeProvider darkMode={false}>
<FieldValueSuggestions
label="Service name"
sourceField={'service'}
onChange={onChange}
selectedValue={['US']}
excludedValue={['Pak']}
filters={[]}
asCombobox={false}
allowExclusions={true}
/>
</EuiThemeProvider>
);
fireEvent.click(screen.getByText('Service name'));
fireEvent.click(await screen.findByText('US'));
fireEvent.click(await screen.findByText('Pak'));
fireEvent.click(await screen.findByText('Apply'));
expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange).toHaveBeenLastCalledWith([], ['US']);
});
});

View file

@ -1,89 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState } from 'react';
import { useValuesList } from '../../../hooks/use_values_list';
import { FieldValueSelection } from './field_value_selection';
import { FieldValueSuggestionsProps } from './types';
import { FieldValueCombobox } from './field_value_combobox';
export function FieldValueSuggestions({
fullWidth,
sourceField,
label,
dataViewTitle,
selectedValue,
excludedValue,
filters,
button,
time,
width,
forceOpen,
setForceOpen,
anchorPosition,
singleSelection,
compressed,
asFilterButton,
usePrependLabel,
allowAllValuesSelection,
required,
allowExclusions = true,
cardinalityField,
inspector,
asCombobox = true,
keepHistory = true,
showLogicalConditionSwitch,
useLogicalAND,
onChange: onSelectionChange,
}: FieldValueSuggestionsProps) {
const [query, setQuery] = useState('');
const { values, loading } = useValuesList({
dataViewTitle,
query,
sourceField,
filters,
time,
inspector,
cardinalityField,
keepHistory,
label,
});
const SelectionComponent = asCombobox ? FieldValueCombobox : FieldValueSelection;
return (
<SelectionComponent
fullWidth={fullWidth}
singleSelection={singleSelection}
values={values}
label={label}
onChange={onSelectionChange}
query={query}
setQuery={setQuery}
loading={loading}
selectedValue={selectedValue}
excludedValue={excludedValue}
button={button}
forceOpen={forceOpen}
setForceOpen={setForceOpen}
anchorPosition={anchorPosition}
width={width}
compressed={compressed}
asFilterButton={asFilterButton}
usePrependLabel={usePrependLabel}
allowExclusions={allowExclusions}
allowAllValuesSelection={singleSelection ? false : allowAllValuesSelection}
required={required}
showLogicalConditionSwitch={showLogicalConditionSwitch}
useLogicalAND={useLogicalAND}
/>
);
}
// eslint-disable-next-line import/no-default-export
export default FieldValueSuggestions;

View file

@ -1,57 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { PopoverAnchorPosition } from '@elastic/eui';
import { Dispatch, SetStateAction } from 'react';
import type { ESFilter } from '@kbn/es-types';
import { IInspectorInfo } from '@kbn/data-plugin/common';
interface CommonProps {
selectedValue?: string[];
excludedValue?: string[];
label: string;
button?: JSX.Element;
width?: number;
singleSelection?: boolean;
forceOpen?: boolean;
setForceOpen?: (val: boolean) => void;
anchorPosition?: PopoverAnchorPosition;
fullWidth?: boolean;
compressed?: boolean;
asFilterButton?: boolean;
showCount?: boolean;
usePrependLabel?: boolean;
allowExclusions?: boolean;
allowAllValuesSelection?: boolean;
cardinalityField?: string;
required?: boolean;
keepHistory?: boolean;
showLogicalConditionSwitch?: boolean;
useLogicalAND?: boolean;
onChange: (val?: string[], excludedValue?: string[], isLogicalAND?: boolean) => void;
}
export type FieldValueSuggestionsProps = CommonProps & {
dataViewTitle?: string;
sourceField: string;
asCombobox?: boolean;
filters: ESFilter[];
time?: { from: string; to: string };
inspector?: IInspectorInfo;
};
export type FieldValueSelectionProps = CommonProps & {
loading?: boolean;
values?: ListItem[];
query?: string;
setQuery: Dispatch<SetStateAction<string>>;
};
export interface ListItem {
label: string;
count: number;
}

View file

@ -1,107 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { injectI18n } from '@kbn/i18n-react';
import { Filter, buildPhrasesFilter, buildPhraseFilter } from '@kbn/es-query';
import { FilterItem } from '@kbn/unified-search-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/common';
import { useKibana } from '@kbn/kibana-react-plugin/public';
export function buildFilterLabel({
field,
value,
label,
dataView,
negate,
}: {
label: string;
value: string | Array<string | number>;
negate: boolean;
field: string;
dataView: DataView;
}) {
const indexField = dataView.getFieldByName(field)!;
const areMultipleValues = Array.isArray(value) && value.length > 1;
const filter = areMultipleValues
? buildPhrasesFilter(indexField, value, dataView)
: buildPhraseFilter(indexField, Array.isArray(value) ? value[0] : value, dataView);
filter.meta.type = areMultipleValues ? 'phrases' : 'phrase';
filter.meta.value = Array.isArray(value)
? !areMultipleValues
? `${value[0]}`
: undefined
: value;
filter.meta.key = label;
filter.meta.alias = null;
filter.meta.negate = negate;
filter.meta.disabled = false;
return filter;
}
export interface FilterValueLabelProps {
field: string;
label: string;
value: string | Array<string | number>;
negate: boolean;
removeFilter: (field: string, value: string | Array<string | number>, notVal: boolean) => void;
invertFilter: (val: {
field: string;
value: string | Array<string | number>;
negate: boolean;
}) => void;
dataView: DataView;
allowExclusion?: boolean;
}
export function FilterValueLabel({
label,
field,
value,
negate,
dataView,
invertFilter,
removeFilter,
allowExclusion = true,
}: FilterValueLabelProps) {
const FilterItemI18n = injectI18n(FilterItem);
const filter = buildFilterLabel({ field, value, label, dataView, negate });
const {
services: { uiSettings },
} = useKibana();
return dataView ? (
<FilterItemI18n
indexPatterns={[dataView]}
id={`${field}-${value}-${negate}`}
filter={filter}
onRemove={() => {
removeFilter(field, value, false);
}}
onUpdate={(filterN: Filter) => {
if (filterN.meta.negate !== negate) {
invertFilter({ field, value, negate });
}
}}
uiSettings={uiSettings!}
hiddenPanelOptions={[
...(allowExclusion ? [] : ['negateFilter' as const]),
'pinFilter',
'editFilter',
'disableFilter',
]}
/>
) : null;
}
// eslint-disable-next-line import/no-default-export
export default FilterValueLabel;

View file

@ -1,102 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { LoadWhenInViewProps } from './load_when_in_view/load_when_in_view';
import { ObservabilityAlertSearchBarProps } from './alert_search_bar/types';
import type { CoreVitalProps, HeaderMenuPortalProps } from './types';
import type {
FieldValueSuggestionsProps,
FieldValueSelectionProps,
} from './field_value_suggestions/types';
import type { DatePickerProps } from './date_picker';
import type { FilterValueLabelProps } from './filter_value_label/filter_value_label';
export { createLazyObservabilityPageTemplate } from './page_template';
export type { LazyObservabilityPageTemplateProps } from './page_template';
const CoreVitalsLazy = lazy(() => import('./core_web_vitals'));
export function getCoreVitalsComponent(props: CoreVitalProps) {
return (
<Suspense fallback={null}>
<CoreVitalsLazy {...props} />
</Suspense>
);
}
const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal'));
export function HeaderMenuPortal(props: HeaderMenuPortalProps) {
return (
<Suspense fallback={<EuiLoadingSpinner />}>
<HeaderMenuPortalLazy {...props} />
</Suspense>
);
}
const FieldValueSelectionLazy = lazy(
() => import('./field_value_suggestions/field_value_selection')
);
export function FieldValueSelection(props: FieldValueSelectionProps) {
return (
<Suspense fallback={null}>
<FieldValueSelectionLazy {...props} />
</Suspense>
);
}
const FieldValueSuggestionsLazy = lazy(() => import('./field_value_suggestions'));
export function FieldValueSuggestions(props: FieldValueSuggestionsProps) {
return (
<Suspense fallback={null}>
<FieldValueSuggestionsLazy {...props} />
</Suspense>
);
}
const FilterValueLabelLazy = lazy(() => import('./filter_value_label/filter_value_label'));
export function FilterValueLabel(props: FilterValueLabelProps) {
return (
<Suspense fallback={null}>
<FilterValueLabelLazy {...props} />
</Suspense>
);
}
const DatePickerLazy = lazy(() => import('./date_picker'));
export function DatePicker(props: DatePickerProps) {
return (
<Suspense fallback={<EuiLoadingSpinner />}>
<DatePickerLazy {...props} />
</Suspense>
);
}
const LoadWhenInViewLazy = lazy(() => import('./load_when_in_view/load_when_in_view'));
export function LoadWhenInView(props: LoadWhenInViewProps) {
return (
<Suspense fallback={<EuiLoadingSpinner />}>
<LoadWhenInViewLazy {...props} />
</Suspense>
);
}
const ObservabilityAlertSearchBarLazy = lazy(() => import('./alert_search_bar/alert_search_bar'));
export function ObservabilityAlertSearchBar(props: ObservabilityAlertSearchBarProps) {
return (
<Suspense fallback={<EuiLoadingSpinner />}>
<ObservabilityAlertSearchBarLazy {...props} />
</Suspense>
);
}

View file

@ -1,52 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useState } from 'react';
import { EuiSkeletonText } from '@elastic/eui';
import useIntersection from 'react-use/lib/useIntersection';
export interface LoadWhenInViewProps {
children: JSX.Element;
initialHeight?: string | number;
placeholderTitle: string;
}
// eslint-disable-next-line import/no-default-export
export default function LoadWhenInView({
children,
placeholderTitle,
initialHeight = 100,
}: LoadWhenInViewProps) {
const intersectionRef = React.useRef(null);
const intersection = useIntersection(intersectionRef, {
root: null,
rootMargin: '0px',
threshold: 0.25,
});
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (intersection && intersection.intersectionRatio > 0.25) {
setIsVisible(true);
}
}, [intersection, intersection?.intersectionRatio]);
return isVisible ? (
children
) : (
<div
data-test-subj="renderOnlyInViewPlaceholderContainer"
ref={intersectionRef}
role="region"
aria-label={placeholderTitle}
style={{ height: initialHeight }}
>
<EuiSkeletonText />
</div>
);
}

View file

@ -1,176 +0,0 @@
## Overview
Observability solutions can register their navigation structures via the Observability plugin, this ensures that these navigation options display in the Observability page template component. This is a two part process, A) register your navigation structure and B) consume and render the shared page template component. These two elements are documented below.
## Navigation registration
To register a solution's navigation structure you'll first need to ensure your solution has the observability plugin specified as a dependency in your `kibana.json` file, e.g.
```json
"requiredPlugins": [
"observability"
],
```
Now within your solution's **public** plugin `setup` lifecycle method you can
call the `registerSections` method, this will register your solution's specific
navigation structure with the overall Observability navigation registry.
The `registerSections` function takes an `Observable` of an array of
`NavigationSection`s. Each section can be defined as
```typescript
export interface NavigationSection {
// the label of the section, should be translated
label: string | undefined;
// the key to sort by in ascending order relative to other entries
sortKey: number;
// the entries to render inside the section
entries: NavigationEntry[];
}
```
Each entry inside of a navigation section is defined as
```typescript
export interface NavigationEntry {
// the label of the menu entry, should be translated
label: string;
// the kibana app id
app: string;
// the path after the application prefix corresponding to this entry
path: string;
// whether to only match when the full path matches, defaults to `false`
matchFullPath?: boolean;
// whether to ignore trailing slashes, defaults to `true`
ignoreTrailingSlash?: boolean;
// shows NEW badge besides the navigation label, which will automatically disappear when menu item is clicked.
isNewFeature?: boolean;
// shows beta badge lab icon if the feature is still beta besides the navigation label
isBeta?: boolean;
}
```
A registration might therefore look like the following:
```typescript
// x-pack/plugins/example_plugin/public/plugin.ts
import { of } from 'rxjs';
export class Plugin implements PluginClass {
constructor(_context: PluginInitializerContext) {}
setup(core: CoreSetup, plugins: PluginsSetup) {
plugins.observability.navigation.registerSections(
of([
{
label: 'A solution section',
sortKey: 200,
entries: [
{ label: 'Home Page', app: 'exampleA', path: '/', matchFullPath: true },
{ label: 'Example Page', app: 'exampleA', path: '/example' },
{ label: 'Another Example Page', app: 'exampleA', path: '/another-example' },
],
},
{
label: 'Another solution section',
sortKey: 300,
entries: [
{ label: 'Example page', app: 'exampleB', path: '/example' },
],
},
])
);
}
start() {}
stop() {}
}
```
Here `app` would match your solution - e.g. logs, metrics, APM, uptime etc. The registry is fully typed so please refer to the types for specific options.
Observables are used to facilitate changes over time, for example within the lifetime of your application a license type or set of user permissions may change and as such you may wish to change the navigation structure. If your navigation needs are simple you can pass a value and forget about it. **Solutions are expected to handle their own permissions, and what should or should not be displayed at any time**, the Observability plugin will not add and remove items for you.
The Observability navigation registry is now aware of your solution's navigation needs ✅
## Page template component
The shared page template component can be used to actually display and render all of the registered navigation structures within your solution.
The `start` contract of the public Observability plugin exposes a React component, under `navigation.PageTemplate`.
This can be accessed like so:
```
const [coreStart, pluginsStart] = await core.getStartServices();
const ObservabilityPageTemplate = pluginsStart.observability.navigation.PageTemplate;
```
Now that you have access to the component you can render your solution's content using it.
```jsx
<ObservabilityPageTemplate
pageHeader={{
pageTitle: SolutionPageTitle,
rightSideItems: [
// Just an example
<DatePicker
rangeFrom={relativeTime.start}
rangeTo={relativeTime.end}
refreshInterval={refreshInterval}
refreshPaused={refreshPaused}
/>,
],
}}
>
// Render anything you like here, this is just an example.
<EuiFlexGroup>
<EuiFlexItem>
// Content
</EuiFlexItem>
<EuiFlexItem>
// Content
</EuiFlexItem>
</EuiFlexGroup>
</ObservabilityPageTemplate>
```
The `<ObservabilityPageTemplate />` component is a wrapper around the `<KibanaPageTemplate />` component (which in turn is a wrapper around the `<EuiPageTemplate>` component). As such the props mostly reflect those available on the wrapped components, again everything is fully typed so please refer to the types for specific options. The `pageSideBar` prop is handled by the component, and will take care of rendering out and managing the items from the registry.
After these two steps we should see something like the following (note the navigation on the left):
![Page template rendered example](./page_template.png)
## Adding NEW badge
You can add a NEW badge beside the label by using the property `isNewFeature?: boolean;`.
```js
setup(core: CoreSetup, plugins: PluginsSetup) {
plugins.observability.navigation.registerSections(
of([
{
label: 'A solution section',
sortKey: 200,
entries: [
{ label: 'Backends', app: 'exampleA', path: '/example', isNewFeature: true },
],
}
])
);
}
```
![NEW Badge example](./badge.png)
The badge is going to be shown until user clicks on the menu item for the first time. Then we'll save an information at local storage, following this pattern `observability.nav_item_badge_visible_${app}${path}`, the above example would save `observability.nav_item_badge_visible_exampleA/example`. And the badge is removed. It'll only show again if the item saved at local storage is removed or set to `false`.
It's recommended to remove the badge (e.g. a new feature promotion) in the subsequent release.
To avoid the navigation flooding with badges, we also want to propose keeping it to maximum 2 active badges for every iteration

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

View file

@ -1,9 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { createLazyObservabilityPageTemplate } from './lazy_page_template';
export type { LazyObservabilityPageTemplateProps } from './lazy_page_template';

View file

@ -1,26 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type {
ObservabilityPageTemplateDependencies,
WrappedPageTemplateProps,
} from './page_template';
export const LazyObservabilityPageTemplate = React.lazy(() => import('./page_template'));
export type LazyObservabilityPageTemplateProps = WrappedPageTemplateProps;
export function createLazyObservabilityPageTemplate(
injectedDeps: ObservabilityPageTemplateDependencies
) {
return (pageTemplateProps: LazyObservabilityPageTemplateProps) => (
<React.Suspense fallback={null}>
<LazyObservabilityPageTemplate {...pageTemplateProps} {...injectedDeps} />
</React.Suspense>
);
}

View file

@ -1,68 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiBadge } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import styled from 'styled-components';
interface Props {
label: string;
localStorageId: string;
}
const LabelContainer = styled.span`
max-width: 72%;
float: left;
&:hover,
&:focus {
text-decoration: underline;
}
`;
const StyledBadge = styled(EuiBadge)`
margin-left: 8px;
`;
/**
* Gets current state from local storage to show or hide the badge.
* Default value: true
* @param localStorageId
*/
function getBadgeVisibility(localStorageId: string) {
const storedItem = window.localStorage.getItem(localStorageId);
if (storedItem) {
return JSON.parse(storedItem) as boolean;
}
return true;
}
/**
* Saves on local storage that this item should no longer be visible
* @param localStorageId
*/
export function hideBadge(localStorageId: string) {
window.localStorage.setItem(localStorageId, JSON.stringify(false));
}
export function NavNameWithBadge({ label, localStorageId }: Props) {
const isBadgeVisible = getBadgeVisibility(localStorageId);
return (
<>
<LabelContainer className="eui-textTruncate">
<span>{label}</span>
</LabelContainer>
{isBadgeVisible && (
<StyledBadge color="accent">
{i18n.translate('xpack.observability.navigation.newBadge', {
defaultMessage: 'NEW',
})}
</StyledBadge>
)}
</>
);
}

View file

@ -1,47 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem, IconType } from '@elastic/eui';
interface Props {
label?: string;
isTechnicalPreview?: boolean;
iconType?: IconType;
}
export function NavNameWithBetaBadge({ label, iconType, isTechnicalPreview }: Props) {
return (
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<span className="eui-textTruncate">
<span>{label}</span>
</span>
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ height: 20 }}>
{isTechnicalPreview ? (
<EuiBetaBadge
color="hollow"
size="s"
label={i18n.translate('xpack.observability.navigation.experimentalBadgeLabel', {
defaultMessage: 'Technical preview',
})}
iconType={iconType}
/>
) : (
<EuiBetaBadge
color="hollow"
size="s"
label={i18n.translate('xpack.observability.navigation.betaBadge', {
defaultMessage: 'Beta',
})}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

View file

@ -1,121 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { I18nProvider } from '@kbn/i18n-react';
import { render } from '@testing-library/react';
import { shallow } from 'enzyme';
import React from 'react';
import { of } from 'rxjs';
import { getKibanaPageTemplateKibanaDependenciesMock as getPageTemplateServices } from '@kbn/shared-ux-page-kibana-template-mocks';
import { guidedOnboardingMock } from '@kbn/guided-onboarding-plugin/public/mocks';
import { createNavigationRegistry } from '../../../services/navigation_registry';
import { createLazyObservabilityPageTemplate } from './lazy_page_template';
import { ObservabilityPageTemplate } from './page_template';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => ({
pathname: '/test-path',
}),
}));
const navigationRegistry = createNavigationRegistry();
navigationRegistry.registerSections(
of([
{
label: 'Test A',
sortKey: 100,
entries: [
{ label: 'Section A Url A', app: 'TestA', path: '/url-a' },
{ label: 'Section A Url B', app: 'TestA', path: '/url-b' },
],
},
{
label: 'Test B',
sortKey: 200,
entries: [
{ label: 'Section B Url A', app: 'TestB', path: '/url-a' },
{ label: 'Section B Url B', app: 'TestB', path: '/url-b' },
],
},
])
);
describe('Page template', () => {
it('Provides a working lazy wrapper', () => {
const LazyObservabilityPageTemplate = createLazyObservabilityPageTemplate({
currentAppId$: of('Test app ID'),
getUrlForApp: () => '/test-url',
navigateToApp: async () => {},
navigationSections$: navigationRegistry.sections$,
getPageTemplateServices,
guidedOnboardingApi: guidedOnboardingMock.createStart().guidedOnboardingApi,
});
const component = shallow(
<LazyObservabilityPageTemplate
pageHeader={{
pageTitle: 'Test title',
rightSideItems: [<span>Test side item</span>],
}}
>
<div>Test structure</div>
</LazyObservabilityPageTemplate>
);
expect(component.exists('lazy')).toBe(true);
});
it('Utilises the KibanaPageTemplate for rendering', () => {
const component = shallow(
<ObservabilityPageTemplate
currentAppId$={of('Test app ID')}
getUrlForApp={() => '/test-url'}
navigateToApp={async () => {}}
navigationSections$={navigationRegistry.sections$}
pageHeader={{
pageTitle: 'Test title',
rightSideItems: [<span>Test side item</span>],
}}
getPageTemplateServices={getPageTemplateServices}
guidedOnboardingApi={guidedOnboardingMock.createStart().guidedOnboardingApi}
>
<div>Test structure</div>
</ObservabilityPageTemplate>
);
expect(component.is('KibanaPageTemplate'));
});
it('Handles outputting the registered navigation structures within a side nav', () => {
const { container } = render(
<I18nProvider>
<ObservabilityPageTemplate
currentAppId$={of('Test app ID')}
getUrlForApp={() => '/test-url'}
navigateToApp={async () => {}}
navigationSections$={navigationRegistry.sections$}
pageHeader={{
pageTitle: 'Test title',
rightSideItems: [<span>Test side item</span>],
}}
getPageTemplateServices={getPageTemplateServices}
guidedOnboardingApi={guidedOnboardingMock.createStart().guidedOnboardingApi}
>
<div>Test structure</div>
</ObservabilityPageTemplate>
</I18nProvider>
);
expect(container).toHaveTextContent('Section A Url A');
expect(container).toHaveTextContent('Section A Url B');
expect(container).toHaveTextContent('Section B Url A');
expect(container).toHaveTextContent('Section B Url B');
});
});

View file

@ -1,209 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiSideNavItemType, EuiPageSectionProps, EuiErrorBoundary } from '@elastic/eui';
import { _EuiPageBottomBarProps } from '@elastic/eui/src/components/page_template/bottom_bar/page_bottom_bar';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import { matchPath, useLocation } from 'react-router-dom';
import useObservable from 'react-use/lib/useObservable';
import type { Observable } from 'rxjs';
import type { ApplicationStart } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
KibanaPageTemplate,
KibanaPageTemplateKibanaProvider,
} from '@kbn/shared-ux-page-kibana-template';
import type {
KibanaPageTemplateProps,
KibanaPageTemplateKibanaDependencies,
} from '@kbn/shared-ux-page-kibana-template';
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
import { ObservabilityAppServices } from '../../../application/types';
import type { NavigationSection } from '../../../services/navigation_registry';
import { ObservabilityTour } from '../tour';
import { NavNameWithBadge, hideBadge } from './nav_name_with_badge';
import { NavNameWithBetaBadge } from './nav_name_with_beta_badge';
export type WrappedPageTemplateProps = Pick<
KibanaPageTemplateProps,
| 'children'
| 'data-test-subj'
| 'paddingSize'
| 'pageHeader'
| 'restrictWidth'
| 'isEmptyState'
| 'noDataConfig'
> & {
showSolutionNav?: boolean;
isPageDataLoaded?: boolean;
pageSectionProps?: EuiPageSectionProps;
bottomBar?: React.ReactNode;
bottomBarProps?: _EuiPageBottomBarProps;
};
export interface ObservabilityPageTemplateDependencies {
currentAppId$: Observable<string | undefined>;
getUrlForApp: ApplicationStart['getUrlForApp'];
navigateToApp: ApplicationStart['navigateToApp'];
navigationSections$: Observable<NavigationSection[]>;
getPageTemplateServices: () => KibanaPageTemplateKibanaDependencies;
guidedOnboardingApi: GuidedOnboardingPluginStart['guidedOnboardingApi'];
}
export type ObservabilityPageTemplateProps = ObservabilityPageTemplateDependencies &
WrappedPageTemplateProps;
export function ObservabilityPageTemplate({
children,
currentAppId$,
getUrlForApp,
navigateToApp,
navigationSections$,
showSolutionNav = true,
isPageDataLoaded = true,
getPageTemplateServices,
bottomBar,
bottomBarProps,
pageSectionProps,
guidedOnboardingApi,
...pageTemplateProps
}: ObservabilityPageTemplateProps): React.ReactElement | null {
const sections = useObservable(navigationSections$, []);
const currentAppId = useObservable(currentAppId$, undefined);
const { pathname: currentPath } = useLocation();
const { services } = useKibana<ObservabilityAppServices>();
const sideNavItems = useMemo<Array<EuiSideNavItemType<unknown>>>(
() =>
sections.map(({ label, entries, isBetaFeature }, sectionIndex) => ({
id: `${sectionIndex}`,
name: isBetaFeature ? <NavNameWithBetaBadge label={label} /> : label,
items: entries.map((entry, entryIndex) => {
const href = getUrlForApp(entry.app, {
path: entry.path,
});
const isSelected =
entry.app === currentAppId &&
(entry.matchPath
? entry.matchPath(currentPath)
: matchPath(currentPath, {
path: entry.path,
exact: !!entry.matchFullPath,
strict: !entry.ignoreTrailingSlash,
}) != null);
const badgeLocalStorageId = `observability.nav_item_badge_visible_${entry.app}${entry.path}`;
const navId = entry.label.toLowerCase().split(' ').join('_');
return {
id: `${sectionIndex}.${entryIndex}`,
name: entry.isBetaFeature ? (
<NavNameWithBetaBadge label={entry.label} />
) : entry.isNewFeature ? (
<NavNameWithBadge label={entry.label} localStorageId={badgeLocalStorageId} />
) : entry.isTechnicalPreview ? (
<NavNameWithBetaBadge
label={entry.label}
iconType="beaker"
isTechnicalPreview={true}
/>
) : (
entry.label
),
href,
isSelected,
'data-nav-id': navId,
'data-test-subj': `observability-nav-${entry.app}-${navId}`,
onClick: (event) => {
if (entry.onClick) {
entry.onClick(event);
}
// Hides NEW badge when the item is clicked
if (entry.isNewFeature) {
hideBadge(badgeLocalStorageId);
}
if (
event.button !== 0 ||
event.defaultPrevented ||
event.metaKey ||
event.altKey ||
event.ctrlKey ||
event.shiftKey
) {
return;
}
event.preventDefault();
navigateToApp(entry.app, {
path: entry.path,
});
},
};
}),
})),
[currentAppId, currentPath, getUrlForApp, navigateToApp, sections]
);
return (
<KibanaPageTemplateKibanaProvider {...getPageTemplateServices()}>
<ObservabilityTour
navigateToApp={navigateToApp}
prependBasePath={services?.http?.basePath.prepend}
guidedOnboardingApi={guidedOnboardingApi}
isPageDataLoaded={isPageDataLoaded}
// The tour is dependent on the solution nav, and should not render if it is not visible
showTour={showSolutionNav}
>
{({ isTourVisible }) => {
return (
<KibanaPageTemplate
restrictWidth={false}
{...pageTemplateProps}
solutionNav={
showSolutionNav
? {
icon: 'logoObservability',
items: sideNavItems,
name: sideNavTitle,
// Only false if tour is active
canBeCollapsed: isTourVisible === false,
}
: undefined
}
>
<EuiErrorBoundary>
<KibanaPageTemplate.Section
component="div"
alignment={pageTemplateProps.isEmptyState ? 'center' : 'top'}
{...pageSectionProps}
>
{children}
</KibanaPageTemplate.Section>
</EuiErrorBoundary>
{bottomBar && (
<KibanaPageTemplate.BottomBar {...bottomBarProps}>
{bottomBar}
</KibanaPageTemplate.BottomBar>
)}
</KibanaPageTemplate>
);
}}
</ObservabilityTour>
</KibanaPageTemplateKibanaProvider>
);
}
// for lazy import
// eslint-disable-next-line import/no-default-export
export default ObservabilityPageTemplate;
const sideNavTitle = i18n.translate('xpack.observability.pageLayout.sideNavTitle', {
defaultMessage: 'Observability',
});

View file

@ -1,8 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { ObservabilityTour, observTourStepStorageKey, useObservabilityTourContext } from './tour';

View file

@ -1,122 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { EuiTourStepProps, ElementTarget } from '@elastic/eui';
interface TourStep {
content: string;
anchor: ElementTarget;
anchorPosition: EuiTourStepProps['anchorPosition'];
title: EuiTourStepProps['title'];
dataTestSubj: string;
offset?: number;
imageConfig?: {
name: string;
altText: string;
};
}
export const tourStepsConfig: TourStep[] = [
{
title: i18n.translate('xpack.observability.tour.observabilityOverviewStep.tourTitle', {
defaultMessage: 'Welcome to Elastic Observability',
}),
content: i18n.translate('xpack.observability.tour.observabilityOverviewStep.tourContent', {
defaultMessage:
'Take a quick tour to learn the benefits of having all of your observability data in one stack.',
}),
anchor: `[id^="SolutionNav"]`,
anchorPosition: 'rightUp',
dataTestSubj: 'overviewStep',
},
{
title: i18n.translate('xpack.observability.tour.streamStep.tourTitle', {
defaultMessage: 'Tail your logs in real time',
}),
content: i18n.translate('xpack.observability.tour.streamStep.tourContent', {
defaultMessage:
'Monitor, filter, and inspect log events flowing in from your applications, servers, virtual machines, and containers.',
}),
anchor: `[data-nav-id="stream"]`,
anchorPosition: 'rightUp',
dataTestSubj: 'streamStep',
imageConfig: {
name: 'onboarding_tour_step_logs.gif',
altText: i18n.translate('xpack.observability.tour.streamStep.imageAltText', {
defaultMessage: 'Logs stream demonstration',
}),
},
},
{
title: i18n.translate('xpack.observability.tour.metricsExplorerStep.tourTitle', {
defaultMessage: 'Monitor your infrastructure health',
}),
content: i18n.translate('xpack.observability.tour.metricsExplorerStep.tourContent', {
defaultMessage:
'Stream, group, and visualize metrics from your systems, cloud, network, and other infrastructure sources.',
}),
anchor: `[data-nav-id="metrics_explorer"]`,
anchorPosition: 'rightUp',
dataTestSubj: 'metricsExplorerStep',
imageConfig: {
name: 'onboarding_tour_step_metrics.gif',
altText: i18n.translate('xpack.observability.tour.metricsExplorerStep.imageAltText', {
defaultMessage: 'Metrics explorer demonstration',
}),
},
},
{
title: i18n.translate('xpack.observability.tour.servicesStep.tourTitle', {
defaultMessage: 'Identify and resolve application issues',
}),
content: i18n.translate('xpack.observability.tour.servicesStep.tourContent', {
defaultMessage:
'Find and fix performance problems quickly by collecting detailed information about your services.',
}),
anchor: `[data-nav-id="services"]`,
anchorPosition: 'rightUp',
dataTestSubj: 'servicesStep',
imageConfig: {
name: 'onboarding_tour_step_services.gif',
altText: i18n.translate('xpack.observability.tour.servicesStep.imageAltText', {
defaultMessage: 'Services demonstration',
}),
},
},
{
title: i18n.translate('xpack.observability.tour.alertsStep.tourTitle', {
defaultMessage: 'Get notified when something changes',
}),
content: i18n.translate('xpack.observability.tour.alertsStep.tourContent', {
defaultMessage:
'Define and detect conditions that trigger alerts with third-party platform integrations like email, PagerDuty, and Slack.',
}),
anchor: `[data-nav-id="alerts"]`,
anchorPosition: 'rightUp',
dataTestSubj: 'alertStep',
imageConfig: {
name: 'onboarding_tour_step_alerts.gif',
altText: i18n.translate('xpack.observability.tour.alertsStep.imageAltText', {
defaultMessage: 'Alerts demonstration',
}),
},
},
{
title: i18n.translate('xpack.observability.tour.guidedSetupStep.tourTitle', {
defaultMessage: 'Do more with Elastic Observability',
}),
content: i18n.translate('xpack.observability.tour.guidedSetupStep.tourContent', {
defaultMessage:
'The easiest way to continue with Elastic Observability is to follow recommended next steps in the data assistant.',
}),
anchor: '#guidedSetupButton',
anchorPosition: 'rightUp',
dataTestSubj: 'guidedSetupStep',
offset: 10,
},
];

View file

@ -1,246 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, {
ReactNode,
useState,
useCallback,
useEffect,
createContext,
useContext,
} from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiTourStep,
EuiTourStepProps,
EuiImage,
EuiSpacer,
EuiText,
useIsWithinBreakpoints,
} from '@elastic/eui';
import { useLocation } from 'react-router-dom';
import { ApplicationStart } from '@kbn/core/public';
import useObservable from 'react-use/lib/useObservable';
import { of } from 'rxjs';
import type { GuidedOnboardingApi } from '@kbn/guided-onboarding-plugin/public/types';
import { observabilityAppId } from '../../../../common';
import { tourStepsConfig } from './steps_config';
const minWidth: EuiTourStepProps['minWidth'] = 360;
const maxWidth: EuiTourStepProps['maxWidth'] = 360;
const offset: EuiTourStepProps['offset'] = 30;
const repositionOnScroll: EuiTourStepProps['repositionOnScroll'] = true;
const overviewPath = '/overview';
const dataAssistantStep = 6;
export const observTourStepStorageKey = 'guidedOnboarding.observability.tourStep';
const getSteps = ({
activeStep,
incrementStep,
endTour,
prependBasePath,
}: {
activeStep: number;
incrementStep: () => void;
endTour: () => void;
prependBasePath?: (imageName: string) => string;
}) => {
const footerAction = (
<EuiFlexGroup gutterSize="s" alignItems="baseline">
<EuiFlexItem>
<EuiButtonEmpty
onClick={() => endTour()}
size="xs"
color="text"
// Used for testing and to track FS usage
data-test-subj="onboarding--observTourSkipButton"
>
{i18n.translate('xpack.observability.tour.skipButtonLabel', {
defaultMessage: 'Skip tour',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton
onClick={() => incrementStep()}
size="s"
color="success"
// Used for testing and to track FS usage
data-test-subj="onboarding--observTourNextStepButton"
>
{i18n.translate('xpack.observability.tour.nextButtonLabel', {
defaultMessage: 'Next',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
const lastStepFooterAction = (
// data-test-subj is used for testing and to track FS usage
<EuiButtonEmpty
size="xs"
color="text"
onClick={() => endTour()}
data-test-subj="onboarding--observTourEndButton"
>
{i18n.translate('xpack.observability.tour.endButtonLabel', {
defaultMessage: 'End tour',
})}
</EuiButtonEmpty>
);
return tourStepsConfig.map((stepConfig, index) => {
const step = index + 1;
const { dataTestSubj, content, offset: stepOffset, imageConfig, ...tourStepProps } = stepConfig;
return (
<EuiTourStep
{...tourStepProps}
key={step}
step={step}
minWidth={minWidth}
maxWidth={maxWidth}
offset={stepOffset ?? offset}
repositionOnScroll={repositionOnScroll}
stepsTotal={tourStepsConfig.length}
isStepOpen={step === activeStep}
onFinish={() => endTour()}
footerAction={activeStep === tourStepsConfig.length ? lastStepFooterAction : footerAction}
panelProps={{
'data-test-subj': dataTestSubj,
}}
content={
<>
<EuiText size="s">
<p>{content}</p>
</EuiText>
{imageConfig && prependBasePath && (
<>
<EuiSpacer size="m" />
<EuiImage
alt={imageConfig.altText}
src={prependBasePath(`/plugins/observability/assets/${imageConfig.name}`)}
size="fullWidth"
/>
</>
)}
</>
}
/>
);
});
};
export interface ObservabilityTourContextValue {
endTour: () => void;
isTourVisible: boolean;
}
const ObservabilityTourContext = createContext<ObservabilityTourContextValue>({
endTour: () => {},
isTourVisible: false,
} as ObservabilityTourContextValue);
export function ObservabilityTour({
children,
navigateToApp,
isPageDataLoaded,
showTour,
prependBasePath,
guidedOnboardingApi,
}: {
children: ({ isTourVisible }: { isTourVisible: boolean }) => ReactNode;
navigateToApp: ApplicationStart['navigateToApp'];
isPageDataLoaded: boolean;
showTour: boolean;
prependBasePath?: (imageName: string) => string;
guidedOnboardingApi?: GuidedOnboardingApi;
}) {
const prevActiveStep = localStorage.getItem(observTourStepStorageKey);
const initialActiveStep = prevActiveStep === null ? 1 : Number(prevActiveStep);
const isGuidedOnboardingActive = useObservable(
// if guided onboarding is not available, return false
guidedOnboardingApi
? guidedOnboardingApi.isGuideStepActive$('kubernetes', 'tour_observability')
: of(false)
);
const [isTourActive, setIsTourActive] = useState(false);
const [activeStep, setActiveStep] = useState(initialActiveStep);
const { pathname: currentPath } = useLocation();
const isSmallBreakpoint = useIsWithinBreakpoints(['s']);
const isOverviewPage = currentPath === overviewPath;
const incrementStep = useCallback(() => {
setActiveStep((prevState) => prevState + 1);
}, []);
const endTour = useCallback(async () => {
// Mark the onboarding guide step as complete
if (guidedOnboardingApi) {
await guidedOnboardingApi.completeGuideStep('kubernetes', 'tour_observability');
}
// Reset EuiTour step state
setActiveStep(1);
}, [guidedOnboardingApi]);
/**
* The tour should only be visible if the following conditions are met:
* - Only pages with the side nav should show the tour (showTour === true)
* - Tour is set to active per the guided onboarding service (isTourActive === true)
* - Any page data must be loaded in order for the tour to render correctly
* - The tour should only render on medium-large screens
*/
const isTourVisible = showTour && isTourActive && isPageDataLoaded && isSmallBreakpoint === false;
const context: ObservabilityTourContextValue = { endTour, isTourVisible };
useEffect(() => {
localStorage.setItem(observTourStepStorageKey, String(activeStep));
}, [activeStep]);
useEffect(() => {
setIsTourActive(Boolean(isGuidedOnboardingActive));
}, [isGuidedOnboardingActive]);
useEffect(() => {
// The user must be on the overview page to view the data assistant step in the tour
if (isTourActive && isOverviewPage === false && activeStep === dataAssistantStep) {
navigateToApp(observabilityAppId, {
path: overviewPath,
});
}
}, [activeStep, isOverviewPage, isTourActive, navigateToApp]);
return (
<ObservabilityTourContext.Provider value={context}>
<>
{children({ isTourVisible })}
{isTourVisible && getSteps({ activeStep, incrementStep, endTour, prependBasePath })}
</>
</ObservabilityTourContext.Provider>
);
}
export const useObservabilityTourContext = (): ObservabilityTourContextValue => {
const ctx = useContext(ObservabilityTourContext);
if (!ctx) {
throw new Error('useObservabilityTourContext can only be called inside of TourContext');
}
return ctx;
};

View file

@ -1,25 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ReactNode } from 'react';
import { AppMountParameters } from '@kbn/core/public';
import { UXMetrics } from './core_web_vitals';
export interface HeaderMenuPortalProps {
children: ReactNode;
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
theme$: AppMountParameters['theme$'];
}
export interface CoreVitalProps {
loading: boolean;
data?: UXMetrics | null;
displayServiceName?: boolean;
serviceName?: string;
totalPageViews?: number;
displayTrafficMetric?: boolean;
}

View file

@ -10,8 +10,8 @@ import useMount from 'react-use/lib/useMount';
import { useLocation, useHistory } from 'react-router-dom';
import { parse } from 'query-string';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { fromQuery, ObservabilityPublicPluginsStart, toQuery } from '..';
import { getAbsoluteTime } from '../utils/date';
import { fromQuery, ObservabilityPublicPluginsStart, toQuery } from '../..';
import { getAbsoluteTime } from '../../utils/date';
export interface DatePickerContextValue {
relativeStart: string;

View file

@ -7,7 +7,7 @@
import { registerDataHandler, getDataHandler } from './data_handler';
import moment from 'moment';
import { ApmIndicesConfig } from '../common/typings';
import { ApmIndicesConfig } from '../../../common/typings';
const sampleAPMIndices = { transaction: 'apm-*' } as ApmIndicesConfig;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { DataHandler, ObservabilityFetchDataPlugins } from './typings/fetch_overview_data';
import { DataHandler, ObservabilityFetchDataPlugins } from '../../typings/fetch_overview_data';
const dataHandlers: Partial<Record<ObservabilityFetchDataPlugins, DataHandler>> = {};

View file

@ -9,13 +9,13 @@ import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { coreMock } from '@kbn/core/public/mocks';
import { registerDataHandler, unregisterDataHandler } from '../data_handler';
import { useHasData } from '../hooks/use_has_data';
import { HasData, ObservabilityFetchDataPlugins } from '../typings/fetch_overview_data';
import { registerDataHandler, unregisterDataHandler } from './data_handler';
import { useHasData } from '../../hooks/use_has_data';
import { HasData, ObservabilityFetchDataPlugins } from '../../typings/fetch_overview_data';
import { HasDataContextProvider } from './has_data_context';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { ApmIndicesConfig } from '../../common/typings';
import { ApmIndicesConfig } from '../../../common/typings';
import { act } from '@testing-library/react';
const sampleAPMIndices = { transaction: 'apm-*' } as ApmIndicesConfig;

View file

@ -10,6 +10,7 @@ import React, { createContext, useEffect, useState } from 'react';
import { useRouteMatch } from 'react-router-dom';
import { asyncForEach } from '@kbn/std';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { FETCH_STATUS } from '@kbn/observability-shared-plugin/public';
import {
ALERT_APP,
APM_APP,
@ -17,14 +18,13 @@ import {
INFRA_METRICS_APP,
UPTIME_APP,
UX_APP,
} from './constants';
import { getDataHandler } from '../data_handler';
import { FETCH_STATUS } from '../hooks/use_fetcher';
import { useDatePickerContext } from '../hooks/use_date_picker_context';
import { getObservabilityAlerts } from '../services/get_observability_alerts';
import { ObservabilityFetchDataPlugins } from '../typings/fetch_overview_data';
import { ApmIndicesConfig } from '../../common/typings';
import { ObservabilityAppServices } from '../application/types';
} from '../constants';
import { getDataHandler } from './data_handler';
import { useDatePickerContext } from '../../hooks/use_date_picker_context';
import { getObservabilityAlerts } from './get_observability_alerts';
import { ObservabilityFetchDataPlugins } from '../../typings/fetch_overview_data';
import { ApmIndicesConfig } from '../../../common/typings';
import { ObservabilityAppServices } from '../../application/types';
type DataContextApps = ObservabilityFetchDataPlugins | 'alert';

View file

@ -1,84 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { createContext, ReactNode, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { RequestAdapter } from '@kbn/inspector-plugin/common';
import { FetcherResult } from '../../hooks/use_fetcher';
import { InspectResponse } from '../../../typings/common';
export interface InspectorContextValue {
addInspectorRequest: <Data>(result: FetcherResult<Data>) => void;
inspectorAdapters: { requests: RequestAdapter };
}
const value: InspectorContextValue = {
addInspectorRequest: () => {},
inspectorAdapters: { requests: new RequestAdapter() },
};
export const InspectorContext = createContext<InspectorContextValue>(value);
export type AddInspectorRequest = (
result: FetcherResult<{
mainStatisticsData?: { _inspect?: InspectResponse };
_inspect?: InspectResponse;
}>
) => void;
export function InspectorContextProvider({ children }: { children: ReactNode }) {
const history = useHistory();
const { inspectorAdapters } = value;
function addInspectorRequest(
result: FetcherResult<{
mainStatisticsData?: { _inspect?: InspectResponse };
_inspect?: InspectResponse;
}>
) {
const operations = result.data?._inspect ?? result.data?.mainStatisticsData?._inspect ?? [];
operations.forEach((operation) => {
if (operation.response) {
const { id, name } = operation;
const requestParams = { id, name };
const requestResponder = inspectorAdapters.requests.start(
id,
requestParams,
operation.startTime
);
requestResponder.json(operation.json as object);
if (operation.stats) {
requestResponder.stats(operation.stats);
}
requestResponder.finish(operation.status, operation.response);
}
});
}
useEffect(() => {
const unregisterCallback = history.listen((newLocation) => {
if (history.location.pathname !== newLocation.pathname) {
inspectorAdapters.requests.reset();
}
});
return () => {
unregisterCallback();
};
}, [history, inspectorAdapters]);
return (
<InspectorContext.Provider value={{ ...value, addInspectorRequest }}>
{children}
</InspectorContext.Provider>
);
}

View file

@ -1,13 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useContext } from 'react';
import { InspectorContext } from './inspector_context';
export function useInspectorContext() {
return useContext(InspectorContext);
}

View file

@ -7,9 +7,9 @@
import { AppMountParameters } from '@kbn/core/public';
import { createContext } from 'react';
import { ObservabilityRuleTypeRegistry } from '../rules/create_observability_rule_type_registry';
import type { LazyObservabilityPageTemplateProps } from '../components/shared/page_template/lazy_page_template';
import { ConfigSchema } from '../plugin';
import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public';
import { ObservabilityRuleTypeRegistry } from '../../rules/create_observability_rule_type_registry';
import { ConfigSchema } from '../../plugin';
export interface PluginContextValue {
config: ConfigSchema;

View file

@ -4,11 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Options, useLinkProps } from './use_link_props';
import { UseLinkPropsOptions, useLinkProps } from '@kbn/observability-shared-plugin/public';
export const createUseRulesLink =
() =>
(options: Options = {}) => {
(options: UseLinkPropsOptions = {}) => {
const linkProps = {
app: 'observability',
pathname: '/alerts/rules',

View file

@ -1,108 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getAlertsPermissions, useGetUserAlertsPermissions } from './use_alert_permission';
import { applicationServiceMock } from '@kbn/core/public/mocks';
import { renderHook } from '@testing-library/react-hooks';
describe('getAlertsPermissions', () => {
it('returns a fallback when featureId is nullish or missing from capabilities', () => {
const { capabilities } = applicationServiceMock.createStartContract();
expect(getAlertsPermissions(capabilities, '')).toEqual({
crud: false,
read: false,
loading: false,
featureId: '',
});
expect(getAlertsPermissions(capabilities, 'abc')).toEqual({
crud: false,
read: false,
loading: false,
featureId: 'abc',
});
});
it('returns proper permissions when featureId is a key in capabilities', () => {
const capabilities = Object.assign(
{},
applicationServiceMock.createStartContract().capabilities,
{
apm: { 'alerting:save': false, 'alerting:show': true, loading: true },
uptime: { loading: true, save: true, show: true },
}
);
expect(getAlertsPermissions(capabilities, 'uptime')).toEqual({
crud: true,
read: true,
loading: false,
featureId: 'uptime',
});
expect(getAlertsPermissions(capabilities, 'apm')).toEqual({
crud: false,
read: true,
loading: false,
featureId: 'apm',
});
});
});
describe('useGetUserAlertPermissions', function () {
const timeout = 1_000;
it(
'updates permissions when featureId changes',
async function () {
const capabilities = Object.assign(
{},
applicationServiceMock.createStartContract().capabilities,
{
apm: { 'alerting:save': false, 'alerting:show': true },
uptime: { save: true, show: true },
}
);
const { result, rerender } = renderHook(
({ featureId }) => useGetUserAlertsPermissions(capabilities, featureId),
{
initialProps: { featureId: 'uptime' },
}
);
expect(result.current.read).toBe(true);
expect(result.current.crud).toBe(true);
rerender({ featureId: 'apm' });
expect(result.current.read).toBe(true);
expect(result.current.crud).toBe(false);
},
timeout
);
it(
'returns default permissions when permissions for featureId are missing',
async function () {
const { capabilities } = applicationServiceMock.createStartContract();
const { result } = renderHook(
({ featureId }) => useGetUserAlertsPermissions(capabilities, featureId),
{
initialProps: { featureId: 'uptime' },
}
);
expect(result.current).toEqual({
crud: false,
read: false,
loading: false,
featureId: null,
});
},
timeout
);
});

View file

@ -1,74 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useEffect, useState } from 'react';
import { RecursiveReadonly } from '@kbn/utility-types';
import { Capabilities } from '@kbn/core/types';
export interface UseGetUserAlertsPermissionsProps {
crud: boolean;
read: boolean;
loading?: boolean;
featureId: string | null;
}
export const getAlertsPermissions = (
uiCapabilities: RecursiveReadonly<Capabilities>,
featureId: string
) => {
if (!featureId || !uiCapabilities[featureId]) {
return {
crud: false,
read: false,
loading: false,
featureId,
};
}
return {
crud: (featureId === 'apm'
? uiCapabilities[featureId]['alerting:save']
: uiCapabilities[featureId].save) as boolean,
read: (featureId === 'apm'
? uiCapabilities[featureId]['alerting:show']
: uiCapabilities[featureId].show) as boolean,
loading: false,
featureId,
};
};
export const useGetUserAlertsPermissions = (
uiCapabilities: RecursiveReadonly<Capabilities>,
featureId?: string
): UseGetUserAlertsPermissionsProps => {
const [alertsPermissions, setAlertsPermissions] = useState<UseGetUserAlertsPermissionsProps>({
crud: false,
read: false,
loading: true,
featureId: null,
});
useEffect(() => {
if (!featureId || !uiCapabilities[featureId]) {
setAlertsPermissions({
crud: false,
read: false,
loading: false,
featureId: null,
});
} else {
setAlertsPermissions((currentAlertPermissions) => {
if (currentAlertPermissions.featureId === featureId) {
return currentAlertPermissions;
}
return getAlertsPermissions(uiCapabilities, featureId);
});
}
}, [alertsPermissions.featureId, featureId, uiCapabilities]);
return alertsPermissions;
};

View file

@ -1,116 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { renderHook } from '@testing-library/react-hooks';
import React, { ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { CoreStart } from '@kbn/core/public';
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import { useBreadcrumbs } from './use_breadcrumbs';
const setBreadcrumbs = jest.fn();
const setTitle = jest.fn();
const kibanaServices = {
application: { getUrlForApp: () => {}, navigateToApp: () => {} },
chrome: { setBreadcrumbs, docTitle: { change: setTitle } },
uiSettings: { get: () => true },
} as unknown as Partial<CoreStart>;
const KibanaContext = createKibanaReactContext(kibanaServices);
function Wrapper({ children }: { children?: ReactNode }) {
return (
<MemoryRouter>
<KibanaContext.Provider>{children}</KibanaContext.Provider>
</MemoryRouter>
);
}
describe('useBreadcrumbs', () => {
afterEach(() => {
setBreadcrumbs.mockClear();
setTitle.mockClear();
});
describe('when setBreadcrumbs and setTitle are not defined', () => {
it('does not set breadcrumbs or the title', () => {
renderHook(() => useBreadcrumbs([]), {
wrapper: ({ children }) => (
<MemoryRouter>
<KibanaContext.Provider
services={
{ ...kibanaServices, chrome: { docTitle: {} } } as unknown as Partial<CoreStart>
}
>
{children}
</KibanaContext.Provider>
</MemoryRouter>
),
});
expect(setBreadcrumbs).not.toHaveBeenCalled();
expect(setTitle).not.toHaveBeenCalled();
});
});
describe('with an empty array', () => {
it('sets the overview breadcrumb', () => {
renderHook(() => useBreadcrumbs([]), { wrapper: Wrapper });
expect(setBreadcrumbs).toHaveBeenCalledWith([
{ href: '/overview', onClick: expect.any(Function), text: 'Observability' },
]);
});
it('sets the overview title', () => {
renderHook(() => useBreadcrumbs([]), { wrapper: Wrapper });
expect(setTitle).toHaveBeenCalledWith(['Observability']);
});
});
describe('given breadcrumbs', () => {
it('sets the breadcrumbs', () => {
renderHook(
() =>
useBreadcrumbs([
{ text: 'One', href: '/one' },
{
text: 'Two',
},
]),
{ wrapper: Wrapper }
);
expect(setBreadcrumbs).toHaveBeenCalledWith([
{ href: '/overview', onClick: expect.any(Function), text: 'Observability' },
{
href: '/one',
onClick: expect.any(Function),
text: 'One',
},
{
text: 'Two',
},
]);
});
it('sets the title', () => {
renderHook(
() =>
useBreadcrumbs([
{ text: 'One', href: '/one' },
{
text: 'Two',
},
]),
{ wrapper: Wrapper }
);
expect(setTitle).toHaveBeenCalledWith(['Two', 'One', 'Observability']);
});
});
});

View file

@ -1,71 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { ChromeBreadcrumb } from '@kbn/core/public';
import { MouseEvent, useEffect } from 'react';
import { useKibana } from '../utils/kibana_react';
import { useQueryParams } from './use_query_params';
function addClickHandlers(
breadcrumbs: ChromeBreadcrumb[],
navigateToHref?: (url: string) => Promise<void>
) {
return breadcrumbs.map((bc) => ({
...bc,
...(bc.href
? {
onClick: (event: MouseEvent) => {
if (navigateToHref && bc.href) {
event.preventDefault();
navigateToHref(bc.href);
}
},
}
: {}),
}));
}
function getTitleFromBreadCrumbs(breadcrumbs: ChromeBreadcrumb[]) {
return breadcrumbs.map(({ text }) => text?.toString() ?? '').reverse();
}
export const useBreadcrumbs = (
extraCrumbs: ChromeBreadcrumb[],
app?: { id: string; label: string }
) => {
const params = useQueryParams();
const {
services: {
chrome: { docTitle, setBreadcrumbs },
application: { getUrlForApp, navigateToUrl },
},
} = useKibana();
const setTitle = docTitle.change;
const appPath = getUrlForApp(app?.id ?? 'observability-overview') ?? '';
useEffect(() => {
const breadcrumbs = [
{
text:
app?.label ??
i18n.translate('xpack.observability.breadcrumbs.observabilityLinkText', {
defaultMessage: 'Observability',
}),
href: appPath + '/overview',
},
...extraCrumbs,
];
if (setBreadcrumbs) {
setBreadcrumbs(addClickHandlers(breadcrumbs, navigateToUrl));
}
if (setTitle) {
setTitle(getTitleFromBreadCrumbs(breadcrumbs));
}
}, [app?.label, appPath, extraCrumbs, navigateToUrl, params, setBreadcrumbs, setTitle]);
};

View file

@ -1,42 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { PartialTheme } from '@elastic/charts';
import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import { useMemo } from 'react';
import { useTheme } from './use_theme';
export function useChartTheme(): PartialTheme[] {
const theme = useTheme();
const baseChartTheme = theme.darkMode
? EUI_CHARTS_THEME_DARK.theme
: EUI_CHARTS_THEME_LIGHT.theme;
return useMemo(
() => [
{
chartMargins: {
left: 10,
right: 10,
top: 35,
bottom: 10,
},
background: {
color: 'transparent',
},
lineSeriesStyle: {
point: { visible: false },
},
areaSeriesStyle: {
point: { visible: false },
},
},
baseChartTheme,
],
[baseChartTheme]
);
}

View file

@ -6,7 +6,7 @@
*/
import { useContext } from 'react';
import { DatePickerContext } from '../context/date_picker_context';
import { DatePickerContext } from '../context/date_picker_context/date_picker_context';
export function useDatePickerContext() {
return useContext(DatePickerContext);

View file

@ -1,118 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { ESSearchResponse } from '@kbn/es-types';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { IInspectorInfo, isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/common';
import { FETCH_STATUS, useFetcher } from './use_fetcher';
import { useInspectorContext } from '../context/inspector/use_inspector_context';
import { getInspectResponse } from '../../common/utils/get_inspect_response';
export const useEsSearch = <DocumentSource extends unknown, TParams extends estypes.SearchRequest>(
params: TParams,
fnDeps: any[],
options: { inspector?: IInspectorInfo; name: string }
) => {
const {
services: { data },
} = useKibana<{ data: DataPublicPluginStart }>();
const { name } = options ?? {};
const { addInspectorRequest } = useInspectorContext();
const { data: response = {}, loading } = useFetcher(() => {
if (params.index) {
const startTime = Date.now();
return new Promise((resolve) => {
const search$ = data.search
.search(
{
params,
},
{
legacyHitsTotal: false,
}
)
.subscribe({
next: (result) => {
if (isCompleteResponse(result)) {
if (addInspectorRequest) {
addInspectorRequest({
data: {
_inspect: [
getInspectResponse({
startTime,
esRequestParams: params,
esResponse: result.rawResponse,
esError: null,
esRequestStatus: 1,
operationName: name,
kibanaRequest: {
route: {
path: '/internal/bsearch',
method: 'POST',
},
} as any,
}),
],
},
status: FETCH_STATUS.SUCCESS,
});
}
// Final result
resolve(result);
search$.unsubscribe();
}
},
error: (err) => {
if (isErrorResponse(err)) {
console.error(err);
if (addInspectorRequest) {
addInspectorRequest({
data: {
_inspect: [
getInspectResponse({
startTime,
esRequestParams: params,
esResponse: null,
esError: { originalError: err, name: err.name, message: err.message },
esRequestStatus: 2,
operationName: name,
kibanaRequest: {
route: {
path: '/internal/bsearch',
method: 'POST',
},
} as any,
}),
],
},
status: FETCH_STATUS.SUCCESS,
});
}
}
},
});
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...fnDeps]);
const { rawResponse } = response as any;
return {
data: rawResponse as ESSearchResponse<DocumentSource, TParams, { restTotalHitsAsInt: false }>,
loading: Boolean(loading),
};
};
export function createEsParams<T extends estypes.SearchRequest>(params: T): T {
return params;
}

View file

@ -9,7 +9,7 @@ import { act, renderHook } from '@testing-library/react-hooks';
import { kibanaStartMock } from '../utils/kibana_react.mock';
import * as pluginContext from './use_plugin_context';
import { createObservabilityRuleTypeRegistryMock } from '..';
import { PluginContextValue } from '../context/plugin_context';
import { PluginContextValue } from '../context/plugin_context/plugin_context';
import { useFetchAlertDetail } from './use_fetch_alert_detail';
import type { TopAlert } from '../typings/alerts';

Some files were not shown because too many files have changed in this diff Show more