[8.8] Add SloGlobalDiagnosis check to SLO List and SLO Create pages (#157488) (#157514)

# Backport

This will backport the following commits from `main` to `8.8`:
- [Add SloGlobalDiagnosis check to SLO List and SLO Create pages
(#157488)](https://github.com/elastic/kibana/pull/157488)

<!--- 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-05-12T15:45:16Z","message":"Add
SloGlobalDiagnosis check to SLO List and SLO Create pages
(#157488)\n\nCo-authored-by: Kevin Delemme
<kdelemme@gmail.com>\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"e75325653c88553454270bf412ddbed700fc022b","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:prev-minor","v8.8.0","v8.9.0"],"number":157488,"url":"https://github.com/elastic/kibana/pull/157488","mergeCommit":{"message":"Add
SloGlobalDiagnosis check to SLO List and SLO Create pages
(#157488)\n\nCo-authored-by: Kevin Delemme
<kdelemme@gmail.com>\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"e75325653c88553454270bf412ddbed700fc022b"}},"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/157488","number":157488,"mergeCommit":{"message":"Add
SloGlobalDiagnosis check to SLO List and SLO Create pages
(#157488)\n\nCo-authored-by: Kevin Delemme
<kdelemme@gmail.com>\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"e75325653c88553454270bf412ddbed700fc022b"}}]}]
BACKPORT-->

Co-authored-by: Coen Warmer <coen.warmer@gmail.com>
This commit is contained in:
Kibana Machine 2023-05-12 12:54:28 -04:00 committed by GitHub
parent e5132c79d4
commit 277be81f38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 198 additions and 39 deletions

View file

@ -0,0 +1,15 @@
/*
* 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.
*/
export function convertErrorForUseInToast(error: Error) {
const newError = {
...error,
message: Object(error).body.message,
};
return newError;
}

View file

