Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[8.12] [SLO] Reset UI for updating outdated SLOs (#172883) #173200

Merged
merged 2 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions x-pack/plugins/observability/common/locators/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const SLOS_WELCOME_PATH = '/slos/welcome' as const;
export const SLO_DETAIL_PATH = '/slos/:sloId' as const;
export const SLO_CREATE_PATH = '/slos/create' as const;
export const SLO_EDIT_PATH = '/slos/edit/:sloId' as const;
export const SLOS_OUTDATED_DEFINITIONS_PATH = '/slos/outdated-definitions' as const;
export const CASES_PATH = '/cases' as const;

export const paths = {
Expand All @@ -31,6 +32,7 @@ export const paths = {
ruleDetails: (ruleId: string) => `${OBSERVABILITY_BASE_PATH}${RULES_PATH}/${encodeURI(ruleId)}`,
slos: `${OBSERVABILITY_BASE_PATH}${SLOS_PATH}`,
slosWelcome: `${OBSERVABILITY_BASE_PATH}${SLOS_WELCOME_PATH}`,
slosOutdatedDefinitions: `${OBSERVABILITY_BASE_PATH}${SLOS_OUTDATED_DEFINITIONS_PATH}`,
sloCreate: `${OBSERVABILITY_BASE_PATH}${SLO_CREATE_PATH}`,
sloCreateWithEncodedForm: (encodedParams: string) =>
`${OBSERVABILITY_BASE_PATH}${SLO_CREATE_PATH}?_a=${encodedParams}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

import { EuiConfirmModal } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { ALL_VALUE, SLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
import React from 'react';

export interface SloDeleteConfirmationModalProps {
slo: SLOWithSummaryResponse;
slo: SLOWithSummaryResponse | SLOResponse;
onCancel: () => void;
onConfirm: () => void;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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 { EuiConfirmModal } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
import React from 'react';

export interface SloResetConfirmationModalProps {
slo: SLOWithSummaryResponse | SLOResponse;
onCancel: () => void;
onConfirm: () => void;
}

export function SloResetConfirmationModal({
slo,
onCancel,
onConfirm,
}: SloResetConfirmationModalProps) {
const { name } = slo;
return (
<EuiConfirmModal
buttonColor="danger"
data-test-subj="sloResetConfirmationModal"
title={i18n.translate('xpack.observability.slo.resetConfirmationModal.title', {
defaultMessage: 'Reset {name}?',
values: { name },
})}
cancelButtonText={i18n.translate(
'xpack.observability.slo.resetConfirmationModal.cancelButtonLabel',
{ defaultMessage: 'Cancel' }
)}
confirmButtonText={i18n.translate(
'xpack.observability.slo.resetConfirmationModal.resetButtonLabel',
{ defaultMessage: 'Reset' }
)}
onCancel={onCancel}
onConfirm={onConfirm}
>
{i18n.translate('xpack.observability.slo.resetConfirmationModal.descriptionText', {
defaultMessage: 'Resetting this SLO will also regenerate the historical data.',
})}
</EuiConfirmModal>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 { EuiButton, EuiCallOut } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useFetchSloDefinitions } from '../../../hooks/slo/use_fetch_slo_definitions';
import { useKibana } from '../../../utils/kibana_react';
import { paths } from '../../../../common/locators/paths';

export function SloOutdatedCallout() {
const {
application: { navigateToUrl },
http: { basePath },
} = useKibana().services;

const handleClick = () => {
navigateToUrl(basePath.prepend(paths.observability.slosOutdatedDefinitions));
};

const { isLoading, data } = useFetchSloDefinitions({ includeOutdatedOnly: true });
if (!isLoading && data && data.total > 0) {
return (
<EuiCallOut
color="warning"
iconType="warning"
title={i18n.translate('xpack.observability.slo.outdatedSloCallout.title', {
defaultMessage: '{total} Outdated SLOs Detected',
values: {
total: data.total,
},
})}
>
<p>
<FormattedMessage
id="xpack.observability.slo.outdatedSloCallout.message"
defaultMessage="We've noticed that you have {total} outdated SLO definitions, these SLOs will not be running or alerting until you've reset them. Please click the button below to review the SLO definitions; you can choose to either reset the SLO definition or remove it."
values={{ total: data.total }}
/>
</p>
<p>
<EuiButton
color="warning"
data-test-subj="o11ySloOutdatedCalloutViewOutdatedSloDefinitionsButton"
fill
onClick={handleClick}
>
<FormattedMessage
id="xpack.observability.outdatedSloCallout.buttonLabel"
defaultMessage="Review Outdated SLO Definitions"
/>
</EuiButton>
</p>
</EuiCallOut>
);
}
return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export const sloKeys = {
historicalSummaries: () => [...sloKeys.all, 'historicalSummary'] as const,
historicalSummary: (list: Array<{ sloId: string; instanceId: string }>) =>
[...sloKeys.historicalSummaries(), list] as const,
definitions: (search: string) => [...sloKeys.all, 'definitions', search] as const,
definitions: (search: string, page: number, perPage: number, includeOutdatedOnly: boolean) =>
[...sloKeys.all, 'definitions', search, page, perPage, includeOutdatedOnly] as const,
globalDiagnosis: () => [...sloKeys.all, 'globalDiagnosis'] as const,
burnRates: (sloId: string, instanceId: string | undefined) =>
[...sloKeys.all, 'burnRates', sloId, instanceId] as const,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,32 @@ export interface UseFetchSloDefinitionsResponse {
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
refetch: () => void;
}

interface Params {
name?: string;
includeOutdatedOnly?: boolean;
page?: number;
perPage?: number;
}

export function useFetchSloDefinitions({ name = '' }: Params): UseFetchSloDefinitionsResponse {
export function useFetchSloDefinitions({
name = '',
includeOutdatedOnly = false,
page = 1,
perPage = 100,
}: Params): UseFetchSloDefinitionsResponse {
const { http } = useKibana().services;
const search = name.endsWith('*') ? name : `${name}*`;

const { isLoading, isError, isSuccess, data } = useQuery({
queryKey: sloKeys.definitions(search),
const { isLoading, isError, isSuccess, data, refetch } = useQuery({
queryKey: sloKeys.definitions(search, page, perPage, includeOutdatedOnly),
queryFn: async ({ signal }) => {
try {
const response = await http.get<FindSLODefinitionsResponse>(
'/api/observability/slos/_definitions',
{ query: { search }, signal }
{ query: { search, includeOutdatedOnly, page, perPage }, signal }
);

return response;
Expand All @@ -43,5 +52,5 @@ export function useFetchSloDefinitions({ name = '' }: Params): UseFetchSloDefini
refetchOnWindowFocus: false,
});

return { isLoading, isError, isSuccess, data };
return { isLoading, isError, isSuccess, data, refetch };
}
52 changes: 52 additions & 0 deletions x-pack/plugins/observability/public/hooks/slo/use_reset_slo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 { useMutation } from '@tanstack/react-query';
import { i18n } from '@kbn/i18n';
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import { useKibana } from '../../utils/kibana_react';

type ServerError = IHttpFetchError<ResponseErrorBody>;

export function useResetSlo() {
const {
http,
notifications: { toasts },
} = useKibana().services;
return useMutation<string, ServerError, { id: string; name: string }>(
['resetSlo'],
({ id, name }) => {
try {
return http.post(`/api/observability/slos/${id}/_reset`);
} catch (error) {
return Promise.reject(
i18n.translate('xpack.observability.slo.slo.reset.errorMessage', {
defaultMessage: 'Failed to reset {name} (id: {id}), something went wrong: {error}',
values: { error: String(error), name, id },
})
);
}
},
{
onError: (error, { name, id }) => {
toasts.addError(new Error(error.body?.message ?? error.message), {
title: i18n.translate('xpack.observability.slo.slo.reset.errorNotification', {
defaultMessage: 'Failed to reset {name} (id: {id})',
values: { name, id },
}),
});
},
onSuccess: (_data, { name }) => {
toasts.addSuccess(
i18n.translate('xpack.observability.slo.slo.reset.successNotification', {
defaultMessage: '{name} reset successfully',
values: { name },
})
);
},
}
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* 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, { useState } from 'react';
import { i18n } from '@kbn/i18n';

import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTablePagination, EuiText } from '@elastic/eui';
import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
import { useKibana } from '../../utils/kibana_react';
import { useLicense } from '../../hooks/use_license';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { useCapabilities } from '../../hooks/slo/use_capabilities';
import { useFetchSloGlobalDiagnosis } from '../../hooks/slo/use_fetch_global_diagnosis';
import { HeaderMenu } from '../overview/components/header_menu/header_menu';
import { useFetchSloDefinitions } from '../../hooks/slo/use_fetch_slo_definitions';
import { paths } from '../../../common/locators/paths';
import { SloListEmpty } from '../slos/components/slo_list_empty';
import { OutdatedSlo } from './outdated_slo';
import { OutdatedSloSearchBar } from './outdated_slo_search_bar';

export function SlosOutdatedDefinitions() {
const {
http: { basePath },
} = useKibana().services;
const { hasWriteCapabilities } = useCapabilities();
const { data: globalDiagnosis } = useFetchSloGlobalDiagnosis();
const { ObservabilityPageTemplate } = usePluginContext();

useBreadcrumbs([
{
href: basePath.prepend(paths.observability.slos),
text: i18n.translate('xpack.observability.breadcrumbs.slosLinkText', {
defaultMessage: 'SLOs',
}),
deepLinkId: 'observability-overview:slos',
},
{
text: i18n.translate('xpack.observability.breadcrumbs.slosOutdatedDefinitions', {
defaultMessage: 'Outdated SLO Definitions',
}),
},
]);

const [search, setSearch] = useState<string>('');
const [activePage, setActivePage] = useState<number>(0);
const [perPage, setPerPage] = useState<number>(10);

const handlePerPageChange = (perPageNumber: number) => {
setPerPage(perPageNumber);
setActivePage(0);
};

const { hasAtLeast } = useLicense();

const { isLoading, data, refetch } = useFetchSloDefinitions({
name: search,
includeOutdatedOnly: true,
page: activePage + 1,
perPage,
});
const { total } = data ?? { total: 0 };

const hasRequiredWritePrivileges =
!!globalDiagnosis?.userPrivileges.write.has_all_requested && hasWriteCapabilities;

const hasPlatinumLicense = hasAtLeast('platinum') === true;

const hasSlosAndHasPermissions = hasPlatinumLicense && hasRequiredWritePrivileges;

const errors = !hasRequiredWritePrivileges ? (
<EuiText>
{i18n.translate('xpack.observability.slo.slosOutdatedDefinitions.sloPermissionsError', {
defaultMessage: 'You must have write permissions for SLOs to access this page',
})}
</EuiText>
) : !hasPlatinumLicense ? (
<EuiText>
{i18n.translate('xpack.observability.slo.slosOutdatedDefinitions.licenseError', {
defaultMessage: 'You must have atleast a platinum license to access this page',
})}
</EuiText>
) : null;

return (
<ObservabilityPageTemplate
data-test-subj="slosOutdatedDefinitions"
pageHeader={{
pageTitle: i18n.translate('xpack.observability.slo.slosOutdatedDefinitions.pageTitle', {
defaultMessage: 'Outdated SLO Definitions',
}),
}}
>
<HeaderMenu />

{!hasSlosAndHasPermissions ? (
errors
) : (
<>
<p>
{i18n.translate('xpack.observability.slo.slosOutdatedDefinitions.description', {
defaultMessage:
'The following SLOs are from a previous version and need to either be reset to upgrade to the latest version OR deleted and removed from the system. When you reset the SLO, the transform will be updated to the latest version and the historical data will be regenerated from the source data.',
})}
</p>
<EuiSpacer size="l" />
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<OutdatedSloSearchBar
initialSearch={search}
onRefresh={refetch}
onSearch={setSearch}
/>
</EuiFlexItem>
{!isLoading && total === 0 && <SloListEmpty />}
{!isLoading &&
total > 0 &&
data &&
data.results.map((slo) => (
<OutdatedSlo slo={slo} onDelete={refetch} onReset={refetch} />
))}
</EuiFlexGroup>
<EuiSpacer size="m" />
{!isLoading && data && (
<EuiTablePagination
activePage={activePage}
pageCount={Math.ceil(total / perPage)}
itemsPerPage={perPage}
onChangePage={setActivePage}
onChangeItemsPerPage={handlePerPageChange}
itemsPerPageOptions={[10, 20, 50, 100]}
/>
)}
</>
)}
</ObservabilityPageTemplate>
);
}
Loading
Loading