- {i18n.translate(
- 'xpack.apm.settings.agentConf.configTable.emptyPromptTitle',
- { defaultMessage: 'No configurations found.' }
- )}
-
- }
- body={
- <>
-
- {i18n.translate(
- 'xpack.apm.settings.agentConf.configTable.emptyPromptText',
- {
- defaultMessage:
- "Let's change that! You can fine-tune agent configuration directly from Kibana without having to redeploy. Get started by creating your first configuration."
- }
- )}
-
- >
- }
- actions={
- setIsFlyoutOpen(true)}>
- {i18n.translate(
- 'xpack.apm.settings.agentConf.configTable.createConfigButtonLabel',
- { defaultMessage: 'Create configuration' }
- )}
-
- }
- />
- );
-
- const failurePrompt = (
-
-
- {i18n.translate(
- 'xpack.apm.settings.agentConf.configTable.configTable.failurePromptText',
- {
- defaultMessage:
- 'The list of agent configurations could not be fetched. Your user may not have the sufficient permissions.'
- }
- )}
-
- >
- }
- />
- );
-
- if (status === 'failure') {
- return failurePrompt;
- }
-
- if (status === 'success' && isEmpty(data)) {
- return emptyStatePrompt;
- }
-
- return (
- }
- columns={columns}
- items={data}
- initialSortField="service.name"
- initialSortDirection="asc"
- initialPageSize={50}
- />
- );
-}
diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx
new file mode 100644
index 0000000000000..267aaddc93f76
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx
@@ -0,0 +1,119 @@
+/*
+ * 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, { useState } from 'react';
+import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
+import { NotificationsStart } from 'kibana/public';
+import { i18n } from '@kbn/i18n';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { AgentConfigurationListAPIResponse } from '../../../../../../../../../plugins/apm/server/lib/settings/agent_configuration/list_configurations';
+import { getOptionLabel } from '../../../../../../../../../plugins/apm/common/agent_configuration/all_option';
+import { callApmApi } from '../../../../../services/rest/createCallApmApi';
+import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext';
+
+type Config = AgentConfigurationListAPIResponse[0];
+
+interface Props {
+ config: Config;
+ onCancel: () => void;
+ onConfirm: () => void;
+}
+
+export function ConfirmDeleteModal({ config, onCancel, onConfirm }: Props) {
+ const [isDeleting, setIsDeleting] = useState(false);
+ const { toasts } = useApmPluginContext().core.notifications;
+
+ return (
+
+ {
+ setIsDeleting(true);
+ await deleteConfig(config, toasts);
+ setIsDeleting(false);
+ onConfirm();
+ }}
+ cancelButtonText={i18n.translate(
+ 'xpack.apm.agentConfig.deleteModal.cancel',
+ { defaultMessage: `Cancel` }
+ )}
+ confirmButtonText={i18n.translate(
+ 'xpack.apm.agentConfig.deleteModal.confirm',
+ { defaultMessage: `Delete` }
+ )}
+ confirmButtonDisabled={isDeleting}
+ buttonColor="danger"
+ defaultFocusedButton="confirm"
+ >
+
+ {i18n.translate('xpack.apm.agentConfig.deleteModal.text', {
+ defaultMessage: `You are about to delete the configuration for service "{serviceName}" and environment "{environment}".`,
+ values: {
+ serviceName: getOptionLabel(config.service.name),
+ environment: getOptionLabel(config.service.environment)
+ }
+ })}
+
+
+
+ );
+}
+
+async function deleteConfig(
+ config: Config,
+ toasts: NotificationsStart['toasts']
+) {
+ try {
+ await callApmApi({
+ pathname: '/api/apm/settings/agent-configuration',
+ method: 'DELETE',
+ params: {
+ body: {
+ service: {
+ name: config.service.name,
+ environment: config.service.environment
+ }
+ }
+ }
+ });
+
+ toasts.addSuccess({
+ title: i18n.translate(
+ 'xpack.apm.agentConfig.deleteSection.deleteConfigSucceededTitle',
+ { defaultMessage: 'Configuration was deleted' }
+ ),
+ text: i18n.translate(
+ 'xpack.apm.agentConfig.deleteSection.deleteConfigSucceededText',
+ {
+ defaultMessage:
+ 'You have successfully deleted a configuration for "{serviceName}". It will take some time to propagate to the agents.',
+ values: { serviceName: getOptionLabel(config.service.name) }
+ }
+ )
+ });
+ } catch (error) {
+ toasts.addDanger({
+ title: i18n.translate(
+ 'xpack.apm.agentConfig.deleteSection.deleteConfigFailedTitle',
+ { defaultMessage: 'Configuration could not be deleted' }
+ ),
+ text: i18n.translate(
+ 'xpack.apm.agentConfig.deleteSection.deleteConfigFailedText',
+ {
+ defaultMessage:
+ 'Something went wrong when deleting a configuration for "{serviceName}". Error: "{errorMessage}"',
+ values: {
+ serviceName: getOptionLabel(config.service.name),
+ errorMessage: error.message
+ }
+ }
+ )
+ });
+ }
+}
diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx
new file mode 100644
index 0000000000000..6d5f65121d8fd
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx
@@ -0,0 +1,221 @@
+/*
+ * 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, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiEmptyPrompt,
+ EuiButton,
+ EuiButtonEmpty,
+ EuiHealth,
+ EuiToolTip,
+ EuiButtonIcon
+} from '@elastic/eui';
+import { isEmpty } from 'lodash';
+import theme from '@elastic/eui/dist/eui_theme_light.json';
+import { FETCH_STATUS } from '../../../../../hooks/useFetcher';
+import { ITableColumn, ManagedTable } from '../../../../shared/ManagedTable';
+import { LoadingStatePrompt } from '../../../../shared/LoadingStatePrompt';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { AgentConfigurationListAPIResponse } from '../../../../../../../../../plugins/apm/server/lib/settings/agent_configuration/list_configurations';
+import { TimestampTooltip } from '../../../../shared/TimestampTooltip';
+import { px, units } from '../../../../../style/variables';
+import { getOptionLabel } from '../../../../../../../../../plugins/apm/common/agent_configuration/all_option';
+import {
+ createAgentConfigurationHref,
+ editAgentConfigurationHref
+} from '../../../../shared/Links/apm/agentConfigurationLinks';
+import { ConfirmDeleteModal } from './ConfirmDeleteModal';
+
+type Config = AgentConfigurationListAPIResponse[0];
+
+export function AgentConfigurationList({
+ status,
+ data,
+ refetch
+}: {
+ status: FETCH_STATUS;
+ data: Config[];
+ refetch: () => void;
+}) {
+ const [configToBeDeleted, setConfigToBeDeleted] = useState(
+ null
+ );
+
+ const emptyStatePrompt = (
+
+ {i18n.translate(
+ 'xpack.apm.agentConfig.configTable.emptyPromptTitle',
+ { defaultMessage: 'No configurations found.' }
+ )}
+
+ }
+ body={
+ <>
+
+ {i18n.translate(
+ 'xpack.apm.agentConfig.configTable.emptyPromptText',
+ {
+ defaultMessage:
+ "Let's change that! You can fine-tune agent configuration directly from Kibana without having to redeploy. Get started by creating your first configuration."
+ }
+ )}
+
+ >
+ }
+ actions={
+
+ {i18n.translate(
+ 'xpack.apm.agentConfig.configTable.createConfigButtonLabel',
+ { defaultMessage: 'Create configuration' }
+ )}
+
+ }
+ />
+ );
+
+ const failurePrompt = (
+
+
+ {i18n.translate(
+ 'xpack.apm.agentConfig.configTable.configTable.failurePromptText',
+ {
+ defaultMessage:
+ 'The list of agent configurations could not be fetched. Your user may not have the sufficient permissions.'
+ }
+ )}
+
+ >
+ }
+ />
+ );
+
+ if (status === FETCH_STATUS.FAILURE) {
+ return failurePrompt;
+ }
+
+ if (status === FETCH_STATUS.SUCCESS && isEmpty(data)) {
+ return emptyStatePrompt;
+ }
+
+ const columns: Array> = [
+ {
+ field: 'applied_by_agent',
+ align: 'center',
+ width: px(units.double),
+ name: '',
+ sortable: true,
+ render: (isApplied: boolean) => (
+
+
+
+ )
+ },
+ {
+ field: 'service.name',
+ name: i18n.translate(
+ 'xpack.apm.agentConfig.configTable.serviceNameColumnLabel',
+ { defaultMessage: 'Service name' }
+ ),
+ sortable: true,
+ render: (_, config: Config) => (
+
+ {getOptionLabel(config.service.name)}
+
+ )
+ },
+ {
+ field: 'service.environment',
+ name: i18n.translate(
+ 'xpack.apm.agentConfig.configTable.environmentColumnLabel',
+ { defaultMessage: 'Service environment' }
+ ),
+ sortable: true,
+ render: (environment: string) => getOptionLabel(environment)
+ },
+ {
+ align: 'right',
+ field: '@timestamp',
+ name: i18n.translate(
+ 'xpack.apm.agentConfig.configTable.lastUpdatedColumnLabel',
+ { defaultMessage: 'Last updated' }
+ ),
+ sortable: true,
+ render: (value: number) => (
+
+ )
+ },
+ {
+ width: px(units.double),
+ name: '',
+ render: (config: Config) => (
+
+ )
+ },
+ {
+ width: px(units.double),
+ name: '',
+ render: (config: Config) => (
+ setConfigToBeDeleted(config)}
+ />
+ )
+ }
+ ];
+
+ return (
+ <>
+ {configToBeDeleted && (
+ setConfigToBeDeleted(null)}
+ onConfirm={() => {
+ setConfigToBeDeleted(null);
+ refetch();
+ }}
+ />
+ )}
+
+ }
+ columns={columns}
+ items={data}
+ initialSortField="service.name"
+ initialSortDirection="asc"
+ initialPageSize={20}
+ />
+ >
+ );
+}
diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx
index 35cc68547d337..8171e339adc82 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useState } from 'react';
+import React from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiTitle,
@@ -16,97 +16,59 @@ import {
} from '@elastic/eui';
import { isEmpty } from 'lodash';
import { useFetcher } from '../../../../hooks/useFetcher';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { AgentConfigurationListAPIResponse } from '../../../../../../../../plugins/apm/server/lib/settings/agent_configuration/list_configurations';
-import { AgentConfigurationList } from './AgentConfigurationList';
+import { AgentConfigurationList } from './List';
import { useTrackPageview } from '../../../../../../../../plugins/observability/public';
-import { AddEditFlyout } from './AddEditFlyout';
-
-export type Config = AgentConfigurationListAPIResponse[0];
+import { createAgentConfigurationHref } from '../../../shared/Links/apm/agentConfigurationLinks';
export function AgentConfigurations() {
- const { data = [], status, refetch } = useFetcher(
+ const { refetch, data = [], status } = useFetcher(
callApmApi =>
- callApmApi({ pathname: `/api/apm/settings/agent-configuration` }),
+ callApmApi({ pathname: '/api/apm/settings/agent-configuration' }),
[],
{ preservePreviousData: false }
);
- const [selectedConfig, setSelectedConfig] = useState(null);
- const [isFlyoutOpen, setIsFlyoutOpen] = useState(false);
useTrackPageview({ app: 'apm', path: 'agent_configuration' });
useTrackPageview({ app: 'apm', path: 'agent_configuration', delay: 15000 });
const hasConfigurations = !isEmpty(data);
- const onClose = () => {
- setSelectedConfig(null);
- setIsFlyoutOpen(false);
- };
-
return (
<>
- {isFlyoutOpen && (
- {
- onClose();
- refetch();
- }}
- onDeleted={() => {
- onClose();
- refetch();
- }}
- />
- )}
-
{i18n.translate(
- 'xpack.apm.settings.agentConf.configurationsPanelTitle',
+ 'xpack.apm.agentConfig.configurationsPanelTitle',
{ defaultMessage: 'Agent remote configuration' }
)}
- {hasConfigurations ? (
- setIsFlyoutOpen(true)} />
- ) : null}
+ {hasConfigurations ? : null}
-
+
>
);
}
-function CreateConfigurationButton({ onClick }: { onClick: () => void }) {
+function CreateConfigurationButton() {
+ const href = createAgentConfigurationHref();
return (
-
- {i18n.translate(
- 'xpack.apm.settings.agentConf.createConfigButtonLabel',
- { defaultMessage: 'Create configuration' }
- )}
+
+ {i18n.translate('xpack.apm.agentConfig.createConfigButtonLabel', {
+ defaultMessage: 'Create configuration'
+ })}
diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx
index eef386731c5c3..f33bb17decd4e 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx
@@ -39,23 +39,20 @@ export const Settings: React.FC = props => {
id: 0,
items: [
{
- name: i18n.translate(
- 'xpack.apm.settings.agentConfiguration',
- {
- defaultMessage: 'Agent Configuration'
- }
- ),
+ name: i18n.translate('xpack.apm.settings.agentConfig', {
+ defaultMessage: 'Agent Configuration'
+ }),
id: '1',
- // @ts-ignore
href: getAPMHref('/settings/agent-configuration', search),
- isSelected: pathname === '/settings/agent-configuration'
+ isSelected: pathname.startsWith(
+ '/settings/agent-configuration'
+ )
},
{
name: i18n.translate('xpack.apm.settings.indices', {
defaultMessage: 'Indices'
}),
id: '2',
- // @ts-ignore
href: getAPMHref('/settings/apm-indices', search),
isSelected: pathname === '/settings/apm-indices'
},
@@ -64,7 +61,6 @@ export const Settings: React.FC = props => {
defaultMessage: 'Customize UI'
}),
id: '3',
- // @ts-ignore
href: getAPMHref('/settings/customize-ui', search),
isSelected: pathname === '/settings/customize-ui'
}
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx
index eba59f6e3ce44..29b3fff2050c8 100644
--- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx
@@ -31,7 +31,7 @@ export const PERSISTENT_APM_PARAMS = [
export function getAPMHref(
path: string,
- currentSearch: string, // TODO: Replace with passing in URL PARAMS here
+ currentSearch: string,
query: APMQueryParams = {}
) {
const currentQuery = toQuery(currentSearch);
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx
new file mode 100644
index 0000000000000..0c747e0773a69
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getAPMHref } from './APMLink';
+import { AgentConfigurationIntake } from '../../../../../../../../plugins/apm/common/agent_configuration/configuration_types';
+import { history } from '../../../../utils/history';
+
+export function editAgentConfigurationHref(
+ configService: AgentConfigurationIntake['service']
+) {
+ const { search } = history.location;
+ return getAPMHref('/settings/agent-configuration/edit', search, {
+ // ignoring because `name` has not been added to url params. Related: https://github.com/elastic/kibana/issues/51963
+ // @ts-ignore
+ name: configService.name,
+ environment: configService.environment
+ });
+}
+
+export function createAgentConfigurationHref() {
+ const { search } = history.location;
+ return getAPMHref('/settings/agent-configuration/create', search);
+}
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx
index a8e6bc0a648af..8698978bfe6fb 100644
--- a/x-pack/legacy/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx
@@ -7,33 +7,38 @@
import React from 'react';
import { EuiSelect } from '@elastic/eui';
import { isEmpty } from 'lodash';
+import { i18n } from '@kbn/i18n';
-const NO_SELECTION = 'NO_SELECTION';
+export const NO_SELECTION = '__NO_SELECTION__';
+const DEFAULT_PLACEHOLDER = i18n.translate('xpack.apm.selectPlaceholder', {
+ defaultMessage: 'Select option:'
+});
/**
* This component addresses some cross-browser inconsistencies of `EuiSelect`
* with `hasNoInitialSelection`. It uses the `placeholder` prop to populate
* the first option as the initial, not selected option.
*/
-export const SelectWithPlaceholder: typeof EuiSelect = props => (
- {
- if (props.onChange) {
- props.onChange(
- Object.assign(e, {
+export const SelectWithPlaceholder: typeof EuiSelect = props => {
+ const placeholder = props.placeholder || DEFAULT_PLACEHOLDER;
+ return (
+ {
+ if (props.onChange) {
+ const customEvent = Object.assign(e, {
target: Object.assign(e.target, {
- value:
- e.target.value === NO_SELECTION ? undefined : e.target.value
+ value: e.target.value === NO_SELECTION ? '' : e.target.value
})
- })
- );
- }
- }}
- />
-);
+ });
+ props.onChange(customEvent);
+ }
+ }}
+ />
+ );
+};
diff --git a/x-pack/legacy/plugins/apm/public/context/LocationContext.tsx b/x-pack/legacy/plugins/apm/public/context/LocationContext.tsx
index 30e6e02c9fbc1..1e9c20494b42e 100644
--- a/x-pack/legacy/plugins/apm/public/context/LocationContext.tsx
+++ b/x-pack/legacy/plugins/apm/public/context/LocationContext.tsx
@@ -11,10 +11,8 @@ import { withRouter } from 'react-router-dom';
const initialLocation = {} as Location;
const LocationContext = createContext(initialLocation);
-const LocationProvider: React.ComponentClass<{}> = withRouter(
- ({ location, children }) => {
- return ;
- }
-);
+const LocationProvider = withRouter(({ location, children }) => {
+ return ;
+});
export { LocationContext, LocationProvider };
diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx
index c2530d6982c3b..95cebd6b2a465 100644
--- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx
+++ b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx
@@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
+/* eslint-disable no-console */
+
import React, { useContext, useEffect, useState, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { IHttpFetchError } from 'src/core/public';
@@ -20,7 +22,7 @@ export enum FETCH_STATUS {
PENDING = 'pending'
}
-interface Result {
+export interface FetcherResult {
data?: Data;
status: FETCH_STATUS;
error?: Error;
@@ -40,13 +42,15 @@ export function useFetcher(
options: {
preservePreviousData?: boolean;
} = {}
-): Result> & { refetch: () => void } {
+): FetcherResult> & { refetch: () => void } {
const { notifications } = useApmPluginContext().core;
const { preservePreviousData = true } = options;
const { setIsLoading } = useLoadingIndicator();
const { dispatchStatus } = useContext(LoadingIndicatorContext);
- const [result, setResult] = useState>>({
+ const [result, setResult] = useState<
+ FetcherResult>
+ >({
data: undefined,
status: FETCH_STATUS.PENDING
});
@@ -80,11 +84,27 @@ export function useFetcher(
data,
status: FETCH_STATUS.SUCCESS,
error: undefined
- } as Result>);
+ } as FetcherResult>);
}
} catch (e) {
- const err = e as IHttpFetchError;
+ const err = e as Error | IHttpFetchError;
+
if (!didCancel) {
+ const errorDetails =
+ 'response' in err ? (
+ <>
+ {err.response?.statusText} ({err.response?.status})
+
+ {i18n.translate('xpack.apm.fetcher.error.url', {
+ defaultMessage: `URL`
+ })}
+
+ {err.response?.url}
+ >
+ ) : (
+ err.message
+ );
+
notifications.toasts.addWarning({
title: i18n.translate('xpack.apm.fetcher.error.title', {
defaultMessage: `Error while fetching resource`
@@ -96,13 +116,8 @@ export function useFetcher(
defaultMessage: `Error`
})}
- {err.response?.statusText} ({err.response?.status})
-
- {i18n.translate('xpack.apm.fetcher.error.url', {
- defaultMessage: `URL`
- })}
-
- {err.response?.url}
+
+ {errorDetails}