mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
[APM] Add Experimental Mode for APM UI (#139553)
* experimental tab * moving apm specific settings * reverting and addin LazyField * one setting to rule them all * renaming tab * new setting option and flyout * flyout footer * refactoring * creating hook * removing utils * saving on kbn adv settings * saved object * refactoring * extracting apm experimental features * auto subs * removing auto-subscribe feature * fixing ci * removing common settings * new api to fetch labs items * renaming * renaming labs settings * fixing ci * handling exception * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * fixing ci * addressing pr comments Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
78f6244109
commit
ed6411e813
18 changed files with 482 additions and 42 deletions
|
@ -20,6 +20,10 @@ export { ComponentRegistry } from './component_registry';
|
||||||
const LazyField = React.lazy(() => import('./management_app/components/field'));
|
const LazyField = React.lazy(() => import('./management_app/components/field'));
|
||||||
export { LazyField };
|
export { LazyField };
|
||||||
|
|
||||||
|
export { toEditableConfig } from './management_app/lib/to_editable_config';
|
||||||
|
|
||||||
export function plugin(initializerContext: PluginInitializerContext) {
|
export function plugin(initializerContext: PluginInitializerContext) {
|
||||||
return new AdvancedSettingsPlugin();
|
return new AdvancedSettingsPlugin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type { FieldState } from './management_app/types';
|
||||||
|
|
|
@ -518,6 +518,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
_meta: { description: 'Non-default value of setting.' },
|
_meta: { description: 'Non-default value of setting.' },
|
||||||
},
|
},
|
||||||
|
'observability:apmLabsButton': {
|
||||||
|
type: 'boolean',
|
||||||
|
_meta: { description: 'Non-default value of setting.' },
|
||||||
|
},
|
||||||
'observability:apmProgressiveLoading': {
|
'observability:apmProgressiveLoading': {
|
||||||
type: 'keyword',
|
type: 'keyword',
|
||||||
_meta: { description: 'Non-default value of setting.' },
|
_meta: { description: 'Non-default value of setting.' },
|
||||||
|
|
|
@ -139,6 +139,7 @@ export interface UsageStats {
|
||||||
'lens:useFieldExistenceSampling': boolean;
|
'lens:useFieldExistenceSampling': boolean;
|
||||||
'metrics:allowCheckingForFailedShards': boolean;
|
'metrics:allowCheckingForFailedShards': boolean;
|
||||||
'observability:apmOperationsTab': boolean;
|
'observability:apmOperationsTab': boolean;
|
||||||
|
'observability:apmLabsButton': boolean;
|
||||||
'observability:apmProgressiveLoading': string;
|
'observability:apmProgressiveLoading': string;
|
||||||
'observability:apmServiceGroupMaxNumberOfServices': number;
|
'observability:apmServiceGroupMaxNumberOfServices': number;
|
||||||
'observability:apmServiceInventoryOptimizedSorting': boolean;
|
'observability:apmServiceInventoryOptimizedSorting': boolean;
|
||||||
|
|
|
@ -8622,6 +8622,12 @@
|
||||||
"description": "Non-default value of setting."
|
"description": "Non-default value of setting."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"observability:apmLabsButton": {
|
||||||
|
"type": "boolean",
|
||||||
|
"_meta": {
|
||||||
|
"description": "Non-default value of setting."
|
||||||
|
}
|
||||||
|
},
|
||||||
"observability:apmProgressiveLoading": {
|
"observability:apmProgressiveLoading": {
|
||||||
"type": "keyword",
|
"type": "keyword",
|
||||||
"_meta": {
|
"_meta": {
|
||||||
|
|
|
@ -19,7 +19,8 @@
|
||||||
"triggersActionsUi",
|
"triggersActionsUi",
|
||||||
"share",
|
"share",
|
||||||
"unifiedSearch",
|
"unifiedSearch",
|
||||||
"dataViews"
|
"dataViews",
|
||||||
|
"advancedSettings"
|
||||||
],
|
],
|
||||||
"optionalPlugins": [
|
"optionalPlugins": [
|
||||||
"actions",
|
"actions",
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EuiButton } from '@elastic/eui';
|
||||||
|
import { LazyField } from '@kbn/advanced-settings-plugin/public';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import {
|
||||||
|
apmLabsButton,
|
||||||
|
apmProgressiveLoading,
|
||||||
|
apmServiceGroupMaxNumberOfServices,
|
||||||
|
defaultApmServiceEnvironment,
|
||||||
|
enableComparisonByDefault,
|
||||||
|
enableInspectEsQueries,
|
||||||
|
} from '@kbn/observability-plugin/common';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||||
|
import { useApmEditableSettings } from '../../../../hooks/use_apm_editable_settings';
|
||||||
|
|
||||||
|
const apmSettingsKeys = [
|
||||||
|
enableComparisonByDefault,
|
||||||
|
defaultApmServiceEnvironment,
|
||||||
|
apmProgressiveLoading,
|
||||||
|
apmServiceGroupMaxNumberOfServices,
|
||||||
|
enableInspectEsQueries,
|
||||||
|
apmLabsButton,
|
||||||
|
];
|
||||||
|
|
||||||
|
export function GeneralSettings() {
|
||||||
|
const { docLinks, notifications } = useApmPluginContext().core;
|
||||||
|
const {
|
||||||
|
handleFieldChange,
|
||||||
|
settingsEditableConfig,
|
||||||
|
unsavedChanges,
|
||||||
|
saveAll,
|
||||||
|
isSaving,
|
||||||
|
} = useApmEditableSettings(apmSettingsKeys);
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
try {
|
||||||
|
const reloadPage = Object.keys(unsavedChanges).some((key) => {
|
||||||
|
return settingsEditableConfig[key].requiresPageReload;
|
||||||
|
});
|
||||||
|
await saveAll();
|
||||||
|
if (reloadPage) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const error = e as Error;
|
||||||
|
notifications.toasts.addDanger({
|
||||||
|
title: i18n.translate('xpack.apm.apmSettings.save.error', {
|
||||||
|
defaultMessage: 'An error occurred while saving the settings',
|
||||||
|
}),
|
||||||
|
text: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{apmSettingsKeys.map((settingKey) => {
|
||||||
|
const editableConfig = settingsEditableConfig[settingKey];
|
||||||
|
return (
|
||||||
|
<LazyField
|
||||||
|
key={settingKey}
|
||||||
|
setting={editableConfig}
|
||||||
|
handleChange={handleFieldChange}
|
||||||
|
enableSaving
|
||||||
|
docLinks={docLinks.links}
|
||||||
|
toasts={notifications.toasts}
|
||||||
|
unsavedChanges={unsavedChanges[settingKey]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<EuiButton
|
||||||
|
fill
|
||||||
|
isLoading={isSaving}
|
||||||
|
disabled={isEmpty(unsavedChanges)}
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
{i18n.translate('xpack.apm.labs.reload', {
|
||||||
|
defaultMessage: 'Reload to apply changes',
|
||||||
|
})}
|
||||||
|
</EuiButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import { CustomLinkOverview } from '../../app/settings/custom_link';
|
||||||
import { Schema } from '../../app/settings/schema';
|
import { Schema } from '../../app/settings/schema';
|
||||||
import { AnomalyDetection } from '../../app/settings/anomaly_detection';
|
import { AnomalyDetection } from '../../app/settings/anomaly_detection';
|
||||||
import { AgentKeys } from '../../app/settings/agent_keys';
|
import { AgentKeys } from '../../app/settings/agent_keys';
|
||||||
|
import { GeneralSettings } from '../../app/settings/general_settings';
|
||||||
|
|
||||||
function page({
|
function page({
|
||||||
title,
|
title,
|
||||||
|
@ -54,6 +55,14 @@ export const settings = {
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
),
|
),
|
||||||
children: {
|
children: {
|
||||||
|
'/settings/general-settings': page({
|
||||||
|
title: i18n.translate(
|
||||||
|
'xpack.apm.views.settings.generalSettings.title',
|
||||||
|
{ defaultMessage: 'General settings' }
|
||||||
|
),
|
||||||
|
element: <GeneralSettings />,
|
||||||
|
tab: 'general-settings',
|
||||||
|
}),
|
||||||
'/settings/agent-configuration': page({
|
'/settings/agent-configuration': page({
|
||||||
tab: 'agent-configuration',
|
tab: 'agent-configuration',
|
||||||
title: i18n.translate(
|
title: i18n.translate(
|
||||||
|
@ -133,7 +142,7 @@ export const settings = {
|
||||||
tab: 'agent-keys',
|
tab: 'agent-keys',
|
||||||
}),
|
}),
|
||||||
'/settings': {
|
'/settings': {
|
||||||
element: <Redirect to="/settings/agent-configuration" />,
|
element: <Redirect to="/settings/general-settings" />,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,12 +8,11 @@
|
||||||
import { EuiPageHeaderProps } from '@elastic/eui';
|
import { EuiPageHeaderProps } from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { History } from 'history';
|
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import { CoreStart } from '@kbn/core/public';
|
import { CoreStart } from '@kbn/core/public';
|
||||||
import { ApmMainTemplate } from './apm_main_template';
|
import { ApmMainTemplate } from './apm_main_template';
|
||||||
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
|
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
|
||||||
import { getLegacyApmHref } from '../../shared/links/apm/apm_link';
|
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||||
|
import { ApmRouter } from '../apm_route_config';
|
||||||
|
|
||||||
type Tab = NonNullable<EuiPageHeaderProps['tabs']>[0] & {
|
type Tab = NonNullable<EuiPageHeaderProps['tabs']>[0] & {
|
||||||
key:
|
key:
|
||||||
|
@ -22,7 +21,8 @@ type Tab = NonNullable<EuiPageHeaderProps['tabs']>[0] & {
|
||||||
| 'anomaly-detection'
|
| 'anomaly-detection'
|
||||||
| 'apm-indices'
|
| 'apm-indices'
|
||||||
| 'custom-links'
|
| 'custom-links'
|
||||||
| 'schema';
|
| 'schema'
|
||||||
|
| 'general-settings';
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -33,8 +33,8 @@ interface Props {
|
||||||
|
|
||||||
export function SettingsTemplate({ children, selectedTab }: Props) {
|
export function SettingsTemplate({ children, selectedTab }: Props) {
|
||||||
const { core } = useApmPluginContext();
|
const { core } = useApmPluginContext();
|
||||||
const history = useHistory();
|
const router = useApmRouter();
|
||||||
const tabs = getTabs({ history, core, selectedTab });
|
const tabs = getTabs({ core, selectedTab, router });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApmMainTemplate
|
<ApmMainTemplate
|
||||||
|
@ -52,51 +52,44 @@ export function SettingsTemplate({ children, selectedTab }: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTabs({
|
function getTabs({
|
||||||
history,
|
|
||||||
core,
|
core,
|
||||||
selectedTab,
|
selectedTab,
|
||||||
|
router,
|
||||||
}: {
|
}: {
|
||||||
history: History;
|
|
||||||
core: CoreStart;
|
core: CoreStart;
|
||||||
selectedTab: Tab['key'];
|
selectedTab: Tab['key'];
|
||||||
|
router: ApmRouter;
|
||||||
}) {
|
}) {
|
||||||
const { basePath } = core.http;
|
|
||||||
const canAccessML = !!core.application.capabilities.ml?.canAccessML;
|
const canAccessML = !!core.application.capabilities.ml?.canAccessML;
|
||||||
const { search } = history.location;
|
|
||||||
|
|
||||||
const tabs: Tab[] = [
|
const tabs: Tab[] = [
|
||||||
|
{
|
||||||
|
key: 'general-settings',
|
||||||
|
label: i18n.translate('xpack.apm.settings.generalSettings', {
|
||||||
|
defaultMessage: 'General settings',
|
||||||
|
}),
|
||||||
|
href: router.link('/settings/general-settings'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'agent-configuration',
|
key: 'agent-configuration',
|
||||||
label: i18n.translate('xpack.apm.settings.agentConfig', {
|
label: i18n.translate('xpack.apm.settings.agentConfig', {
|
||||||
defaultMessage: 'Agent Configuration',
|
defaultMessage: 'Agent Configuration',
|
||||||
}),
|
}),
|
||||||
href: getLegacyApmHref({
|
href: router.link('/settings/agent-configuration'),
|
||||||
basePath,
|
|
||||||
path: `/settings/agent-configuration`,
|
|
||||||
search,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'agent-keys',
|
key: 'agent-keys',
|
||||||
label: i18n.translate('xpack.apm.settings.agentKeys', {
|
label: i18n.translate('xpack.apm.settings.agentKeys', {
|
||||||
defaultMessage: 'Agent Keys',
|
defaultMessage: 'Agent Keys',
|
||||||
}),
|
}),
|
||||||
href: getLegacyApmHref({
|
href: router.link('/settings/agent-keys'),
|
||||||
basePath,
|
|
||||||
path: `/settings/agent-keys`,
|
|
||||||
search,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'anomaly-detection',
|
key: 'anomaly-detection',
|
||||||
label: i18n.translate('xpack.apm.settings.anomalyDetection', {
|
label: i18n.translate('xpack.apm.settings.anomalyDetection', {
|
||||||
defaultMessage: 'Anomaly detection',
|
defaultMessage: 'Anomaly detection',
|
||||||
}),
|
}),
|
||||||
href: getLegacyApmHref({
|
href: router.link('/settings/anomaly-detection'),
|
||||||
basePath,
|
|
||||||
path: `/settings/anomaly-detection`,
|
|
||||||
search,
|
|
||||||
}),
|
|
||||||
hidden: !canAccessML,
|
hidden: !canAccessML,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -104,29 +97,21 @@ function getTabs({
|
||||||
label: i18n.translate('xpack.apm.settings.customizeApp', {
|
label: i18n.translate('xpack.apm.settings.customizeApp', {
|
||||||
defaultMessage: 'Custom Links',
|
defaultMessage: 'Custom Links',
|
||||||
}),
|
}),
|
||||||
href: getLegacyApmHref({
|
href: router.link('/settings/custom-links'),
|
||||||
basePath,
|
|
||||||
path: `/settings/custom-links`,
|
|
||||||
search,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'apm-indices',
|
key: 'apm-indices',
|
||||||
label: i18n.translate('xpack.apm.settings.indices', {
|
label: i18n.translate('xpack.apm.settings.indices', {
|
||||||
defaultMessage: 'Indices',
|
defaultMessage: 'Indices',
|
||||||
}),
|
}),
|
||||||
href: getLegacyApmHref({
|
href: router.link('/settings/apm-indices'),
|
||||||
basePath,
|
|
||||||
path: `/settings/apm-indices`,
|
|
||||||
search,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'schema',
|
key: 'schema',
|
||||||
label: i18n.translate('xpack.apm.settings.schema', {
|
label: i18n.translate('xpack.apm.settings.schema', {
|
||||||
defaultMessage: 'Schema',
|
defaultMessage: 'Schema',
|
||||||
}),
|
}),
|
||||||
href: getLegacyApmHref({ basePath, path: `/settings/schema`, search }),
|
href: router.link('/settings/schema'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EuiHeaderLink, EuiHeaderLinks } from '@elastic/eui';
|
import { EuiHeaderLink, EuiHeaderLinks } from '@elastic/eui';
|
||||||
|
import { apmLabsButton } from '@kbn/observability-plugin/common';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { getAlertingCapabilities } from '../../alerting/get_alerting_capabilities';
|
import { getAlertingCapabilities } from '../../alerting/get_alerting_capabilities';
|
||||||
|
@ -15,6 +16,7 @@ import { AlertingPopoverAndFlyout } from './alerting_popover_flyout';
|
||||||
import { AnomalyDetectionSetupLink } from './anomaly_detection_setup_link';
|
import { AnomalyDetectionSetupLink } from './anomaly_detection_setup_link';
|
||||||
import { useServiceName } from '../../../hooks/use_service_name';
|
import { useServiceName } from '../../../hooks/use_service_name';
|
||||||
import { InspectorHeaderLink } from './inspector_header_link';
|
import { InspectorHeaderLink } from './inspector_header_link';
|
||||||
|
import { Labs } from './labs';
|
||||||
|
|
||||||
export function ApmHeaderActionMenu() {
|
export function ApmHeaderActionMenu() {
|
||||||
const { core, plugins } = useApmPluginContext();
|
const { core, plugins } = useApmPluginContext();
|
||||||
|
@ -40,8 +42,14 @@ export function ApmHeaderActionMenu() {
|
||||||
return basePath.prepend(path);
|
return basePath.prepend(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLabsButtonEnabled = core.uiSettings.get<boolean>(
|
||||||
|
apmLabsButton,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiHeaderLinks gutterSize="xs">
|
<EuiHeaderLinks gutterSize="xs">
|
||||||
|
{isLabsButtonEnabled && <Labs />}
|
||||||
<EuiHeaderLink
|
<EuiHeaderLink
|
||||||
color="text"
|
color="text"
|
||||||
href={apmHref('/storage-explorer')}
|
href={apmHref('/storage-explorer')}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EuiButtonEmpty } from '@elastic/eui';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { LabsFlyout } from './labs_flyout';
|
||||||
|
|
||||||
|
export function Labs() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
function toggleFlyoutVisibility() {
|
||||||
|
setIsOpen((state) => !state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EuiButtonEmpty color="text" onClick={toggleFlyoutVisibility}>
|
||||||
|
{i18n.translate('xpack.apm.labs', { defaultMessage: 'Labs' })}
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
{isOpen && <LabsFlyout onClose={toggleFlyoutVisibility} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
EuiButton,
|
||||||
|
EuiButtonEmpty,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiFlyout,
|
||||||
|
EuiFlyoutBody,
|
||||||
|
EuiFlyoutFooter,
|
||||||
|
EuiFlyoutHeader,
|
||||||
|
EuiHorizontalRule,
|
||||||
|
EuiIcon,
|
||||||
|
EuiLoadingContent,
|
||||||
|
EuiTitle,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { LazyField } from '@kbn/advanced-settings-plugin/public';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import React from 'react';
|
||||||
|
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||||
|
import { useApmEditableSettings } from '../../../../hooks/use_apm_editable_settings';
|
||||||
|
import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LabsFlyout({ onClose }: Props) {
|
||||||
|
const { docLinks, notifications } = useApmPluginContext().core;
|
||||||
|
|
||||||
|
const { data, status } = useFetcher(
|
||||||
|
(callApmApi) => callApmApi('GET /internal/apm/settings/labs'),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const labsItems = data?.labsItems || [];
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleFieldChange,
|
||||||
|
settingsEditableConfig,
|
||||||
|
unsavedChanges,
|
||||||
|
saveAll,
|
||||||
|
isSaving,
|
||||||
|
cleanUnsavedChanges,
|
||||||
|
} = useApmEditableSettings(labsItems);
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
try {
|
||||||
|
const reloadPage = Object.keys(unsavedChanges).some((key) => {
|
||||||
|
return settingsEditableConfig[key].requiresPageReload;
|
||||||
|
});
|
||||||
|
|
||||||
|
await saveAll();
|
||||||
|
|
||||||
|
if (reloadPage) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const error = e as Error;
|
||||||
|
notifications.toasts.addDanger({
|
||||||
|
title: i18n.translate('xpack.apm.apmSettings.save.error', {
|
||||||
|
defaultMessage: 'An error occurred while saving the settings',
|
||||||
|
}),
|
||||||
|
text: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handelCancel() {
|
||||||
|
cleanUnsavedChanges();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
status === FETCH_STATUS.NOT_INITIATED || status === FETCH_STATUS.LOADING;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiFlyout onClose={onClose}>
|
||||||
|
<EuiFlyoutHeader hasBorder>
|
||||||
|
<EuiFlexGroup gutterSize="m">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiIcon type="beaker" size="xl" />
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiTitle>
|
||||||
|
<h2>
|
||||||
|
{i18n.translate('xpack.apm.labs', {
|
||||||
|
defaultMessage: 'Labs',
|
||||||
|
})}
|
||||||
|
</h2>
|
||||||
|
</EuiTitle>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFlyoutHeader>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<EuiLoadingContent lines={3} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<EuiFlyoutBody>
|
||||||
|
{labsItems.map((settingKey, i) => {
|
||||||
|
const editableConfig = settingsEditableConfig[settingKey];
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LazyField
|
||||||
|
key={settingKey}
|
||||||
|
setting={editableConfig}
|
||||||
|
handleChange={handleFieldChange}
|
||||||
|
enableSaving
|
||||||
|
docLinks={docLinks.links}
|
||||||
|
toasts={notifications.toasts}
|
||||||
|
unsavedChanges={unsavedChanges[settingKey]}
|
||||||
|
/>
|
||||||
|
<EuiHorizontalRule />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</EuiFlyoutBody>
|
||||||
|
<EuiFlyoutFooter>
|
||||||
|
<EuiFlexGroup justifyContent="spaceBetween">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButtonEmpty onClick={handelCancel}>
|
||||||
|
{i18n.translate('xpack.apm.labs.cancel', {
|
||||||
|
defaultMessage: 'Cancel',
|
||||||
|
})}
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButton fill isLoading={isSaving} onClick={handleSave}>
|
||||||
|
{i18n.translate('xpack.apm.labs.reload', {
|
||||||
|
defaultMessage: 'Reload to apply changes',
|
||||||
|
})}
|
||||||
|
</EuiButton>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFlyoutFooter>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</EuiFlyout>
|
||||||
|
);
|
||||||
|
}
|
104
x-pack/plugins/apm/public/hooks/use_apm_editable_settings.tsx
Normal file
104
x-pack/plugins/apm/public/hooks/use_apm_editable_settings.tsx
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { FieldState } from '@kbn/advanced-settings-plugin/public';
|
||||||
|
import { toEditableConfig } from '@kbn/advanced-settings-plugin/public';
|
||||||
|
import { IUiSettingsClient } from '@kbn/core/public';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
|
function getEditableConfig({
|
||||||
|
settingsKeys,
|
||||||
|
uiSettings,
|
||||||
|
}: {
|
||||||
|
settingsKeys: string[];
|
||||||
|
uiSettings?: IUiSettingsClient;
|
||||||
|
}) {
|
||||||
|
if (!uiSettings) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const uiSettingsDefinition = uiSettings.getAll();
|
||||||
|
const config: Record<string, ReturnType<typeof toEditableConfig>> = {};
|
||||||
|
|
||||||
|
settingsKeys.forEach((key) => {
|
||||||
|
const settingDef = uiSettingsDefinition?.[key];
|
||||||
|
if (settingDef) {
|
||||||
|
const editableConfig = toEditableConfig({
|
||||||
|
def: settingDef,
|
||||||
|
name: key,
|
||||||
|
value: settingDef.userValue,
|
||||||
|
isCustom: uiSettings.isCustom(key),
|
||||||
|
isOverridden: uiSettings.isOverridden(key),
|
||||||
|
});
|
||||||
|
config[key] = editableConfig;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApmEditableSettings(settingsKeys: string[]) {
|
||||||
|
const { services } = useKibana();
|
||||||
|
const { uiSettings } = services;
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [forceReloadSettings, setForceReloadSettings] = useState(0);
|
||||||
|
const [unsavedChanges, setUnsavedChanges] = useState<
|
||||||
|
Record<string, FieldState>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
const settingsEditableConfig = useMemo(
|
||||||
|
() => {
|
||||||
|
return getEditableConfig({ settingsKeys, uiSettings });
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[uiSettings, settingsKeys, forceReloadSettings]
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleFieldChange(key: string, fieldState: FieldState) {
|
||||||
|
setUnsavedChanges((state) => {
|
||||||
|
const newState = { ...state };
|
||||||
|
const { value, defVal } = settingsEditableConfig[key];
|
||||||
|
const currentValue = value === undefined ? defVal : value;
|
||||||
|
if (currentValue === fieldState.value) {
|
||||||
|
// Delete property from unsaved object if user changes it to the value that was already saved
|
||||||
|
delete newState[key];
|
||||||
|
} else {
|
||||||
|
newState[key] = fieldState;
|
||||||
|
}
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanUnsavedChanges() {
|
||||||
|
setUnsavedChanges({});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveAll() {
|
||||||
|
if (uiSettings && !isEmpty(unsavedChanges)) {
|
||||||
|
try {
|
||||||
|
setIsSaving(true);
|
||||||
|
const arr = Object.entries(unsavedChanges).map(([key, fieldState]) =>
|
||||||
|
uiSettings.set(key, fieldState.value)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(arr);
|
||||||
|
setForceReloadSettings((state) => ++state);
|
||||||
|
cleanUnsavedChanges();
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
settingsEditableConfig,
|
||||||
|
unsavedChanges,
|
||||||
|
handleFieldChange,
|
||||||
|
saveAll,
|
||||||
|
isSaving,
|
||||||
|
cleanUnsavedChanges,
|
||||||
|
};
|
||||||
|
}
|
|
@ -41,6 +41,7 @@ import { timeRangeMetadataRoute } from '../time_range_metadata/route';
|
||||||
import { traceRouteRepository } from '../traces/route';
|
import { traceRouteRepository } from '../traces/route';
|
||||||
import { transactionRouteRepository } from '../transactions/route';
|
import { transactionRouteRepository } from '../transactions/route';
|
||||||
import { storageExplorerRouteRepository } from '../storage_explorer/route';
|
import { storageExplorerRouteRepository } from '../storage_explorer/route';
|
||||||
|
import { labsRouteRepository } from '../settings/labs/route';
|
||||||
|
|
||||||
function getTypedGlobalApmServerRouteRepository() {
|
function getTypedGlobalApmServerRouteRepository() {
|
||||||
const repository = {
|
const repository = {
|
||||||
|
@ -75,6 +76,7 @@ function getTypedGlobalApmServerRouteRepository() {
|
||||||
...infrastructureRouteRepository,
|
...infrastructureRouteRepository,
|
||||||
...debugTelemetryRoute,
|
...debugTelemetryRoute,
|
||||||
...timeRangeMetadataRoute,
|
...timeRangeMetadataRoute,
|
||||||
|
...labsRouteRepository,
|
||||||
};
|
};
|
||||||
|
|
||||||
return repository;
|
return repository;
|
||||||
|
|
22
x-pack/plugins/apm/server/routes/settings/labs/route.ts
Normal file
22
x-pack/plugins/apm/server/routes/settings/labs/route.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { uiSettings } from '@kbn/observability-plugin/server';
|
||||||
|
import { createApmServerRoute } from '../../apm_routes/create_apm_server_route';
|
||||||
|
|
||||||
|
const getLabsRoute = createApmServerRoute({
|
||||||
|
endpoint: 'GET /internal/apm/settings/labs',
|
||||||
|
options: { tags: ['access:apm'] },
|
||||||
|
handler: async (): Promise<{ labsItems: string[] }> => {
|
||||||
|
const labsItems = Object.entries(uiSettings)
|
||||||
|
.filter(([key, value]): boolean | undefined => value.showInLabs)
|
||||||
|
.map(([key]): string => key);
|
||||||
|
return { labsItems };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const labsRouteRepository = getLabsRoute;
|
|
@ -12,15 +12,18 @@ export { formatDurationFromTimeUnitChar } from './utils/formatters';
|
||||||
export { ProcessorEvent } from './processor_event';
|
export { ProcessorEvent } from './processor_event';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
enableNewSyntheticsView,
|
||||||
enableInspectEsQueries,
|
enableInspectEsQueries,
|
||||||
maxSuggestions,
|
maxSuggestions,
|
||||||
enableComparisonByDefault,
|
enableComparisonByDefault,
|
||||||
defaultApmServiceEnvironment,
|
defaultApmServiceEnvironment,
|
||||||
apmServiceInventoryOptimizedSorting,
|
|
||||||
apmProgressiveLoading,
|
apmProgressiveLoading,
|
||||||
|
enableServiceGroups,
|
||||||
|
apmServiceInventoryOptimizedSorting,
|
||||||
apmServiceGroupMaxNumberOfServices,
|
apmServiceGroupMaxNumberOfServices,
|
||||||
apmTraceExplorerTab,
|
apmTraceExplorerTab,
|
||||||
apmOperationsTab,
|
apmOperationsTab,
|
||||||
|
apmLabsButton,
|
||||||
} from './ui_settings_keys';
|
} from './ui_settings_keys';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -18,3 +18,4 @@ export const apmServiceGroupMaxNumberOfServices =
|
||||||
'observability:apmServiceGroupMaxNumberOfServices';
|
'observability:apmServiceGroupMaxNumberOfServices';
|
||||||
export const apmTraceExplorerTab = 'observability:apmTraceExplorerTab';
|
export const apmTraceExplorerTab = 'observability:apmTraceExplorerTab';
|
||||||
export const apmOperationsTab = 'observability:apmOperationsTab';
|
export const apmOperationsTab = 'observability:apmOperationsTab';
|
||||||
|
export const apmLabsButton = 'observability:apmLabsButton';
|
||||||
|
|
|
@ -51,3 +51,5 @@ export const plugin = (initContext: PluginInitializerContext) =>
|
||||||
|
|
||||||
export type { Mappings, ObservabilityPluginSetup, ScopedAnnotationsClient };
|
export type { Mappings, ObservabilityPluginSetup, ScopedAnnotationsClient };
|
||||||
export { createOrUpdateIndex, unwrapEsResponse, WrappedElasticsearchClientError };
|
export { createOrUpdateIndex, unwrapEsResponse, WrappedElasticsearchClientError };
|
||||||
|
|
||||||
|
export { uiSettings } from './ui_settings';
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
apmServiceGroupMaxNumberOfServices,
|
apmServiceGroupMaxNumberOfServices,
|
||||||
apmTraceExplorerTab,
|
apmTraceExplorerTab,
|
||||||
apmOperationsTab,
|
apmOperationsTab,
|
||||||
|
apmLabsButton,
|
||||||
} from '../common/ui_settings_keys';
|
} from '../common/ui_settings_keys';
|
||||||
|
|
||||||
const technicalPreviewLabel = i18n.translate(
|
const technicalPreviewLabel = i18n.translate(
|
||||||
|
@ -30,10 +31,12 @@ const technicalPreviewLabel = i18n.translate(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type UiSettings = UiSettingsParams<boolean | number | string> & { showInLabs?: boolean };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* uiSettings definitions for Observability.
|
* uiSettings definitions for Observability.
|
||||||
*/
|
*/
|
||||||
export const uiSettings: Record<string, UiSettingsParams<boolean | number | string>> = {
|
export const uiSettings: Record<string, UiSettings> = {
|
||||||
[enableNewSyntheticsView]: {
|
[enableNewSyntheticsView]: {
|
||||||
category: [observabilityFeatureId],
|
category: [observabilityFeatureId],
|
||||||
name: i18n.translate('xpack.observability.enableNewSyntheticsViewExperimentName', {
|
name: i18n.translate('xpack.observability.enableNewSyntheticsViewExperimentName', {
|
||||||
|
@ -60,6 +63,7 @@ export const uiSettings: Record<string, UiSettingsParams<boolean | number | stri
|
||||||
defaultMessage: 'Inspect Elasticsearch queries in API responses.',
|
defaultMessage: 'Inspect Elasticsearch queries in API responses.',
|
||||||
}),
|
}),
|
||||||
schema: schema.boolean(),
|
schema: schema.boolean(),
|
||||||
|
requiresPageReload: true,
|
||||||
},
|
},
|
||||||
[maxSuggestions]: {
|
[maxSuggestions]: {
|
||||||
category: [observabilityFeatureId],
|
category: [observabilityFeatureId],
|
||||||
|
@ -160,6 +164,7 @@ export const uiSettings: Record<string, UiSettingsParams<boolean | number | stri
|
||||||
}),
|
}),
|
||||||
schema: schema.boolean(),
|
schema: schema.boolean(),
|
||||||
requiresPageReload: true,
|
requiresPageReload: true,
|
||||||
|
showInLabs: true,
|
||||||
},
|
},
|
||||||
[apmServiceInventoryOptimizedSorting]: {
|
[apmServiceInventoryOptimizedSorting]: {
|
||||||
category: [observabilityFeatureId],
|
category: [observabilityFeatureId],
|
||||||
|
@ -178,6 +183,7 @@ export const uiSettings: Record<string, UiSettingsParams<boolean | number | stri
|
||||||
value: false,
|
value: false,
|
||||||
requiresPageReload: false,
|
requiresPageReload: false,
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
showInLabs: true,
|
||||||
},
|
},
|
||||||
[apmServiceGroupMaxNumberOfServices]: {
|
[apmServiceGroupMaxNumberOfServices]: {
|
||||||
category: [observabilityFeatureId],
|
category: [observabilityFeatureId],
|
||||||
|
@ -204,6 +210,7 @@ export const uiSettings: Record<string, UiSettingsParams<boolean | number | stri
|
||||||
value: false,
|
value: false,
|
||||||
requiresPageReload: true,
|
requiresPageReload: true,
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
showInLabs: true,
|
||||||
},
|
},
|
||||||
[apmOperationsTab]: {
|
[apmOperationsTab]: {
|
||||||
category: [observabilityFeatureId],
|
category: [observabilityFeatureId],
|
||||||
|
@ -219,5 +226,20 @@ export const uiSettings: Record<string, UiSettingsParams<boolean | number | stri
|
||||||
value: false,
|
value: false,
|
||||||
requiresPageReload: true,
|
requiresPageReload: true,
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
showInLabs: true,
|
||||||
|
},
|
||||||
|
[apmLabsButton]: {
|
||||||
|
category: [observabilityFeatureId],
|
||||||
|
name: i18n.translate('xpack.observability.apmLabs', {
|
||||||
|
defaultMessage: 'Enable labs button in APM',
|
||||||
|
}),
|
||||||
|
description: i18n.translate('xpack.observability.apmLabsDescription', {
|
||||||
|
defaultMessage:
|
||||||
|
'This flag determines if the viewer has access to the Labs button, a quick way to enable and disable technical preview features in APM.',
|
||||||
|
}),
|
||||||
|
schema: schema.boolean(),
|
||||||
|
value: false,
|
||||||
|
requiresPageReload: true,
|
||||||
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue