From f320d560b1458da7cd3041a78947fbbfe4985fc7 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 27 Mar 2024 14:54:52 +0500 Subject: [PATCH] [SLO] Open burn rule editing in flyout (#179373) ## Summary Open burn rule editing in flyout !! Also handled mouse on click for badges in card view !! https://github.com/elastic/kibana/assets/3505601/2c39ee6a-c011-439c-b268-3b02eda77cc0 --- .../slo_active_alerts_badge.tsx | 46 ++++++++++++------- .../public/hooks/use_fetch_rules_for_slo.ts | 30 ++++++------ .../slo_details/components/header_control.tsx | 32 +++++++++++-- .../slo_details/components/header_title.tsx | 3 +- .../components/badges/slo_rules_badge.tsx | 13 ++++-- .../components/card_view/slo_card_item.tsx | 15 +++++- .../card_view/slo_card_item_actions.tsx | 4 ++ .../card_view/slo_card_item_badges.tsx | 2 +- .../components/card_view/slos_card_view.tsx | 4 +- .../common/edit_burn_rate_rule_flyout.tsx | 40 ++++++++++++++++ .../slos/components/slo_item_actions.tsx | 21 +++++++-- .../pages/slos/components/slo_list_item.tsx | 15 +++++- .../slo_list_view/slo_list_view.tsx | 3 +- 13 files changed, 175 insertions(+), 53 deletions(-) create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/edit_burn_rate_rule_flyout.tsx diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/slo_status_badge/slo_active_alerts_badge.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/slo_status_badge/slo_active_alerts_badge.tsx index 75a08174ac60b..3c01e57faa398 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/slo_status_badge/slo_active_alerts_badge.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/slo_status_badge/slo_active_alerts_badge.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import { EuiBadge, EuiFlexItem } from '@elastic/eui'; +import { EuiBadge, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { MouseEvent } from 'react'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import { observabilityPaths } from '@kbn/observability-plugin/common'; import { useKibana } from '../../../utils/kibana_react'; @@ -43,22 +43,36 @@ export function SloActiveAlertsBadge({ slo, activeAlerts, viewMode = 'default' } return ( - - {viewMode !== 'default' - ? activeAlerts - : i18n.translate('xpack.slo.slo.activeAlertsBadge.label', { - defaultMessage: '{count, plural, one {# alert} other {# alerts}}', - values: { count: activeAlerts }, - })} - + ) => { + e.stopPropagation(); // stops propagation of metric onElementClick + }} + css={{ cursor: 'pointer' }} + > + {viewMode !== 'default' + ? activeAlerts + : i18n.translate('xpack.slo.slo.activeAlertsBadge.label', { + defaultMessage: '{count, plural, one {# alert} other {# alerts}}', + values: { count: activeAlerts }, + })} + + ); } diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_rules_for_slo.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_rules_for_slo.ts index 80c3b315bfa48..51a8337e4dd82 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_rules_for_slo.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_rules_for_slo.ts @@ -5,42 +5,33 @@ * 2.0. */ -import type { Rule } from '@kbn/triggers-actions-ui-plugin/public'; +import type { Rule, AsApiContract } from '@kbn/triggers-actions-ui-plugin/public'; +import { transformRule } from '@kbn/triggers-actions-ui-plugin/public'; import { useQuery } from '@tanstack/react-query'; import { BurnRateRuleParams } from '../typings'; import { useKibana } from '../utils/kibana_react'; import { sloKeys } from './query_key_factory'; -type SloId = string; - interface Params { - sloIds?: SloId[]; + sloIds?: string[]; } interface RuleApiResponse { page: number; total: number; per_page: number; - data: Array>; -} - -export interface UseFetchRulesForSloResponse { - isLoading: boolean; - isSuccess: boolean; - isError: boolean; - data: Record>> | undefined; + data: Array>>; } -export function useFetchRulesForSlo({ sloIds = [] }: Params): UseFetchRulesForSloResponse { +export function useFetchRulesForSlo({ sloIds = [] }: Params) { const { http } = useKibana().services; - const { isLoading, isError, isSuccess, data } = useQuery({ + const { isLoading, isError, isSuccess, data, refetch } = useQuery({ queryKey: sloKeys.rule(sloIds), queryFn: async () => { try { const body = JSON.stringify({ filter: sloIds.map((sloId) => `alert.attributes.params.sloId:${sloId}`).join(' or '), - fields: ['params', 'name'], per_page: 1000, }); @@ -48,9 +39,13 @@ export function useFetchRulesForSlo({ sloIds = [] }: Params): UseFetchRulesForSl body, }); + const rules = response.data.map((rule) => transformRule(rule)) as Array< + Rule + >; + const init = sloIds.reduce((acc, sloId) => ({ ...acc, [sloId]: [] }), {}); - return response.data.reduce( + return rules.reduce( (acc, rule) => ({ ...acc, [rule.params.sloId]: acc[rule.params.sloId].concat(rule), @@ -66,10 +61,13 @@ export function useFetchRulesForSlo({ sloIds = [] }: Params): UseFetchRulesForSl keepPreviousData: true, }); + const refetchRules = refetch as () => void; + return { data, isLoading, isSuccess, isError, + refetchRules, }; } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx index 5e0652e85cdff..a84e1082050a9 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx @@ -14,6 +14,8 @@ import type { RulesParams } from '@kbn/observability-plugin/public'; import { rulesLocatorID } from '@kbn/observability-plugin/common'; import { SLO_BURN_RATE_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { sloFeatureId } from '@kbn/observability-plugin/common'; +import { EditBurnRateRuleFlyout } from '../../slos/components/common/edit_burn_rate_rule_flyout'; +import { useFetchRulesForSlo } from '../../../hooks/use_fetch_rules_for_slo'; import { useKibana } from '../../../utils/kibana_react'; import { paths } from '../../../../common/locators/paths'; import { SloDeleteConfirmationModal } from '../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal'; @@ -24,7 +26,7 @@ import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../utils/slo/conve import { isApmIndicatorType } from '../../../utils/slo/indicator'; export interface Props { - slo: SLOWithSummaryResponse | undefined; + slo?: SLOWithSummaryResponse; isLoading: boolean; } @@ -42,10 +44,18 @@ export function HeaderControl({ isLoading, slo }: Props) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isRuleFlyoutVisible, setRuleFlyoutVisibility] = useState(false); + const [isEditRuleFlyoutOpen, setIsEditRuleFlyoutOpen] = useState(false); + const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false); const { mutate: deleteSlo } = useDeleteSlo(); + const { data: rulesBySlo, refetchRules } = useFetchRulesForSlo({ + sloIds: slo ? [slo.id] : undefined, + }); + + const rules = slo ? rulesBySlo?.[slo?.id] ?? [] : []; + const handleActionsClick = () => setIsPopoverOpen((value) => !value); const closePopover = () => setIsPopoverOpen(false); @@ -65,10 +75,15 @@ export function HeaderControl({ isLoading, slo }: Props) { }; const handleNavigateToRules = async () => { - const locator = locators.get(rulesLocatorID); + if (rules.length === 1) { + setIsEditRuleFlyoutOpen(true); + setIsPopoverOpen(false); + } else { + const locator = locators.get(rulesLocatorID); - if (slo?.id && locator) { - locator.navigate({ params: { sloId: slo.id } }, { replace: false }); + if (slo?.id && locator) { + locator.navigate({ params: { sloId: slo.id } }, { replace: false }); + } } }; @@ -168,7 +183,8 @@ export function HeaderControl({ isLoading, slo }: Props) { data-test-subj="sloDetailsHeaderControlPopoverManageRules" > {i18n.translate('xpack.slo.sloDetails.headerControl.manageRules', { - defaultMessage: 'Manage rules', + defaultMessage: 'Manage burn rate {count, plural, one {rule} other {rules}}', + values: { count: rules.length }, })} , ] @@ -215,6 +231,12 @@ export function HeaderControl({ isLoading, slo }: Props) { )} /> + {slo && isRuleFlyoutVisible ? ( - + ) => { + e.stopPropagation(); // stops propagation of metric onElementClick + }} + /> ); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item.tsx index 802faafc014ce..5f24aed1d8ca6 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item.tsx @@ -24,6 +24,7 @@ import { import { ALL_VALUE, HistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; import React, { useState } from 'react'; +import { EditBurnRateRuleFlyout } from '../common/edit_burn_rate_rule_flyout'; import { SloDeleteConfirmationModal } from '../../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal'; import { BurnRateRuleParams } from '../../../../typings'; import { useKibana } from '../../../../utils/kibana_react'; @@ -43,7 +44,7 @@ export interface Props { activeAlerts?: number; loading: boolean; error: boolean; - cardsPerRow: number; + refetchRules: () => void; } export const useSloCardColor = (status?: SLOWithSummaryResponse['summary']['status']) => { @@ -67,12 +68,13 @@ const getFirstGroupBy = (slo: SLOWithSummaryResponse) => { return slo.groupBy && ![slo.groupBy].flat().includes(ALL_VALUE) ? firstGroupBy : ''; }; -export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, cardsPerRow }: Props) { +export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, refetchRules }: Props) { const containerRef = React.useRef(null); const [isMouseOver, setIsMouseOver] = useState(false); const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); const [isAddRuleFlyoutOpen, setIsAddRuleFlyoutOpen] = useState(false); + const [isEditRuleFlyoutOpen, setIsEditRuleFlyoutOpen] = useState(false); const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false); const [isDashboardAttachmentReady, setDashboardAttachmentReady] = useState(false); const historicalSliData = formatHistoricalData(historicalSummary, 'sli_value'); @@ -124,10 +126,12 @@ export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, cards {(isMouseOver || isActionsPopoverOpen) && ( )} @@ -139,6 +143,13 @@ export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, cards setIsAddRuleFlyoutOpen={setIsAddRuleFlyoutOpen} /> + + {isDeleteConfirmationModalOpen ? ( void; setDeleteConfirmationModalOpen: (value: boolean) => void; setIsAddRuleFlyoutOpen: (value: boolean) => void; + setIsEditRuleFlyoutOpen: (value: boolean) => void; setDashboardAttachmentReady: (value: boolean) => void; + rules?: Array>; } export function SloCardItemActions(props: Props) { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item_badges.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item_badges.tsx index 078a706dcb52a..39eb9f4e0ce18 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item_badges.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item_badges.tsx @@ -56,8 +56,8 @@ export function SloCardItemBadges({ slo, activeAlerts, rules, handleCreateRule } <> - + [slo.id, slo.instanceId ?? ALL_VALUE] as [string, string] ); const { data: activeAlertsBySlo } = useFetchActiveAlerts({ sloIdsAndInstanceIds }); - const { data: rulesBySlo } = useFetchRulesForSlo({ + const { data: rulesBySlo, refetchRules } = useFetchRulesForSlo({ sloIds: sloIdsAndInstanceIds.map((item) => item[0]), }); const { isLoading: historicalSummaryLoading, data: historicalSummaries = [] } = @@ -82,7 +82,7 @@ export function SloListCardView({ sloList, loading, error }: Props) { )?.data } historicalSummaryLoading={historicalSummaryLoading} - cardsPerRow={Number(columns)} + refetchRules={refetchRules} /> ))} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/edit_burn_rate_rule_flyout.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/edit_burn_rate_rule_flyout.tsx new file mode 100644 index 0000000000000..1a8577e4a83fe --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/edit_burn_rate_rule_flyout.tsx @@ -0,0 +1,40 @@ +/* + * 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 React from 'react'; +import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; +import { useKibana } from '../../../../utils/kibana_react'; +import { BurnRateRuleParams } from '../../../../typings'; + +export function EditBurnRateRuleFlyout({ + refetchRules, + rule, + isEditRuleFlyoutOpen, + setIsEditRuleFlyoutOpen, +}: { + rule?: Rule; + isEditRuleFlyoutOpen: boolean; + setIsEditRuleFlyoutOpen: (value: boolean) => void; + refetchRules: () => void; +}) { + const { + triggersActionsUi: { getEditRuleFlyout: EditRuleFlyout }, + } = useKibana().services; + + const handleSavedRule = async () => { + refetchRules(); + setIsEditRuleFlyoutOpen(false); + }; + + const handleCloseRuleFlyout = async () => { + setIsEditRuleFlyoutOpen(false); + }; + + return isEditRuleFlyoutOpen && rule ? ( + + ) : null; +} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx index 26f4ba28546c2..97b55852d180c 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx @@ -19,6 +19,8 @@ import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import styled from 'styled-components'; import { RulesParams } from '@kbn/observability-plugin/public'; import { rulesLocatorID } from '@kbn/observability-plugin/common'; +import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; +import { BurnRateRuleParams } from '../../../typings'; import { useKibana } from '../../../utils/kibana_react'; import { useCloneSlo } from '../../../hooks/use_clone_slo'; import { useCapabilities } from '../../../hooks/use_capabilities'; @@ -30,8 +32,10 @@ interface Props { setIsActionsPopoverOpen: (value: boolean) => void; setDeleteConfirmationModalOpen: (value: boolean) => void; setIsAddRuleFlyoutOpen: (value: boolean) => void; + setIsEditRuleFlyoutOpen: (value: boolean) => void; setDashboardAttachmentReady?: (value: boolean) => void; btnProps?: Partial; + rules?: Array>; } const CustomShadowPanel = styled(EuiPanel)<{ shadow: string }>` ${(props) => props.shadow} @@ -56,9 +60,11 @@ function IconPanel({ children, hasPanel }: { children: JSX.Element; hasPanel: bo export function SloItemActions({ slo, + rules, isActionsPopoverOpen, setIsActionsPopoverOpen, setIsAddRuleFlyoutOpen, + setIsEditRuleFlyoutOpen, setDeleteConfirmationModalOpen, setDashboardAttachmentReady, btnProps, @@ -98,8 +104,14 @@ export function SloItemActions({ }; const handleNavigateToRules = async () => { - const locator = locators.get(rulesLocatorID); - locator?.navigate({ params: { sloId: slo.id } }, { replace: false }); + if (rules?.length === 1) { + // if there is only one rule we can edit inline in flyout + setIsEditRuleFlyoutOpen(true); + setIsActionsPopoverOpen(false); + } else { + const locator = locators.get(rulesLocatorID); + locator?.navigate({ params: { sloId: slo.id } }, { replace: false }); + } }; const handleDelete = () => { @@ -185,8 +197,9 @@ export function SloItemActions({ onClick={handleNavigateToRules} data-test-subj="sloActionsManageRules" > - {i18n.translate('xpack.slo.item.actions.manageRules', { - defaultMessage: 'Manage rules', + {i18n.translate('xpack.slo.item.actions.manageBurnRateRules', { + defaultMessage: 'Manage burn rate {count, plural, one {rule} other {rules}}', + values: { count: rules?.length ?? 0 }, })} , > | undefined; + rules?: Array>; historicalSummary?: HistoricalSummaryResponse[]; historicalSummaryLoading: boolean; activeAlerts?: number; + refetchRules: () => void; } export function SloListItem({ slo, rules, + refetchRules, historicalSummary = [], historicalSummaryLoading, activeAlerts, }: SloListItemProps) { const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); const [isAddRuleFlyoutOpen, setIsAddRuleFlyoutOpen] = useState(false); + const [isEditRuleFlyoutOpen, setIsEditRuleFlyoutOpen] = useState(false); const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false); const { sloDetailsUrl } = useSloFormattedSummary(slo); @@ -95,8 +99,10 @@ export function SloListItem({ @@ -108,6 +114,13 @@ export function SloListItem({ setIsAddRuleFlyoutOpen={setIsAddRuleFlyoutOpen} /> + + {isDeleteConfirmationModalOpen ? ( [slo.id, slo.instanceId ?? ALL_VALUE] as [string, string] ); const { data: activeAlertsBySlo } = useFetchActiveAlerts({ sloIdsAndInstanceIds }); - const { data: rulesBySlo } = useFetchRulesForSlo({ + const { data: rulesBySlo, refetchRules } = useFetchRulesForSlo({ sloIds: sloIdsAndInstanceIds.map((item) => item[0]), }); const { isLoading: historicalSummaryLoading, data: historicalSummaries = [] } = @@ -58,6 +58,7 @@ export function SloListView({ sloList, loading, error }: Props) { } historicalSummaryLoading={historicalSummaryLoading} slo={slo} + refetchRules={refetchRules} /> ))}