mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# Backport This will backport the following commits from `main` to `8.8`: - [Fix React Errors in SLOs (#155953)](https://github.com/elastic/kibana/pull/155953) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Coen Warmer","email":"coen.warmer@gmail.com"},"sourceCommit":{"committedDate":"2023-04-27T17:25:54Z","message":"Fix React Errors in SLOs (#155953)","sha":"6c5aafba04767ea6411b5b2cd613f1ab29ecfd75","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["backport","release_note:skip","Team: Actionable Observability","v8.8.0","v8.9.0"],"number":155953,"url":"https://github.com/elastic/kibana/pull/155953","mergeCommit":{"message":"Fix React Errors in SLOs (#155953)","sha":"6c5aafba04767ea6411b5b2cd613f1ab29ecfd75"}},"sourceBranch":"main","suggestedTargetBranches":["8.8"],"targetPullRequestStates":[{"branch":"8.8","label":"v8.8.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/155953","number":155953,"mergeCommit":{"message":"Fix React Errors in SLOs (#155953)","sha":"6c5aafba04767ea6411b5b2cd613f1ab29ecfd75"}}]}] BACKPORT-->
This commit is contained in:
parent
f21d997505
commit
db4634bd03
24 changed files with 516 additions and 327 deletions
|
@ -18,6 +18,7 @@ export const paths = {
|
|||
ruleDetails: (ruleId?: string | null) =>
|
||||
ruleId ? `${RULES_PAGE_LINK}/${encodeURI(ruleId)}` : RULES_PAGE_LINK,
|
||||
slos: SLOS_PAGE_LINK,
|
||||
slosWelcome: `${SLOS_PAGE_LINK}/welcome`,
|
||||
sloCreate: `${SLOS_PAGE_LINK}/create`,
|
||||
sloEdit: (sloId: string) => `${SLOS_PAGE_LINK}/edit/${encodeURI(sloId)}`,
|
||||
sloDetails: (sloId: string) => `${SLOS_PAGE_LINK}/${encodeURI(sloId)}`,
|
||||
|
|
|
@ -12,6 +12,7 @@ export const useFetchSloList = (): UseFetchSloListResponse => {
|
|||
return {
|
||||
isInitialLoading: false,
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
isSuccess: true,
|
||||
sloList,
|
||||
|
|
|
@ -7,15 +7,19 @@
|
|||
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { CreateSLOInput, CreateSLOResponse, FindSLOResponse } from '@kbn/slo-schema';
|
||||
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
|
||||
export function useCloneSlo() {
|
||||
const { http } = useKibana().services;
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const cloneSlo = useMutation<
|
||||
return useMutation<
|
||||
CreateSLOResponse,
|
||||
string,
|
||||
{ slo: CreateSLOInput; idToCopyFrom?: string },
|
||||
|
@ -41,7 +45,10 @@ export function useCloneSlo() {
|
|||
|
||||
const optimisticUpdate = {
|
||||
...data,
|
||||
results: [...(data?.results || []), { ...sloUsedToClone, name: slo.name, id: uuidv1() }],
|
||||
results: [
|
||||
...(data?.results || []),
|
||||
{ ...sloUsedToClone, name: slo.name, id: uuidv1(), summary: undefined },
|
||||
],
|
||||
total: data?.total && data.total + 1,
|
||||
};
|
||||
|
||||
|
@ -50,20 +57,31 @@ export function useCloneSlo() {
|
|||
queryClient.setQueryData(queryKey, optimisticUpdate);
|
||||
}
|
||||
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.observability.slo.clone.successNotification', {
|
||||
defaultMessage: 'Successfully created {name}',
|
||||
values: { name: slo.name },
|
||||
})
|
||||
);
|
||||
|
||||
// Return a context object with the snapshotted value
|
||||
return { previousSloList: data };
|
||||
},
|
||||
// If the mutation fails, use the context returned from onMutate to roll back
|
||||
onError: (_err, _slo, context) => {
|
||||
onError: (_err, { slo }, context) => {
|
||||
if (context?.previousSloList) {
|
||||
queryClient.setQueryData(['fetchSloList'], context.previousSloList);
|
||||
}
|
||||
toasts.addDanger(
|
||||
i18n.translate('xpack.observability.slo.clone.errorNotification', {
|
||||
defaultMessage: 'Failed to clone {name}',
|
||||
values: { name: slo.name },
|
||||
})
|
||||
);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['fetchSloList']);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return cloneSlo;
|
||||
}
|
||||
|
|
|
@ -5,27 +5,70 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import type { CreateSLOInput, CreateSLOResponse } from '@kbn/slo-schema';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { CreateSLOInput, CreateSLOResponse, FindSLOResponse } from '@kbn/slo-schema';
|
||||
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
|
||||
export function useCreateSlo() {
|
||||
const { http } = useKibana().services;
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const createSlo = useMutation(
|
||||
return useMutation(
|
||||
({ slo }: { slo: CreateSLOInput }) => {
|
||||
const body = JSON.stringify(slo);
|
||||
return http.post<CreateSLOResponse>(`/api/observability/slos`, { body });
|
||||
},
|
||||
{
|
||||
mutationKey: ['createSlo'],
|
||||
onSuccess: () => {
|
||||
onSuccess: (_data, { slo: { name } }) => {
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.observability.slo.create.successNotification', {
|
||||
defaultMessage: 'Successfully created {name}',
|
||||
values: { name },
|
||||
})
|
||||
);
|
||||
queryClient.invalidateQueries(['fetchSloList']);
|
||||
},
|
||||
onError: (error, { slo: { name } }) => {
|
||||
toasts.addError(new Error(String(error)), {
|
||||
title: i18n.translate('xpack.observability.slo.create.errorNotification', {
|
||||
defaultMessage: 'Something went wrong while creating {name}',
|
||||
values: { name },
|
||||
}),
|
||||
});
|
||||
},
|
||||
onMutate: async ({ slo }) => {
|
||||
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
|
||||
await queryClient.cancelQueries(['fetchSloList']);
|
||||
|
||||
const latestFetchSloListRequest = (
|
||||
queryClient.getQueriesData<FindSLOResponse>(['fetchSloList']) || []
|
||||
).at(0);
|
||||
|
||||
const [queryKey, data] = latestFetchSloListRequest || [];
|
||||
|
||||
const newItem = { ...slo, id: uuidv1() };
|
||||
|
||||
const optimisticUpdate = {
|
||||
...data,
|
||||
results: [...(data?.results || []), { ...newItem }],
|
||||
total: data?.total ? data.total + 1 : 1,
|
||||
};
|
||||
|
||||
// Optimistically update to the new value
|
||||
if (queryKey) {
|
||||
queryClient.setQueryData(queryKey, optimisticUpdate);
|
||||
}
|
||||
|
||||
// Return a context object with the snapshotted value
|
||||
return { previousSloList: data };
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return createSlo;
|
||||
}
|
||||
|
|
|
@ -6,21 +6,24 @@
|
|||
*/
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FindSLOResponse } from '@kbn/slo-schema';
|
||||
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
|
||||
export function useDeleteSlo(sloId: string) {
|
||||
const { http } = useKibana().services;
|
||||
export function useDeleteSlo() {
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const deleteSlo = useMutation<
|
||||
string,
|
||||
string,
|
||||
{ id: string },
|
||||
{ id: string; name: string },
|
||||
{ previousSloList: FindSLOResponse | undefined }
|
||||
>(
|
||||
['deleteSlo', sloId],
|
||||
['deleteSlo'],
|
||||
({ id }) => {
|
||||
try {
|
||||
return http.delete<string>(`/api/observability/slos/${id}`);
|
||||
|
@ -50,16 +53,30 @@ export function useDeleteSlo(sloId: string) {
|
|||
queryClient.setQueryData(queryKey, optimisticUpdate);
|
||||
}
|
||||
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.observability.slo.slo.delete.successNotification', {
|
||||
defaultMessage: 'Deleted {name}',
|
||||
values: { name: slo.name },
|
||||
})
|
||||
);
|
||||
|
||||
// Return a context object with the snapshotted value
|
||||
return { previousSloList: data };
|
||||
},
|
||||
// If the mutation fails, use the context returned from onMutate to roll back
|
||||
onError: (_err, _slo, context) => {
|
||||
onError: (_err, slo, context) => {
|
||||
if (context?.previousSloList) {
|
||||
queryClient.setQueryData(['fetchSloList'], context.previousSloList);
|
||||
}
|
||||
|
||||
toasts.addDanger(
|
||||
i18n.translate('xpack.observability.slo.slo.delete.errorNotification', {
|
||||
defaultMessage: 'Failed to delete {name}',
|
||||
values: { name: slo.name },
|
||||
})
|
||||
);
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess: (_success, slo) => {
|
||||
queryClient.invalidateQueries(['fetchSloList']);
|
||||
},
|
||||
}
|
||||
|
|
|
@ -77,8 +77,9 @@ export function useFetchRulesForSlo({ sloIds }: Params): UseFetchRulesForSloResp
|
|||
// ignore error for retrieving slos
|
||||
}
|
||||
},
|
||||
enabled: Boolean(sloIds),
|
||||
enabled: Boolean(sloIds?.length),
|
||||
refetchOnWindowFocus: false,
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ interface SLOListParams {
|
|||
export interface UseFetchSloListResponse {
|
||||
isInitialLoading: boolean;
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
isSuccess: boolean;
|
||||
isError: boolean;
|
||||
sloList: FindSLOResponse | undefined;
|
||||
|
@ -96,14 +97,19 @@ export function useFetchSloList({
|
|||
queryClient.invalidateQueries(['fetchActiveAlerts'], {
|
||||
exact: false,
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries(['fetchRulesForSlo'], {
|
||||
exact: false,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
sloList: data,
|
||||
isLoading: isLoading || isRefetching,
|
||||
isInitialLoading,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isSuccess,
|
||||
isError,
|
||||
refetch,
|
||||
|
|
|
@ -6,27 +6,43 @@
|
|||
*/
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { UpdateSLOInput, UpdateSLOResponse } from '@kbn/slo-schema';
|
||||
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
|
||||
export function useUpdateSlo() {
|
||||
const { http } = useKibana().services;
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const updateSlo = useMutation(
|
||||
return useMutation(
|
||||
({ sloId, slo }: { sloId: string; slo: UpdateSLOInput }) => {
|
||||
const body = JSON.stringify(slo);
|
||||
return http.put<UpdateSLOResponse>(`/api/observability/slos/${sloId}`, { body });
|
||||
},
|
||||
{
|
||||
mutationKey: ['updateSlo'],
|
||||
onSuccess: () => {
|
||||
onSuccess: (_data, { slo: { name } }) => {
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.observability.slo.update.successNotification', {
|
||||
defaultMessage: 'Successfully updated {name}',
|
||||
values: { name },
|
||||
})
|
||||
);
|
||||
queryClient.invalidateQueries(['fetchSloList']);
|
||||
queryClient.invalidateQueries(['fetchHistoricalSummary']);
|
||||
},
|
||||
onError: (error, { slo: { name } }) => {
|
||||
toasts.addError(new Error(String(error)), {
|
||||
title: i18n.translate('xpack.observability.slo.update.errorNotification', {
|
||||
defaultMessage: 'Something went wrong when updating {name}',
|
||||
values: { name },
|
||||
}),
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return updateSlo;
|
||||
}
|
||||
|
|
|
@ -5,21 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useIsMutating } from '@tanstack/react-query';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
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';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { useCloneSlo } from '../../../hooks/slo/use_clone_slo';
|
||||
import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo';
|
||||
import { isApmIndicatorType } from '../../../utils/slo/indicator';
|
||||
import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url';
|
||||
import { SLO_BURN_RATE_RULE_ID } from '../../../../common/constants';
|
||||
import { sloFeatureId } from '../../../../common';
|
||||
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,
|
||||
|
@ -35,24 +34,23 @@ export function HeaderControl({ isLoading, slo }: Props) {
|
|||
const {
|
||||
application: { navigateToUrl },
|
||||
http: { basePath },
|
||||
notifications: { toasts },
|
||||
triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout },
|
||||
} = useKibana<ObservabilityAppServices>().services;
|
||||
} = 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 { mutate: cloneSlo } = useCloneSlo();
|
||||
const { mutate: deleteSlo } = useDeleteSlo();
|
||||
|
||||
const handleActionsClick = () => setIsPopoverOpen((value) => !value);
|
||||
const closePopover = () => setIsPopoverOpen(false);
|
||||
|
||||
const handleEdit = () => {
|
||||
if (slo) {
|
||||
navigateToUrl(basePath.prepend(paths.observability.sloEdit(slo.id)));
|
||||
navigate(basePath.prepend(paths.observability.sloEdit(slo.id)));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -94,7 +92,7 @@ export function HeaderControl({ isLoading, slo }: Props) {
|
|||
transactionType,
|
||||
});
|
||||
|
||||
navigateToUrl(basePath.prepend(url));
|
||||
navigate(basePath.prepend(url));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -106,16 +104,9 @@ export function HeaderControl({ isLoading, slo }: Props) {
|
|||
transformSloResponseToCreateSloInput({ ...slo, name: `[Copy] ${slo.name}` })!
|
||||
);
|
||||
|
||||
await cloneSlo({ slo: newSlo, idToCopyFrom: slo.id });
|
||||
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));
|
||||
navigate(basePath.prepend(paths.observability.slos));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -128,10 +119,18 @@ export function HeaderControl({ isLoading, slo }: Props) {
|
|||
setDeleteConfirmationModalOpen(false);
|
||||
};
|
||||
|
||||
const handleDeleteSuccess = () => {
|
||||
navigateToUrl(basePath.prepend(paths.observability.slos));
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (slo) {
|
||||
deleteSlo({ id: slo.id, name: slo.name });
|
||||
navigate(basePath.prepend(paths.observability.slos));
|
||||
}
|
||||
};
|
||||
|
||||
const navigate = useCallback(
|
||||
(url: string) => setTimeout(() => navigateToUrl(url)),
|
||||
[navigateToUrl]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPopover
|
||||
|
@ -144,7 +143,7 @@ export function HeaderControl({ isLoading, slo }: Props) {
|
|||
iconType="arrowDown"
|
||||
iconSize="s"
|
||||
onClick={handleActionsClick}
|
||||
disabled={isLoading || isDeleting || !slo}
|
||||
disabled={isLoading || !slo}
|
||||
>
|
||||
{i18n.translate('xpack.observability.slo.sloDetails.headerControl.actions', {
|
||||
defaultMessage: 'Actions',
|
||||
|
@ -254,7 +253,7 @@ export function HeaderControl({ isLoading, slo }: Props) {
|
|||
<SloDeleteConfirmationModal
|
||||
slo={slo}
|
||||
onCancel={handleDeleteCancel}
|
||||
onSuccess={handleDeleteSuccess}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
|
|
|
@ -6,19 +6,21 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useLicense } from '../../hooks/use_license';
|
||||
import { useCapabilities } from '../../hooks/slo/use_capabilities';
|
||||
import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details';
|
||||
import { useFetchHistoricalSummary } from '../../hooks/slo/use_fetch_historical_summary';
|
||||
import { useFetchActiveAlerts } from '../../hooks/slo/use_fetch_active_alerts';
|
||||
import { useCloneSlo } from '../../hooks/slo/use_clone_slo';
|
||||
import { useDeleteSlo } from '../../hooks/slo/use_delete_slo';
|
||||
import { render } from '../../utils/test_helper';
|
||||
import { SloDetailsPage } from './slo_details';
|
||||
import { buildSlo } from '../../data/slo/slo';
|
||||
import { paths } from '../../config/paths';
|
||||
import { useFetchHistoricalSummary } from '../../hooks/slo/use_fetch_historical_summary';
|
||||
import { useCapabilities } from '../../hooks/slo/use_capabilities';
|
||||
import { useFetchActiveAlerts } from '../../hooks/slo/use_fetch_active_alerts';
|
||||
import {
|
||||
HEALTHY_STEP_DOWN_ROLLING_SLO,
|
||||
historicalSummaryData,
|
||||
|
@ -34,21 +36,26 @@ jest.mock('react-router-dom', () => ({
|
|||
jest.mock('../../utils/kibana_react');
|
||||
jest.mock('../../hooks/use_breadcrumbs');
|
||||
jest.mock('../../hooks/use_license');
|
||||
jest.mock('../../hooks/slo/use_capabilities');
|
||||
jest.mock('../../hooks/slo/use_fetch_active_alerts');
|
||||
jest.mock('../../hooks/slo/use_fetch_slo_details');
|
||||
jest.mock('../../hooks/slo/use_fetch_historical_summary');
|
||||
jest.mock('../../hooks/slo/use_capabilities');
|
||||
jest.mock('../../hooks/slo/use_clone_slo');
|
||||
jest.mock('../../hooks/slo/use_delete_slo');
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mock;
|
||||
const useParamsMock = useParams as jest.Mock;
|
||||
const useLicenseMock = useLicense as jest.Mock;
|
||||
const useCapabilitiesMock = useCapabilities as jest.Mock;
|
||||
const useFetchActiveAlertsMock = useFetchActiveAlerts as jest.Mock;
|
||||
const useFetchSloDetailsMock = useFetchSloDetails as jest.Mock;
|
||||
const useFetchHistoricalSummaryMock = useFetchHistoricalSummary as jest.Mock;
|
||||
const useCapabilitiesMock = useCapabilities as jest.Mock;
|
||||
const useCloneSloMock = useCloneSlo as jest.Mock;
|
||||
const useDeleteSloMock = useDeleteSlo as jest.Mock;
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
const mockBasePathPrepend = jest.fn();
|
||||
const mockClone = jest.fn();
|
||||
const mockDelete = jest.fn();
|
||||
|
||||
const mockKibana = () => {
|
||||
useKibanaMock.mockReturnValue({
|
||||
|
@ -57,12 +64,13 @@ const mockKibana = () => {
|
|||
charts: chartPluginMock.createStartContract(),
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: mockBasePathPrepend,
|
||||
prepend: (url: string) => url,
|
||||
},
|
||||
},
|
||||
notifications: {
|
||||
toasts: {
|
||||
addSuccess: jest.fn(),
|
||||
addDanger: jest.fn(),
|
||||
addError: jest.fn(),
|
||||
},
|
||||
},
|
||||
|
@ -92,6 +100,8 @@ describe('SLO Details Page', () => {
|
|||
sloHistoricalSummaryResponse: historicalSummaryData,
|
||||
});
|
||||
useFetchActiveAlertsMock.mockReturnValue({ isLoading: false, data: {} });
|
||||
useCloneSloMock.mockReturnValue({ mutate: mockClone });
|
||||
useDeleteSloMock.mockReturnValue({ mutate: mockDelete });
|
||||
});
|
||||
|
||||
describe('when the incorrect license is found', () => {
|
||||
|
@ -103,7 +113,7 @@ describe('SLO Details Page', () => {
|
|||
|
||||
render(<SloDetailsPage />);
|
||||
|
||||
expect(mockNavigate).toBeCalledWith(mockBasePathPrepend(paths.observability.slos));
|
||||
expect(mockNavigate).toBeCalledWith(paths.observability.slos);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -209,7 +219,26 @@ describe('SLO Details Page', () => {
|
|||
render(<SloDetailsPage />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('o11yHeaderControlActionsButton'));
|
||||
expect(screen.queryByTestId('sloDetailsHeaderControlPopoverClone')).toBeTruthy();
|
||||
|
||||
const button = screen.queryByTestId('sloDetailsHeaderControlPopoverClone');
|
||||
|
||||
expect(button).toBeTruthy();
|
||||
|
||||
fireEvent.click(button!);
|
||||
|
||||
const { id, createdAt, enabled, revision, summary, updatedAt, ...newSlo } = slo;
|
||||
|
||||
expect(mockClone).toBeCalledWith({
|
||||
idToCopyFrom: slo.id,
|
||||
slo: {
|
||||
...newSlo,
|
||||
name: `[Copy] ${newSlo.name}`,
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toBeCalledWith(paths.observability.slos);
|
||||
});
|
||||
});
|
||||
|
||||
it("renders a 'Delete' button under actions menu", async () => {
|
||||
|
@ -221,10 +250,25 @@ describe('SLO Details Page', () => {
|
|||
render(<SloDetailsPage />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('o11yHeaderControlActionsButton'));
|
||||
expect(screen.queryByTestId('sloDetailsHeaderControlPopoverDelete')).toBeTruthy();
|
||||
|
||||
const manageRulesButton = screen.queryByTestId('sloDetailsHeaderControlPopoverManageRules');
|
||||
expect(manageRulesButton).toBeTruthy();
|
||||
const button = screen.queryByTestId('sloDetailsHeaderControlPopoverDelete');
|
||||
|
||||
expect(button).toBeTruthy();
|
||||
|
||||
fireEvent.click(button!);
|
||||
|
||||
const deleteModalConfirmButton = screen.queryByTestId('confirmModalConfirmButton');
|
||||
|
||||
fireEvent.click(deleteModalConfirmButton!);
|
||||
|
||||
expect(mockDelete).toBeCalledWith({
|
||||
id: slo.id,
|
||||
name: slo.name,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toBeCalledWith(paths.observability.slos);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the Overview tab by default', async () => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { useLocation, useHistory } from 'react-router-dom';
|
||||
import {
|
||||
|
@ -51,7 +51,6 @@ export function SloEditForm({ slo }: Props) {
|
|||
const {
|
||||
application: { navigateToUrl },
|
||||
http: { basePath },
|
||||
notifications: { toasts },
|
||||
triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout },
|
||||
} = useKibana().services;
|
||||
|
||||
|
@ -121,64 +120,39 @@ export function SloEditForm({ slo }: Props) {
|
|||
const values = getValues();
|
||||
|
||||
if (isEditMode) {
|
||||
try {
|
||||
const processedValues = transformValuesToUpdateSLOInput(values);
|
||||
const processedValues = transformValuesToUpdateSLOInput(values);
|
||||
|
||||
if (isCreateRuleCheckboxChecked) {
|
||||
await updateSlo({ sloId: slo.id, slo: processedValues });
|
||||
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.observability.slo.sloEdit.update.success', {
|
||||
defaultMessage: 'Successfully updated {name}',
|
||||
values: { name: getValues().name },
|
||||
})
|
||||
navigate(
|
||||
basePath.prepend(
|
||||
`${paths.observability.sloEdit(slo.id)}?${CREATE_RULE_SEARCH_PARAM}=true`
|
||||
)
|
||||
);
|
||||
|
||||
if (isCreateRuleCheckboxChecked) {
|
||||
navigateToUrl(
|
||||
basePath.prepend(
|
||||
`${paths.observability.sloEdit(slo.id)}?${CREATE_RULE_SEARCH_PARAM}=true`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
navigateToUrl(basePath.prepend(paths.observability.slos));
|
||||
}
|
||||
} catch (error) {
|
||||
toasts.addError(new Error(error), {
|
||||
title: i18n.translate('xpack.observability.slo.sloEdit.creation.error', {
|
||||
defaultMessage: 'Something went wrong',
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
updateSlo({ sloId: slo.id, slo: processedValues });
|
||||
navigate(basePath.prepend(paths.observability.slos));
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const processedValues = transformValuesToCreateSLOInput(values);
|
||||
const processedValues = transformValuesToCreateSLOInput(values);
|
||||
|
||||
if (isCreateRuleCheckboxChecked) {
|
||||
const { id } = await createSlo({ slo: processedValues });
|
||||
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.observability.slo.sloEdit.creation.success', {
|
||||
defaultMessage: 'Successfully created {name}',
|
||||
values: { name: getValues().name },
|
||||
})
|
||||
navigate(
|
||||
basePath.prepend(`${paths.observability.sloEdit(id)}?${CREATE_RULE_SEARCH_PARAM}=true`)
|
||||
);
|
||||
|
||||
if (isCreateRuleCheckboxChecked) {
|
||||
navigateToUrl(
|
||||
basePath.prepend(`${paths.observability.sloEdit(id)}?${CREATE_RULE_SEARCH_PARAM}=true`)
|
||||
);
|
||||
} else {
|
||||
navigateToUrl(basePath.prepend(paths.observability.slos));
|
||||
}
|
||||
} catch (error) {
|
||||
toasts.addError(new Error(error), {
|
||||
title: i18n.translate('xpack.observability.slo.sloEdit.creation.error', {
|
||||
defaultMessage: 'Something went wrong',
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
createSlo({ slo: processedValues });
|
||||
navigate(basePath.prepend(paths.observability.slos));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const navigate = useCallback(
|
||||
(url: string) => setTimeout(() => navigateToUrl(url)),
|
||||
[navigateToUrl]
|
||||
);
|
||||
|
||||
const handleChangeCheckbox = () => {
|
||||
setIsCreateRuleCheckboxChecked(!isCreateRuleCheckboxChecked);
|
||||
};
|
||||
|
|
|
@ -539,46 +539,6 @@ describe('SLO Edit Page', () => {
|
|||
});
|
||||
|
||||
describe('when submitting has completed successfully', () => {
|
||||
it('renders a success toast', async () => {
|
||||
const slo = buildSlo();
|
||||
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
jest
|
||||
.spyOn(Router, 'useLocation')
|
||||
.mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' });
|
||||
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo });
|
||||
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue('success'),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue('success'),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<SloEditPage />);
|
||||
|
||||
expect(screen.queryByTestId('sloFormSubmitButton')).toBeEnabled();
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId('sloFormSubmitButton'));
|
||||
});
|
||||
|
||||
expect(mockAddSuccess).toBeCalled();
|
||||
});
|
||||
|
||||
it('navigates to the SLO List page when checkbox to create new rule is not checked', async () => {
|
||||
const slo = buildSlo();
|
||||
|
||||
|
@ -615,8 +575,9 @@ describe('SLO Edit Page', () => {
|
|||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId('sloFormSubmitButton'));
|
||||
});
|
||||
|
||||
expect(mockNavigate).toBeCalledWith(mockBasePathPrepend(paths.observability.slos));
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toBeCalledWith(mockBasePathPrepend(paths.observability.slos));
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to the SLO Edit page when checkbox to create new rule is checked', async () => {
|
||||
|
@ -657,9 +618,11 @@ describe('SLO Edit Page', () => {
|
|||
fireEvent.click(screen.getByTestId('sloFormSubmitButton'));
|
||||
});
|
||||
|
||||
expect(mockNavigate).toBeCalledWith(
|
||||
mockBasePathPrepend(`${paths.observability.sloEdit(slo.id)}?create-rule=true`)
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toBeCalledWith(
|
||||
mockBasePathPrepend(`${paths.observability.sloEdit(slo.id)}?create-rule=true`)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('opens the Add Rule Flyout when visiting an existing SLO with search params set', async () => {
|
||||
|
@ -698,47 +661,5 @@ describe('SLO Edit Page', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when submitting has not completed successfully', () => {
|
||||
it('renders an error toast', async () => {
|
||||
const slo = buildSlo();
|
||||
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
jest
|
||||
.spyOn(Router, 'useLocation')
|
||||
.mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' });
|
||||
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo });
|
||||
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn().mockRejectedValue('argh, I died'),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn().mockRejectedValue('argh, I died'),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<SloEditPage />);
|
||||
|
||||
expect(screen.queryByTestId('sloFormSubmitButton')).toBeEnabled();
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId('sloFormSubmitButton'));
|
||||
});
|
||||
|
||||
expect(mockAddError).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiSkeletonRectangle } from '@elastic/eui';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
||||
|
@ -14,25 +14,54 @@ import { SloIndicatorTypeBadge } from './slo_indicator_type_badge';
|
|||
import { SloStatusBadge } from '../../../../components/slo/slo_status_badge';
|
||||
import { SloActiveAlertsBadge } from '../../../../components/slo/slo_status_badge/slo_active_alerts_badge';
|
||||
import { SloTimeWindowBadge } from './slo_time_window_badge';
|
||||
import { SloRulesBadge } from './slo_rules_badge';
|
||||
import type { ActiveAlerts } from '../../../../hooks/slo/use_fetch_active_alerts';
|
||||
import type { SloRule } from '../../../../hooks/slo/use_fetch_rules_for_slo';
|
||||
import { SloRulesBadge } from './slo_rules_badge';
|
||||
|
||||
export interface Props {
|
||||
activeAlerts?: ActiveAlerts;
|
||||
isLoading: boolean;
|
||||
rules: Array<Rule<SloRule>> | undefined;
|
||||
slo: SLOWithSummaryResponse;
|
||||
onClickRuleBadge: () => void;
|
||||
}
|
||||
|
||||
export function SloBadges({ activeAlerts, rules, slo, onClickRuleBadge }: Props) {
|
||||
export function SloBadges({ activeAlerts, isLoading, rules, slo, onClickRuleBadge }: Props) {
|
||||
return (
|
||||
<EuiFlexGroup direction="row" responsive={false} gutterSize="s" alignItems="center">
|
||||
<SloStatusBadge slo={slo} />
|
||||
<SloIndicatorTypeBadge slo={slo} />
|
||||
<SloTimeWindowBadge slo={slo} />
|
||||
<SloActiveAlertsBadge slo={slo} activeAlerts={activeAlerts} />
|
||||
<SloRulesBadge rules={rules} onClick={onClickRuleBadge} />
|
||||
{isLoading ? (
|
||||
<>
|
||||
<EuiSkeletonRectangle
|
||||
isLoading
|
||||
contentAriaLabel="Loading"
|
||||
width="54.16px"
|
||||
height="20px"
|
||||
borderRadius="s"
|
||||
/>
|
||||
<EuiSkeletonRectangle
|
||||
isLoading
|
||||
contentAriaLabel="Loading"
|
||||
width="54.16px"
|
||||
height="20px"
|
||||
borderRadius="s"
|
||||
/>
|
||||
<EuiSkeletonRectangle
|
||||
isLoading
|
||||
contentAriaLabel="Loading"
|
||||
width="54.16px"
|
||||
height="20px"
|
||||
borderRadius="s"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SloStatusBadge slo={slo} />
|
||||
<SloIndicatorTypeBadge slo={slo} />
|
||||
<SloTimeWindowBadge slo={slo} />
|
||||
<SloActiveAlertsBadge slo={slo} activeAlerts={activeAlerts} />
|
||||
<SloRulesBadge rules={rules} onClick={onClickRuleBadge} />
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,94 +5,49 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiConfirmModal } from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo';
|
||||
|
||||
export interface SloDeleteConfirmationModalProps {
|
||||
slo: SLOWithSummaryResponse;
|
||||
onCancel: () => void;
|
||||
onSuccess?: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export function SloDeleteConfirmationModal({
|
||||
slo: { id, name },
|
||||
slo: { name },
|
||||
onCancel,
|
||||
onSuccess,
|
||||
onConfirm,
|
||||
}: SloDeleteConfirmationModalProps) {
|
||||
const {
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
const { mutate: deleteSlo, isSuccess, isError } = useDeleteSlo(id);
|
||||
|
||||
if (isSuccess) {
|
||||
toasts.addSuccess(getDeleteSuccesfulMessage(name));
|
||||
onSuccess?.();
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
toasts.addDanger(getDeleteFailMessage(name));
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
setIsVisible(false);
|
||||
deleteSlo({ id });
|
||||
};
|
||||
|
||||
return isVisible ? (
|
||||
return (
|
||||
<EuiConfirmModal
|
||||
buttonColor="danger"
|
||||
data-test-subj="sloDeleteConfirmationModal"
|
||||
title={getTitle()}
|
||||
cancelButtonText={getCancelButtonText()}
|
||||
confirmButtonText={getConfirmButtonText(name)}
|
||||
title={i18n.translate('xpack.observability.slo.slo.deleteConfirmationModal.title', {
|
||||
defaultMessage: 'Are you sure?',
|
||||
})}
|
||||
cancelButtonText={i18n.translate(
|
||||
'xpack.observability.slo.slo.deleteConfirmationModal.cancelButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
)}
|
||||
confirmButtonText={i18n.translate(
|
||||
'xpack.observability.slo.slo.deleteConfirmationModal.deleteButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Delete {name}',
|
||||
values: { name },
|
||||
}
|
||||
)}
|
||||
onCancel={onCancel}
|
||||
onConfirm={handleConfirm}
|
||||
onConfirm={onConfirm}
|
||||
>
|
||||
{i18n.translate('xpack.observability.slo.slo.deleteConfirmationModal.descriptionText', {
|
||||
defaultMessage: "You can't recover {name} after deleting.",
|
||||
values: { name },
|
||||
})}
|
||||
</EuiConfirmModal>
|
||||
) : null;
|
||||
);
|
||||
}
|
||||
|
||||
const getTitle = () =>
|
||||
i18n.translate('xpack.observability.slo.slo.deleteConfirmationModal.title', {
|
||||
defaultMessage: 'Are you sure?',
|
||||
});
|
||||
|
||||
const getCancelButtonText = () =>
|
||||
i18n.translate('xpack.observability.slo.slo.deleteConfirmationModal.cancelButtonLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
});
|
||||
|
||||
const getConfirmButtonText = (name: string) =>
|
||||
i18n.translate('xpack.observability.slo.slo.deleteConfirmationModal.deleteButtonLabel', {
|
||||
defaultMessage: 'Delete {name}',
|
||||
values: { name },
|
||||
});
|
||||
|
||||
const getDeleteSuccesfulMessage = (name: string) =>
|
||||
i18n.translate(
|
||||
'xpack.observability.slo.slo.deleteConfirmationModal.successNotification.descriptionText',
|
||||
{
|
||||
defaultMessage: 'Deleted {name}',
|
||||
values: { name },
|
||||
}
|
||||
);
|
||||
|
||||
const getDeleteFailMessage = (name: string) =>
|
||||
i18n.translate(
|
||||
'xpack.observability.slo.slo.deleteConfirmationModal.errorNotification.descriptionText',
|
||||
{
|
||||
defaultMessage: 'Failed to delete {name}',
|
||||
values: { name },
|
||||
}
|
||||
);
|
||||
|
|
|
@ -29,7 +29,7 @@ export function SloList({ autoRefresh }: Props) {
|
|||
const [sort, setSort] = useState<SortType>('creationTime');
|
||||
const [indicatorTypeFilter, setIndicatorTypeFilter] = useState<FilterType[]>([]);
|
||||
|
||||
const { isLoading, isError, sloList, refetch } = useFetchSloList({
|
||||
const { isInitialLoading, isLoading, isRefetching, isError, sloList, refetch } = useFetchSloList({
|
||||
page: activePage + 1,
|
||||
name: query,
|
||||
sortBy: sort,
|
||||
|
@ -69,7 +69,15 @@ export function SloList({ autoRefresh }: Props) {
|
|||
<EuiFlexGroup direction="column" gutterSize="m" data-test-subj="sloList">
|
||||
<EuiFlexItem grow>
|
||||
<SloListSearchFilterSortBar
|
||||
loading={isLoading || isCreatingSlo || isCloningSlo || isUpdatingSlo || isDeletingSlo}
|
||||
loading={
|
||||
isInitialLoading ||
|
||||
isLoading ||
|
||||
isRefetching ||
|
||||
isCreatingSlo ||
|
||||
isCloningSlo ||
|
||||
isUpdatingSlo ||
|
||||
isDeletingSlo
|
||||
}
|
||||
onChangeQuery={handleChangeQuery}
|
||||
onChangeSort={handleChangeSort}
|
||||
onChangeIndicatorTypeFilter={handleChangeIndicatorTypeFilter}
|
||||
|
@ -77,7 +85,7 @@ export function SloList({ autoRefresh }: Props) {
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<SloListItems sloList={results} loading={isLoading} error={isError} />
|
||||
<SloListItems sloList={results} loading={isLoading || isRefetching} error={isError} />
|
||||
</EuiFlexItem>
|
||||
|
||||
{results.length ? (
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useIsMutating, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiContextMenuItem,
|
||||
|
@ -45,6 +45,7 @@ export interface SloListItemProps {
|
|||
historicalSummary?: HistoricalSummaryResponse[];
|
||||
historicalSummaryLoading: boolean;
|
||||
activeAlerts?: ActiveAlerts;
|
||||
onConfirmDelete: (slo: SLOWithSummaryResponse) => void;
|
||||
}
|
||||
|
||||
export function SloListItem({
|
||||
|
@ -53,6 +54,7 @@ export function SloListItem({
|
|||
historicalSummary = [],
|
||||
historicalSummaryLoading,
|
||||
activeAlerts,
|
||||
onConfirmDelete,
|
||||
}: SloListItemProps) {
|
||||
const {
|
||||
application: { navigateToUrl },
|
||||
|
@ -65,7 +67,6 @@ export function SloListItem({
|
|||
const filteredRuleTypes = useGetFilteredRuleTypes();
|
||||
|
||||
const { mutate: cloneSlo } = useCloneSlo();
|
||||
const isDeletingSlo = Boolean(useIsMutating(['deleteSlo', slo.id]));
|
||||
|
||||
const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false);
|
||||
const [isAddRuleFlyoutOpen, setIsAddRuleFlyoutOpen] = useState(false);
|
||||
|
@ -114,21 +115,17 @@ export function SloListItem({
|
|||
setIsActionsPopoverOpen(false);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = () => {
|
||||
setDeleteConfirmationModalOpen(false);
|
||||
onConfirmDelete(slo);
|
||||
};
|
||||
|
||||
const handleDeleteCancel = () => {
|
||||
setDeleteConfirmationModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
data-test-subj="sloItem"
|
||||
color={isDeletingSlo ? 'subdued' : undefined}
|
||||
hasBorder
|
||||
hasShadow={false}
|
||||
style={{
|
||||
opacity: isDeletingSlo ? 0.3 : 1,
|
||||
transition: 'opacity 0.1s ease-in',
|
||||
}}
|
||||
>
|
||||
<EuiPanel data-test-subj="sloItem" hasBorder hasShadow={false}>
|
||||
<EuiFlexGroup responsive={false} alignItems="center">
|
||||
{/* CONTENT */}
|
||||
<EuiFlexItem grow>
|
||||
|
@ -137,13 +134,18 @@ export function SloListItem({
|
|||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<EuiLink data-test-subj="o11ySloListItemLink" onClick={handleViewDetails}>
|
||||
{slo.name}
|
||||
</EuiLink>
|
||||
{slo.summary ? (
|
||||
<EuiLink data-test-subj="o11ySloListItemLink" onClick={handleViewDetails}>
|
||||
{slo.name}
|
||||
</EuiLink>
|
||||
) : (
|
||||
<span>{slo.name}</span>
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<SloBadges
|
||||
activeAlerts={activeAlerts}
|
||||
isLoading={!slo.summary}
|
||||
rules={rules}
|
||||
slo={slo}
|
||||
onClickRuleBadge={handleCreateRule}
|
||||
|
@ -152,11 +154,13 @@ export function SloListItem({
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<SloSummary
|
||||
slo={slo}
|
||||
historicalSummary={historicalSummary}
|
||||
historicalSummaryLoading={historicalSummaryLoading}
|
||||
/>
|
||||
{slo.summary ? (
|
||||
<SloSummary
|
||||
slo={slo}
|
||||
historicalSummary={historicalSummary}
|
||||
historicalSummaryLoading={historicalSummaryLoading}
|
||||
/>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
@ -267,7 +271,11 @@ export function SloListItem({
|
|||
) : null}
|
||||
|
||||
{isDeleteConfirmationModalOpen ? (
|
||||
<SloDeleteConfirmationModal slo={slo} onCancel={handleDeleteCancel} />
|
||||
<SloDeleteConfirmationModal
|
||||
slo={slo}
|
||||
onCancel={handleDeleteCancel}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
/>
|
||||
) : null}
|
||||
</EuiPanel>
|
||||
);
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
|||
import { useFetchActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts';
|
||||
import { useFetchRulesForSlo } from '../../../hooks/slo/use_fetch_rules_for_slo';
|
||||
import { useFetchHistoricalSummary } from '../../../hooks/slo/use_fetch_historical_summary';
|
||||
import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo';
|
||||
import { SloListItem } from './slo_list_item';
|
||||
import { SloListEmpty } from './slo_list_empty';
|
||||
import { SloListError } from './slo_list_error';
|
||||
|
@ -29,6 +30,8 @@ export function SloListItems({ sloList, loading, error }: Props) {
|
|||
const { isLoading: historicalSummaryLoading, sloHistoricalSummaryResponse } =
|
||||
useFetchHistoricalSummary({ sloIds });
|
||||
|
||||
const { mutate: deleteSlo } = useDeleteSlo();
|
||||
|
||||
if (!loading && !error && sloList.length === 0) {
|
||||
return <SloListEmpty />;
|
||||
}
|
||||
|
@ -36,6 +39,10 @@ export function SloListItems({ sloList, loading, error }: Props) {
|
|||
return <SloListError />;
|
||||
}
|
||||
|
||||
const handleDelete = (slo: SLOWithSummaryResponse) => {
|
||||
deleteSlo({ id: slo.id, name: slo.name });
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{sloList.map((slo) => (
|
||||
|
@ -46,6 +53,7 @@ export function SloListItems({ sloList, loading, error }: Props) {
|
|||
historicalSummary={sloHistoricalSummaryResponse?.[slo.id]}
|
||||
historicalSummaryLoading={historicalSummaryLoading}
|
||||
slo={slo}
|
||||
onConfirmDelete={handleDelete}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen, act } from '@testing-library/react';
|
||||
import { screen, act, waitFor } from '@testing-library/react';
|
||||
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
|
||||
|
@ -23,6 +23,7 @@ import { SlosPage } from './slos';
|
|||
import { emptySloList, sloList } from '../../data/slo/slo';
|
||||
import { historicalSummaryData } from '../../data/slo/historical_summary_data';
|
||||
import { useCapabilities } from '../../hooks/slo/use_capabilities';
|
||||
import { paths } from '../../config/paths';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
|
@ -69,7 +70,7 @@ const mockKibana = () => {
|
|||
charts: chartPluginMock.createSetupContract(),
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: jest.fn(),
|
||||
prepend: (url: string) => url,
|
||||
},
|
||||
},
|
||||
notifications: {
|
||||
|
@ -98,17 +99,22 @@ describe('SLOs Page', () => {
|
|||
});
|
||||
|
||||
describe('when the incorrect license is found', () => {
|
||||
it('renders the welcome prompt with subscription buttons', async () => {
|
||||
beforeEach(() => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList: emptySloList });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
|
||||
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
sloHistoricalSummaryResponse: {},
|
||||
});
|
||||
});
|
||||
it('navigates to the SLOs Welcome Page', async () => {
|
||||
await act(async () => {
|
||||
render(<SlosPage />);
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy();
|
||||
expect(screen.queryByTestId('slosPageWelcomePromptSignupForCloudButton')).toBeTruthy();
|
||||
expect(screen.queryByTestId('slosPageWelcomePromptSignupForLicenseButton')).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toBeCalledWith(paths.observability.slosWelcome);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -117,14 +123,20 @@ describe('SLOs Page', () => {
|
|||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
});
|
||||
|
||||
it('renders the SLOs Welcome Prompt when the API has finished loading and there are no results', async () => {
|
||||
it('navigates to the SLOs Welcome Page when the API has finished loading and there are no results', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList: emptySloList });
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
sloHistoricalSummaryResponse: {},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(<SlosPage />);
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toBeCalledWith(paths.observability.slosWelcome);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have a create new SLO button', async () => {
|
||||
|
@ -198,7 +210,9 @@ describe('SLOs Page', () => {
|
|||
|
||||
button.click();
|
||||
|
||||
expect(mockNavigate).toBeCalled();
|
||||
expect(mockNavigate).toBeCalledWith(
|
||||
`${paths.observability.sloEdit(sloList.results.at(0)?.id || '')}`
|
||||
);
|
||||
});
|
||||
|
||||
it('allows creating a new rule for an SLO', async () => {
|
||||
|
@ -250,7 +264,10 @@ describe('SLOs Page', () => {
|
|||
|
||||
screen.getByTestId('confirmModalConfirmButton').click();
|
||||
|
||||
expect(mockDeleteSlo).toBeCalledWith({ id: sloList.results.at(0)?.id });
|
||||
expect(mockDeleteSlo).toBeCalledWith({
|
||||
id: sloList.results.at(0)?.id,
|
||||
name: sloList.results.at(0)?.name,
|
||||
});
|
||||
});
|
||||
|
||||
it('allows cloning an SLO', async () => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
|
@ -16,7 +16,6 @@ import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
|
|||
import { useCapabilities } from '../../hooks/slo/use_capabilities';
|
||||
import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list';
|
||||
import { SloList } from './components/slo_list';
|
||||
import { SloListWelcomePrompt } from './components/slo_list_welcome_prompt';
|
||||
import { AutoRefreshButton } from './components/auto_refresh_button';
|
||||
import { HeaderTitle } from './components/header_title';
|
||||
import { FeedbackButton } from '../../components/slo/feedback_button/feedback_button';
|
||||
|
@ -55,14 +54,16 @@ export function SlosPage() {
|
|||
setIsAutoRefreshing(!isAutoRefreshing);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if ((!isLoading && total === 0) || hasAtLeast('platinum') === false) {
|
||||
navigateToUrl(basePath.prepend(paths.observability.slosWelcome));
|
||||
}
|
||||
}, [basePath, hasAtLeast, isLoading, navigateToUrl, total]);
|
||||
|
||||
if (isInitialLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((!isLoading && total === 0) || !hasAtLeast('platinum')) {
|
||||
return <SloListWelcomePrompt />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ObservabilityPageTemplate
|
||||
pageHeader={{
|
||||
|
|
Before Width: | Height: | Size: 695 KiB After Width: | Height: | Size: 695 KiB |
|
@ -8,12 +8,12 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory } from '@storybook/react';
|
||||
|
||||
import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator';
|
||||
import { SloListWelcomePrompt as Component } from './slo_list_welcome_prompt';
|
||||
import { KibanaReactStorybookDecorator } from '../../utils/kibana_react.storybook_decorator';
|
||||
import { SlosWelcomePage as Component } from './slos_welcome';
|
||||
|
||||
export default {
|
||||
component: Component,
|
||||
title: 'app/SLO/ListPage/SloListWelcomePrompt',
|
||||
title: 'app/SLO/SlosWelcomePage',
|
||||
decorators: [KibanaReactStorybookDecorator],
|
||||
};
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 { screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import { render } from '../../utils/test_helper';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list';
|
||||
import { useLicense } from '../../hooks/use_license';
|
||||
import { SlosWelcomePage } from './slos_welcome';
|
||||
import { emptySloList, sloList } from '../../data/slo/slo';
|
||||
import { useCapabilities } from '../../hooks/slo/use_capabilities';
|
||||
import { paths } from '../../config/paths';
|
||||
|
||||
jest.mock('../../utils/kibana_react');
|
||||
jest.mock('../../hooks/use_breadcrumbs');
|
||||
jest.mock('../../hooks/use_license');
|
||||
jest.mock('../../hooks/slo/use_fetch_slo_list');
|
||||
jest.mock('../../hooks/slo/use_capabilities');
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mock;
|
||||
const useLicenseMock = useLicense as jest.Mock;
|
||||
const useFetchSloListMock = useFetchSloList as jest.Mock;
|
||||
const useCapabilitiesMock = useCapabilities as jest.Mock;
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
|
||||
const mockKibana = () => {
|
||||
useKibanaMock.mockReturnValue({
|
||||
services: {
|
||||
application: { navigateToUrl: mockNavigate },
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: (url: string) => url,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('SLOs Welcome Page', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockKibana();
|
||||
useCapabilitiesMock.mockReturnValue({ hasWriteCapabilities: true, hasReadCapabilities: true });
|
||||
});
|
||||
|
||||
describe('when the incorrect license is found', () => {
|
||||
it('renders the welcome message with subscription buttons', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList: emptySloList });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
|
||||
|
||||
render(<SlosWelcomePage />);
|
||||
|
||||
expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy();
|
||||
expect(screen.queryByTestId('slosPageWelcomePromptSignupForCloudButton')).toBeTruthy();
|
||||
expect(screen.queryByTestId('slosPageWelcomePromptSignupForLicenseButton')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the correct license is found', () => {
|
||||
beforeEach(() => {
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
});
|
||||
|
||||
describe('when loading is done and no results are found', () => {
|
||||
beforeEach(() => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, emptySloList });
|
||||
});
|
||||
|
||||
it('should display the welcome message with a Create new SLO button which should navigate to the SLO Creation page', async () => {
|
||||
render(<SlosWelcomePage />);
|
||||
expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy();
|
||||
|
||||
const createNewSloButton = screen.queryByTestId('o11ySloListWelcomePromptCreateSloButton');
|
||||
expect(createNewSloButton).toBeTruthy();
|
||||
createNewSloButton?.click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toBeCalledWith(paths.observability.sloCreate);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when loading is done and results are found', () => {
|
||||
beforeEach(() => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList });
|
||||
});
|
||||
|
||||
it('should navigate to the SLO List page', async () => {
|
||||
render(<SlosWelcomePage />);
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toBeCalledWith(paths.observability.slos);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
EuiPageTemplate,
|
||||
EuiButton,
|
||||
|
@ -18,13 +18,14 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { useLicense } from '../../../hooks/use_license';
|
||||
import { usePluginContext } from '../../../hooks/use_plugin_context';
|
||||
import { paths } from '../../../config/paths';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { useLicense } from '../../hooks/use_license';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list';
|
||||
import { paths } from '../../config/paths';
|
||||
import illustration from './assets/illustration.svg';
|
||||
|
||||
export function SloListWelcomePrompt() {
|
||||
export function SlosWelcomePage() {
|
||||
const {
|
||||
application: { navigateToUrl },
|
||||
http: { basePath },
|
||||
|
@ -32,14 +33,24 @@ export function SloListWelcomePrompt() {
|
|||
const { ObservabilityPageTemplate } = usePluginContext();
|
||||
|
||||
const { hasAtLeast } = useLicense();
|
||||
|
||||
const hasRightLicense = hasAtLeast('platinum');
|
||||
|
||||
const { isLoading, sloList } = useFetchSloList();
|
||||
const { total } = sloList || { total: 0 };
|
||||
|
||||
const handleClickCreateSlo = () => {
|
||||
navigateToUrl(basePath.prepend(paths.observability.sloCreate));
|
||||
};
|
||||
|
||||
return (
|
||||
const hasSlosAndHasPermissions = total > 0 && hasAtLeast('platinum') === true;
|
||||
|
||||
useEffect(() => {
|
||||
if (hasSlosAndHasPermissions) {
|
||||
navigateToUrl(basePath.prepend(paths.observability.slos));
|
||||
}
|
||||
}, [basePath, hasSlosAndHasPermissions, navigateToUrl]);
|
||||
|
||||
return hasSlosAndHasPermissions || isLoading ? null : (
|
||||
<ObservabilityPageTemplate data-test-subj="slosPageWelcomePrompt">
|
||||
<EuiPageTemplate.EmptyPrompt
|
||||
title={
|
|
@ -17,6 +17,7 @@ import { OverviewPage } from '../pages/overview/overview';
|
|||
import { RulesPage } from '../pages/rules/rules';
|
||||
import { RuleDetailsPage } from '../pages/rule_details';
|
||||
import { SlosPage } from '../pages/slos/slos';
|
||||
import { SlosWelcomePage } from '../pages/slos_welcome/slos_welcome';
|
||||
import { SloDetailsPage } from '../pages/slo_details/slo_details';
|
||||
import { SloEditPage } from '../pages/slo_edit/slo_edit';
|
||||
import { casesPath } from '../../common';
|
||||
|
@ -135,6 +136,13 @@ export const routes = {
|
|||
params: {},
|
||||
exact: true,
|
||||
},
|
||||
'/slos/welcome': {
|
||||
handler: () => {
|
||||
return <SlosWelcomePage />;
|
||||
},
|
||||
params: {},
|
||||
exact: true,
|
||||
},
|
||||
'/slos/edit/:sloId': {
|
||||
handler: () => {
|
||||
return <SloEditPage />;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue