From 1dcf1f86a856a7251df7bd06b47820ed64d033b8 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Wed, 26 Apr 2023 19:20:43 +0200 Subject: [PATCH] [SLO][SLO Detail] Make SLO Detail > Header Actions menu and SLO List > SLO List item > Actions menu more consistent (#155868) Co-authored-by: Kevin Delemme Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../slo/feedback_button/feedback_button.tsx | 9 +- .../slo_details/components/header_control.tsx | 126 +++++++++++++++--- .../pages/slo_details/slo_details.test.tsx | 31 +++++ .../public/pages/slo_details/slo_details.tsx | 13 +- .../badges/slo_indicator_type_badge.tsx | 4 +- .../slo_delete_confirmation_modal.tsx | 3 + .../pages/slos/components/slo_list_item.tsx | 8 +- .../observability/public/pages/slos/slos.tsx | 4 +- 8 files changed, 163 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/observability/public/components/slo/feedback_button/feedback_button.tsx b/x-pack/plugins/observability/public/components/slo/feedback_button/feedback_button.tsx index f72e4b008f390..dbd45bf74dee4 100644 --- a/x-pack/plugins/observability/public/components/slo/feedback_button/feedback_button.tsx +++ b/x-pack/plugins/observability/public/components/slo/feedback_button/feedback_button.tsx @@ -5,16 +5,21 @@ * 2.0. */ +import React from 'react'; import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; const SLO_FEEDBACK_LINK = 'https://ela.st/slo-feedback'; -export function FeedbackButton() { +interface Props { + disabled?: boolean; +} + +export function FeedbackButton({ disabled }: Props) { return ( (false); + const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false); + + const { mutateAsync: cloneSlo } = useCloneSlo(); + const isDeleting = Boolean(useIsMutating(['deleteSlo', slo?.id])); const handleActionsClick = () => setIsPopoverOpen((value) => !value); const closePopover = () => setIsPopoverOpen(false); @@ -95,6 +108,40 @@ export function HeaderControl({ isLoading, slo }: Props) { } }; + const handleClone = async () => { + if (slo) { + setIsPopoverOpen(false); + + const newSlo = transformValuesToCreateSLOInput( + transformSloResponseToCreateSloInput({ ...slo, name: `[Copy] ${slo.name}` })! + ); + + await cloneSlo({ slo: newSlo, idToCopyFrom: slo.id }); + + toasts.addSuccess( + i18n.translate('xpack.observability.slo.sloDetails.headerControl.cloneSuccess', { + defaultMessage: 'Successfully created {name}', + values: { name: newSlo.name }, + }) + ); + + navigateToUrl(basePath.prepend(paths.observability.slos)); + } + }; + + const handleDelete = () => { + setDeleteConfirmationModalOpen(true); + setIsPopoverOpen(false); + }; + + const handleDeleteCancel = () => { + setDeleteConfirmationModalOpen(false); + }; + + const handleDeleteSuccess = () => { + navigateToUrl(basePath.prepend(paths.observability.slos)); + }; + return ( <> {i18n.translate('xpack.observability.slo.sloDetails.headerControl.actions', { defaultMessage: 'Actions', @@ -118,11 +165,12 @@ export function HeaderControl({ isLoading, slo }: Props) { closePopover={closePopover} > @@ -133,6 +181,7 @@ export function HeaderControl({ isLoading, slo }: Props) { @@ -146,6 +195,7 @@ export function HeaderControl({ isLoading, slo }: Props) { @@ -153,24 +203,50 @@ export function HeaderControl({ isLoading, slo }: Props) { defaultMessage: 'Manage rules', })} , - ].concat( - !!slo && isApmIndicatorType(slo.indicator.type) - ? [ - - {i18n.translate( - 'xpack.observability.slos.sloDetails.headerControl.exploreInApm', - { - defaultMessage: 'Explore in APM', - } - )} - , - ] - : [] - )} + ] + .concat( + !!slo && isApmIndicatorType(slo.indicator.type) ? ( + + {i18n.translate( + 'xpack.observability.slos.sloDetails.headerControl.exploreInApm', + { + defaultMessage: 'Service details', + } + )} + + ) : ( + [] + ) + ) + .concat( + + {i18n.translate('xpack.observability.slo.slo.item.actions.clone', { + defaultMessage: 'Clone', + })} + , + + {i18n.translate('xpack.observability.slo.slo.item.actions.delete', { + defaultMessage: 'Delete', + })} + + )} /> @@ -183,6 +259,14 @@ export function HeaderControl({ isLoading, slo }: Props) { initialValues={{ name: `${slo.name} burn rate`, params: { sloId: slo.id } }} /> ) : null} + + {slo && isDeleteConfirmationModalOpen ? ( + + ) : null} ); } diff --git a/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx b/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx index 9cd8836c6cdf7..1dc86a2901a44 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx @@ -61,6 +61,12 @@ const mockKibana = () => { prepend: mockBasePathPrepend, }, }, + notifications: { + toasts: { + addSuccess: jest.fn(), + addError: jest.fn(), + }, + }, share: { url: { locators: { @@ -199,6 +205,31 @@ describe('SLO Details Page', () => { render(); fireEvent.click(screen.getByTestId('o11yHeaderControlActionsButton')); + expect(screen.queryByTestId('sloDetailsHeaderControlPopoverManageRules')).toBeTruthy(); + }); + + it("renders a 'Clone' button under actions menu", async () => { + const slo = buildSlo(); + useParamsMock.mockReturnValue(slo.id); + useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo }); + useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); + + render(); + + fireEvent.click(screen.getByTestId('o11yHeaderControlActionsButton')); + expect(screen.queryByTestId('sloDetailsHeaderControlPopoverClone')).toBeTruthy(); + }); + + it("renders a 'Delete' button under actions menu", async () => { + const slo = buildSlo(); + useParamsMock.mockReturnValue(slo.id); + useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo }); + useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); + + render(); + + fireEvent.click(screen.getByTestId('o11yHeaderControlActionsButton')); + expect(screen.queryByTestId('sloDetailsHeaderControlPopoverDelete')).toBeTruthy(); const manageRulesButton = screen.queryByTestId('sloDetailsHeaderControlPopoverManageRules'); expect(manageRulesButton).toBeTruthy(); diff --git a/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx b/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx index 48401ca29f94e..29a7f573e3c14 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx @@ -7,6 +7,7 @@ import React, { useState } from 'react'; import { useParams } from 'react-router-dom'; +import { useIsMutating } from '@tanstack/react-query'; import { EuiBreadcrumbProps } from '@elastic/eui/src/components/breadcrumbs/breadcrumb'; import { EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -44,6 +45,8 @@ export function SloDetailsPage() { const { isLoading, slo } = useFetchSloDetails({ sloId, shouldRefetch: isAutoRefreshing }); + const isCloningOrDeleting = Boolean(useIsMutating()); + useBreadcrumbs(getBreadcrumbs(basePath, slo)); const isSloNotFound = !isLoading && slo === undefined; @@ -55,6 +58,8 @@ export function SloDetailsPage() { navigateToUrl(basePath.prepend(paths.observability.slos)); } + const isPerformingAction = isLoading || isCloningOrDeleting; + const handleToggleAutoRefresh = () => { setIsAutoRefreshing(!isAutoRefreshing); }; @@ -62,15 +67,15 @@ export function SloDetailsPage() { return ( , + pageTitle: , rightSideItems: [ - , + , , - , + , ], bottomBorder: false, }} diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.tsx index 87f005e2fa5c0..ad73af3d73bfa 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.tsx @@ -63,7 +63,7 @@ export function SloIndicatorTypeBadge({ slo }: Props) { @@ -73,7 +73,7 @@ export function SloIndicatorTypeBadge({ slo }: Props) { onClickAriaLabel={i18n.translate( 'xpack.observability.slo.indicatorTypeBadge.exploreInApm', { - defaultMessage: 'Explore {service} in APM', + defaultMessage: 'View {service} details', values: { service: slo.indicator.params.service }, } )} diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_delete_confirmation_modal.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_delete_confirmation_modal.tsx index 4a292bef8c14b..b1b2d341a9bd8 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_delete_confirmation_modal.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_delete_confirmation_modal.tsx @@ -15,11 +15,13 @@ import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo'; export interface SloDeleteConfirmationModalProps { slo: SLOWithSummaryResponse; onCancel: () => void; + onSuccess?: () => void; } export function SloDeleteConfirmationModal({ slo: { id, name }, onCancel, + onSuccess, }: SloDeleteConfirmationModalProps) { const { notifications: { toasts }, @@ -31,6 +33,7 @@ export function SloDeleteConfirmationModal({ if (isSuccess) { toasts.addSuccess(getDeleteSuccesfulMessage(name)); + onSuccess?.(); } if (isError) { diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx index 0e58573b740c2..3fe0f5d31f543 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx @@ -184,12 +184,12 @@ export function SloListItem({ onClick={handleClickActions} /> } - panelPaddingSize="none" + panelPaddingSize="m" closePopover={handleClickActions} isOpen={isActionsPopoverOpen} > {i18n.translate('xpack.observability.slo.slo.item.actions.createRule', { - defaultMessage: 'Create new Alert rule', + defaultMessage: 'Create new alert rule', })} ,