diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx index be4edbe2ea270..9f3a65583ddb7 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx @@ -41,6 +41,7 @@ interface Props { export function AgentConfigurationList({ status, data, refetch }: Props) { const { core } = useApmPluginContext(); + const canSave = core.application.capabilities.apm.save; const { basePath } = core.http; const { search } = useLocation(); const theme = useTheme(); @@ -180,28 +181,36 @@ export function AgentConfigurationList({ status, data, refetch }: Props) { ), }, - { - width: px(units.double), - name: '', - render: (config: Config) => ( - - ), - }, - { - width: px(units.double), - name: '', - render: (config: Config) => ( - setConfigToBeDeleted(config)} - /> - ), - }, + ...(canSave + ? [ + { + width: px(units.double), + name: '', + render: (config: Config) => ( + + ), + }, + { + width: px(units.double), + name: '', + render: (config: Config) => ( + setConfigToBeDeleted(config)} + /> + ), + }, + ] + : []), ]; return ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx index c408d5e960cf3..c1f5ec154792d 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -3,6 +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 { EuiToolTip } from '@elastic/eui'; import { EuiButton, EuiFlexGroup, @@ -73,15 +74,35 @@ function CreateConfigurationButton() { const { basePath } = core.http; const { search } = useLocation(); const href = createAgentConfigurationHref(search, basePath); + const canSave = core.application.capabilities.apm.save; return ( - - {i18n.translate('xpack.apm.agentConfig.createConfigButtonLabel', { - defaultMessage: 'Create configuration', - })} - + + + {i18n.translate('xpack.apm.agentConfig.createConfigButtonLabel', { + defaultMessage: 'Create configuration', + })} + + diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx index 56b3eaf425af7..3b4c127aab1e5 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx @@ -3,17 +3,40 @@ * 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 { EuiButton } from '@elastic/eui'; +import { EuiButton, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; export function CreateCustomLinkButton({ onClick }: { onClick: () => void }) { + const { core } = useApmPluginContext(); + const canSave = core.application.capabilities.apm.save; return ( - - {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.createCustomLink', - { defaultMessage: 'Create custom link' } - )} - + + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.createCustomLink', + { defaultMessage: 'Create custom link' } + )} + + ); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx index d512ea19c7892..4bc1adee04bf4 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx @@ -13,6 +13,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { isEmpty } from 'lodash'; +import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; import { units, px } from '../../../../../style/variables'; import { ManagedTable } from '../../../../shared/ManagedTable'; @@ -26,6 +27,8 @@ interface Props { export function CustomLinkTable({ items = [], onCustomLinkSelected }: Props) { const [searchTerm, setSearchTerm] = useState(''); + const { core } = useApmPluginContext(); + const canSave = core.application.capabilities.apm.save; const columns = [ { @@ -61,22 +64,26 @@ export function CustomLinkTable({ items = [], onCustomLinkSelected }: Props) { width: px(units.triple), name: '', actions: [ - { - name: i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.table.editButtonLabel', - { defaultMessage: 'Edit' } - ), - description: i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.table.editButtonDescription', - { defaultMessage: 'Edit this custom link' } - ), - icon: 'pencil', - color: 'primary', - type: 'icon', - onClick: (customLink: CustomLink) => { - onCustomLinkSelected(customLink); - }, - }, + ...(canSave + ? [ + { + name: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.table.editButtonLabel', + { defaultMessage: 'Edit' } + ), + description: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.table.editButtonDescription', + { defaultMessage: 'Edit this custom link' } + ), + icon: 'pencil', + color: 'primary', + type: 'icon', + onClick: (customLink: CustomLink) => { + onCustomLinkSelected(customLink); + }, + }, + ] + : []), ], }, ]; diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx index 1da7d415b5660..4477ee5a99be3 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx @@ -7,22 +7,26 @@ import { fireEvent, render, - waitFor, RenderResult, + waitFor, } from '@testing-library/react'; import React from 'react'; import { act } from 'react-dom/test-utils'; -import * as apmApi from '../../../../../services/rest/createCallApmApi'; +import { CustomLinkOverview } from '.'; import { License } from '../../../../../../../licensing/common/license'; -import * as hooks from '../../../../../hooks/use_fetcher'; +import { ApmPluginContextValue } from '../../../../../context/apm_plugin/apm_plugin_context'; +import { + mockApmPluginContextValue, + MockApmPluginContextWrapper, +} from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { LicenseContext } from '../../../../../context/license/license_context'; -import { CustomLinkOverview } from '.'; +import * as hooks from '../../../../../hooks/use_fetcher'; +import * as apmApi from '../../../../../services/rest/createCallApmApi'; import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../../../utils/testHelpers'; import * as saveCustomLink from './CreateEditCustomLinkFlyout/saveCustomLink'; -import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; const data = [ { @@ -39,6 +43,16 @@ const data = [ }, ]; +function getMockAPMContext({ canSave }: { canSave: boolean }) { + return ({ + ...mockApmPluginContextValue, + core: { + ...mockApmPluginContextValue.core, + application: { capabilities: { apm: { save: canSave }, ml: {} } }, + }, + } as unknown) as ApmPluginContextValue; +} + describe('CustomLink', () => { beforeAll(() => { jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({}); @@ -70,9 +84,11 @@ describe('CustomLink', () => { }); it('shows when no link is available', () => { const component = render( - - - + + + + + ); expectTextsInDocument(component, ['No links found.']); }); @@ -91,6 +107,34 @@ describe('CustomLink', () => { jest.clearAllMocks(); }); + it('enables create button when user has writte privileges', () => { + const mockContext = getMockAPMContext({ canSave: true }); + + const { getByTestId } = render( + + + + + + ); + const createButton = getByTestId('createButton') as HTMLButtonElement; + expect(createButton.disabled).toBeFalsy(); + }); + + it('enables edit button on custom link table when user has writte privileges', () => { + const mockContext = getMockAPMContext({ canSave: true }); + + const { getAllByText } = render( + + + + + + ); + + expect(getAllByText('Edit').length).toEqual(2); + }); + it('shows a table with all custom link', () => { const component = render( @@ -108,9 +152,11 @@ describe('CustomLink', () => { }); it('checks if create custom link button is available and working', () => { + const mockContext = getMockAPMContext({ canSave: true }); + const { queryByText, getByText } = render( - + @@ -137,9 +183,10 @@ describe('CustomLink', () => { }); const openFlyout = () => { + const mockContext = getMockAPMContext({ canSave: true }); const component = render( - + @@ -173,9 +220,10 @@ describe('CustomLink', () => { }); it('deletes a custom link', async () => { + const mockContext = getMockAPMContext({ canSave: true }); const component = render( - + @@ -356,4 +404,34 @@ describe('CustomLink', () => { expectTextsNotInDocument(component, ['Start free 30-day trial']); }); }); + + describe('with read-only user', () => { + it('disables create custom link button', () => { + const mockContext = getMockAPMContext({ canSave: false }); + + const { getByTestId } = render( + + + + + + ); + const createButton = getByTestId('createButton') as HTMLButtonElement; + expect(createButton.disabled).toBeTruthy(); + }); + + it('removes edit button on custom link table', () => { + const mockContext = getMockAPMContext({ canSave: false }); + + const { queryAllByText } = render( + + + + + + ); + + expect(queryAllByText('Edit').length).toEqual(0); + }); + }); }); diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts index fdf2fe3521d7e..70755540721dd 100644 --- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts +++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts @@ -64,7 +64,7 @@ export const createCustomLinkRoute = createRoute({ params: t.type({ body: payloadRt, }), - options: { tags: ['access:apm'] }, + options: { tags: ['access:apm', 'access:apm_write'] }, handler: async ({ context, request }) => { if (!isActiveGoldLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE);