Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SLO][SLO Detail] Make SLO Detail > Header Actions menu and SLO List > SLO List item > Actions menu more consistent #155868

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<EuiButton
data-test-subj="sloFeedbackButton"
isDisabled={disabled}
href={SLO_FEEDBACK_LINK}
target="_blank"
color="warning"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
* 2.0.
*/

import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import React, { useState } from 'react';
import { useIsMutating } from '@tanstack/react-query';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';

import { useCapabilities } from '../../../hooks/slo/use_capabilities';
Expand All @@ -17,6 +18,12 @@ import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../utils/slo/conve
import { SLO_BURN_RATE_RULE_ID } from '../../../../common/constants';
import { rulesLocatorID, sloFeatureId } from '../../../../common';
import { paths } from '../../../config/paths';
import { useCloneSlo } from '../../../hooks/slo/use_clone_slo';
import {
transformSloResponseToCreateSloInput,
transformValuesToCreateSLOInput,
} from '../../slo_edit/helpers/process_slo_form_values';
import { SloDeleteConfirmationModal } from '../../slos/components/slo_delete_confirmation_modal';
import type { RulesParams } from '../../../locators/rules';

export interface Props {
Expand All @@ -28,14 +35,20 @@ export function HeaderControl({ isLoading, slo }: Props) {
const {
application: { navigateToUrl },
http: { basePath },
notifications: { toasts },
share: {
url: { locators },
},
triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout },
} = useKibana().services;
const { hasWriteCapabilities } = useCapabilities();

const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [isRuleFlyoutVisible, setRuleFlyoutVisibility] = useState<boolean>(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);
Expand Down Expand Up @@ -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 (
<>
<EuiPopover
Expand All @@ -107,7 +154,7 @@ export function HeaderControl({ isLoading, slo }: Props) {
iconType="arrowDown"
iconSize="s"
onClick={handleActionsClick}
disabled={isLoading || !slo}
disabled={isLoading || isDeleting || !slo}
>
{i18n.translate('xpack.observability.slo.sloDetails.headerControl.actions', {
defaultMessage: 'Actions',
Expand All @@ -118,11 +165,12 @@ export function HeaderControl({ isLoading, slo }: Props) {
closePopover={closePopover}
>
<EuiContextMenuPanel
size="s"
size="m"
items={[
<EuiContextMenuItem
key="edit"
disabled={!hasWriteCapabilities}
icon="pencil"
onClick={handleEdit}
data-test-subj="sloDetailsHeaderControlPopoverEdit"
>
Expand All @@ -133,6 +181,7 @@ export function HeaderControl({ isLoading, slo }: Props) {
<EuiContextMenuItem
key="createBurnRateRule"
disabled={!hasWriteCapabilities}
icon="bell"
onClick={handleOpenRuleFlyout}
data-test-subj="sloDetailsHeaderControlPopoverCreateRule"
>
Expand All @@ -146,31 +195,58 @@ export function HeaderControl({ isLoading, slo }: Props) {
<EuiContextMenuItem
key="manageRules"
disabled={!hasWriteCapabilities}
icon="gear"
onClick={handleNavigateToRules}
data-test-subj="sloDetailsHeaderControlPopoverManageRules"
>
{i18n.translate('xpack.observability.slo.sloDetails.headerControl.manageRules', {
defaultMessage: 'Manage rules',
})}
</EuiContextMenuItem>,
].concat(
!!slo && isApmIndicatorType(slo.indicator.type)
? [
<EuiContextMenuItem
key="exploreInApm"
onClick={handleNavigateToApm}
data-test-subj="sloDetailsHeaderControlPopoverExploreInApm"
>
{i18n.translate(
'xpack.observability.slos.sloDetails.headerControl.exploreInApm',
{
defaultMessage: 'Explore in APM',
}
)}
</EuiContextMenuItem>,
]
: []
)}
]
.concat(
!!slo && isApmIndicatorType(slo.indicator.type) ? (
<EuiContextMenuItem
key="exploreInApm"
icon="bullseye"
onClick={handleNavigateToApm}
data-test-subj="sloDetailsHeaderControlPopoverExploreInApm"
>
{i18n.translate(
'xpack.observability.slos.sloDetails.headerControl.exploreInApm',
{
defaultMessage: 'Service details',
}
)}
</EuiContextMenuItem>
) : (
[]
)
)
.concat(
<EuiContextMenuItem
key="clone"
disabled={!hasWriteCapabilities}
icon="copy"
onClick={handleClone}
data-test-subj="sloDetailsHeaderControlPopoverClone"
>
{i18n.translate('xpack.observability.slo.slo.item.actions.clone', {
defaultMessage: 'Clone',
})}
</EuiContextMenuItem>,
<EuiContextMenuItem
key="delete"
icon="trash"
disabled={!hasWriteCapabilities}
onClick={handleDelete}
data-test-subj="sloDetailsHeaderControlPopoverDelete"
>
{i18n.translate('xpack.observability.slo.slo.item.actions.delete', {
defaultMessage: 'Delete',
})}
</EuiContextMenuItem>
)}
/>
</EuiPopover>

Expand All @@ -183,6 +259,14 @@ export function HeaderControl({ isLoading, slo }: Props) {
initialValues={{ name: `${slo.name} burn rate`, params: { sloId: slo.id } }}
/>
) : null}

{slo && isDeleteConfirmationModalOpen ? (
<SloDeleteConfirmationModal
slo={slo}
onCancel={handleDeleteCancel}
onSuccess={handleDeleteSuccess}
/>
) : null}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ const mockKibana = () => {
prepend: mockBasePathPrepend,
},
},
notifications: {
toasts: {
addSuccess: jest.fn(),
addError: jest.fn(),
},
},
share: {
url: {
locators: {
Expand Down Expand Up @@ -199,6 +205,31 @@ describe('SLO Details Page', () => {
render(<SloDetailsPage />);

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(<SloDetailsPage />);

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(<SloDetailsPage />);

fireEvent.click(screen.getByTestId('o11yHeaderControlActionsButton'));
expect(screen.queryByTestId('sloDetailsHeaderControlPopoverDelete')).toBeTruthy();

const manageRulesButton = screen.queryByTestId('sloDetailsHeaderControlPopoverManageRules');
expect(manageRulesButton).toBeTruthy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -55,22 +58,24 @@ export function SloDetailsPage() {
navigateToUrl(basePath.prepend(paths.observability.slos));
}

const isPerformingAction = isLoading || isCloningOrDeleting;

const handleToggleAutoRefresh = () => {
setIsAutoRefreshing(!isAutoRefreshing);
};

return (
<ObservabilityPageTemplate
pageHeader={{
pageTitle: <HeaderTitle isLoading={isLoading} slo={slo} />,
pageTitle: <HeaderTitle isLoading={isPerformingAction} slo={slo} />,
rightSideItems: [
<HeaderControl isLoading={isLoading} slo={slo} />,
<HeaderControl isLoading={isPerformingAction} slo={slo} />,
<AutoRefreshButton
disabled={isLoading}
disabled={isPerformingAction}
isAutoRefreshing={isAutoRefreshing}
onClick={handleToggleAutoRefresh}
/>,
<FeedbackButton />,
<FeedbackButton disabled={isPerformingAction} />,
],
bottomBorder: false,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function SloIndicatorTypeBadge({ slo }: Props) {
<EuiToolTip
position="top"
content={i18n.translate('xpack.observability.slo.indicatorTypeBadge.exploreInApm', {
defaultMessage: 'Explore {service} in APM',
defaultMessage: 'View {service} details',
values: { service: slo.indicator.params.service },
})}
>
Expand All @@ -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 },
}
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -31,6 +33,7 @@ export function SloDeleteConfirmationModal({

if (isSuccess) {
toasts.addSuccess(getDeleteSuccesfulMessage(name));
onSuccess?.();
}

if (isError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,12 @@ export function SloListItem({
onClick={handleClickActions}
/>
}
panelPaddingSize="none"
panelPaddingSize="m"
closePopover={handleClickActions}
isOpen={isActionsPopoverOpen}
>
<EuiContextMenuPanel
size="s"
size="m"
items={[
<EuiContextMenuItem
key="view"
Expand Down Expand Up @@ -220,12 +220,12 @@ export function SloListItem({
data-test-subj="sloActionsCreateRule"
>
{i18n.translate('xpack.observability.slo.slo.item.actions.createRule', {
defaultMessage: 'Create new Alert rule',
defaultMessage: 'Create new alert rule',
})}
</EuiContextMenuItem>,
<EuiContextMenuItem
key="manageRules"
icon="list"
icon="gear"
disabled={!hasWriteCapabilities}
onClick={handleNavigateToRules}
data-test-subj="sloActionsManageRules"
Expand Down
Loading