[8.12] [SLOs] Fix cloning SLO by opening pre filled form (#172927) (#173504)

# Backport

This will backport the following commits from `main` to `8.12`:
- [[SLOs] Fix cloning SLO by opening pre filled form
(#172927)](https://github.com/elastic/kibana/pull/172927)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT
[{"author":{"name":"Shahzad","email":"shahzad31comp@gmail.com"},"sourceCommit":{"committedDate":"2023-12-18T10:52:32Z","message":"[SLOs]
Fix cloning SLO by opening pre filled form
(#172927)","sha":"3419469e39e0bb4443267bc80fb2500629c772ba","branchLabelMapping":{"^v8.13.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:prev-minor","Team:obs-ux-management","v8.13.0"],"number":172927,"url":"https://github.com/elastic/kibana/pull/172927","mergeCommit":{"message":"[SLOs]
Fix cloning SLO by opening pre filled form
(#172927)","sha":"3419469e39e0bb4443267bc80fb2500629c772ba"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.13.0","labelRegex":"^v8.13.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/172927","number":172927,"mergeCommit":{"message":"[SLOs]
Fix cloning SLO by opening pre filled form
(#172927)","sha":"3419469e39e0bb4443267bc80fb2500629c772ba"}}]}]
BACKPORT-->

Co-authored-by: Shahzad <shahzad31comp@gmail.com>
This commit is contained in:
Kibana Machine 2023-12-18 07:16:28 -05:00 committed by GitHub
parent dab8881b8f
commit 292b615297
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 109 additions and 212 deletions

View file

@ -112,7 +112,7 @@ export function SloOverview({
return ( return (
<div ref={containerRef} style={{ width: '100%' }}> <div ref={containerRef} style={{ width: '100%' }}>
<SloCardChart slo={slo} historicalSliData={historicalSliData ?? []} cardsPerRow={4} /> <SloCardChart slo={slo} historicalSliData={historicalSliData ?? []} />
<SloCardBadgesPortal containerRef={containerRef}> <SloCardBadgesPortal containerRef={containerRef}>
<SloCardItemBadges <SloCardItemBadges
slo={slo} slo={slo}

View file

@ -5,83 +5,28 @@
* 2.0. * 2.0.
*/ */
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import { encode } from '@kbn/rison';
import { i18n } from '@kbn/i18n'; import { useCallback } from 'react';
import type { CreateSLOInput, CreateSLOResponse, FindSLOResponse } from '@kbn/slo-schema'; import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import { QueryKey, useMutation, useQueryClient } from '@tanstack/react-query'; import { paths } from '../../../common/locators/paths';
import { v4 as uuidv4 } from 'uuid';
import { useKibana } from '../../utils/kibana_react'; import { useKibana } from '../../utils/kibana_react';
import { sloKeys } from './query_key_factory';
type ServerError = IHttpFetchError<ResponseErrorBody>;
export function useCloneSlo() { export function useCloneSlo() {
const { const {
http, http: { basePath },
notifications: { toasts }, application: { navigateToUrl },
} = useKibana().services; } = useKibana().services;
const queryClient = useQueryClient();
return useMutation< return useCallback(
CreateSLOResponse, (slo: SLOWithSummaryResponse) => {
ServerError, navigateToUrl(
{ slo: CreateSLOInput; originalSloId?: string }, basePath.prepend(
{ previousData?: FindSLOResponse; queryKey?: QueryKey } paths.observability.sloCreateWithEncodedForm(
>( encode({ ...slo, name: `[Copy] ${slo.name}`, id: undefined })
['cloneSlo'], )
({ slo }: { slo: CreateSLOInput; originalSloId?: string }) => { )
const body = JSON.stringify(slo); );
return http.post<CreateSLOResponse>(`/api/observability/slos`, { body });
}, },
{ [navigateToUrl, basePath]
onMutate: async ({ slo, originalSloId }) => {
await queryClient.cancelQueries({ queryKey: sloKeys.lists(), exact: false });
const queriesData = queryClient.getQueriesData<FindSLOResponse>({
queryKey: sloKeys.lists(),
exact: false,
});
const [queryKey, previousData] = queriesData?.at(0) ?? [];
const originalSlo = previousData?.results?.find((el) => el.id === originalSloId);
const optimisticUpdate = {
page: previousData?.page ?? 1,
perPage: previousData?.perPage ?? 25,
total: previousData?.total ? previousData.total + 1 : 1,
results: [
...(previousData?.results ?? []),
{ ...originalSlo, name: slo.name, id: uuidv4(), summary: undefined },
],
};
if (queryKey) {
queryClient.setQueryData(queryKey, optimisticUpdate);
}
return { queryKey, previousData };
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (error, { slo }, context) => {
if (context?.previousData && context?.queryKey) {
queryClient.setQueryData(context.queryKey, context.previousData);
}
toasts.addError(new Error(error.body?.message ?? error.message), {
title: i18n.translate('xpack.observability.slo.clone.errorNotification', {
defaultMessage: 'Failed to clone {name}',
values: { name: slo.name },
}),
});
},
onSuccess: (_data, { slo }) => {
toasts.addSuccess(
i18n.translate('xpack.observability.slo.clone.successNotification', {
defaultMessage: 'Successfully created {name}',
values: { name: slo.name },
})
);
queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false });
},
}
); );
} }

View file

@ -10,20 +10,16 @@ import { i18n } from '@kbn/i18n';
import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import { useCloneSlo } from '../../../hooks/slo/use_clone_slo';
import { SloDeleteConfirmationModal } from '../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal'; import { SloDeleteConfirmationModal } from '../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal';
import { useCapabilities } from '../../../hooks/slo/use_capabilities'; import { useCapabilities } from '../../../hooks/slo/use_capabilities';
import { useKibana } from '../../../utils/kibana_react'; import { useKibana } from '../../../utils/kibana_react';
import { useCloneSlo } from '../../../hooks/slo/use_clone_slo';
import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo'; import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo';
import { isApmIndicatorType } from '../../../utils/slo/indicator'; import { isApmIndicatorType } from '../../../utils/slo/indicator';
import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url'; import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url';
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants'; import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants';
import { rulesLocatorID, sloFeatureId } from '../../../../common'; import { rulesLocatorID, sloFeatureId } from '../../../../common';
import { paths } from '../../../../common/locators/paths'; import { paths } from '../../../../common/locators/paths';
import {
transformSloResponseToCreateSloForm,
transformCreateSLOFormToCreateSLOInput,
} from '../../slo_edit/helpers/process_slo_form_values';
import type { RulesParams } from '../../../locators/rules'; import type { RulesParams } from '../../../locators/rules';
export interface Props { export interface Props {
@ -47,7 +43,6 @@ export function HeaderControl({ isLoading, slo }: Props) {
const [isRuleFlyoutVisible, setRuleFlyoutVisibility] = useState<boolean>(false); const [isRuleFlyoutVisible, setRuleFlyoutVisibility] = useState<boolean>(false);
const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false); const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false);
const { mutate: cloneSlo } = useCloneSlo();
const { mutate: deleteSlo } = useDeleteSlo(); const { mutate: deleteSlo } = useDeleteSlo();
const handleActionsClick = () => setIsPopoverOpen((value) => !value); const handleActionsClick = () => setIsPopoverOpen((value) => !value);
@ -101,17 +96,12 @@ export function HeaderControl({ isLoading, slo }: Props) {
} }
}; };
const navigateToClone = useCloneSlo();
const handleClone = async () => { const handleClone = async () => {
if (slo) { if (slo) {
setIsPopoverOpen(false); setIsPopoverOpen(false);
navigateToClone(slo);
const newSlo = transformCreateSLOFormToCreateSLOInput(
transformSloResponseToCreateSloForm({ ...slo, name: `[Copy] ${slo.name}` })!
);
cloneSlo({ slo: newSlo, originalSloId: slo.id });
navigate(basePath.prepend(paths.observability.slos));
} }
}; };

View file

@ -17,7 +17,6 @@ import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details';
import { useFetchHistoricalSummary } from '../../hooks/slo/use_fetch_historical_summary'; import { useFetchHistoricalSummary } from '../../hooks/slo/use_fetch_historical_summary';
import { useFetchActiveAlerts } from '../../hooks/slo/use_fetch_active_alerts'; import { useFetchActiveAlerts } from '../../hooks/slo/use_fetch_active_alerts';
import { ActiveAlerts } from '../../hooks/slo/active_alerts'; import { ActiveAlerts } from '../../hooks/slo/active_alerts';
import { useCloneSlo } from '../../hooks/slo/use_clone_slo';
import { useDeleteSlo } from '../../hooks/slo/use_delete_slo'; import { useDeleteSlo } from '../../hooks/slo/use_delete_slo';
import { render } from '../../utils/test_helper'; import { render } from '../../utils/test_helper';
import { SloDetailsPage } from './slo_details'; import { SloDetailsPage } from './slo_details';
@ -30,6 +29,7 @@ import {
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { buildApmAvailabilityIndicator } from '../../data/slo/indicator'; import { buildApmAvailabilityIndicator } from '../../data/slo/indicator';
import { ALL_VALUE } from '@kbn/slo-schema'; import { ALL_VALUE } from '@kbn/slo-schema';
import { encode } from '@kbn/rison';
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
@ -44,7 +44,6 @@ jest.mock('../../hooks/slo/use_capabilities');
jest.mock('../../hooks/slo/use_fetch_active_alerts'); jest.mock('../../hooks/slo/use_fetch_active_alerts');
jest.mock('../../hooks/slo/use_fetch_slo_details'); jest.mock('../../hooks/slo/use_fetch_slo_details');
jest.mock('../../hooks/slo/use_fetch_historical_summary'); jest.mock('../../hooks/slo/use_fetch_historical_summary');
jest.mock('../../hooks/slo/use_clone_slo');
jest.mock('../../hooks/slo/use_delete_slo'); jest.mock('../../hooks/slo/use_delete_slo');
const useKibanaMock = useKibana as jest.Mock; const useKibanaMock = useKibana as jest.Mock;
@ -55,12 +54,10 @@ const useCapabilitiesMock = useCapabilities as jest.Mock;
const useFetchActiveAlertsMock = useFetchActiveAlerts as jest.Mock; const useFetchActiveAlertsMock = useFetchActiveAlerts as jest.Mock;
const useFetchSloDetailsMock = useFetchSloDetails as jest.Mock; const useFetchSloDetailsMock = useFetchSloDetails as jest.Mock;
const useFetchHistoricalSummaryMock = useFetchHistoricalSummary as jest.Mock; const useFetchHistoricalSummaryMock = useFetchHistoricalSummary as jest.Mock;
const useCloneSloMock = useCloneSlo as jest.Mock;
const useDeleteSloMock = useDeleteSlo as jest.Mock; const useDeleteSloMock = useDeleteSlo as jest.Mock;
const mockNavigate = jest.fn(); const mockNavigate = jest.fn();
const mockLocator = jest.fn(); const mockLocator = jest.fn();
const mockClone = jest.fn();
const mockDelete = jest.fn(); const mockDelete = jest.fn();
const mockCapabilities = { const mockCapabilities = {
apm: { show: true }, apm: { show: true },
@ -120,7 +117,6 @@ describe('SLO Details Page', () => {
data: historicalSummaryData, data: historicalSummaryData,
}); });
useFetchActiveAlertsMock.mockReturnValue({ isLoading: false, data: new ActiveAlerts() }); useFetchActiveAlertsMock.mockReturnValue({ isLoading: false, data: new ActiveAlerts() });
useCloneSloMock.mockReturnValue({ mutate: mockClone });
useDeleteSloMock.mockReturnValue({ mutate: mockDelete }); useDeleteSloMock.mockReturnValue({ mutate: mockDelete });
useLocationMock.mockReturnValue({ search: '' }); useLocationMock.mockReturnValue({ search: '' });
}); });
@ -248,29 +244,12 @@ describe('SLO Details Page', () => {
fireEvent.click(button!); fireEvent.click(button!);
const {
id,
createdAt,
enabled,
revision,
summary,
settings,
updatedAt,
instanceId,
version,
...newSlo
} = slo;
expect(mockClone).toBeCalledWith({
originalSloId: slo.id,
slo: {
...newSlo,
name: `[Copy] ${newSlo.name}`,
},
});
await waitFor(() => { await waitFor(() => {
expect(mockNavigate).toBeCalledWith(paths.observability.slos); expect(mockNavigate).toBeCalledWith(
paths.observability.sloCreateWithEncodedForm(
encode({ ...slo, name: `[Copy] ${slo.name}`, id: undefined })
)
);
}); });
}); });

View file

@ -18,8 +18,7 @@ import { i18n } from '@kbn/i18n';
import type { GetSLOResponse } from '@kbn/slo-schema'; import type { GetSLOResponse } from '@kbn/slo-schema';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { sloFeatureId } from '../../../../common'; import { BurnRateRuleFlyout } from '../../slos/components/common/burn_rate_rule_flyout';
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants';
import { paths } from '../../../../common/locators/paths'; import { paths } from '../../../../common/locators/paths';
import { useCreateSlo } from '../../../hooks/slo/use_create_slo'; import { useCreateSlo } from '../../../hooks/slo/use_create_slo';
import { useFetchRulesForSlo } from '../../../hooks/slo/use_fetch_rules_for_slo'; import { useFetchRulesForSlo } from '../../../hooks/slo/use_fetch_rules_for_slo';
@ -54,7 +53,6 @@ export function SloEditForm({ slo }: Props) {
const { const {
application: { navigateToUrl }, application: { navigateToUrl },
http: { basePath }, http: { basePath },
triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout },
} = useKibana().services; } = useKibana().services;
const isEditMode = slo !== undefined; const isEditMode = slo !== undefined;
@ -146,10 +144,6 @@ export function SloEditForm({ slo }: Props) {
setIsCreateRuleCheckboxChecked(!isCreateRuleCheckboxChecked); setIsCreateRuleCheckboxChecked(!isCreateRuleCheckboxChecked);
}; };
const handleCloseRuleFlyout = async () => {
navigateToUrl(basePath.prepend(paths.observability.slos));
};
return ( return (
<> <>
<FormProvider {...methods}> <FormProvider {...methods}>
@ -256,17 +250,11 @@ export function SloEditForm({ slo }: Props) {
</EuiFlexGroup> </EuiFlexGroup>
</FormProvider> </FormProvider>
{isAddRuleFlyoutOpen && slo ? ( <BurnRateRuleFlyout
<AddRuleFlyout slo={slo as GetSLOResponse}
canChangeTrigger={false} isAddRuleFlyoutOpen={isAddRuleFlyoutOpen}
consumer={sloFeatureId} canChangeTrigger={false}
initialValues={{ name: `${watch('name')} burn rate rule`, params: { sloId: slo.id } }} />
ruleTypeId={SLO_BURN_RATE_RULE_TYPE_ID}
onClose={handleCloseRuleFlyout}
onSave={handleCloseRuleFlyout}
useRuleProducer
/>
) : null}
</> </>
); );
} }

View file

@ -33,7 +33,7 @@ export function SloEditPage() {
const { sloId } = useParams<{ sloId: string | undefined }>(); const { sloId } = useParams<{ sloId: string | undefined }>();
const { hasAtLeast } = useLicense(); const { hasAtLeast } = useLicense();
const hasRightLicense = hasAtLeast('platinum'); const hasRightLicense = hasAtLeast('platinum');
const { data: slo, isInitialLoading } = useFetchSloDetails({ sloId }); const { data: slo } = useFetchSloDetails({ sloId });
useBreadcrumbs([ useBreadcrumbs([
{ {
@ -66,10 +66,6 @@ export function SloEditPage() {
navigateToUrl(basePath.prepend(paths.observability.slos)); navigateToUrl(basePath.prepend(paths.observability.slos));
} }
if (sloId && isInitialLoading) {
return null;
}
return ( return (
<ObservabilityPageTemplate <ObservabilityPageTemplate
pageHeader={{ pageHeader={{

View file

@ -19,6 +19,7 @@ import { EuiIcon, EuiPanel, useEuiBackgroundColor } from '@elastic/eui';
import { ALL_VALUE, HistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { ALL_VALUE, HistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import { SloCardBadgesPortal } from './badges_portal'; import { SloCardBadgesPortal } from './badges_portal';
import { useSloListActions } from '../../hooks/use_slo_list_actions'; import { useSloListActions } from '../../hooks/use_slo_list_actions';
import { BurnRateRuleFlyout } from '../common/burn_rate_rule_flyout'; import { BurnRateRuleFlyout } from '../common/burn_rate_rule_flyout';
@ -52,7 +53,7 @@ export const useSloCardColor = (status?: SLOWithSummaryResponse['summary']['stat
return colors[status ?? 'NO_DATA']; return colors[status ?? 'NO_DATA'];
}; };
const getSubTitle = (slo: SLOWithSummaryResponse, cardsPerRow: number) => { const getSubTitle = (slo: SLOWithSummaryResponse) => {
return slo.groupBy && slo.groupBy !== ALL_VALUE ? `${slo.groupBy}: ${slo.instanceId}` : ''; return slo.groupBy && slo.groupBy !== ALL_VALUE ? `${slo.groupBy}: ${slo.instanceId}` : '';
}; };
@ -88,14 +89,14 @@ export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, cards
} }
}} }}
paddingSize="none" paddingSize="none"
style={{ css={css`
height: '182px', height: 182px;
overflow: 'hidden', overflow: hidden;
position: 'relative', position: relative;
}} `}
title={slo.summary.status} title={slo.summary.status}
> >
<SloCardChart slo={slo} historicalSliData={historicalSliData} cardsPerRow={cardsPerRow} /> <SloCardChart slo={slo} historicalSliData={historicalSliData} />
{(isMouseOver || isActionsPopoverOpen) && ( {(isMouseOver || isActionsPopoverOpen) && (
<SloCardItemActions <SloCardItemActions
slo={slo} slo={slo}
@ -135,11 +136,9 @@ export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, cards
export function SloCardChart({ export function SloCardChart({
slo, slo,
cardsPerRow,
historicalSliData, historicalSliData,
}: { }: {
slo: SLOWithSummaryResponse; slo: SLOWithSummaryResponse;
cardsPerRow: number;
historicalSliData?: Array<{ key?: number; value?: number }>; historicalSliData?: Array<{ key?: number; value?: number }>;
}) { }) {
const { const {
@ -147,7 +146,7 @@ export function SloCardChart({
} = useKibana().services; } = useKibana().services;
const cardColor = useSloCardColor(slo.summary.status); const cardColor = useSloCardColor(slo.summary.status);
const subTitle = getSubTitle(slo, cardsPerRow); const subTitle = getSubTitle(slo);
const { sliValue, sloTarget, sloDetailsUrl } = useSloFormattedSummary(slo); const { sliValue, sloTarget, sloDetailsUrl } = useSloFormattedSummary(slo);
return ( return (

View file

@ -68,27 +68,29 @@ export function SloListCardView({
} }
return ( return (
<EuiFlexGrid columns={columns}> <EuiFlexGrid columns={columns} gutterSize="m">
{sloList.map((slo) => ( {sloList
<EuiFlexItem key={`${slo.id}-${slo.instanceId ?? 'ALL_VALUE'}`}> .filter((slo) => slo.summary)
<SloCardItem .map((slo) => (
slo={slo} <EuiFlexItem key={`${slo.id}-${slo.instanceId ?? 'ALL_VALUE'}`}>
loading={loading} <SloCardItem
error={error} slo={slo}
activeAlerts={activeAlertsBySlo.get(slo)} loading={loading}
rules={rulesBySlo?.[slo.id]} error={error}
historicalSummary={ activeAlerts={activeAlertsBySlo.get(slo)}
historicalSummaries.find( rules={rulesBySlo?.[slo.id]}
(historicalSummary) => historicalSummary={
historicalSummary.sloId === slo.id && historicalSummaries.find(
historicalSummary.instanceId === (slo.instanceId ?? ALL_VALUE) (historicalSummary) =>
)?.data historicalSummary.sloId === slo.id &&
} historicalSummary.instanceId === (slo.instanceId ?? ALL_VALUE)
historicalSummaryLoading={historicalSummaryLoading} )?.data
cardsPerRow={Number(cardsPerRow)} }
/> historicalSummaryLoading={historicalSummaryLoading}
</EuiFlexItem> cardsPerRow={Number(cardsPerRow)}
))} />
</EuiFlexItem>
))}
</EuiFlexGrid> </EuiFlexGrid>
); );
} }

View file

@ -8,6 +8,7 @@
import React from 'react'; import React from 'react';
import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { paths } from '../../../../../common/locators/paths';
import { useGetFilteredRuleTypes } from '../../../../hooks/use_get_filtered_rule_types'; import { useGetFilteredRuleTypes } from '../../../../hooks/use_get_filtered_rule_types';
import { sloKeys } from '../../../../hooks/slo/query_key_factory'; import { sloKeys } from '../../../../hooks/slo/query_key_factory';
import { useKibana } from '../../../../utils/kibana_react'; import { useKibana } from '../../../../utils/kibana_react';
@ -17,13 +18,17 @@ import { sloFeatureId } from '../../../../../common';
export function BurnRateRuleFlyout({ export function BurnRateRuleFlyout({
slo, slo,
isAddRuleFlyoutOpen, isAddRuleFlyoutOpen,
canChangeTrigger,
setIsAddRuleFlyoutOpen, setIsAddRuleFlyoutOpen,
}: { }: {
slo: SLOWithSummaryResponse; slo?: SLOWithSummaryResponse;
isAddRuleFlyoutOpen: boolean; isAddRuleFlyoutOpen: boolean;
setIsAddRuleFlyoutOpen: (value: boolean) => void; canChangeTrigger?: boolean;
setIsAddRuleFlyoutOpen?: (value: boolean) => void;
}) { }) {
const { const {
application: { navigateToUrl },
http: { basePath },
triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout }, triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout },
} = useKibana().services; } = useKibana().services;
@ -32,19 +37,30 @@ export function BurnRateRuleFlyout({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const handleSavedRule = async () => { const handleSavedRule = async () => {
queryClient.invalidateQueries({ queryKey: sloKeys.rules(), exact: false }); if (setIsAddRuleFlyoutOpen) {
queryClient.invalidateQueries({ queryKey: sloKeys.rules(), exact: false });
} else {
navigateToUrl(basePath.prepend(paths.observability.slos));
}
}; };
return isAddRuleFlyoutOpen ? ( const handleCloseRuleFlyout = async () => {
if (setIsAddRuleFlyoutOpen) {
setIsAddRuleFlyoutOpen(false);
} else {
navigateToUrl(basePath.prepend(paths.observability.slos));
}
};
return isAddRuleFlyoutOpen && slo ? (
<AddRuleFlyout <AddRuleFlyout
canChangeTrigger={canChangeTrigger}
consumer={sloFeatureId} consumer={sloFeatureId}
filteredRuleTypes={filteredRuleTypes} filteredRuleTypes={filteredRuleTypes}
ruleTypeId={SLO_BURN_RATE_RULE_TYPE_ID} ruleTypeId={SLO_BURN_RATE_RULE_TYPE_ID}
initialValues={{ name: `${slo.name} Burn Rate rule`, params: { sloId: slo.id } }} initialValues={{ name: `${slo.name} Burn Rate rule`, params: { sloId: slo.id } }}
onSave={handleSavedRule} onSave={handleSavedRule}
onClose={() => { onClose={handleCloseRuleFlyout}
setIsAddRuleFlyoutOpen(false);
}}
useRuleProducer useRuleProducer
/> />
) : null; ) : null;

View file

@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n';
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useCloneSlo } from '../../../../hooks/slo/use_clone_slo';
import { rulesLocatorID, sloFeatureId } from '../../../../../common'; import { rulesLocatorID, sloFeatureId } from '../../../../../common';
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../../common/constants'; import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../../common/constants';
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
@ -28,7 +29,6 @@ import { SloStatusBadge } from '../../../../components/slo/slo_status_badge';
import { SloActiveAlertsBadge } from '../../../../components/slo/slo_status_badge/slo_active_alerts_badge'; import { SloActiveAlertsBadge } from '../../../../components/slo/slo_status_badge/slo_active_alerts_badge';
import { sloKeys } from '../../../../hooks/slo/query_key_factory'; import { sloKeys } from '../../../../hooks/slo/query_key_factory';
import { useCapabilities } from '../../../../hooks/slo/use_capabilities'; import { useCapabilities } from '../../../../hooks/slo/use_capabilities';
import { useCloneSlo } from '../../../../hooks/slo/use_clone_slo';
import { useDeleteSlo } from '../../../../hooks/slo/use_delete_slo'; import { useDeleteSlo } from '../../../../hooks/slo/use_delete_slo';
import { useFetchActiveAlerts } from '../../../../hooks/slo/use_fetch_active_alerts'; import { useFetchActiveAlerts } from '../../../../hooks/slo/use_fetch_active_alerts';
import { useFetchHistoricalSummary } from '../../../../hooks/slo/use_fetch_historical_summary'; import { useFetchHistoricalSummary } from '../../../../hooks/slo/use_fetch_historical_summary';
@ -37,10 +37,6 @@ import { useGetFilteredRuleTypes } from '../../../../hooks/use_get_filtered_rule
import { RulesParams } from '../../../../locators/rules'; import { RulesParams } from '../../../../locators/rules';
import { useKibana } from '../../../../utils/kibana_react'; import { useKibana } from '../../../../utils/kibana_react';
import { formatHistoricalData } from '../../../../utils/slo/chart_data_formatter'; import { formatHistoricalData } from '../../../../utils/slo/chart_data_formatter';
import {
transformCreateSLOFormToCreateSLOInput,
transformSloResponseToCreateSloForm,
} from '../../../slo_edit/helpers/process_slo_form_values';
import { SloRulesBadge } from '../badges/slo_rules_badge'; import { SloRulesBadge } from '../badges/slo_rules_badge';
import { SloListEmpty } from '../slo_list_empty'; import { SloListEmpty } from '../slo_list_empty';
import { SloListError } from '../slo_list_error'; import { SloListError } from '../slo_list_error';
@ -72,7 +68,6 @@ export function SloListCompactView({ sloList, loading, error }: Props) {
const filteredRuleTypes = useGetFilteredRuleTypes(); const filteredRuleTypes = useGetFilteredRuleTypes();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { mutate: cloneSlo } = useCloneSlo();
const { mutate: deleteSlo } = useDeleteSlo(); const { mutate: deleteSlo } = useDeleteSlo();
const [sloToAddRule, setSloToAddRule] = useState<SLOWithSummaryResponse | undefined>(undefined); const [sloToAddRule, setSloToAddRule] = useState<SLOWithSummaryResponse | undefined>(undefined);
@ -102,6 +97,8 @@ export function SloListCompactView({ sloList, loading, error }: Props) {
list: sloList.map((slo) => ({ sloId: slo.id, instanceId: slo.instanceId ?? ALL_VALUE })), list: sloList.map((slo) => ({ sloId: slo.id, instanceId: slo.instanceId ?? ALL_VALUE })),
}); });
const navigateToClone = useCloneSlo();
const actions: Array<DefaultItemAction<SLOWithSummaryResponse>> = [ const actions: Array<DefaultItemAction<SLOWithSummaryResponse>> = [
{ {
type: 'icon', type: 'icon',
@ -180,11 +177,7 @@ export function SloListCompactView({ sloList, loading, error }: Props) {
'data-test-subj': 'sloActionsClone', 'data-test-subj': 'sloActionsClone',
enabled: (_) => hasWriteCapabilities, enabled: (_) => hasWriteCapabilities,
onClick: (slo: SLOWithSummaryResponse) => { onClick: (slo: SLOWithSummaryResponse) => {
const newSlo = transformCreateSLOFormToCreateSLOInput( navigateToClone(slo);
transformSloResponseToCreateSloForm({ ...slo, name: `[Copy] ${slo.name}` })!
);
cloneSlo({ slo: newSlo, originalSloId: slo.id });
}, },
}, },
{ {

View file

@ -17,16 +17,12 @@ import { i18n } from '@kbn/i18n';
import React from 'react'; import React from 'react';
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
import styled from 'styled-components'; import styled from 'styled-components';
import { useCapabilities } from '../../../hooks/slo/use_capabilities';
import { useCloneSlo } from '../../../hooks/slo/use_clone_slo'; import { useCloneSlo } from '../../../hooks/slo/use_clone_slo';
import { useCapabilities } from '../../../hooks/slo/use_capabilities';
import { useKibana } from '../../../utils/kibana_react'; import { useKibana } from '../../../utils/kibana_react';
import { paths } from '../../../../common/locators/paths'; import { paths } from '../../../../common/locators/paths';
import { RulesParams } from '../../../locators/rules'; import { RulesParams } from '../../../locators/rules';
import { rulesLocatorID } from '../../../../common'; import { rulesLocatorID } from '../../../../common';
import {
transformCreateSLOFormToCreateSLOInput,
transformSloResponseToCreateSloForm,
} from '../../slo_edit/helpers/process_slo_form_values';
interface Props { interface Props {
slo: SLOWithSummaryResponse; slo: SLOWithSummaryResponse;
@ -73,7 +69,6 @@ export function SloItemActions({
}, },
} = useKibana().services; } = useKibana().services;
const { hasWriteCapabilities } = useCapabilities(); const { hasWriteCapabilities } = useCapabilities();
const { mutate: cloneSlo } = useCloneSlo();
const sloDetailsUrl = basePath.prepend( const sloDetailsUrl = basePath.prepend(
paths.observability.sloDetails( paths.observability.sloDetails(
@ -94,20 +89,17 @@ export function SloItemActions({
navigateToUrl(basePath.prepend(paths.observability.sloEdit(slo.id))); navigateToUrl(basePath.prepend(paths.observability.sloEdit(slo.id)));
}; };
const navigateToClone = useCloneSlo();
const handleClone = () => {
navigateToClone(slo);
};
const handleNavigateToRules = async () => { const handleNavigateToRules = async () => {
const locator = locators.get<RulesParams>(rulesLocatorID); const locator = locators.get<RulesParams>(rulesLocatorID);
locator?.navigate({ params: { sloId: slo.id } }, { replace: false }); locator?.navigate({ params: { sloId: slo.id } }, { replace: false });
}; };
const handleClone = () => {
const newSlo = transformCreateSLOFormToCreateSLOInput(
transformSloResponseToCreateSloForm({ ...slo, name: `[Copy] ${slo.name}` })!
);
cloneSlo({ slo: newSlo, originalSloId: slo.id });
setIsActionsPopoverOpen(false);
};
const handleDelete = () => { const handleDelete = () => {
setDeleteConfirmationModalOpen(true); setDeleteConfirmationModalOpen(true);
setIsActionsPopoverOpen(false); setIsActionsPopoverOpen(false);

View file

@ -15,7 +15,6 @@ import { paths } from '../../../common/locators/paths';
import { historicalSummaryData } from '../../data/slo/historical_summary_data'; import { historicalSummaryData } from '../../data/slo/historical_summary_data';
import { emptySloList, sloList } from '../../data/slo/slo'; import { emptySloList, sloList } from '../../data/slo/slo';
import { useCapabilities } from '../../hooks/slo/use_capabilities'; import { useCapabilities } from '../../hooks/slo/use_capabilities';
import { useCloneSlo } from '../../hooks/slo/use_clone_slo';
import { useCreateSlo } from '../../hooks/slo/use_create_slo'; import { useCreateSlo } from '../../hooks/slo/use_create_slo';
import { useDeleteSlo } from '../../hooks/slo/use_delete_slo'; import { useDeleteSlo } from '../../hooks/slo/use_delete_slo';
import { useFetchHistoricalSummary } from '../../hooks/slo/use_fetch_historical_summary'; import { useFetchHistoricalSummary } from '../../hooks/slo/use_fetch_historical_summary';
@ -24,6 +23,7 @@ import { useLicense } from '../../hooks/use_license';
import { useKibana } from '../../utils/kibana_react'; import { useKibana } from '../../utils/kibana_react';
import { render } from '../../utils/test_helper'; import { render } from '../../utils/test_helper';
import { SlosPage } from './slos'; import { SlosPage } from './slos';
import { encode } from '@kbn/rison';
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
@ -35,7 +35,6 @@ jest.mock('../../utils/kibana_react');
jest.mock('../../hooks/use_license'); jest.mock('../../hooks/use_license');
jest.mock('../../hooks/slo/use_fetch_slo_list'); jest.mock('../../hooks/slo/use_fetch_slo_list');
jest.mock('../../hooks/slo/use_create_slo'); jest.mock('../../hooks/slo/use_create_slo');
jest.mock('../../hooks/slo/use_clone_slo');
jest.mock('../../hooks/slo/use_delete_slo'); jest.mock('../../hooks/slo/use_delete_slo');
jest.mock('../../hooks/slo/use_fetch_historical_summary'); jest.mock('../../hooks/slo/use_fetch_historical_summary');
jest.mock('../../hooks/slo/use_capabilities'); jest.mock('../../hooks/slo/use_capabilities');
@ -44,17 +43,14 @@ const useKibanaMock = useKibana as jest.Mock;
const useLicenseMock = useLicense as jest.Mock; const useLicenseMock = useLicense as jest.Mock;
const useFetchSloListMock = useFetchSloList as jest.Mock; const useFetchSloListMock = useFetchSloList as jest.Mock;
const useCreateSloMock = useCreateSlo as jest.Mock; const useCreateSloMock = useCreateSlo as jest.Mock;
const useCloneSloMock = useCloneSlo as jest.Mock;
const useDeleteSloMock = useDeleteSlo as jest.Mock; const useDeleteSloMock = useDeleteSlo as jest.Mock;
const useFetchHistoricalSummaryMock = useFetchHistoricalSummary as jest.Mock; const useFetchHistoricalSummaryMock = useFetchHistoricalSummary as jest.Mock;
const useCapabilitiesMock = useCapabilities as jest.Mock; const useCapabilitiesMock = useCapabilities as jest.Mock;
const mockCreateSlo = jest.fn(); const mockCreateSlo = jest.fn();
const mockCloneSlo = jest.fn();
const mockDeleteSlo = jest.fn(); const mockDeleteSlo = jest.fn();
useCreateSloMock.mockReturnValue({ mutate: mockCreateSlo }); useCreateSloMock.mockReturnValue({ mutate: mockCreateSlo });
useCloneSloMock.mockReturnValue({ mutate: mockCloneSlo });
useDeleteSloMock.mockReturnValue({ mutate: mockDeleteSlo }); useDeleteSloMock.mockReturnValue({ mutate: mockDeleteSlo });
const mockNavigate = jest.fn(); const mockNavigate = jest.fn();
@ -358,7 +354,14 @@ describe('SLOs Page', () => {
button.click(); button.click();
expect(mockCloneSlo).toBeCalled(); await waitFor(() => {
const slo = sloList.results.at(0);
expect(mockNavigate).toBeCalledWith(
paths.observability.sloCreateWithEncodedForm(
encode({ ...slo, name: `[Copy] ${slo!.name}`, id: undefined })
)
);
});
}); });
}); });
}); });

View file

@ -28923,8 +28923,6 @@
"xpack.observability.slo.alerting.burnRate.reasonForInstanceId": "{actionGroupName} : Le taux d'avancement pour le (les) dernier(s) {longWindowDuration} est de {longWindowBurnRate} et pour le (les) dernier(s) {shortWindowDuration} est de {shortWindowBurnRate} pour {instanceId}. Alerter si supérieur à {burnRateThreshold} pour les deux fenêtres", "xpack.observability.slo.alerting.burnRate.reasonForInstanceId": "{actionGroupName} : Le taux d'avancement pour le (les) dernier(s) {longWindowDuration} est de {longWindowBurnRate} et pour le (les) dernier(s) {shortWindowDuration} est de {shortWindowBurnRate} pour {instanceId}. Alerter si supérieur à {burnRateThreshold} pour les deux fenêtres",
"xpack.observability.slo.burnRate.breachedStatustSubtitle": "Au rythme actuel, le budget d'erreur sera épuisé en {hour} heures.", "xpack.observability.slo.burnRate.breachedStatustSubtitle": "Au rythme actuel, le budget d'erreur sera épuisé en {hour} heures.",
"xpack.observability.slo.burnRate.threshold": "Le seuil est {threshold}x", "xpack.observability.slo.burnRate.threshold": "Le seuil est {threshold}x",
"xpack.observability.slo.clone.errorNotification": "Échec du clonage de {name}",
"xpack.observability.slo.clone.successNotification": "{name} créé avec succès",
"xpack.observability.slo.create.errorNotification": "Un problème est survenu lors de la création de {name}", "xpack.observability.slo.create.errorNotification": "Un problème est survenu lors de la création de {name}",
"xpack.observability.slo.create.successNotification": "{name} créé avec succès", "xpack.observability.slo.create.successNotification": "{name} créé avec succès",
"xpack.observability.slo.deleteConfirmationModal.title": "Supprimer {name} ?", "xpack.observability.slo.deleteConfirmationModal.title": "Supprimer {name} ?",

View file

@ -28923,8 +28923,6 @@
"xpack.observability.slo.alerting.burnRate.reasonForInstanceId": "{actionGroupName}:過去{longWindowDuration}のバーンレートは{longWindowBurnRate}、{instanceId}の過去{shortWindowDuration}のバーンレートは{shortWindowBurnRate}です。両期間とも{burnRateThreshold}を超えたらアラート", "xpack.observability.slo.alerting.burnRate.reasonForInstanceId": "{actionGroupName}:過去{longWindowDuration}のバーンレートは{longWindowBurnRate}、{instanceId}の過去{shortWindowDuration}のバーンレートは{shortWindowBurnRate}です。両期間とも{burnRateThreshold}を超えたらアラート",
"xpack.observability.slo.burnRate.breachedStatustSubtitle": "現在のレートでは、エラー予算は{hour}時間後に使い果たされます。", "xpack.observability.slo.burnRate.breachedStatustSubtitle": "現在のレートでは、エラー予算は{hour}時間後に使い果たされます。",
"xpack.observability.slo.burnRate.threshold": "しきい値は{threshold}xです", "xpack.observability.slo.burnRate.threshold": "しきい値は{threshold}xです",
"xpack.observability.slo.clone.errorNotification": "{name}を複製できませんでした",
"xpack.observability.slo.clone.successNotification": "{name}の作成が正常に完了しました",
"xpack.observability.slo.create.errorNotification": "{name}の作成中に問題が発生しました", "xpack.observability.slo.create.errorNotification": "{name}の作成中に問題が発生しました",
"xpack.observability.slo.create.successNotification": "{name}の作成が正常に完了しました", "xpack.observability.slo.create.successNotification": "{name}の作成が正常に完了しました",
"xpack.observability.slo.deleteConfirmationModal.title": "{name}を削除しますか?", "xpack.observability.slo.deleteConfirmationModal.title": "{name}を削除しますか?",

View file

@ -28920,8 +28920,6 @@
"xpack.observability.slo.alerting.burnRate.reasonForInstanceId": "{actionGroupName}:过去 {longWindowDuration} 的消耗速度为 {longWindowBurnRate},且对于 {instanceId},过去 {shortWindowDuration} 为 {shortWindowBurnRate}。两个窗口超出 {burnRateThreshold} 时告警", "xpack.observability.slo.alerting.burnRate.reasonForInstanceId": "{actionGroupName}:过去 {longWindowDuration} 的消耗速度为 {longWindowBurnRate},且对于 {instanceId},过去 {shortWindowDuration} 为 {shortWindowBurnRate}。两个窗口超出 {burnRateThreshold} 时告警",
"xpack.observability.slo.burnRate.breachedStatustSubtitle": "按照当前的速率,错误预算将在 {hour} 小时后耗尽。", "xpack.observability.slo.burnRate.breachedStatustSubtitle": "按照当前的速率,错误预算将在 {hour} 小时后耗尽。",
"xpack.observability.slo.burnRate.threshold": "阈值为 {threshold}x", "xpack.observability.slo.burnRate.threshold": "阈值为 {threshold}x",
"xpack.observability.slo.clone.errorNotification": "无法克隆 {name}",
"xpack.observability.slo.clone.successNotification": "已成功创建 {name}",
"xpack.observability.slo.create.errorNotification": "创建 {name} 时出现问题", "xpack.observability.slo.create.errorNotification": "创建 {name} 时出现问题",
"xpack.observability.slo.create.successNotification": "已成功创建 {name}", "xpack.observability.slo.create.successNotification": "已成功创建 {name}",
"xpack.observability.slo.deleteConfirmationModal.title": "删除 {name}", "xpack.observability.slo.deleteConfirmationModal.title": "删除 {name}",