@ -0,0 +1,86 @@
/*
* 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 {
QueryObserverResult,
RefetchOptions,
RefetchQueryFilters,
useQuery,
} from '@tanstack/react-query';
import { i18n } from '@kbn/i18n';
import type { PublicLicenseJSON } from '@kbn/licensing-plugin/public';
import type { SecurityGetUserPrivilegesResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { useKibana } from '../../utils/kibana_react';
import { convertErrorForUseInToast } from './helpers/convert_error_for_use_in_toast';
interface SloGlobalDiagnosisResponse {
licenseAndFeatures: PublicLicenseJSON;
userPrivileges: SecurityGetUserPrivilegesResponse;
sloResources: {
[x: string]: 'OK' | 'NOT_OK';
};
}
export interface UseFetchSloGlobalDiagnoseResponse {
isInitialLoading: boolean;
isLoading: boolean;
isRefetching: boolean;
isSuccess: boolean;
isError: boolean;
globalSloDiagnosis: SloGlobalDiagnosisResponse | undefined;
refetch: <TPageData>(
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined
) => Promise<QueryObserverResult<SloGlobalDiagnosisResponse | undefined, unknown>>;
}
export function useFetchSloGlobalDiagnosis(): UseFetchSloGlobalDiagnoseResponse {
const {
http,
notifications: { toasts },
} = useKibana().services;
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery(
{
queryKey: ['fetchSloGlobalDiagnosis'],
queryFn: async ({ signal }) => {
try {
const response = await http.get<SloGlobalDiagnosisResponse>(
'/internal/observability/slos/_diagnosis',
{
query: {},
signal,
}
);
return response;
} catch (error) {
throw convertErrorForUseInToast(error);
}
},
keepPreviousData: true,
refetchOnWindowFocus: false,
retry: false,
onError: (error: Error) => {
toasts.addError(error, {
title: i18n.translate('xpack.observability.slo.globalDiagnosis.errorNotification', {
defaultMessage: 'You do not have the right permissions to use this feature.',
}),
});
},
}
);
return {
globalSloDiagnosis: data,
isLoading,
isInitialLoading,
isRefetching,
isSuccess,
isError,
refetch,
};
}

View file

@ -15,9 +15,10 @@ import { usePluginContext } from '../../hooks/use_plugin_context';
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details';
import { useLicense } from '../../hooks/use_license';
import { SloEditForm } from './components/slo_edit_form';
import { FeedbackButton } from '../../components/slo/feedback_button/feedback_button';
import { useCapabilities } from '../../hooks/slo/use_capabilities';
import { useFetchSloGlobalDiagnosis } from '../../hooks/slo/use_fetch_global_diagnosis';
import { FeedbackButton } from '../../components/slo/feedback_button/feedback_button';
import { SloEditForm } from './components/slo_edit_form';
export function SloEditPage() {
const {
@ -25,6 +26,7 @@ export function SloEditPage() {
http: { basePath },
} = useKibana().services;
const { hasWriteCapabilities } = useCapabilities();
const { isError: hasErrorInGlobalDiagnosis } = useFetchSloGlobalDiagnosis();
const { ObservabilityPageTemplate } = usePluginContext();
const { sloId } = useParams<{ sloId: string | undefined }>();
@ -43,7 +45,7 @@ export function SloEditPage() {
const { slo, isInitialLoading } = useFetchSloDetails({ sloId });
if (hasRightLicense === false || !hasWriteCapabilities) {
if (hasRightLicense === false || !hasWriteCapabilities || hasErrorInGlobalDiagnosis) {
navigateToUrl(basePath.prepend(paths.observability.slos));
}

View file

@ -11,6 +11,7 @@ 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 { useFetchSloGlobalDiagnosis } from '../../hooks/slo/use_fetch_global_diagnosis';
import { useLicense } from '../../hooks/use_license';
import { SlosWelcomePage } from './slos_welcome';
import { emptySloList, sloList } from '../../data/slo/slo';
@ -22,11 +23,13 @@ jest.mock('../../hooks/use_breadcrumbs');
jest.mock('../../hooks/use_license');
jest.mock('../../hooks/slo/use_fetch_slo_list');
jest.mock('../../hooks/slo/use_capabilities');
jest.mock('../../hooks/slo/use_fetch_global_diagnosis');
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 useGlobalDiagnosisMock = useFetchSloGlobalDiagnosis as jest.Mock;
const mockNavigate = jest.fn();
@ -54,6 +57,9 @@ describe('SLOs Welcome Page', () => {
it('renders the welcome message with subscription buttons', async () => {
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList: emptySloList });
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
useGlobalDiagnosisMock.mockReturnValue({
isError: false,
});
render(<SlosWelcomePage />);
@ -66,6 +72,9 @@ describe('SLOs Welcome Page', () => {
describe('when the correct license is found', () => {
beforeEach(() => {
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
useGlobalDiagnosisMock.mockReturnValue({
isError: false,
});
});
describe('when loading is done and no results are found', () => {
@ -79,6 +88,24 @@ describe('SLOs Welcome Page', () => {
hasReadCapabilities: true,
});
render(<SlosWelcomePage />);
expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy();
const createNewSloButton = screen.queryByTestId('o11ySloListWelcomePromptCreateSloButton');
expect(createNewSloButton).toBeDisabled();
});
it('disables the create slo button when no cluster permissions capabilities', async () => {
useCapabilitiesMock.mockReturnValue({
hasWriteCapabilities: true,
hasReadCapabilities: true,
});
useGlobalDiagnosisMock.mockReturnValue({
isError: true,
});
render(<SlosWelcomePage />);
expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy();
@ -87,6 +114,10 @@ describe('SLOs Welcome Page', () => {
});
it('should display the welcome message with a Create new SLO button which should navigate to the SLO Creation page', async () => {
useGlobalDiagnosisMock.mockReturnValue({
isError: false,
});
render(<SlosWelcomePage />);
expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy();
@ -103,6 +134,9 @@ describe('SLOs Welcome Page', () => {
describe('when loading is done and results are found', () => {
beforeEach(() => {
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList });
useGlobalDiagnosisMock.mockReturnValue({
isError: false,
});
});
it('should navigate to the SLO List page', async () => {

View file

@ -21,10 +21,11 @@ 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 { useCapabilities } from '../../hooks/slo/use_capabilities';
import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list';
import { paths } from '../../config/paths';
import illustration from './assets/illustration.svg';
import { useCapabilities } from '../../hooks/slo/use_capabilities';
import { useFetchSloGlobalDiagnosis } from '../../hooks/slo/use_fetch_global_diagnosis';
export function SlosWelcomePage() {
const {
@ -32,6 +33,7 @@ export function SlosWelcomePage() {
http: { basePath },
} = useKibana().services;
const { hasWriteCapabilities } = useCapabilities();
const { isError: hasErrorInGlobalDiagnosis } = useFetchSloGlobalDiagnosis();
const { ObservabilityPageTemplate } = usePluginContext();
const { hasAtLeast } = useLicense();
@ -44,7 +46,8 @@ export function SlosWelcomePage() {
navigateToUrl(basePath.prepend(paths.observability.sloCreate));
};
const hasSlosAndHasPermissions = total > 0 && hasAtLeast('platinum') === true;
const hasSlosAndHasPermissions =
total > 0 && hasAtLeast('platinum') === true && !hasErrorInGlobalDiagnosis;
useEffect(() => {
if (hasSlosAndHasPermissions) {
@ -110,7 +113,7 @@ export function SlosWelcomePage() {
fill
color="primary"
onClick={handleClickCreateSlo}
disabled={!hasWriteCapabilities}
disabled={!hasWriteCapabilities || hasErrorInGlobalDiagnosis}
>
{i18n.translate('xpack.observability.slo.sloList.welcomePrompt.buttonLabel', {
defaultMessage: 'Create SLO',

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { badRequest } from '@hapi/boom';
import { badRequest, forbidden, failedDependency } from '@hapi/boom';
import {
createSLOParamsSchema,
deleteSLOParamsSchema,
@ -275,7 +275,15 @@ const getDiagnosisRoute = createObservabilityServerRoute({
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
const licensing = await context.licensing;
return getGlobalDiagnosis(esClient, licensing);
try {
const response = await getGlobalDiagnosis(esClient, licensing);
return response;
} catch (error) {
if (error.cause.statusCode === 403) {
throw forbidden('Insufficient Elasticsearch cluster permissions to access feature.');
}
throw failedDependency(error);
}
},
});

View file

@ -26,15 +26,19 @@ export async function getGlobalDiagnosis(
esClient: ElasticsearchClient,
licensing: LicensingApiRequestHandlerContext
) {
const licenseInfo = licensing.license.toJSON();
const userPrivileges = await esClient.security.getUserPrivileges();
const sloResources = await getSloResourcesDiagnosis(esClient);
try {
const licenseInfo = licensing.license.toJSON();
const userPrivileges = await esClient.security.getUserPrivileges();
const sloResources = await getSloResourcesDiagnosis(esClient);
return {
licenseAndFeatures: licenseInfo,
userPrivileges,
sloResources,
};
return {
licenseAndFeatures: licenseInfo,
userPrivileges,
sloResources,
};
} catch (error) {
throw error;
}
}
export async function getSloDiagnosis(
@ -79,29 +83,36 @@ export async function getSloDiagnosis(
}
async function getSloResourcesDiagnosis(esClient: ElasticsearchClient) {
const indexTemplateExists = await esClient.indices.existsIndexTemplate({
name: SLO_INDEX_TEMPLATE_NAME,
});
const mappingsTemplateExists = await esClient.cluster.existsComponentTemplate({
name: SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME,
});
const settingsTemplateExists = await esClient.cluster.existsComponentTemplate({
name: SLO_COMPONENT_TEMPLATE_SETTINGS_NAME,
});
let ingestPipelineExists = true;
try {
await esClient.ingest.getPipeline({ id: SLO_INGEST_PIPELINE_NAME });
} catch (err) {
ingestPipelineExists = false;
}
const indexTemplateExists = await esClient.indices.existsIndexTemplate({
name: SLO_INDEX_TEMPLATE_NAME,
});
return {
[SLO_INDEX_TEMPLATE_NAME]: indexTemplateExists ? OK : NOT_OK,
[SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME]: mappingsTemplateExists ? OK : NOT_OK,
[SLO_COMPONENT_TEMPLATE_SETTINGS_NAME]: settingsTemplateExists ? OK : NOT_OK,
[SLO_INGEST_PIPELINE_NAME]: ingestPipelineExists ? OK : NOT_OK,
};
const mappingsTemplateExists = await esClient.cluster.existsComponentTemplate({
name: SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME,
});
const settingsTemplateExists = await esClient.cluster.existsComponentTemplate({
name: SLO_COMPONENT_TEMPLATE_SETTINGS_NAME,
});
let ingestPipelineExists = true;
try {
await esClient.ingest.getPipeline({ id: SLO_INGEST_PIPELINE_NAME });
} catch (err) {
ingestPipelineExists = false;
throw err;
}
return {
[SLO_INDEX_TEMPLATE_NAME]: indexTemplateExists ? OK : NOT_OK,
[SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME]: mappingsTemplateExists ? OK : NOT_OK,
[SLO_COMPONENT_TEMPLATE_SETTINGS_NAME]: settingsTemplateExists ? OK : NOT_OK,
[SLO_INGEST_PIPELINE_NAME]: ingestPipelineExists ? OK : NOT_OK,
};
} catch (err) {
if (err.meta.statusCode === 403) {
throw new Error('Insufficient permissions to access Elasticsearch Cluster', { cause: err });
}
}
}