mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Reorganize Observability Plugin (#157970)
This commit is contained in:
parent
b754a2df75
commit
ab27caff47
208 changed files with 355 additions and 5015 deletions
23
.github/CODEOWNERS
vendored
23
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
|
@ -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]
|
|
@ -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;
|
|
@ -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[] = [
|
||||
{
|
|
@ -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 = '';
|
|
@ -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 {
|
|
@ -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,
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
@ -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
|
|
@ -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;
|
|
@ -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
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
|
@ -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 {
|
||||
/**
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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 {
|
|
@ -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';
|
|
@ -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;
|
|
@ -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 {
|
|
@ -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;
|
||||
|
|
@ -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;
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { BurnRateRuleParams } from '../../../typings';
|
||||
import { BurnRateRuleParams } from '../../typings';
|
||||
import { validateBurnRateRule } from './validation';
|
||||
|
||||
const VALID_PARAMS: BurnRateRuleParams = {
|
|
@ -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[] };
|
|
@ -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,
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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" />
|
||||
|
|
|
@ -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} />;
|
||||
}
|
|
@ -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',
|
||||
});
|
|
@ -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',
|
||||
});
|
|
@ -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',
|
||||
});
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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):
|
||||
|
||||

|
||||
|
||||
## 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 },
|
||||
],
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||

|
||||
|
||||
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 |
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 |
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
});
|
|
@ -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';
|
|
@ -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,
|
||||
},
|
||||
];
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
@ -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>> = {};
|
||||
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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]);
|
||||
};
|
|
@ -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]
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue