Skip to content

Commit

Permalink
[SLO][SLO Detail] Make SLO Detail > Header Actions menu and SLO List …
Browse files Browse the repository at this point in the history
…> SLO List item > Actions menu more consistent (elastic#155868)

Co-authored-by: Kevin Delemme <[email protected]>
Co-authored-by: kibanamachine <[email protected]>
(cherry picked from commit 1dcf1f8)

# Conflicts:
#	x-pack/plugins/observability/public/pages/slo_details/components/header_control.tsx
#	x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx
  • Loading branch information
CoenWarmer committed Apr 28, 2023
1 parent d8718ff commit e6df4b5
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 35 deletions.
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 { isApmIndicatorType } from '../../../utils/slo/indicator';
Expand All @@ -18,6 +19,12 @@ import { paths } from '../../../config/paths';
import { useKibana } from '../../../utils/kibana_react';
import { ObservabilityAppServices } from '../../../application/types';
import { useCapabilities } from '../../../hooks/slo/use_capabilities';
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';

export interface Props {
slo: SLOWithSummaryResponse | undefined;
Expand All @@ -28,11 +35,17 @@ export function HeaderControl({ isLoading, slo }: Props) {
const {
application: { navigateToUrl },
http: { basePath },
notifications: { toasts },
triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout },
} = useKibana<ObservabilityAppServices>().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 @@ -85,6 +98,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 @@ -97,7 +144,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 @@ -108,11 +155,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 @@ -123,6 +171,7 @@ export function HeaderControl({ isLoading, slo }: Props) {
<EuiContextMenuItem
key="createBurnRateRule"
disabled={!hasWriteCapabilities}
icon="bell"
onClick={handleOpenRuleFlyout}
data-test-subj="sloDetailsHeaderControlPopoverCreateRule"
>
Expand All @@ -136,31 +185,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 @@ -173,6 +249,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 @@ -60,6 +60,12 @@ const mockKibana = () => {
prepend: mockBasePathPrepend,
},
},
notifications: {
toasts: {
addSuccess: jest.fn(),
addError: jest.fn(),
},
},
triggersActionsUi: {
getAddRuleFlyout: jest.fn(() => (
<div data-test-subj="add-rule-flyout">mocked component</div>
Expand Down Expand Up @@ -182,6 +188,45 @@ describe('SLO Details Page', () => {
expect(screen.queryByTestId('sloDetailsHeaderControlPopoverCreateRule')).toBeTruthy();
});

it("renders a 'Manage rules' 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('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();
});

it('renders the Overview tab by default', async () => {
const slo = buildSlo();
useParamsMock.mockReturnValue(slo.id);
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
Loading

0 comments on commit e6df4b5

Please sign in to comment.