diff --git a/.eslintrc.js b/.eslintrc.js index f1e0b7d9353e..2edd9f5d433f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -133,7 +133,7 @@ module.exports = { * Licence headers */ { - files: ['**/*.{js,ts,tsx}'], + files: ['**/*.{js,ts,tsx}', '!plugins/**/*'], rules: { '@kbn/eslint/require-license-header': [ 'error', diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 7c50dbf542d0..da109331ae0f 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -126,16 +126,22 @@ control the capturing process. [cols="2*<"] |=== | `xpack.reporting.capture.timeouts.openUrl` - | How long to allow the Reporting browser to wait for the initial data of the - {kib} page to load. Defaults to `30000` (30 seconds). + | Specify how long to allow the Reporting browser to wait for the "Loading..." screen + to dismiss and find the initial data for the Kibana page. If the time is + exceeded, a page screenshot is captured showing the current state, and the download link shows a warning message. + Defaults to `30000` (30 seconds). | `xpack.reporting.capture.timeouts.waitForElements` - | How long to allow the Reporting browser to wait for the visualization panels to - load on the {kib} page. Defaults to `30000` (30 seconds). + | Specify how long to allow the Reporting browser to wait for all visualization + panels to load on the Kibana page. If the time is exceeded, a page screenshot + is captured showing the current state, and the download link shows a warning message. Defaults to `30000` (30 + seconds). | `xpack.reporting.capture.timeouts.renderComplete` - | How long to allow the Reporting browser to wait for each visualization to - signal that it is done renderings. Defaults to `30000` (30 seconds). + | Specify how long to allow the Reporting browser to wait for all visualizations to + fetch and render the data. If the time is exceeded, a + page screenshot is captured showing the current state, and the download link shows a warning message. Defaults to + `30000` (30 seconds). |=== diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index 2633771c8b03..5d1ca923cbc8 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -146,7 +146,7 @@ export function Cytoscape({ }; const dataHandler: cytoscape.EventHandler = event => { - if (cy) { + if (cy && cy.elements().length > 0) { if (serviceName) { resetConnectedEdgeStyle(cy.getElementById(serviceName)); // Add the "primary" class to the node if its id matches the serviceName. diff --git a/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts b/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts index 8b4ae27cb306..841528d3910b 100644 --- a/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts +++ b/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts @@ -70,4 +70,8 @@ export const METRIC_FORMATTERS: MetricFormatters = { formatter: InfraFormatterType.number, template: '{{value}} seconds', }, + ['rdsLatency']: { + formatter: InfraFormatterType.number, + template: '{{value}} ms', + }, }; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index 8fdba86f233d..a176ba756652 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -103,9 +103,13 @@ export const Expressions: React.FC = props => { const addExpression = useCallback(() => { const exp = alertParams.criteria?.slice() || []; - exp.push(defaultExpression); + exp.push({ + ...defaultExpression, + timeSize: timeSize ?? defaultExpression.timeSize, + timeUnit: timeUnit ?? defaultExpression.timeUnit, + }); setAlertParams('criteria', exp); - }, [setAlertParams, alertParams.criteria]); + }, [setAlertParams, alertParams.criteria, timeSize, timeUnit]); const removeExpression = useCallback( (id: number) => { diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx index c2ee552e3155..97c0bb98962d 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx @@ -110,10 +110,14 @@ export const Expressions: React.FC = props => { ); const addExpression = useCallback(() => { - const exp = alertParams.criteria.slice(); - exp.push(defaultExpression); + const exp = alertParams.criteria?.slice() || []; + exp.push({ + ...defaultExpression, + timeSize: timeSize ?? defaultExpression.timeSize, + timeUnit: timeUnit ?? defaultExpression.timeUnit, + }); setAlertParams('criteria', exp); - }, [setAlertParams, alertParams.criteria]); + }, [setAlertParams, alertParams.criteria, timeSize, timeUnit]); const removeExpression = useCallback( (id: number) => { @@ -185,6 +189,31 @@ export const Expressions: React.FC = props => { [onFilterChange] ); + const preFillAlertCriteria = useCallback(() => { + const md = alertsContext.metadata; + if (md && md.options) { + setAlertParams('criteria', [ + { + ...defaultExpression, + metric: md.options.metric!.type, + } as InventoryMetricConditions, + ]); + } else { + setAlertParams('criteria', [defaultExpression]); + } + }, [alertsContext.metadata, setAlertParams]); + + const preFillAlertFilter = useCallback(() => { + const md = alertsContext.metadata; + if (md && md.filter) { + setAlertParams('filterQueryText', md.filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(md.filter, derivedIndexPattern) || '' + ); + } + }, [alertsContext.metadata, derivedIndexPattern, setAlertParams]); + useEffect(() => { const md = alertsContext.metadata; if (!alertParams.nodeType) { @@ -195,31 +224,19 @@ export const Expressions: React.FC = props => { } } - if (!alertParams.criteria) { - if (md && md.options) { - setAlertParams('criteria', [ - { - ...defaultExpression, - metric: md.options.metric!.type, - } as InventoryMetricConditions, - ]); - } else { - setAlertParams('criteria', [defaultExpression]); - } + if (alertParams.criteria && alertParams.criteria.length) { + setTimeSize(alertParams.criteria[0].timeSize); + setTimeUnit(alertParams.criteria[0].timeUnit); + } else { + preFillAlertCriteria(); } if (!alertParams.filterQuery) { - if (md && md.filter) { - setAlertParams('filterQueryText', md.filter); - setAlertParams( - 'filterQuery', - convertKueryToElasticSearchQuery(md.filter, derivedIndexPattern) || '' - ); - } + preFillAlertFilter(); } if (!alertParams.sourceId) { - setAlertParams('sourceId', source?.id); + setAlertParams('sourceId', source?.id || 'default'); } }, [alertsContext.metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps @@ -235,11 +252,13 @@ export const Expressions: React.FC = props => { - + + + {alertParams.criteria && @@ -425,11 +444,13 @@ export const ExpressionRow: React.FC = props => { /> {metric && ( - -
-
{metricUnit[metric]?.label || ''}
-
-
+
+ {metricUnit[metric]?.label || ''} +
)} @@ -502,4 +523,5 @@ const metricUnit: Record = { s3UploadBytes: { label: 'bytes' }, s3DownloadBytes: { label: 'bytes' }, sqsOldestMessage: { label: 'seconds' }, + rdsLatency: { label: 'ms' }, }; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx index faafdf1b81ee..2c72c658ce09 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx @@ -24,7 +24,7 @@ interface Props { metric?: { value: SnapshotMetricType; text: string }; metrics: Array<{ value: string; text: string }>; errors: IErrorObject; - onChange: (metric: SnapshotMetricType) => void; + onChange: (metric?: SnapshotMetricType) => void; popupPosition?: | 'upCenter' | 'upLeft' @@ -65,11 +65,11 @@ export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosit } )} value={metric?.text || firstFieldOption.text} - isActive={aggFieldPopoverOpen || !metric} + isActive={Boolean(aggFieldPopoverOpen || (errors.metric && errors.metric.length > 0))} onClick={() => { setAggFieldPopoverOpen(true); }} - color={metric ? 'secondary' : 'danger'} + color={errors.metric?.length ? 'danger' : 'secondary'} /> } isOpen={aggFieldPopoverOpen} @@ -89,16 +89,12 @@ export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosit - 0 && metric !== undefined} - error={errors.metric} - > + 0} error={errors.metric}> 0 && metric !== undefined} + isInvalid={errors.metric.length > 0} placeholder={firstFieldOption.text} options={availablefieldsOptions} noSuggestions={!availablefieldsOptions.length} @@ -110,6 +106,8 @@ export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosit if (selectedOptions.length > 0) { onChange(selectedOptions[0].value as SnapshotMetricType); setAggFieldPopoverOpen(false); + } else { + onChange(); } }} /> diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx index 06dbf5315b83..e089ae912e11 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx @@ -10,15 +10,21 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback } from 'react'; import { LoadingOverlayWrapper } from '../../../loading_overlay_wrapper'; import { IndexSetupRow } from './index_setup_row'; -import { AvailableIndex } from './validation'; +import { AvailableIndex, ValidationIndicesError } from './validation'; export const AnalysisSetupIndicesForm: React.FunctionComponent<{ disabled?: boolean; indices: AvailableIndex[]; isValidating: boolean; onChangeSelectedIndices: (selectedIndices: AvailableIndex[]) => void; - valid: boolean; -}> = ({ disabled = false, indices, isValidating, onChangeSelectedIndices, valid }) => { + validationErrors?: ValidationIndicesError[]; +}> = ({ + disabled = false, + indices, + isValidating, + onChangeSelectedIndices, + validationErrors = [], +}) => { const changeIsIndexSelected = useCallback( (indexName: string, isSelected: boolean) => { onChangeSelectedIndices( @@ -41,6 +47,8 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{ [indices, onChangeSelectedIndices] ); + const isInvalid = validationErrors.length > 0; + return ( - + <> {indices.map(index => ( void; startTime: number | undefined; endTime: number | undefined; -}> = ({ disabled = false, setStartTime, setEndTime, startTime, endTime }) => { - const now = useMemo(() => moment(), []); + validationErrors?: TimeRangeValidationError[]; +}> = ({ + disabled = false, + setStartTime, + setEndTime, + startTime, + endTime, + validationErrors = [], +}) => { + const [now] = useState(() => moment()); const selectedEndTimeIsToday = !endTime || moment(endTime).isSame(now, 'day'); + const startTimeValue = useMemo(() => { return startTime ? moment(startTime) : undefined; }, [startTime]); const endTimeValue = useMemo(() => { return endTime ? moment(endTime) : undefined; }, [endTime]); + + const startTimeValidationErrorMessages = useMemo( + () => getStartTimeValidationErrorMessages(validationErrors), + [validationErrors] + ); + + const endTimeValidationErrorMessages = useMemo( + () => getEndTimeValidationErrorMessages(validationErrors), + [validationErrors] + ); + return ( } > - + 0} + label={startTimeLabel} + > setStartTime(undefined) } : undefined} @@ -91,7 +117,12 @@ export const AnalysisSetupTimerangeForm: React.FunctionComponent<{ - + 0} + label={endTimeLabel} + > setEndTime(undefined) } : undefined} @@ -122,3 +153,31 @@ export const AnalysisSetupTimerangeForm: React.FunctionComponent<{ ); }; + +const getStartTimeValidationErrorMessages = (validationErrors: TimeRangeValidationError[]) => + validationErrors.flatMap(validationError => { + switch (validationError.error) { + case 'INVALID_TIME_RANGE': + return [ + i18n.translate('xpack.infra.analysisSetup.startTimeBeforeEndTimeErrorMessage', { + defaultMessage: 'The start time must be before the end time.', + }), + ]; + default: + return []; + } + }); + +const getEndTimeValidationErrorMessages = (validationErrors: TimeRangeValidationError[]) => + validationErrors.flatMap(validationError => { + switch (validationError.error) { + case 'INVALID_TIME_RANGE': + return [ + i18n.translate('xpack.infra.analysisSetup.endTimeAfterStartTimeErrorMessage', { + defaultMessage: 'The end time must be after the start time.', + }), + ]; + default: + return []; + } + }); diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx index 18dc2e5aa9bd..2eb67e0c0ce7 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback } from 'react'; import { DatasetFilter } from '../../../../../common/log_analysis'; import { IndexSetupDatasetFilter } from './index_setup_dataset_filter'; -import { AvailableIndex, ValidationIndicesUIError } from './validation'; +import { AvailableIndex, ValidationUIError } from './validation'; export const IndexSetupRow: React.FC<{ index: AvailableIndex; @@ -61,7 +61,7 @@ export const IndexSetupRow: React.FC<{ ); }; -const formatValidationError = (errors: ValidationIndicesUIError[]): React.ReactNode => { +const formatValidationError = (errors: ValidationUIError[]): React.ReactNode => { return errors.map(error => { switch (error.error) { case 'INDEX_NOT_FOUND': diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx index 85aa7ce51324..c9b14a1ffe47 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx @@ -4,16 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiSpacer, EuiForm, EuiCallOut } from '@elastic/eui'; +import { EuiCallOut, EuiForm, EuiSpacer } from '@elastic/eui'; import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; - import { SetupStatus } from '../../../../../common/log_analysis'; import { AnalysisSetupIndicesForm } from './analysis_setup_indices_form'; import { AnalysisSetupTimerangeForm } from './analysis_setup_timerange_form'; -import { AvailableIndex, ValidationIndicesUIError } from './validation'; +import { + AvailableIndex, + TimeRangeValidationError, + timeRangeValidationErrorRT, + ValidationIndicesError, + validationIndicesErrorRT, + ValidationUIError, +} from './validation'; interface InitialConfigurationStepProps { setStartTime: (startTime: number | undefined) => void; @@ -24,7 +30,7 @@ interface InitialConfigurationStepProps { validatedIndices: AvailableIndex[]; setupStatus: SetupStatus; setValidatedIndices: (selectedIndices: AvailableIndex[]) => void; - validationErrors?: ValidationIndicesUIError[]; + validationErrors?: ValidationUIError[]; } export const createInitialConfigurationStep = ( @@ -47,6 +53,11 @@ export const InitialConfigurationStep: React.FunctionComponent { const disabled = useMemo(() => !editableFormStatus.includes(setupStatus.type), [setupStatus]); + const [indexValidationErrors, timeRangeValidationErrors, globalValidationErrors] = useMemo( + () => partitionValidationErrors(validationErrors), + [validationErrors] + ); + return ( <> @@ -57,16 +68,17 @@ export const InitialConfigurationStep: React.FunctionComponent - + ); @@ -88,7 +100,7 @@ const initialConfigurationStepTitle = i18n.translate( } ); -const ValidationErrors: React.FC<{ errors: ValidationIndicesUIError[] }> = ({ errors }) => { +const ValidationErrors: React.FC<{ errors: ValidationUIError[] }> = ({ errors }) => { if (errors.length === 0) { return null; } @@ -107,7 +119,7 @@ const ValidationErrors: React.FC<{ errors: ValidationIndicesUIError[] }> = ({ er ); }; -const formatValidationError = (error: ValidationIndicesUIError): React.ReactNode => { +const formatValidationError = (error: ValidationUIError): React.ReactNode => { switch (error.error) { case 'NETWORK_ERROR': return ( @@ -129,3 +141,19 @@ const formatValidationError = (error: ValidationIndicesUIError): React.ReactNode return ''; } }; + +const partitionValidationErrors = (validationErrors: ValidationUIError[]) => + validationErrors.reduce< + [ValidationIndicesError[], TimeRangeValidationError[], ValidationUIError[]] + >( + ([indicesErrors, timeRangeErrors, otherErrors], error) => { + if (validationIndicesErrorRT.is(error)) { + return [[...indicesErrors, error], timeRangeErrors, otherErrors]; + } else if (timeRangeValidationErrorRT.is(error)) { + return [indicesErrors, [...timeRangeErrors, error], otherErrors]; + } else { + return [indicesErrors, timeRangeErrors, [...otherErrors, error]]; + } + }, + [[], [], []] + ); diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx index d69e544aeab1..4a3899f2d391 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx @@ -4,15 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ValidationIndicesError } from '../../../../../common/http_api'; +import * as rt from 'io-ts'; +import { ValidationIndicesError, validationIndicesErrorRT } from '../../../../../common/http_api'; import { DatasetFilter } from '../../../../../common/log_analysis'; -export { ValidationIndicesError }; +export { ValidationIndicesError, validationIndicesErrorRT }; -export type ValidationIndicesUIError = +export const timeRangeValidationErrorRT = rt.strict({ + error: rt.literal('INVALID_TIME_RANGE'), +}); + +export type TimeRangeValidationError = rt.TypeOf; + +export type ValidationUIError = | ValidationIndicesError | { error: 'NETWORK_ERROR' } - | { error: 'TOO_FEW_SELECTED_INDICES' }; + | { error: 'TOO_FEW_SELECTED_INDICES' } + | TimeRangeValidationError; interface ValidAvailableIndex { validity: 'valid'; diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts index d46e8bc2485f..9f757497aff8 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts @@ -16,7 +16,7 @@ import { import { AvailableIndex, ValidationIndicesError, - ValidationIndicesUIError, + ValidationUIError, } from '../../../components/logging/log_analysis_setup/initial_configuration_step'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { ModuleDescriptor, ModuleSourceConfiguration } from './log_analysis_module_types'; @@ -46,6 +46,11 @@ export const useAnalysisSetupState = ({ const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs); const [endTime, setEndTime] = useState(undefined); + const isTimeRangeValid = useMemo( + () => (startTime != null && endTime != null ? startTime < endTime : true), + [endTime, startTime] + ); + const [validatedIndices, setValidatedIndices] = useState( sourceConfiguration.indices.map(indexName => ({ name: indexName, @@ -201,35 +206,54 @@ export const useAnalysisSetupState = ({ [validateDatasetsRequest.state, validateIndicesRequest.state] ); - const validationErrors = useMemo(() => { + const validationErrors = useMemo(() => { if (isValidating) { return []; } - if (validateIndicesRequest.state === 'rejected') { - return [{ error: 'NETWORK_ERROR' }]; - } - - if (selectedIndexNames.length === 0) { - return [{ error: 'TOO_FEW_SELECTED_INDICES' }]; - } - - return validatedIndices.reduce((errors, index) => { - return index.validity === 'invalid' && selectedIndexNames.includes(index.name) - ? [...errors, ...index.errors] - : errors; - }, []); - }, [isValidating, validateIndicesRequest.state, selectedIndexNames, validatedIndices]); + return [ + // validate request status + ...(validateIndicesRequest.state === 'rejected' || + validateDatasetsRequest.state === 'rejected' + ? [{ error: 'NETWORK_ERROR' as const }] + : []), + // validation request results + ...validatedIndices.reduce((errors, index) => { + return index.validity === 'invalid' && selectedIndexNames.includes(index.name) + ? [...errors, ...index.errors] + : errors; + }, []), + // index count + ...(selectedIndexNames.length === 0 ? [{ error: 'TOO_FEW_SELECTED_INDICES' as const }] : []), + // time range + ...(!isTimeRangeValid ? [{ error: 'INVALID_TIME_RANGE' as const }] : []), + ]; + }, [ + isValidating, + validateIndicesRequest.state, + validateDatasetsRequest.state, + validatedIndices, + selectedIndexNames, + isTimeRangeValid, + ]); const prevStartTime = usePrevious(startTime); const prevEndTime = usePrevious(endTime); const prevValidIndexNames = usePrevious(validIndexNames); useEffect(() => { + if (!isTimeRangeValid) { + return; + } + validateIndices(); - }, [validateIndices]); + }, [isTimeRangeValid, validateIndices]); useEffect(() => { + if (!isTimeRangeValid) { + return; + } + if ( startTime !== prevStartTime || endTime !== prevEndTime || @@ -239,6 +263,7 @@ export const useAnalysisSetupState = ({ } }, [ endTime, + isTimeRangeValid, prevEndTime, prevStartTime, prevValidIndexNames, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts index f8c7a10f1283..479c292035ae 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts @@ -71,6 +71,10 @@ const METRIC_FORMATTERS: MetricFormatters = { formatter: InfraFormatterType.number, template: '{{value}} seconds', }, + ['rdsLatency']: { + formatter: InfraFormatterType.number, + template: '{{value}} ms', + }, }; export const createInventoryMetricFormatter = (metric: SnapshotMetricInput) => ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts index e9b736e379b5..2936eea21805 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts @@ -14,17 +14,6 @@ export { DATASOURCE_SAVED_OBJECT_TYPE, } from '../../../../common'; -export const BASE_PATH = '/app/ingestManager'; -export const EPM_PATH = '/epm'; -export const EPM_LIST_ALL_PACKAGES_PATH = EPM_PATH; -export const EPM_LIST_INSTALLED_PACKAGES_PATH = `${EPM_PATH}/installed`; -export const EPM_DETAIL_VIEW_PATH = `${EPM_PATH}/detail/:pkgkey/:panel?`; -export const AGENT_CONFIG_PATH = '/configs'; -export const AGENT_CONFIG_DETAILS_PATH = `${AGENT_CONFIG_PATH}/`; -export const DATA_STREAM_PATH = '/data-streams'; -export const FLEET_PATH = '/fleet'; -export const FLEET_AGENTS_PATH = `${FLEET_PATH}/agents`; -export const FLEET_AGENT_DETAIL_PATH = `${FLEET_AGENTS_PATH}/`; -export const FLEET_ENROLLMENT_TOKENS_PATH = `/fleet/enrollment-tokens`; +export * from './page_paths'; export const INDEX_NAME = '.kibana'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts new file mode 100644 index 000000000000..73771fa3cb34 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type StaticPage = + | 'overview' + | 'integrations' + | 'integrations_all' + | 'integrations_installed' + | 'configurations' + | 'configurations_list' + | 'fleet' + | 'fleet_enrollment_tokens' + | 'data_streams'; + +export type DynamicPage = + | 'integration_details' + | 'configuration_details' + | 'add_datasource_from_configuration' + | 'add_datasource_from_integration' + | 'edit_datasource' + | 'fleet_agent_list' + | 'fleet_agent_details'; + +export type Page = StaticPage | DynamicPage; + +export interface DynamicPagePathValues { + [key: string]: string; +} + +export const BASE_PATH = '/app/ingestManager'; + +// If routing paths are changed here, please also check to see if +// `pagePathGetters()`, below, needs any modifications +export const PAGE_ROUTING_PATHS = { + overview: '/', + integrations: '/integrations/:tabId?', + integrations_all: '/integrations', + integrations_installed: '/integrations/installed', + integration_details: '/integrations/detail/:pkgkey/:panel?', + configurations: '/configs', + configurations_list: '/configs', + configuration_details: '/configs/:configId/:tabId?', + configuration_details_yaml: '/configs/:configId/yaml', + configuration_details_settings: '/configs/:configId/settings', + add_datasource_from_configuration: '/configs/:configId/add-datasource', + add_datasource_from_integration: '/integrations/:pkgkey/add-datasource', + edit_datasource: '/configs/:configId/edit-datasource/:datasourceId', + fleet: '/fleet', + fleet_agent_list: '/fleet/agents', + fleet_agent_details: '/fleet/agents/:agentId/:tabId?', + fleet_agent_details_events: '/fleet/agents/:agentId', + fleet_agent_details_details: '/fleet/agents/:agentId/details', + fleet_enrollment_tokens: '/fleet/enrollment-tokens', + data_streams: '/data-streams', +}; + +export const pagePathGetters: { + [key in StaticPage]: () => string; +} & + { + [key in DynamicPage]: (values: DynamicPagePathValues) => string; + } = { + overview: () => '/', + integrations: () => '/integrations', + integrations_all: () => '/integrations', + integrations_installed: () => '/integrations/installed', + integration_details: ({ pkgkey, panel }) => + `/integrations/detail/${pkgkey}${panel ? `/${panel}` : ''}`, + configurations: () => '/configs', + configurations_list: () => '/configs', + configuration_details: ({ configId, tabId }) => `/configs/${configId}${tabId ? `/${tabId}` : ''}`, + add_datasource_from_configuration: ({ configId }) => `/configs/${configId}/add-datasource`, + add_datasource_from_integration: ({ pkgkey }) => `/integrations/${pkgkey}/add-datasource`, + edit_datasource: ({ configId, datasourceId }) => + `/configs/${configId}/edit-datasource/${datasourceId}`, + fleet: () => '/fleet', + fleet_agent_list: ({ kuery }) => `/fleet/agents${kuery ? `?kuery=${kuery}` : ''}`, + fleet_agent_details: ({ agentId, tabId }) => + `/fleet/agents/${agentId}${tabId ? `/${tabId}` : ''}`, + fleet_enrollment_tokens: () => '/fleet/enrollment-tokens', + data_streams: () => '/data-streams', +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts index 66c7333150fb..a752ad2a8912 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts @@ -8,6 +8,7 @@ export { useCapabilities } from './use_capabilities'; export { useCore, CoreContext } from './use_core'; export { useConfig, ConfigContext } from './use_config'; export { useSetupDeps, useStartDeps, DepsContext } from './use_deps'; +export { useBreadcrumbs } from './use_breadcrumbs'; export { useLink } from './use_link'; export { usePackageIconType, UsePackageIconType } from './use_package_icon_type'; export { usePagination, Pagination } from './use_pagination'; @@ -15,3 +16,4 @@ export { useDebounce } from './use_debounce'; export * from './use_request'; export * from './use_input'; export * from './use_url_params'; +export * from './use_fleet_status'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx index ff6656e969c9..207c757fd5b1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx @@ -3,11 +3,225 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { i18n } from '@kbn/i18n'; import { ChromeBreadcrumb } from 'src/core/public'; +import { BASE_PATH, Page, DynamicPagePathValues, pagePathGetters } from '../constants'; import { useCore } from './use_core'; -export function useBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]) { - const { chrome } = useCore(); - return chrome.setBreadcrumbs(newBreadcrumbs); +const BASE_BREADCRUMB: ChromeBreadcrumb = { + href: pagePathGetters.overview(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.appTitle', { + defaultMessage: 'Ingest Manager', + }), +}; + +const breadcrumbGetters: { + [key in Page]: (values: DynamicPagePathValues) => ChromeBreadcrumb[]; +} = { + overview: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.overviewPageTitle', { + defaultMessage: 'Overview', + }), + }, + ], + integrations: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + defaultMessage: 'Integrations', + }), + }, + ], + integrations_all: () => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integrations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + defaultMessage: 'Integrations', + }), + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.allIntegrationsPageTitle', { + defaultMessage: 'All', + }), + }, + ], + integrations_installed: () => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integrations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + defaultMessage: 'Integrations', + }), + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.installedIntegrationsPageTitle', { + defaultMessage: 'Installed', + }), + }, + ], + integration_details: ({ pkgTitle }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integrations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + defaultMessage: 'Integrations', + }), + }, + { text: pkgTitle }, + ], + configurations: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.configurationsPageTitle', { + defaultMessage: 'Configurations', + }), + }, + ], + configurations_list: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.configurationsPageTitle', { + defaultMessage: 'Configurations', + }), + }, + ], + configuration_details: ({ configName }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.configurations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.configurationsPageTitle', { + defaultMessage: 'Configurations', + }), + }, + { text: configName }, + ], + add_datasource_from_configuration: ({ configName, configId }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.configurations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.configurationsPageTitle', { + defaultMessage: 'Configurations', + }), + }, + { + href: pagePathGetters.configuration_details({ configId }), + text: configName, + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.addDatasourcePageTitle', { + defaultMessage: 'Add data source', + }), + }, + ], + add_datasource_from_integration: ({ pkgTitle, pkgkey }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integrations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + defaultMessage: 'Integrations', + }), + }, + { + href: pagePathGetters.integration_details({ pkgkey }), + text: pkgTitle, + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.addDatasourcePageTitle', { + defaultMessage: 'Add data source', + }), + }, + ], + edit_datasource: ({ configName, configId }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.configurations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.configurationsPageTitle', { + defaultMessage: 'Configurations', + }), + }, + { + href: pagePathGetters.configuration_details({ configId }), + text: configName, + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.editDatasourcePageTitle', { + defaultMessage: 'Edit data source', + }), + }, + ], + fleet: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { + defaultMessage: 'Fleet', + }), + }, + ], + fleet_agent_list: () => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.fleet(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { + defaultMessage: 'Fleet', + }), + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle', { + defaultMessage: 'Agents', + }), + }, + ], + fleet_agent_details: ({ agentHost }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.fleet(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { + defaultMessage: 'Fleet', + }), + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle', { + defaultMessage: 'Agents', + }), + }, + { text: agentHost }, + ], + fleet_enrollment_tokens: () => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.fleet(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { + defaultMessage: 'Fleet', + }), + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetEnrollmentTokensPageTitle', { + defaultMessage: 'Enrollment tokens', + }), + }, + ], + data_streams: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.datastreamsPageTitle', { + defaultMessage: 'Data streams', + }), + }, + ], +}; + +export function useBreadcrumbs(page: Page, values: DynamicPagePathValues = {}) { + const { chrome, http } = useCore(); + const breadcrumbs: ChromeBreadcrumb[] = breadcrumbGetters[page](values).map(breadcrumb => ({ + ...breadcrumb, + href: breadcrumb.href ? http.basePath.prepend(`${BASE_PATH}#${breadcrumb.href}`) : undefined, + })); + const docTitle: string[] = [...breadcrumbs] + .reverse() + .map(breadcrumb => breadcrumb.text as string); + chrome.docTitle.change(docTitle); + chrome.setBreadcrumbs(breadcrumbs); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts index f6c5b8bc03fc..58537b2075c1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts @@ -6,9 +6,9 @@ import { useCore } from './'; -const BASE_PATH = '/app/kibana'; +const KIBANA_BASE_PATH = '/app/kibana'; export function useKibanaLink(path: string = '/') { const core = useCore(); - return core.http.basePath.prepend(`${BASE_PATH}#${path}`); + return core.http.basePath.prepend(`${KIBANA_BASE_PATH}#${path}`); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_link.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_link.ts index 333606cec802..1b17c5cb0b1f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_link.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_link.ts @@ -4,10 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BASE_PATH } from '../constants'; +import { + BASE_PATH, + StaticPage, + DynamicPage, + DynamicPagePathValues, + pagePathGetters, +} from '../constants'; import { useCore } from './'; -export function useLink(path: string = '/') { +const getPath = (page: StaticPage | DynamicPage, values: DynamicPagePathValues = {}): string => { + return values ? pagePathGetters[page](values) : pagePathGetters[page as StaticPage](); +}; + +export const useLink = () => { const core = useCore(); - return core.http.basePath.prepend(`${BASE_PATH}#${path}`); -} + return { + getPath, + getHref: (page: StaticPage | DynamicPage, values?: DynamicPagePathValues) => { + const path = getPath(page, values); + return core.http.basePath.prepend(`${BASE_PATH}#${path}`); + }, + }; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index 3612497e723c..f6a386314272 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -18,7 +18,7 @@ import { IngestManagerConfigType, IngestManagerStartDeps, } from '../../plugin'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from './constants'; +import { PAGE_ROUTING_PATHS } from './constants'; import { DefaultLayout, WithoutHeaderLayout } from './layouts'; import { Loading, Error } from './components'; import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp, DataStreamApp } from './sections'; @@ -174,42 +174,42 @@ const IngestManagerRoutes = ({ ...rest }) => { } return ( - + - - + + - + - + - + - + - + - - + + - + ); }; @@ -265,3 +265,8 @@ export function renderApp( ReactDOM.unmountComponentAtNode(element); }; } + +export const teardownIngestManager = (coreStart: CoreStart) => { + coreStart.chrome.docTitle.reset(); + coreStart.chrome.setBreadcrumbs([]); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index e9d7fcb1cf5c..fbe7c736e2df 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -10,7 +10,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Section } from '../sections'; import { AlphaMessaging, SettingFlyout } from '../components'; import { useLink, useConfig } from '../hooks'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../constants'; interface Props { showSettings?: boolean; @@ -39,8 +38,8 @@ export const DefaultLayout: React.FunctionComponent = ({ section, children, }) => { + const { getHref } = useLink(); const { epm, fleet } = useConfig(); - const [isSettingsFlyoutOpen, setIsSettingsFlyoutOpen] = React.useState(false); return ( @@ -60,7 +59,7 @@ export const DefaultLayout: React.FunctionComponent = ({ - + = ({ = ({ defaultMessage="Integrations" /> - + = ({ = ({ defaultMessage="Fleet" /> - + ( ({ count, agentConfigId }) => { - const FLEET_URI = useLink(FLEET_AGENTS_PATH); + const { getHref } = useLink(); const displayValue = ( ( /> ); return count > 0 ? ( - + {displayValue} ) : ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx index 46233fdb5950..577f08cdc331 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { useRouteMatch, useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -17,16 +17,15 @@ import { EuiSpacer, } from '@elastic/eui'; import { EuiStepProps } from '@elastic/eui/src/components/steps/step'; -import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; import { AgentConfig, PackageInfo, NewDatasource } from '../../../types'; import { useLink, + useBreadcrumbs, sendCreateDatasource, useCore, useConfig, sendGetAgentStatus, } from '../../../hooks'; -import { useLinks as useEPMLinks } from '../../epm/hooks'; import { ConfirmDeployConfigModal } from '../components'; import { CreateDatasourcePageLayout } from './components'; import { CreateDatasourceFrom, DatasourceFormState } from './types'; @@ -48,6 +47,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const { params: { configId, pkgkey }, } = useRouteMatch(); + const { getHref, getPath } = useLink(); const history = useHistory(); const from: CreateDatasourceFrom = configId ? 'config' : 'package'; const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); @@ -95,32 +95,46 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { // Datasource validation state const [validationResults, setValidationResults] = useState(); + // Form state + const [formState, setFormState] = useState('INVALID'); + // Update package info method - const updatePackageInfo = (updatedPackageInfo: PackageInfo | undefined) => { - if (updatedPackageInfo) { - setPackageInfo(updatedPackageInfo); - setFormState('VALID'); - } else { - setFormState('INVALID'); - setPackageInfo(undefined); - } + const updatePackageInfo = useCallback( + (updatedPackageInfo: PackageInfo | undefined) => { + if (updatedPackageInfo) { + setPackageInfo(updatedPackageInfo); + if (agentConfig) { + setFormState('VALID'); + } + } else { + setFormState('INVALID'); + setPackageInfo(undefined); + } - // eslint-disable-next-line no-console - console.debug('Package info updated', updatedPackageInfo); - }; + // eslint-disable-next-line no-console + console.debug('Package info updated', updatedPackageInfo); + }, + [agentConfig, setPackageInfo, setFormState] + ); // Update agent config method - const updateAgentConfig = (updatedAgentConfig: AgentConfig | undefined) => { - if (updatedAgentConfig) { - setAgentConfig(updatedAgentConfig); - } else { - setFormState('INVALID'); - setAgentConfig(undefined); - } + const updateAgentConfig = useCallback( + (updatedAgentConfig: AgentConfig | undefined) => { + if (updatedAgentConfig) { + setAgentConfig(updatedAgentConfig); + if (packageInfo) { + setFormState('VALID'); + } + } else { + setFormState('INVALID'); + setAgentConfig(undefined); + } - // eslint-disable-next-line no-console - console.debug('Agent config updated', updatedAgentConfig); - }; + // eslint-disable-next-line no-console + console.debug('Agent config updated', updatedAgentConfig); + }, + [packageInfo, setAgentConfig, setFormState] + ); const hasErrors = validationResults ? validationHasErrors(validationResults) : false; @@ -156,18 +170,13 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { } }; - // Cancel url - const CONFIG_URL = useLink( - `${AGENT_CONFIG_DETAILS_PATH}${agentConfig ? agentConfig.id : configId}` - ); - const PACKAGE_URL = useEPMLinks().toDetailView({ - name: (pkgkey || '-').split('-')[0], - version: (pkgkey || '-').split('-')[1], - }); - const cancelUrl = from === 'config' ? CONFIG_URL : PACKAGE_URL; + // Cancel path + const cancelUrl = + from === 'config' + ? getHref('configuration_details', { configId: agentConfig?.id || configId }) + : getHref('integration_details', { pkgkey }); // Save datasource - const [formState, setFormState] = useState('INVALID'); const saveDatasource = async () => { setFormState('LOADING'); const result = await sendCreateDatasource(datasource); @@ -186,7 +195,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { } const { error } = await saveDatasource(); if (!error) { - history.push(`${AGENT_CONFIG_DETAILS_PATH}${agentConfig ? agentConfig.id : configId}`); + history.push(getPath('configuration_details', { configId: agentConfig?.id || configId })); notifications.toasts.addSuccess({ title: i18n.translate('xpack.ingestManager.createDatasource.addedNotificationTitle', { defaultMessage: `Successfully added '{datasourceName}'`, @@ -219,33 +228,43 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { packageInfo, }; + const stepSelectConfig = useMemo( + () => ( + + ), + [pkgkey, updatePackageInfo, agentConfig, updateAgentConfig] + ); + + const stepSelectPackage = useMemo( + () => ( + + ), + [configId, updateAgentConfig, packageInfo, updatePackageInfo] + ); + const steps: EuiStepProps[] = [ from === 'package' ? { title: i18n.translate('xpack.ingestManager.createDatasource.stepSelectAgentConfigTitle', { defaultMessage: 'Select an agent configuration', }), - children: ( - - ), + children: stepSelectConfig, } : { title: i18n.translate('xpack.ingestManager.createDatasource.stepSelectPackageTitle', { defaultMessage: 'Select an integration', }), - children: ( - - ), + children: stepSelectPackage, }, { title: i18n.translate('xpack.ingestManager.createDatasource.stepDefineDatasourceTitle', { @@ -280,6 +299,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { ) : null, }, ]; + return ( {formState === 'CONFIRM' && agentConfig && ( @@ -290,6 +310,16 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { onCancel={() => setFormState('VALID')} /> )} + {from === 'package' + ? packageInfo && ( + + ) + : agentConfig && ( + + )} {/* TODO #64541 - Remove classes */} @@ -331,3 +361,19 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { ); }; + +const ConfigurationBreadcrumb: React.FunctionComponent<{ + configName: string; + configId: string; +}> = ({ configName, configId }) => { + useBreadcrumbs('add_datasource_from_configuration', { configName, configId }); + return null; +}; + +const IntegrationBreadcrumb: React.FunctionComponent<{ + pkgTitle: string; + pkgkey: string; +}> = ({ pkgTitle, pkgkey }) => { + useBreadcrumbs('add_datasource_from_integration', { pkgTitle, pkgkey }); + return null; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx index a0418c5f256c..3ad862c5e43f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx @@ -20,7 +20,6 @@ import { AgentConfig, Datasource } from '../../../../../types'; import { TableRowActions } from '../../../components/table_row_actions'; import { DangerEuiContextMenuItem } from '../../../components/danger_eui_context_menu_item'; import { useCapabilities, useLink } from '../../../../../hooks'; -import { useAgentConfigLink } from '../../hooks/use_details_uri'; import { DatasourceDeleteProvider } from '../../../components/datasource_delete_provider'; import { useConfigRefresh } from '../../hooks/use_config'; import { PackageIcon } from '../../../../../components/package_icon'; @@ -54,9 +53,8 @@ export const DatasourcesTable: React.FunctionComponent = ({ config, ...rest }) => { + const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; - const addDatasourceLink = useAgentConfigLink('add-datasource', { configId: config.id }); - const editDatasourceLink = useLink(`/configs/${config.id}/edit-datasource`); const refreshConfig = useConfigRefresh(); // With the datasources provided on input, generate the list of datasources @@ -216,7 +214,10 @@ export const DatasourcesTable: React.FunctionComponent = ({ = ({ ], }, ], - [config, editDatasourceLink, hasWriteCapabilities, refreshConfig] + [config, getHref, hasWriteCapabilities, refreshConfig] ); return ( @@ -274,9 +275,10 @@ export const DatasourcesTable: React.FunctionComponent = ({ search={{ toolsRight: [ (({ configId }) => { + const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; - const addDatasourceLink = useAgentConfigLink('add-datasource', { configId }); return ( (({ configId }) => { /> } actions={ - + ( fleet: { enabled: isFleetEnabled }, } = useConfig(); const history = useHistory(); + const { getPath } = useLink(); const hasWriteCapabilites = useCapabilities().write; const refreshConfig = useConfigRefresh(); const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); @@ -147,7 +148,7 @@ export const ConfigSettingsView = memo<{ config: AgentConfig }>( validation={validation} isEditing={true} onDelete={() => { - history.push(AGENT_CONFIG_PATH); + history.push(getPath('configurations_list')); }} /> {/* TODO #64541 - Remove classes */} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/constants.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/constants.ts deleted file mode 100644 index 787791f985c7..000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; - -export const DETAILS_ROUTER_PATH = `${AGENT_CONFIG_DETAILS_PATH}:configId`; -export const DETAILS_ROUTER_SUB_PATH = `${DETAILS_ROUTER_PATH}/:tabId`; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/use_details_uri.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/use_details_uri.ts deleted file mode 100644 index 9332ce3e0f90..000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/use_details_uri.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { generatePath } from 'react-router-dom'; -import { useLink } from '../../../../hooks'; -import { AGENT_CONFIG_PATH } from '../../../../constants'; -import { DETAILS_ROUTER_PATH, DETAILS_ROUTER_SUB_PATH } from '../constants'; - -type AgentConfigUriArgs = - | ['list'] - | ['details', { configId: string }] - | ['details-yaml', { configId: string }] - | ['details-settings', { configId: string }] - | ['datasource', { configId: string; datasourceId: string }] - | ['add-datasource', { configId: string }]; - -/** - * Returns a Uri that starts at the Agent Config Route path (`/configs/`). - * These are good for use when needing to use React Router's redirect or - * `history.push(routePath)`. - * @param args - */ -export const useAgentConfigUri = (...args: AgentConfigUriArgs) => { - switch (args[0]) { - case 'list': - return AGENT_CONFIG_PATH; - case 'details': - return generatePath(DETAILS_ROUTER_PATH, args[1]); - case 'details-yaml': - return `${generatePath(DETAILS_ROUTER_SUB_PATH, { ...args[1], tabId: 'yaml' })}`; - case 'details-settings': - return `${generatePath(DETAILS_ROUTER_SUB_PATH, { ...args[1], tabId: 'settings' })}`; - case 'add-datasource': - return `${generatePath(DETAILS_ROUTER_SUB_PATH, { ...args[1], tabId: 'add-datasource' })}`; - case 'datasource': - const [, options] = args; - return `${generatePath(DETAILS_ROUTER_PATH, options)}?datasourceId=${options.datasourceId}`; - } - return '/'; -}; - -/** - * Returns a full Link that includes Kibana basepath (ex. `/app/ingestManager#/configs`). - * These are good for use in `href` properties - * @param args - */ -export const useAgentConfigLink = (...args: AgentConfigUriArgs) => { - const BASE_URI = useLink(''); - const AGENT_CONFIG_ROUTE = useAgentConfigUri(...args); - return `${BASE_URI}${AGENT_CONFIG_ROUTE}`; -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx index 82879c174b7d..f80b981b69d3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { Redirect, useRouteMatch, Switch, Route } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; @@ -21,13 +21,13 @@ import { } from '@elastic/eui'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import styled from 'styled-components'; -import { useGetOneAgentConfig } from '../../../hooks'; +import { AgentConfig } from '../../../types'; +import { PAGE_ROUTING_PATHS } from '../../../constants'; +import { useGetOneAgentConfig, useLink, useBreadcrumbs } from '../../../hooks'; import { Loading } from '../../../components'; import { WithHeaderLayout } from '../../../layouts'; import { ConfigRefreshContext, useGetAgentStatus, AgentStatusRefreshContext } from './hooks'; import { LinkedAgentCount } from '../components'; -import { useAgentConfigLink } from './hooks/use_details_uri'; -import { DETAILS_ROUTER_PATH, DETAILS_ROUTER_SUB_PATH } from './constants'; import { ConfigDatasourcesView } from './components/datasources'; import { ConfigYamlView } from './components/yaml'; import { ConfigSettingsView } from './components/settings'; @@ -38,23 +38,11 @@ const Divider = styled.div` border-left: ${props => props.theme.eui.euiBorderThin}; `; -export const AgentConfigDetailsPage = memo(() => { - return ( - - - - - - - - - ); -}); - -export const AgentConfigDetailsLayout: React.FunctionComponent = () => { +export const AgentConfigDetailsPage: React.FunctionComponent = () => { const { params: { configId, tabId = '' }, } = useRouteMatch<{ configId: string; tabId?: string }>(); + const { getHref } = useLink(); const agentConfigRequest = useGetOneAgentConfig(configId); const agentConfig = agentConfigRequest.data ? agentConfigRequest.data.item : null; const { isLoading, error, sendRequest: refreshAgentConfig } = agentConfigRequest; @@ -63,17 +51,16 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { const { refreshAgentStatus } = agentStatusRequest; const agentStatus = agentStatusRequest.data?.results; - // Links - const configListLink = useAgentConfigLink('list'); - const configDetailsLink = useAgentConfigLink('details', { configId }); - const configDetailsYamlLink = useAgentConfigLink('details-yaml', { configId }); - const configDetailsSettingsLink = useAgentConfigLink('details-settings', { configId }); - const headerLeftContent = useMemo( () => ( - + { ) : null} ), - [configListLink, agentConfig, configId] + [getHref, agentConfig, configId] ); const headerRightContent = useMemo( @@ -184,7 +171,7 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.configDetails.subTabs.datasourcesTabText', { defaultMessage: 'Data sources', }), - href: configDetailsLink, + href: getHref('configuration_details', { configId, tabId: 'datasources' }), isSelected: tabId === '' || tabId === 'datasources', }, { @@ -192,7 +179,7 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.configDetails.subTabs.yamlTabText', { defaultMessage: 'YAML', }), - href: configDetailsYamlLink, + href: getHref('configuration_details', { configId, tabId: 'yaml' }), isSelected: tabId === 'yaml', }, { @@ -200,11 +187,11 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.configDetails.subTabs.settingsTabText', { defaultMessage: 'Settings', }), - href: configDetailsSettingsLink, + href: getHref('configuration_details', { configId, tabId: 'settings' }), isSelected: tabId === 'settings', }, ]; - }, [configDetailsLink, configDetailsSettingsLink, configDetailsYamlLink, tabId]); + }, [getHref, configId, tabId]); if (redirectToAgentConfigList) { return ; @@ -254,28 +241,37 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { rightColumn={headerRightContent} tabs={(headerTabs as unknown) as EuiTabProps[]} > - - { - return ; - }} - /> - { - return ; - }} - /> - { - return ; - }} - /> - + ); }; + +const AgentConfigDetailsContent: React.FunctionComponent<{ agentConfig: AgentConfig }> = ({ + agentConfig, +}) => { + useBreadcrumbs('configuration_details', { configName: agentConfig.name }); + return ( + + { + return ; + }} + /> + { + return ; + }} + /> + { + return ; + }} + /> + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx index 089a5a91df88..92be20a2761e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx @@ -16,10 +16,10 @@ import { EuiFlexItem, EuiSpacer, } from '@elastic/eui'; -import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; import { AgentConfig, PackageInfo, NewDatasource } from '../../../types'; import { useLink, + useBreadcrumbs, useCore, useConfig, sendUpdateDatasource, @@ -53,6 +53,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { params: { configId, datasourceId }, } = useRouteMatch(); const history = useHistory(); + const { getHref, getPath } = useLink(); const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); useEffect(() => { @@ -185,8 +186,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { }; // Cancel url - const CONFIG_URL = useLink(`${AGENT_CONFIG_DETAILS_PATH}${configId}`); - const cancelUrl = CONFIG_URL; + const cancelUrl = getHref('configuration_details', { configId }); // Save datasource const [formState, setFormState] = useState('INVALID'); @@ -208,7 +208,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { } const { error } = await saveDatasource(); if (!error) { - history.push(`${AGENT_CONFIG_DETAILS_PATH}${configId}`); + history.push(getPath('configuration_details', { configId })); notifications.toasts.addSuccess({ title: i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationTitle', { defaultMessage: `Successfully updated '{datasourceName}'`, @@ -262,6 +262,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { /> ) : ( <> + {formState === 'CONFIRM' && ( { ); }; + +const Breadcrumb: React.FunctionComponent<{ configName: string; configId: string }> = ({ + configName, + configId, +}) => { + useBreadcrumbs('edit_datasource', { configName, configId }); + return null; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx index ef88aa5d17f1..74fa67078f74 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx @@ -5,26 +5,32 @@ */ import React from 'react'; import { HashRouter as Router, Switch, Route } from 'react-router-dom'; +import { PAGE_ROUTING_PATHS } from '../../constants'; +import { useBreadcrumbs } from '../../hooks'; import { AgentConfigListPage } from './list_page'; import { AgentConfigDetailsPage } from './details_page'; import { CreateDatasourcePage } from './create_datasource_page'; import { EditDatasourcePage } from './edit_datasource_page'; -export const AgentConfigApp: React.FunctionComponent = () => ( - - - - - - - - - - - - - - - - -); +export const AgentConfigApp: React.FunctionComponent = () => { + useBreadcrumbs('configurations'); + + return ( + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index 9b565a0452c9..ff3124d57485 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -22,11 +22,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { useHistory } from 'react-router-dom'; import { AgentConfig } from '../../../types'; -import { - AGENT_CONFIG_DETAILS_PATH, - AGENT_CONFIG_SAVED_OBJECT_TYPE, - AGENT_CONFIG_PATH, -} from '../../../constants'; +import { AGENT_CONFIG_SAVED_OBJECT_TYPE } from '../../../constants'; import { WithHeaderLayout } from '../../../layouts'; import { useCapabilities, @@ -35,11 +31,11 @@ import { useLink, useConfig, useUrlParams, + useBreadcrumbs, } from '../../../hooks'; import { CreateAgentConfigFlyout } from './components'; import { SearchBar } from '../../../components/search_bar'; import { LinkedAgentCount } from '../components'; -import { useAgentConfigLink } from '../details_page/hooks/use_details_uri'; import { TableRowActions } from '../components/table_row_actions'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ @@ -81,14 +77,17 @@ const AgentConfigListPageLayout: React.FunctionComponent = ({ children }) => ( const ConfigRowActions = memo<{ config: AgentConfig; onDelete: () => void }>( ({ config, onDelete }) => { + const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; - const detailsLink = useAgentConfigLink('details', { configId: config.id }); - const addDatasourceLink = useAgentConfigLink('add-datasource', { configId: config.id }); return ( + void }>( void }>( ); export const AgentConfigListPage: React.FunctionComponent<{}> = () => { + useBreadcrumbs('configurations_list'); + const { getHref, getPath } = useLink(); // Config information const hasWriteCapabilites = useCapabilities().write; const { fleet: { enabled: isFleetEnabled }, } = useConfig(); - // Base URL paths - const DETAILS_URI = useLink(AGENT_CONFIG_DETAILS_PATH); - // Table and search states const { urlParams, toUrlParams } = useUrlParams(); const [search, setSearch] = useState( @@ -142,14 +140,16 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { (isOpen: boolean) => { if (isOpen !== isCreateAgentConfigFlyoutOpen) { if (isOpen) { - history.push(`${AGENT_CONFIG_PATH}?${toUrlParams({ ...urlParams, create: null })}`); + history.push( + `${getPath('configurations_list')}?${toUrlParams({ ...urlParams, create: null })}` + ); } else { const { create, ...params } = urlParams; - history.push(`${AGENT_CONFIG_PATH}?${toUrlParams(params)}`); + history.push(`${getPath('configurations_list')}?${toUrlParams(params)}`); } } }, - [history, isCreateAgentConfigFlyoutOpen, toUrlParams, urlParams] + [getPath, history, isCreateAgentConfigFlyoutOpen, toUrlParams, urlParams] ); // Fetch agent configs @@ -174,7 +174,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { @@ -253,7 +253,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { } return cols; - }, [DETAILS_URI, isFleetEnabled, sendRequest]); + }, [getHref, isFleetEnabled, sendRequest]); const createAgentConfigButton = useMemo( () => ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx index 7b0641e66fd4..0fdba54a0414 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx @@ -5,13 +5,14 @@ */ import React from 'react'; import { HashRouter as Router, Route, Switch } from 'react-router-dom'; +import { PAGE_ROUTING_PATHS } from '../../constants'; import { DataStreamListPage } from './list_page'; export const DataStreamApp: React.FunctionComponent = () => { return ( - + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx index cff138c6a16c..09873a3cdaa8 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { DataStream } from '../../../types'; import { WithHeaderLayout } from '../../../layouts'; -import { useGetDataStreams, useStartDeps, usePagination } from '../../../hooks'; +import { useGetDataStreams, useStartDeps, usePagination, useBreadcrumbs } from '../../../hooks'; import { PackageIcon } from '../../../components/package_icon'; import { DataStreamRowActions } from './components/data_stream_row_actions'; @@ -55,6 +55,8 @@ const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => ( ); export const DataStreamListPage: React.FunctionComponent<{}> = () => { + useBreadcrumbs('data_streams'); + const { data: { fieldFormats }, } = useStartDeps(); @@ -239,7 +241,12 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { sorting={true} search={{ toolsRight: [ - sendRequest()}> + sendRequest()} + > } - href={url} + href={getHref('integration_details', { pkgkey: `${name}-${urlVersion}` })} /> ); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx index d4ed3624a6e6..436163bafcfe 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx @@ -3,32 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { generatePath } from 'react-router-dom'; import { useCore } from '../../../hooks/use_core'; import { PLUGIN_ID } from '../../../constants'; import { epmRouteService } from '../../../services'; -import { DetailViewPanelName } from '../../../types'; -import { BASE_PATH, EPM_PATH, EPM_DETAIL_VIEW_PATH } from '../../../constants'; - -// TODO: get this from server/packages/handlers.ts (move elsewhere?) -// seems like part of the name@version change -interface DetailParams { - name: string; - version: string; - panel?: DetailViewPanelName; - withAppRoot?: boolean; -} const removeRelativePath = (relativePath: string): string => new URL(relativePath, 'http://example.com').pathname; export function useLinks() { const { http } = useCore(); - function appRoot(path: string) { - // include '#' because we're using HashRouter - return http.basePath.prepend(BASE_PATH + '#' + path); - } - return { toAssets: (path: string) => http.basePath.prepend( @@ -49,13 +32,5 @@ export function useLinks() { const filePath = `${epmRouteService.getInfoPath(pkgkey)}/${imagePath}`; return http.basePath.prepend(filePath); }, - toListView: () => appRoot(EPM_PATH), - toDetailView: ({ name, version, panel, withAppRoot = true }: DetailParams) => { - // panel is optional, but `generatePath` won't accept `path: undefined` - // so use this to pass `{ pkgkey }` or `{ pkgkey, panel }` - const params = Object.assign({ pkgkey: `${name}-${version}` }, panel ? { panel } : {}); - const path = generatePath(EPM_DETAIL_VIEW_PATH, params); - return withAppRoot ? appRoot(path) : path; - }, }; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx index 244a9a2c7426..36b81e786b93 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx @@ -6,12 +6,12 @@ import createContainer from 'constate'; import React, { useCallback, useState } from 'react'; +import { useHistory } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { NotificationsStart } from 'src/core/public'; import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; import { PackageInfo } from '../../../types'; -import { sendInstallPackage, sendRemovePackage } from '../../../hooks'; -import { useLinks } from '.'; +import { sendInstallPackage, sendRemovePackage, useLink } from '../../../hooks'; import { InstallStatus } from '../../../types'; interface PackagesInstall { @@ -29,7 +29,8 @@ type InstallPackageProps = Pick & { type SetPackageInstallStatusProps = Pick & PackageInstallItem; function usePackageInstall({ notifications }: { notifications: NotificationsStart }) { - const { toDetailView } = useLinks(); + const history = useHistory(); + const { getPath } = useLink(); const [packages, setPackage] = useState({}); const setPackageInstallStatus = useCallback( @@ -88,12 +89,11 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar } else { setPackageInstallStatus({ name, status: InstallStatus.installed, version }); if (fromUpdate) { - const settingsUrl = toDetailView({ - name, - version, + const settingsPath = getPath('integration_details', { + pkgkey: `${name}-${version}`, panel: 'settings', }); - window.location.href = settingsUrl; + history.push(settingsPath); } notifications.toasts.addSuccess({ title: toMountPoint( @@ -113,7 +113,7 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar }); } }, - [getPackageInstallStatus, notifications.toasts, setPackageInstallStatus, toDetailView] + [getPackageInstallStatus, notifications.toasts, setPackageInstallStatus, getPath, history] ); const uninstallPackage = useCallback( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx index 2c8ee7ca2fcf..ca1a8df53404 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx @@ -6,24 +6,26 @@ import React from 'react'; import { HashRouter as Router, Switch, Route } from 'react-router-dom'; -import { useConfig } from '../../hooks'; +import { PAGE_ROUTING_PATHS } from '../../constants'; +import { useConfig, useBreadcrumbs } from '../../hooks'; import { CreateDatasourcePage } from '../agent_config/create_datasource_page'; import { EPMHomePage } from './screens/home'; import { Detail } from './screens/detail'; export const EPMApp: React.FunctionComponent = () => { + useBreadcrumbs('integrations'); const { epm } = useConfig(); return epm.enabled ? ( - + - + - + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx index c82b7ed2297a..7459c943fa83 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx @@ -7,29 +7,22 @@ import React, { Fragment } from 'react'; import { EuiTitle } from '@elastic/eui'; import { Redirect } from 'react-router-dom'; -import { useLinks, useGetPackageInstallStatus } from '../../hooks'; +import { useGetPackageInstallStatus } from '../../hooks'; import { InstallStatus } from '../../../../types'; +import { useLink } from '../../../../hooks'; interface DataSourcesPanelProps { name: string; version: string; } export const DataSourcesPanel = ({ name, version }: DataSourcesPanelProps) => { - const { toDetailView } = useLinks(); + const { getPath } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); // if they arrive at this page and the package is not installed, send them to overview // this happens if they arrive with a direct url or they uninstall while on this tab if (packageInstallStatus.status !== InstallStatus.installed) - return ( - - ); + return ; return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx index cf51296d468a..5c2d1373d0b0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx @@ -9,11 +9,9 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiTitle, IconType, EuiButton } from '@elastic/eui'; import { PackageInfo } from '../../../../types'; -import { EPM_PATH } from '../../../../constants'; import { useCapabilities, useLink } from '../../../../hooks'; import { IconPanel } from '../../components/icon_panel'; import { NavButtonBack } from '../../components/nav_button_back'; -import { useLinks } from '../../hooks'; import { CenterColumn, LeftColumn, RightColumn } from './layout'; import { UpdateIcon } from '../../components/icons'; @@ -36,14 +34,13 @@ export function Header(props: HeaderProps) { installedVersion = props.savedObject.attributes.version; } const hasWriteCapabilites = useCapabilities().write; - const { toListView } = useLinks(); - const ADD_DATASOURCE_URI = useLink(`${EPM_PATH}/${name}-${version}/add-datasource`); + const { getHref } = useLink(); const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; return ( & Pick; export function DetailLayout(props: LayoutProps) { - const { name: packageName, version, icons, restrictWidth } = props; + const { name: packageName, version, icons, restrictWidth, title: packageTitle } = props; const iconType = usePackageIconType({ packageName, version, icons }); + useBreadcrumbs('integration_details', { pkgTitle: packageTitle }); return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx index aa63cf2ba175..65a437269ec6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx @@ -8,7 +8,8 @@ import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiButtonEmptyProps } from '@elastic/eui'; import { PackageInfo, entries, DetailViewPanelName, InstallStatus } from '../../../../types'; -import { useLinks, useGetPackageInstallStatus } from '../../hooks'; +import { useLink } from '../../../../hooks'; +import { useGetPackageInstallStatus } from '../../hooks'; export type NavLinkProps = Pick & { active: DetailViewPanelName; @@ -27,7 +28,7 @@ const PanelDisplayNames: Record = { }; export function SideNavLinks({ name, version, active }: NavLinkProps) { - const { toDetailView } = useLinks(); + const { getHref } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); @@ -35,7 +36,7 @@ export function SideNavLinks({ name, version, active }: NavLinkProps) { {entries(PanelDisplayNames).map(([panel, display]) => { const Link = styled(EuiButtonEmpty).attrs({ - href: toDetailView({ name, version, panel }), + href: getHref('integration_details', { pkgkey: `${name}-${version}`, panel }), })` font-weight: ${p => active === panel diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx index 983a322de108..84ad3593a5bf 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx @@ -8,11 +8,8 @@ import React, { useState } from 'react'; import { useRouteMatch, Switch, Route } from 'react-router-dom'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import { i18n } from '@kbn/i18n'; -import { - EPM_LIST_ALL_PACKAGES_PATH, - EPM_LIST_INSTALLED_PACKAGES_PATH, -} from '../../../../constants'; -import { useLink, useGetCategories, useGetPackages } from '../../../../hooks'; +import { PAGE_ROUTING_PATHS } from '../../../../constants'; +import { useLink, useGetCategories, useGetPackages, useBreadcrumbs } from '../../../../hooks'; import { WithHeaderLayout } from '../../../../layouts'; import { CategorySummaryItem } from '../../../../types'; import { PackageListGrid } from '../../components/package_list_grid'; @@ -23,9 +20,7 @@ export function EPMHomePage() { const { params: { tabId }, } = useRouteMatch<{ tabId?: string }>(); - - const ALL_PACKAGES_URI = useLink(EPM_LIST_ALL_PACKAGES_PATH); - const INSTALLED_PACKAGES_URI = useLink(EPM_LIST_INSTALLED_PACKAGES_PATH); + const { getHref } = useLink(); return ( - + - + @@ -65,6 +60,7 @@ export function EPMHomePage() { } function InstalledPackages() { + useBreadcrumbs('integrations_installed'); const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages(); const [selectedCategory, setSelectedCategory] = useState(''); @@ -117,6 +113,7 @@ function InstalledPackages() { } function AvailablePackages() { + useBreadcrumbs('integrations_all'); const [selectedCategory, setSelectedCategory] = useState(''); const { data: categoryPackagesRes, isLoading: isLoadingPackages } = useGetPackages({ category: selectedCategory, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx index 6a1e6dc22690..03f1a67fe95a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx @@ -14,7 +14,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Agent, AgentConfig } from '../../../../types'; -import { AGENT_CONFIG_DETAILS_PATH } from '../../../../constants'; import { useLink } from '../../../../hooks'; import { AgentHealth } from '../../components'; @@ -22,7 +21,7 @@ export const AgentDetailsContent: React.FunctionComponent<{ agent: Agent; agentConfig?: AgentConfig; }> = memo(({ agent, agentConfig }) => { - const agentConfigUrl = useLink(AGENT_CONFIG_DETAILS_PATH); + const { getHref } = useLink(); return ( {[ @@ -53,7 +52,7 @@ export const AgentDetailsContent: React.FunctionComponent<{ defaultMessage: 'Agent configuration', }), description: agentConfig ? ( - + {agentConfig.name || agent.config_id} ) : ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx index aa46f7cf976c..2ebc495d5dda 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx @@ -19,16 +19,13 @@ import { import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { AgentRefreshContext } from './hooks'; -import { - FLEET_AGENTS_PATH, - FLEET_AGENT_DETAIL_PATH, - AGENT_CONFIG_DETAILS_PATH, -} from '../../../constants'; +import { Agent, AgentConfig } from '../../../types'; +import { PAGE_ROUTING_PATHS } from '../../../constants'; import { Loading, Error } from '../../../components'; -import { useGetOneAgent, useGetOneAgentConfig, useLink } from '../../../hooks'; +import { useGetOneAgent, useGetOneAgentConfig, useLink, useBreadcrumbs } from '../../../hooks'; import { WithHeaderLayout } from '../../../layouts'; import { AgentHealth } from '../components'; +import { AgentRefreshContext } from './hooks'; import { AgentEventsTable, AgentDetailsActionMenu, AgentDetailsContent } from './components'; const Divider = styled.div` @@ -41,6 +38,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { const { params: { agentId, tabId = '' }, } = useRouteMatch<{ agentId: string; tabId?: string }>(); + const { getHref } = useLink(); const { isLoading, isInitialRequest, @@ -56,16 +54,16 @@ export const AgentDetailsPage: React.FunctionComponent = () => { sendRequest: sendAgentConfigRequest, } = useGetOneAgentConfig(agentData?.item?.config_id); - const agentListUrl = useLink(FLEET_AGENTS_PATH); - const agentActivityTabUrl = useLink(`${FLEET_AGENT_DETAIL_PATH}${agentId}/activity`); - const agentDetailsTabUrl = useLink(`${FLEET_AGENT_DETAIL_PATH}${agentId}/details`); - const agentConfigUrl = useLink(AGENT_CONFIG_DETAILS_PATH); - const headerLeftContent = useMemo( () => ( - + { ), - [agentData, agentId, agentListUrl] + [agentData, agentId, getHref] ); const headerRightContent = useMemo( @@ -114,7 +112,9 @@ export const AgentDetailsPage: React.FunctionComponent = () => { content: isAgentConfigLoading ? ( ) : agentConfigData?.item ? ( - + {agentConfigData.item.name || agentData.item.config_id} ) : ( @@ -143,7 +143,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { ) : ( undefined ), - [agentConfigData, agentConfigUrl, agentData, isAgentConfigLoading] + [agentConfigData, agentData, getHref, isAgentConfigLoading] ); const headerTabs = useMemo(() => { @@ -153,7 +153,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.agentDetails.subTabs.activityLogTab', { defaultMessage: 'Activity log', }), - href: agentActivityTabUrl, + href: getHref('fleet_agent_details', { agentId, tabId: 'activity' }), isSelected: !tabId || tabId === 'activity', }, { @@ -161,11 +161,11 @@ export const AgentDetailsPage: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.agentDetails.subTabs.detailsTab', { defaultMessage: 'Agent details', }), - href: agentDetailsTabUrl, + href: getHref('fleet_agent_details', { agentId, tabId: 'details' }), isSelected: tabId === 'details', }, ]; - }, [agentActivityTabUrl, agentDetailsTabUrl, tabId]); + }, [getHref, agentId, tabId]); return ( { error={error} /> ) : agentData && agentData.item ? ( - - { - return ( - - ); - }} - /> - { - return ; - }} - /> - + ) : ( { error={i18n.translate( 'xpack.ingestManager.agentDetails.agentNotFoundErrorDescription', { - defaultMessage: 'Cannot found agent ID {agentId}', + defaultMessage: 'Cannot find agent ID {agentId}', values: { agentId, }, @@ -233,3 +218,32 @@ export const AgentDetailsPage: React.FunctionComponent = () => { ); }; + +const AgentDetailsPageContent: React.FunctionComponent<{ + agent: Agent; + agentConfig?: AgentConfig; +}> = ({ agent, agentConfig }) => { + useBreadcrumbs('fleet_agent_details', { + agentHost: + typeof agent.local_metadata.host === 'object' && + typeof agent.local_metadata.host.hostname === 'string' + ? agent.local_metadata.host.hostname + : '-', + }); + return ( + + { + return ; + }} + /> + { + return ; + }} + /> + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 84056df2aca3..56cc0028f0cf 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -34,17 +34,14 @@ import { useGetAgents, useUrlParams, useLink, + useBreadcrumbs, } from '../../../hooks'; -import { ConnectedLink, AgentReassignConfigFlyout } from '../components'; +import { AgentReassignConfigFlyout } from '../components'; import { SearchBar } from '../../../components/search_bar'; import { AgentHealth } from '../components/agent_health'; import { AgentUnenrollProvider } from '../components/agent_unenroll_provider'; import { AgentStatusKueryHelper } from '../../../services'; -import { - FLEET_AGENT_DETAIL_PATH, - AGENT_CONFIG_DETAILS_PATH, - AGENT_SAVED_OBJECT_TYPE, -} from '../../../constants'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ overflow: 'hidden', @@ -77,8 +74,8 @@ const statusFilters = [ const RowActions = React.memo<{ agent: Agent; onReassignClick: () => void; refresh: () => void }>( ({ agent, refresh, onReassignClick }) => { + const { getHref } = useLink(); const hasWriteCapabilites = useCapabilities().write; - const DETAILS_URI = useLink(FLEET_AGENT_DETAIL_PATH); const [isOpen, setIsOpen] = useState(false); const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); @@ -101,7 +98,11 @@ const RowActions = React.memo<{ agent: Agent; onReassignClick: () => void; refre > + = () => { + useBreadcrumbs('fleet_agent_list'); + const { getHref } = useLink(); const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || ''; const hasWriteCapabilites = useCapabilities().write; + // Agent data states const [showInactive, setShowInactive] = useState(false); @@ -241,8 +245,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : []; const { isLoading: isAgentConfigsLoading } = agentConfigsRequest; - const CONFIG_DETAILS_URI = useLink(AGENT_CONFIG_DETAILS_PATH); - const columns = [ { field: 'local_metadata.host.hostname', @@ -250,9 +252,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Host', }), render: (host: string, agent: Agent) => ( - + {safeMetadata(host)} - + ), }, { @@ -274,7 +276,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx index ff7c2f705e7b..43173124d6ba 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx @@ -23,10 +23,14 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AgentConfig } from '../../../../types'; import { EnrollmentStepAgentConfig } from './config_selection'; -import { useGetOneEnrollmentAPIKey, useCore, useGetSettings, useLink } from '../../../../hooks'; +import { + useGetOneEnrollmentAPIKey, + useCore, + useGetSettings, + useLink, + useFleetStatus, +} from '../../../../hooks'; import { ManualInstructions } from '../../../../components/enrollment_instructions'; -import { FLEET_PATH } from '../../../../constants'; -import { useFleetStatus } from '../../../../hooks/use_fleet_status'; interface Props { onClose: () => void; @@ -37,9 +41,9 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ onClose, agentConfigs = [], }) => { + const { getHref } = useLink(); const core = useCore(); const fleetStatus = useFleetStatus(); - const fleetLink = useLink(FLEET_PATH); const [selectedAPIKeyId, setSelectedAPIKeyId] = useState(); @@ -120,7 +124,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ defaultMessage="Fleet needs to be set up before agents can be enrolled. {link}" values={{ link: ( - + = ({ children }) => { + const { getHref } = useLink(); const hasWriteCapabilites = useCapabilities().write; const agentStatusRequest = useGetAgentStatus(undefined, { pollIntervalMs: REFRESH_INTERVAL_MS, @@ -163,8 +164,8 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { defaultMessage="Agents" /> ), - isSelected: routeMatch.path === FLEET_AGENTS_PATH, - href: useLink(FLEET_AGENTS_PATH), + isSelected: routeMatch.path === PAGE_ROUTING_PATHS.fleet_agent_list, + href: getHref('fleet_agent_list'), }, { name: ( @@ -173,8 +174,8 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { defaultMessage="Enrollment tokens" /> ), - isSelected: routeMatch.path === FLEET_ENROLLMENT_TOKENS_PATH, - href: useLink(FLEET_ENROLLMENT_TOKENS_PATH), + isSelected: routeMatch.path === PAGE_ROUTING_PATHS.fleet_enrollment_tokens, + href: getHref('fleet_enrollment_tokens'), }, ] as unknown) as EuiTabProps[] } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/child_routes.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/child_routes.tsx deleted file mode 100644 index 8af0e0a5cbc2..000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/child_routes.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { Route, Switch } from 'react-router-dom'; - -interface RouteConfig { - path: string; - component: React.ComponentType; - routes?: RouteConfig[]; -} - -export const ChildRoutes: React.FunctionComponent<{ - routes?: RouteConfig[]; - useSwitch?: boolean; - [other: string]: any; -}> = ({ routes, useSwitch = true, ...rest }) => { - if (!routes) { - return null; - } - const Parent = useSwitch ? Switch : React.Fragment; - return ( - - {routes.map(route => ( - { - const Component = route.component; - return ; - }} - /> - ))} - - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/connected_link.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/connected_link.tsx deleted file mode 100644 index 489ee85ffe28..000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/connected_link.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { EuiLink } from '@elastic/eui'; -import { Link, withRouter } from 'react-router-dom'; - -export function ConnectedLinkComponent({ - location, - path, - query, - disabled, - children, - ...props -}: { - location: any; - path: string; - disabled: boolean; - query: any; - [key: string]: any; -}) { - if (disabled) { - return ; - } - - // Shorthand for pathname - const pathname = path || _.get(props.to, 'pathname') || location.pathname; - - return ( - - ); -} - -export const ConnectedLink = withRouter(ConnectedLinkComponent); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx index c11e3a49c769..add495ce0c19 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx @@ -20,6 +20,7 @@ import { import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../../constants'; import { + useBreadcrumbs, usePagination, useGetEnrollmentAPIKeys, useGetAgentConfigs, @@ -125,6 +126,7 @@ const DeleteButton: React.FunctionComponent<{ apiKey: EnrollmentAPIKey; refresh: }; export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { + useBreadcrumbs('fleet_enrollment_tokens'); const [flyoutOpen, setFlyoutOpen] = useState(false); const [search, setSearch] = useState(''); const { pagination, setPagination, pageSizeOptions } = usePagination(); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx index c820a9b867b6..9bb77ca44b84 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx @@ -5,17 +5,18 @@ */ import React from 'react'; import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom'; +import { PAGE_ROUTING_PATHS } from '../../constants'; import { Loading } from '../../components'; -import { useConfig, useCore } from '../../hooks'; +import { useConfig, useCore, useFleetStatus, useBreadcrumbs } from '../../hooks'; import { AgentListPage } from './agent_list_page'; import { SetupPage } from './setup_page'; import { AgentDetailsPage } from './agent_details_page'; import { NoAccessPage } from './error_pages/no_access'; import { EnrollmentTokenListPage } from './enrollment_token_list_page'; import { ListLayout } from './components/list_layout'; -import { useFleetStatus } from '../../hooks/use_fleet_status'; export const FleetApp: React.FunctionComponent = () => { + useBreadcrumbs('fleet'); const core = useCore(); const { fleet } = useConfig(); @@ -41,16 +42,20 @@ export const FleetApp: React.FunctionComponent = () => { return ( - } /> - + } + /> + - + - + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx index 0f6d3c5b55ce..6e61a55466e8 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx @@ -16,10 +16,10 @@ import { import { OverviewPanel } from './overview_panel'; import { OverviewStats } from './overview_stats'; import { useLink, useGetAgentStatus } from '../../../hooks'; -import { FLEET_PATH } from '../../../constants'; import { Loading } from '../../fleet/components'; export const OverviewAgentSection = () => { + const { getHref } = useLink(); const agentStatusRequest = useGetAgentStatus({}); return ( @@ -34,7 +34,7 @@ export const OverviewAgentSection = () => { /> - + = ({ agentConfigs, }) => { + const { getHref } = useLink(); const datasourcesRequest = useGetDatasources({ page: 1, perPage: 10000, @@ -40,7 +40,7 @@ export const OverviewConfigurationSection: React.FC<{ agentConfigs: AgentConfig[ /> - + { + const { getHref } = useLink(); const datastreamRequest = useGetDataStreams(); const { data: { fieldFormats }, @@ -55,7 +55,7 @@ export const OverviewDatastreamSection: React.FC = () => { /> - + { + const { getHref } = useLink(); const packagesRequest = useGetPackages(); const res = packagesRequest.data?.response; const total = res?.length ?? 0; @@ -40,7 +40,7 @@ export const OverviewIntegrationSection: React.FC = () => { /> - + { + useBreadcrumbs('overview'); + // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts index 2c6ed9d81744..fd4e08f61949 100644 --- a/x-pack/plugins/ingest_manager/public/plugin.ts +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -64,8 +64,13 @@ export class IngestManagerPlugin IngestManagerStartDeps, IngestManagerStart ]; - const { renderApp } = await import('./applications/ingest_manager'); - return renderApp(coreStart, params, deps, startDeps, config); + const { renderApp, teardownIngestManager } = await import('./applications/ingest_manager'); + const unmount = renderApp(coreStart, params, deps, startDeps, config); + + return () => { + unmount(); + teardownIngestManager(coreStart); + }; }, }); } diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts index 80a33c26d86d..666d46f03078 100644 --- a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts @@ -136,12 +136,13 @@ export const getListHandler: RequestHandler = async (context, request, response) dashboards: enhancedDashboards, }; } + return { index: indexName, dataset: datasetBuckets.length ? datasetBuckets[0].key : '', namespace: namespaceBuckets.length ? namespaceBuckets[0].key : '', type: typeBuckets.length ? typeBuckets[0].key : '', - package: pkg, + package: pkgSavedObject.length ? pkg : '', package_version: packageMetadata[pkg] ? packageMetadata[pkg].version : '', last_activity: lastActivity, size_in_bytes: indexStats[indexName] ? indexStats[indexName].total.store.size_in_bytes : 0,