mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Add Platinum license check for SLO APIs and SLO pages (#149055)
Closes https://github.com/elastic/kibana/issues/148298
This commit is contained in:
parent
bcd4260154
commit
5854bceb62
16 changed files with 811 additions and 518 deletions
35
x-pack/plugins/observability/public/hooks/use_license.ts
Normal file
35
x-pack/plugins/observability/public/hooks/use_license.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { useCallback } from 'react';
|
||||
import { Observable } from 'rxjs';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { ILicense, LicenseType } from '@kbn/licensing-plugin/public';
|
||||
|
||||
import { useKibana } from '../utils/kibana_react';
|
||||
|
||||
interface UseLicenseReturnValue {
|
||||
getLicense: () => ILicense | null;
|
||||
hasAtLeast: (level: LicenseType) => boolean | undefined;
|
||||
}
|
||||
|
||||
export const useLicense = (): UseLicenseReturnValue => {
|
||||
const { licensing } = useKibana().services;
|
||||
const license = useObservable<ILicense | null>(licensing?.license$ ?? new Observable(), null);
|
||||
|
||||
return {
|
||||
getLicense: () => license,
|
||||
hasAtLeast: useCallback(
|
||||
(level: LicenseType) => {
|
||||
if (!license) return;
|
||||
|
||||
return !!license && license.isAvailable && license.isActive && license.hasAtLeast(level);
|
||||
},
|
||||
[license]
|
||||
),
|
||||
};
|
||||
};
|
|
@ -8,15 +8,16 @@
|
|||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
|
||||
import { ConfigSchema } from '../../plugin';
|
||||
import { Subset } from '../../typings';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { kibanaStartMock } from '../../utils/kibana_react.mock';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useLicense } from '../../hooks/use_license';
|
||||
import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details';
|
||||
import { render } from '../../utils/test_helper';
|
||||
import { SloDetailsPage } from '.';
|
||||
import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { anSLO } from '../../data/slo';
|
||||
import type { ConfigSchema } from '../../plugin';
|
||||
import type { Subset } from '../../typings';
|
||||
import { paths } from '../../config';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
|
@ -24,19 +25,25 @@ jest.mock('react-router-dom', () => ({
|
|||
}));
|
||||
|
||||
jest.mock('../../utils/kibana_react');
|
||||
jest.mock('../../hooks/slo/use_fetch_slo_details');
|
||||
jest.mock('../../hooks/use_breadcrumbs');
|
||||
jest.mock('../../hooks/use_license');
|
||||
jest.mock('../../hooks/slo/use_fetch_slo_details');
|
||||
|
||||
const useFetchSloDetailsMock = useFetchSloDetails as jest.Mock;
|
||||
const useParamsMock = useParams as jest.Mock;
|
||||
const useKibanaMock = useKibana as jest.Mock;
|
||||
const useParamsMock = useParams as jest.Mock;
|
||||
const useLicenseMock = useLicense as jest.Mock;
|
||||
const useFetchSloDetailsMock = useFetchSloDetails as jest.Mock;
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
const mockBasePathPrepend = jest.fn();
|
||||
|
||||
const mockKibana = () => {
|
||||
useKibanaMock.mockReturnValue({
|
||||
services: {
|
||||
...kibanaStartMock.startContract(),
|
||||
application: { navigateToUrl: mockNavigate },
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: jest.fn(),
|
||||
prepend: mockBasePathPrepend,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -55,38 +62,64 @@ describe('SLO Details Page', () => {
|
|||
mockKibana();
|
||||
});
|
||||
|
||||
it('renders the not found page when the feature flag is not enabled', async () => {
|
||||
useParamsMock.mockReturnValue(anSLO.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ loading: false, slo: anSLO });
|
||||
render(<SloDetailsPage />, { unsafe: { slo: { enabled: false } } });
|
||||
describe('when the feature flag is not enabled', () => {
|
||||
it('renders the not found page', async () => {
|
||||
useParamsMock.mockReturnValue(anSLO.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ loading: false, slo: anSLO });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeTruthy();
|
||||
render(<SloDetailsPage />, { unsafe: { slo: { enabled: false } } });
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the not found page when the SLO cannot be found', async () => {
|
||||
useParamsMock.mockReturnValue('inexistant');
|
||||
useFetchSloDetailsMock.mockReturnValue({ loading: false, slo: undefined });
|
||||
render(<SloDetailsPage />, config);
|
||||
describe('when the feature flag is enabled', () => {
|
||||
describe('when the incorrect license is found', () => {
|
||||
it('navigates to the SLO List page', async () => {
|
||||
useParamsMock.mockReturnValue(anSLO.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ loading: false, slo: anSLO });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeTruthy();
|
||||
});
|
||||
render(<SloDetailsPage />, { unsafe: { slo: { enabled: true } } });
|
||||
|
||||
it('renders the loading spiner when fetching the SLO', async () => {
|
||||
useParamsMock.mockReturnValue(anSLO.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ loading: true, slo: undefined });
|
||||
render(<SloDetailsPage />, config);
|
||||
expect(mockNavigate).toBeCalledWith(mockBasePathPrepend(paths.observability.slos));
|
||||
});
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeFalsy();
|
||||
expect(screen.queryByTestId('loadingTitle')).toBeTruthy();
|
||||
expect(screen.queryByTestId('loadingDetails')).toBeTruthy();
|
||||
});
|
||||
describe('when the correct license is found', () => {
|
||||
it('renders the not found page when the SLO cannot be found', async () => {
|
||||
useParamsMock.mockReturnValue('inexistant');
|
||||
useFetchSloDetailsMock.mockReturnValue({ loading: false, slo: undefined });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
it('renders the SLO details page when the feature flag is enabled', async () => {
|
||||
useParamsMock.mockReturnValue(anSLO.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ loading: false, slo: anSLO });
|
||||
render(<SloDetailsPage />, config);
|
||||
render(<SloDetailsPage />, config);
|
||||
|
||||
expect(screen.queryByTestId('sloDetailsPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloDetails')).toBeTruthy();
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders the loading spinner when fetching the SLO', async () => {
|
||||
useParamsMock.mockReturnValue(anSLO.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ loading: true, slo: undefined });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SloDetailsPage />, config);
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeFalsy();
|
||||
expect(screen.queryByTestId('loadingTitle')).toBeTruthy();
|
||||
expect(screen.queryByTestId('loadingDetails')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders the SLO details page', async () => {
|
||||
useParamsMock.mockReturnValue(anSLO.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ loading: false, slo: anSLO });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SloDetailsPage />, config);
|
||||
|
||||
expect(screen.queryByTestId('sloDetailsPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloDetails')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,39 +6,51 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { IBasePath } from '@kbn/core-http-browser';
|
||||
import { EuiBreadcrumbProps } from '@elastic/eui/src/components/breadcrumbs/breadcrumb';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { SLOResponse } from '@kbn/slo-schema';
|
||||
import { ObservabilityAppServices } from '../../application/types';
|
||||
import { paths } from '../../config';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { IBasePath } from '@kbn/core-http-browser';
|
||||
import type { SLOResponse } from '@kbn/slo-schema';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details';
|
||||
import { useLicense } from '../../hooks/use_license';
|
||||
import PageNotFound from '../404';
|
||||
import { isSloFeatureEnabled } from '../slos/helpers/is_slo_feature_enabled';
|
||||
import { SLOS_BREADCRUMB_TEXT } from '../slos/translations';
|
||||
import { SloDetailsPathParams } from './types';
|
||||
import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details';
|
||||
import { SloDetails } from './components/slo_details';
|
||||
import { SLO_DETAILS_BREADCRUMB_TEXT } from './translations';
|
||||
import { PageTitle } from './components/page_title';
|
||||
import { paths } from '../../config';
|
||||
import type { SloDetailsPathParams } from './types';
|
||||
import type { ObservabilityAppServices } from '../../application/types';
|
||||
|
||||
export function SloDetailsPage() {
|
||||
const { http } = useKibana<ObservabilityAppServices>().services;
|
||||
const {
|
||||
application: { navigateToUrl },
|
||||
http: { basePath },
|
||||
} = useKibana<ObservabilityAppServices>().services;
|
||||
|
||||
const { ObservabilityPageTemplate, config } = usePluginContext();
|
||||
const { sloId } = useParams<SloDetailsPathParams>();
|
||||
|
||||
const { hasAtLeast } = useLicense();
|
||||
const hasRightLicense = hasAtLeast('platinum');
|
||||
|
||||
const { loading, slo } = useFetchSloDetails(sloId);
|
||||
useBreadcrumbs(getBreadcrumbs(http.basePath, slo));
|
||||
|
||||
useBreadcrumbs(getBreadcrumbs(basePath, slo));
|
||||
|
||||
const isSloNotFound = !loading && slo === undefined;
|
||||
|
||||
if (!isSloFeatureEnabled(config) || isSloNotFound) {
|
||||
return <PageNotFound />;
|
||||
}
|
||||
|
||||
if (hasRightLicense === false) {
|
||||
navigateToUrl(basePath.prepend(paths.observability.slos));
|
||||
}
|
||||
|
||||
return (
|
||||
<ObservabilityPageTemplate
|
||||
pageHeader={{
|
||||
|
@ -58,10 +70,16 @@ function getBreadcrumbs(basePath: IBasePath, slo: SLOResponse | undefined): EuiB
|
|||
return [
|
||||
{
|
||||
href: basePath.prepend(paths.observability.slos),
|
||||
text: SLOS_BREADCRUMB_TEXT,
|
||||
text: i18n.translate('xpack.observability.breadcrumbs.slosLinkText', {
|
||||
defaultMessage: 'SLOs',
|
||||
}),
|
||||
},
|
||||
{
|
||||
text: slo?.name ?? SLO_DETAILS_BREADCRUMB_TEXT,
|
||||
text:
|
||||
slo?.name ??
|
||||
i18n.translate('xpack.observability.breadcrumbs.sloDetailsLinkText', {
|
||||
defaultMessage: 'Details',
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SLO_DETAILS_PAGE_TITLE = i18n.translate('xpack.observability.sloDetailsPageTitle', {
|
||||
defaultMessage: 'SLO Details',
|
||||
});
|
||||
|
||||
export const SLO_DETAILS_BREADCRUMB_TEXT = i18n.translate(
|
||||
'xpack.observability.breadcrumbs.sloDetailsLinkText',
|
||||
{
|
||||
defaultMessage: 'Details',
|
||||
}
|
||||
);
|
|
@ -1,79 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SLO Edit Page when a sloId route param is provided calls the updateSlo hook if all required values are filled in 1`] = `
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"2f17deb0-725a-11ed-ab7c-4bb641cfc57e",
|
||||
Object {
|
||||
"budgetingMethod": "occurrences",
|
||||
"description": "irrelevant",
|
||||
"indicator": Object {
|
||||
"params": Object {
|
||||
"filter": "baz: foo and bar > 2",
|
||||
"good": "http_status: 2xx",
|
||||
"index": "some-index",
|
||||
"total": "a query",
|
||||
},
|
||||
"type": "sli.kql.custom",
|
||||
},
|
||||
"name": "irrelevant",
|
||||
"objective": Object {
|
||||
"target": 0.98,
|
||||
},
|
||||
"settings": Object {
|
||||
"frequency": "1m",
|
||||
"syncDelay": "1m",
|
||||
"timestampField": "@timestamp",
|
||||
},
|
||||
"timeWindow": Object {
|
||||
"duration": "30d",
|
||||
"isRolling": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`SLO Edit Page when no sloId route param is provided calls the createSlo hook if all required values are filled in 1`] = `
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"budgetingMethod": "occurrences",
|
||||
"description": "irrelevant",
|
||||
"indicator": Object {
|
||||
"params": Object {
|
||||
"filter": "irrelevant",
|
||||
"good": "irrelevant",
|
||||
"index": "some-index",
|
||||
"total": "irrelevant",
|
||||
},
|
||||
"type": "sli.kql.custom",
|
||||
},
|
||||
"name": "irrelevant",
|
||||
"objective": Object {
|
||||
"target": 0.985,
|
||||
},
|
||||
"timeWindow": Object {
|
||||
"duration": "7d",
|
||||
"isRolling": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
EuiAvatar,
|
||||
EuiButton,
|
||||
|
@ -73,28 +73,31 @@ export function SloEditForm({ slo }: Props) {
|
|||
}
|
||||
};
|
||||
|
||||
if (success) {
|
||||
toasts.addSuccess(
|
||||
isEditMode
|
||||
? i18n.translate('xpack.observability.slos.sloEdit.update.success', {
|
||||
defaultMessage: 'Successfully updated {name}',
|
||||
values: { name: getValues().name },
|
||||
})
|
||||
: i18n.translate('xpack.observability.slos.sloEdit.creation.success', {
|
||||
defaultMessage: 'Successfully created {name}',
|
||||
values: { name: getValues().name },
|
||||
})
|
||||
);
|
||||
navigateToUrl(basePath.prepend(paths.observability.slos));
|
||||
}
|
||||
useEffect(() => {
|
||||
if (success) {
|
||||
toasts.addSuccess(
|
||||
isEditMode
|
||||
? i18n.translate('xpack.observability.slos.sloEdit.update.success', {
|
||||
defaultMessage: 'Successfully updated {name}',
|
||||
values: { name: getValues().name },
|
||||
})
|
||||
: i18n.translate('xpack.observability.slos.sloEdit.creation.success', {
|
||||
defaultMessage: 'Successfully created {name}',
|
||||
values: { name: getValues().name },
|
||||
})
|
||||
);
|
||||
|
||||
if (error) {
|
||||
toasts.addError(new Error(error), {
|
||||
title: i18n.translate('xpack.observability.slos.sloEdit.creation.error', {
|
||||
defaultMessage: 'Something went wrong',
|
||||
}),
|
||||
});
|
||||
}
|
||||
navigateToUrl(basePath.prepend(paths.observability.slos));
|
||||
}
|
||||
|
||||
if (error) {
|
||||
toasts.addError(new Error(error), {
|
||||
title: i18n.translate('xpack.observability.slos.sloEdit.creation.error', {
|
||||
defaultMessage: 'Something went wrong',
|
||||
}),
|
||||
});
|
||||
}
|
||||
}, [success, error, toasts, isEditMode, getValues, navigateToUrl, basePath]);
|
||||
|
||||
return (
|
||||
<EuiTimeline data-test-subj="sloForm">
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
import React from 'react';
|
||||
import Router from 'react-router-dom';
|
||||
import { waitFor, fireEvent, screen } from '@testing-library/dom';
|
||||
import { BasePath } from '@kbn/core-http-server-internal';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { render } from '../../utils/test_helper';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { useLicense } from '../../hooks/use_license';
|
||||
import { useFetchIndices } from '../../hooks/use_fetch_indices';
|
||||
import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details';
|
||||
import { useCreateOrUpdateSlo } from '../../hooks/slo/use_create_slo';
|
||||
|
@ -31,6 +31,7 @@ jest.mock('react-router-dom', () => ({
|
|||
}));
|
||||
|
||||
jest.mock('../../hooks/use_breadcrumbs');
|
||||
jest.mock('../../hooks/use_license');
|
||||
jest.mock('../../hooks/use_fetch_indices');
|
||||
jest.mock('../../hooks/slo/use_fetch_slo_details');
|
||||
jest.mock('../../hooks/slo/use_create_slo');
|
||||
|
@ -41,6 +42,8 @@ jest.mock('../../utils/kibana_react', () => ({
|
|||
useKibana: jest.fn(() => mockUseKibanaReturnValue),
|
||||
}));
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mock;
|
||||
const useLicenseMock = useLicense as jest.Mock;
|
||||
const useFetchIndicesMock = useFetchIndices as jest.Mock;
|
||||
const useFetchSloMock = useFetchSloDetails as jest.Mock;
|
||||
const useCreateOrUpdateSloMock = useCreateOrUpdateSlo as jest.Mock;
|
||||
|
@ -48,21 +51,26 @@ const useCreateOrUpdateSloMock = useCreateOrUpdateSlo as jest.Mock;
|
|||
const mockAddSuccess = jest.fn();
|
||||
const mockAddError = jest.fn();
|
||||
const mockNavigate = jest.fn();
|
||||
const mockBasePathPrepend = jest.fn();
|
||||
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
application: { navigateToUrl: mockNavigate },
|
||||
http: {
|
||||
basePath: new BasePath('', undefined),
|
||||
},
|
||||
notifications: {
|
||||
toasts: {
|
||||
addSuccess: mockAddSuccess,
|
||||
addError: mockAddError,
|
||||
const mockKibana = () => {
|
||||
useKibanaMock.mockReturnValue({
|
||||
services: {
|
||||
application: { navigateToUrl: mockNavigate },
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: mockBasePathPrepend,
|
||||
},
|
||||
},
|
||||
notifications: {
|
||||
toasts: {
|
||||
addSuccess: mockAddSuccess,
|
||||
addError: mockAddError,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const config: Subset<ConfigSchema> = {
|
||||
unsafe: {
|
||||
|
@ -73,6 +81,7 @@ const config: Subset<ConfigSchema> = {
|
|||
describe('SLO Edit Page', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockKibana();
|
||||
|
||||
// Silence all the ref errors in Eui components.
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
@ -84,6 +93,7 @@ describe('SLO Edit Page', () => {
|
|||
it('renders the not found page when no sloId param is passed', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: undefined });
|
||||
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: undefined });
|
||||
|
||||
render(<SloEditPage />, { unsafe: { slo: { enabled: false } } });
|
||||
|
@ -94,6 +104,7 @@ describe('SLO Edit Page', () => {
|
|||
it('renders the not found page when sloId param is passed', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '1234' });
|
||||
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: undefined });
|
||||
|
||||
render(<SloEditPage />, { unsafe: { slo: { enabled: false } } });
|
||||
|
@ -102,270 +113,387 @@ describe('SLO Edit Page', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when no sloId route param is provided', () => {
|
||||
it('renders the SLO Edit page in pristine state', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: undefined });
|
||||
describe('when the feature flag is enabled', () => {
|
||||
describe('when the incorrect license is found', () => {
|
||||
it('navigates to the SLO List page', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '1234' });
|
||||
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: undefined });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useCreateOrUpdateSloMock.mockReturnValue({
|
||||
loading: false,
|
||||
success: false,
|
||||
error: '',
|
||||
createSlo: jest.fn(),
|
||||
updateSlo: jest.fn(),
|
||||
});
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: undefined });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useCreateOrUpdateSloMock.mockReturnValue({
|
||||
loading: false,
|
||||
success: false,
|
||||
error: '',
|
||||
createSlo: jest.fn(),
|
||||
updateSlo: jest.fn(),
|
||||
});
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
expect(screen.queryByTestId('slosEditPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloForm')).toBeTruthy();
|
||||
|
||||
expect(screen.queryByTestId('sloFormIndicatorTypeSelect')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormCustomKqlIndexInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.index
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormCustomKqlFilterQueryInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type === 'sli.kql.custom'
|
||||
? SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.filter
|
||||
: ''
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormCustomKqlGoodQueryInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type === 'sli.kql.custom'
|
||||
? SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.good
|
||||
: ''
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormCustomKqlTotalQueryInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type === 'sli.kql.custom'
|
||||
? SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.total
|
||||
: ''
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormBudgetingMethodSelect')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.budgetingMethod
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormTimeWindowDurationSelect')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.timeWindow.duration as any
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormObjectiveTargetInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.objective.target
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormNameInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.name
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormDescriptionTextArea')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.description
|
||||
);
|
||||
});
|
||||
|
||||
it.skip('calls the createSlo hook if all required values are filled in', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: undefined });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: undefined });
|
||||
const mockCreate = jest.fn();
|
||||
const mockUpdate = jest.fn();
|
||||
useCreateOrUpdateSloMock.mockReturnValue({
|
||||
loading: false,
|
||||
success: false,
|
||||
error: '',
|
||||
createSlo: mockCreate,
|
||||
updateSlo: mockUpdate,
|
||||
});
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
userEvent.type(screen.getByTestId('sloFormCustomKqlIndexInput'), 'some-index');
|
||||
userEvent.type(screen.getByTestId('sloFormCustomKqlFilterQueryInput'), 'irrelevant');
|
||||
userEvent.type(screen.getByTestId('sloFormCustomKqlGoodQueryInput'), 'irrelevant');
|
||||
userEvent.type(screen.getByTestId('sloFormCustomKqlTotalQueryInput'), 'irrelevant');
|
||||
userEvent.selectOptions(screen.getByTestId('sloFormBudgetingMethodSelect'), 'occurrences');
|
||||
userEvent.selectOptions(screen.getByTestId('sloFormTimeWindowDurationSelect'), '7d');
|
||||
userEvent.clear(screen.getByTestId('sloFormObjectiveTargetInput'));
|
||||
userEvent.type(screen.getByTestId('sloFormObjectiveTargetInput'), '98.5');
|
||||
userEvent.type(screen.getByTestId('sloFormNameInput'), 'irrelevant');
|
||||
userEvent.type(screen.getByTestId('sloFormDescriptionTextArea'), 'irrelevant');
|
||||
|
||||
const t = Date.now();
|
||||
await waitFor(() => expect(screen.getByTestId('sloFormSubmitButton')).toBeEnabled());
|
||||
console.log('end waiting for submit button: ', Math.ceil(Date.now() - t));
|
||||
|
||||
fireEvent.click(screen.getByTestId('sloFormSubmitButton')!);
|
||||
|
||||
expect(mockCreate).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a sloId route param is provided', () => {
|
||||
it('renders the SLO Edit page with prefilled form values', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: anSLO });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useCreateOrUpdateSloMock.mockReturnValue({
|
||||
loading: false,
|
||||
success: false,
|
||||
error: '',
|
||||
createSlo: jest.fn(),
|
||||
updateSlo: jest.fn(),
|
||||
});
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
expect(screen.queryByTestId('slosEditPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloForm')).toBeTruthy();
|
||||
|
||||
expect(screen.queryByTestId('sloFormIndicatorTypeSelect')).toHaveValue(anSLO.indicator.type);
|
||||
|
||||
expect(screen.queryByTestId('sloFormCustomKqlIndexInput')).toHaveValue(
|
||||
anSLO.indicator.params.index
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormCustomKqlFilterQueryInput')).toHaveValue(
|
||||
anSLO.indicator.type === 'sli.kql.custom' ? anSLO.indicator.params.filter : ''
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormCustomKqlGoodQueryInput')).toHaveValue(
|
||||
anSLO.indicator.type === 'sli.kql.custom' ? anSLO.indicator.params.good : ''
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormCustomKqlTotalQueryInput')).toHaveValue(
|
||||
anSLO.indicator.type === 'sli.kql.custom' ? anSLO.indicator.params.total : ''
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormBudgetingMethodSelect')).toHaveValue(
|
||||
anSLO.budgetingMethod
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormTimeWindowDurationSelect')).toHaveValue(
|
||||
anSLO.timeWindow.duration
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormObjectiveTargetInput')).toHaveValue(
|
||||
anSLO.objective.target * 100
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormNameInput')).toHaveValue(anSLO.name);
|
||||
expect(screen.queryByTestId('sloFormDescriptionTextArea')).toHaveValue(anSLO.description);
|
||||
});
|
||||
|
||||
it('calls the updateSlo hook if all required values are filled in', async () => {
|
||||
// Note: the `anSLO` object is considered to have (at least)
|
||||
// values for all required fields.
|
||||
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: anSLO });
|
||||
const mockCreate = jest.fn();
|
||||
const mockUpdate = jest.fn();
|
||||
useCreateOrUpdateSloMock.mockReturnValue({
|
||||
loading: false,
|
||||
success: false,
|
||||
error: '',
|
||||
createSlo: mockCreate,
|
||||
updateSlo: mockUpdate,
|
||||
});
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('sloFormSubmitButton')).toBeEnabled());
|
||||
|
||||
fireEvent.click(screen.queryByTestId('sloFormSubmitButton')!);
|
||||
|
||||
expect(mockUpdate).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('blocks submitting if not all required values are filled in', async () => {
|
||||
// Note: the `anSLO` object is considered to have (at least)
|
||||
// values for all required fields.
|
||||
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [],
|
||||
});
|
||||
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: { ...anSLO, name: '' } });
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('sloFormSubmitButton')).toBeDisabled();
|
||||
expect(mockNavigate).toBeCalledWith(mockBasePathPrepend(paths.observability.slos));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('if submitting has completed successfully', () => {
|
||||
it('renders a success toast', async () => {
|
||||
// Note: the `anSLO` object is considered to have (at least)
|
||||
// values for all required fields.
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: anSLO });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useCreateOrUpdateSloMock.mockReturnValue({
|
||||
loading: false,
|
||||
success: true,
|
||||
error: '',
|
||||
createSlo: jest.fn(),
|
||||
updateSlo: jest.fn(),
|
||||
});
|
||||
render(<SloEditPage />, config);
|
||||
expect(mockAddSuccess).toBeCalled();
|
||||
});
|
||||
describe('when the correct license is found', () => {
|
||||
describe('when no sloId route param is provided', () => {
|
||||
it('renders the SLO Edit page in pristine state', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: undefined });
|
||||
|
||||
it('navigates to the SLO List page', async () => {
|
||||
// Note: the `anSLO` object is considered to have (at least)
|
||||
// values for all required fields.
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: anSLO });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useCreateOrUpdateSloMock.mockReturnValue({
|
||||
loading: false,
|
||||
success: true,
|
||||
error: '',
|
||||
createSlo: jest.fn(),
|
||||
updateSlo: jest.fn(),
|
||||
});
|
||||
render(<SloEditPage />, config);
|
||||
expect(mockNavigate).toBeCalledWith(paths.observability.slos);
|
||||
});
|
||||
});
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: undefined });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useCreateOrUpdateSloMock.mockReturnValue({
|
||||
loading: false,
|
||||
success: false,
|
||||
error: '',
|
||||
createSlo: jest.fn(),
|
||||
updateSlo: jest.fn(),
|
||||
});
|
||||
|
||||
describe('if submitting has not completed successfully', () => {
|
||||
it('renders an error toast', async () => {
|
||||
// Note: the `anSLO` object is considered to have (at least)
|
||||
// values for all required fields.
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: anSLO });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
expect(screen.queryByTestId('slosEditPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloForm')).toBeTruthy();
|
||||
|
||||
expect(screen.queryByTestId('sloFormIndicatorTypeSelect')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormCustomKqlIndexInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.index
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormCustomKqlFilterQueryInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type === 'sli.kql.custom'
|
||||
? SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.filter
|
||||
: ''
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormCustomKqlGoodQueryInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type === 'sli.kql.custom'
|
||||
? SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.good
|
||||
: ''
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormCustomKqlTotalQueryInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type === 'sli.kql.custom'
|
||||
? SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.total
|
||||
: ''
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormBudgetingMethodSelect')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.budgetingMethod
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormTimeWindowDurationSelect')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.timeWindow.duration as any
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormObjectiveTargetInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.objective.target
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormNameInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.name
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormDescriptionTextArea')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.description
|
||||
);
|
||||
});
|
||||
|
||||
it.skip('calls the createSlo hook if all required values are filled in', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: undefined });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: undefined });
|
||||
const mockCreate = jest.fn();
|
||||
const mockUpdate = jest.fn();
|
||||
useCreateOrUpdateSloMock.mockReturnValue({
|
||||
loading: false,
|
||||
success: false,
|
||||
error: '',
|
||||
createSlo: mockCreate,
|
||||
updateSlo: mockUpdate,
|
||||
});
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
userEvent.type(screen.getByTestId('sloFormCustomKqlIndexInput'), 'some-index');
|
||||
userEvent.type(screen.getByTestId('sloFormCustomKqlFilterQueryInput'), 'irrelevant');
|
||||
userEvent.type(screen.getByTestId('sloFormCustomKqlGoodQueryInput'), 'irrelevant');
|
||||
userEvent.type(screen.getByTestId('sloFormCustomKqlTotalQueryInput'), 'irrelevant');
|
||||
userEvent.selectOptions(
|
||||
screen.getByTestId('sloFormBudgetingMethodSelect'),
|
||||
'occurrences'
|
||||
);
|
||||
userEvent.selectOptions(screen.getByTestId('sloFormTimeWindowDurationSelect'), '7d');
|
||||
userEvent.clear(screen.getByTestId('sloFormObjectiveTargetInput'));
|
||||
userEvent.type(screen.getByTestId('sloFormObjectiveTargetInput'), '98.5');
|
||||
userEvent.type(screen.getByTestId('sloFormNameInput'), 'irrelevant');
|
||||
userEvent.type(screen.getByTestId('sloFormDescriptionTextArea'), 'irrelevant');
|
||||
|
||||
const t = Date.now();
|
||||
await waitFor(() => expect(screen.getByTestId('sloFormSubmitButton')).toBeEnabled());
|
||||
console.log('end waiting for submit button: ', Math.ceil(Date.now() - t));
|
||||
|
||||
fireEvent.click(screen.getByTestId('sloFormSubmitButton')!);
|
||||
|
||||
expect(mockCreate).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"budgetingMethod": "occurrences",
|
||||
"description": "irrelevant",
|
||||
"indicator": Object {
|
||||
"params": Object {
|
||||
"filter": "irrelevant",
|
||||
"good": "irrelevant",
|
||||
"index": "some-index",
|
||||
"total": "irrelevant",
|
||||
},
|
||||
"type": "sli.kql.custom",
|
||||
},
|
||||
"name": "irrelevant",
|
||||
"objective": Object {
|
||||
"target": 0.985,
|
||||
},
|
||||
"timeWindow": Object {
|
||||
"duration": "7d",
|
||||
"isRolling": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
useCreateOrUpdateSloMock.mockReturnValue({
|
||||
loading: false,
|
||||
success: false,
|
||||
error: 'Argh, API died',
|
||||
createSlo: jest.fn(),
|
||||
updateSlo: jest.fn(),
|
||||
|
||||
describe('when a sloId route param is provided', () => {
|
||||
it('renders the SLO Edit page with prefilled form values', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: anSLO });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useCreateOrUpdateSloMock.mockReturnValue({
|
||||
loading: false,
|
||||
success: false,
|
||||
error: '',
|
||||
createSlo: jest.fn(),
|
||||
updateSlo: jest.fn(),
|
||||
});
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
expect(screen.queryByTestId('slosEditPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloForm')).toBeTruthy();
|
||||
|
||||
expect(screen.queryByTestId('sloFormIndicatorTypeSelect')).toHaveValue(
|
||||
anSLO.indicator.type
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormCustomKqlIndexInput')).toHaveValue(
|
||||
anSLO.indicator.params.index
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormCustomKqlFilterQueryInput')).toHaveValue(
|
||||
anSLO.indicator.type === 'sli.kql.custom' ? anSLO.indicator.params.filter : ''
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormCustomKqlGoodQueryInput')).toHaveValue(
|
||||
anSLO.indicator.type === 'sli.kql.custom' ? anSLO.indicator.params.good : ''
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormCustomKqlTotalQueryInput')).toHaveValue(
|
||||
anSLO.indicator.type === 'sli.kql.custom' ? anSLO.indicator.params.total : ''
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormBudgetingMethodSelect')).toHaveValue(
|
||||
anSLO.budgetingMethod
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormTimeWindowDurationSelect')).toHaveValue(
|
||||
anSLO.timeWindow.duration
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormObjectiveTargetInput')).toHaveValue(
|
||||
anSLO.objective.target * 100
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormNameInput')).toHaveValue(anSLO.name);
|
||||
expect(screen.queryByTestId('sloFormDescriptionTextArea')).toHaveValue(anSLO.description);
|
||||
});
|
||||
|
||||
it('calls the updateSlo hook if all required values are filled in', async () => {
|
||||
// Note: the `anSLO` object is considered to have (at least)
|
||||
// values for all required fields.
|
||||
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: anSLO });
|
||||
const mockCreate = jest.fn();
|
||||
const mockUpdate = jest.fn();
|
||||
useCreateOrUpdateSloMock.mockReturnValue({
|
||||
loading: false,
|
||||
success: false,
|
||||
error: '',
|
||||
createSlo: mockCreate,
|
||||
updateSlo: mockUpdate,
|
||||
});
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('sloFormSubmitButton')).toBeEnabled());
|
||||
|
||||
fireEvent.click(screen.queryByTestId('sloFormSubmitButton')!);
|
||||
|
||||
expect(mockUpdate).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"2f17deb0-725a-11ed-ab7c-4bb641cfc57e",
|
||||
Object {
|
||||
"budgetingMethod": "occurrences",
|
||||
"description": "irrelevant",
|
||||
"indicator": Object {
|
||||
"params": Object {
|
||||
"filter": "baz: foo and bar > 2",
|
||||
"good": "http_status: 2xx",
|
||||
"index": "some-index",
|
||||
"total": "a query",
|
||||
},
|
||||
"type": "sli.kql.custom",
|
||||
},
|
||||
"name": "irrelevant",
|
||||
"objective": Object {
|
||||
"target": 0.98,
|
||||
},
|
||||
"settings": Object {
|
||||
"frequency": "1m",
|
||||
"syncDelay": "1m",
|
||||
"timestampField": "@timestamp",
|
||||
},
|
||||
"timeWindow": Object {
|
||||
"duration": "30d",
|
||||
"isRolling": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('blocks submitting if not all required values are filled in', async () => {
|
||||
// Note: the `anSLO` object is considered to have (at least)
|
||||
// values for all required fields.
|
||||
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [],
|
||||
});
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: { ...anSLO, name: '' } });
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('sloFormSubmitButton')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when submitting has completed successfully', () => {
|
||||
it('renders a success toast', async () => {
|
||||
// Note: the `anSLO` object is considered to have (at least)
|
||||
// values for all required fields.
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: anSLO });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useCreateOrUpdateSloMock.mockReturnValue({
|
||||
loading: false,
|
||||
success: true,
|
||||
error: '',
|
||||
createSlo: jest.fn(),
|
||||
updateSlo: jest.fn(),
|
||||
});
|
||||
render(<SloEditPage />, config);
|
||||
expect(mockAddSuccess).toBeCalled();
|
||||
});
|
||||
|
||||
it('navigates to the SLO List page', async () => {
|
||||
// Note: the `anSLO` object is considered to have (at least)
|
||||
// values for all required fields.
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: anSLO });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useCreateOrUpdateSloMock.mockReturnValue({
|
||||
loading: false,
|
||||
success: true,
|
||||
error: '',
|
||||
createSlo: jest.fn(),
|
||||
updateSlo: jest.fn(),
|
||||
});
|
||||
render(<SloEditPage />, config);
|
||||
expect(mockNavigate).toBeCalledWith(mockBasePathPrepend(paths.observability.slos));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when submitting has not completed successfully', () => {
|
||||
it('renders an error toast', async () => {
|
||||
// Note: the `anSLO` object is considered to have (at least)
|
||||
// values for all required fields.
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
useFetchSloMock.mockReturnValue({ loading: false, slo: anSLO });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
loading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useCreateOrUpdateSloMock.mockReturnValue({
|
||||
loading: false,
|
||||
success: false,
|
||||
error: 'Argh, API died',
|
||||
createSlo: jest.fn(),
|
||||
updateSlo: jest.fn(),
|
||||
});
|
||||
render(<SloEditPage />, config);
|
||||
expect(mockAddError).toBeCalled();
|
||||
});
|
||||
});
|
||||
render(<SloEditPage />, config);
|
||||
expect(mockAddError).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,25 +9,31 @@ import React from 'react';
|
|||
import { useParams } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { ObservabilityAppServices } from '../../application/types';
|
||||
import { paths } from '../../config';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details';
|
||||
import { useLicense } from '../../hooks/use_license';
|
||||
import { SloEditForm } from './components/slo_edit_form';
|
||||
import PageNotFound from '../404';
|
||||
import { isSloFeatureEnabled } from '../slos/helpers/is_slo_feature_enabled';
|
||||
|
||||
export function SloEditPage() {
|
||||
const { http } = useKibana<ObservabilityAppServices>().services;
|
||||
const {
|
||||
application: { navigateToUrl },
|
||||
http: { basePath },
|
||||
} = useKibana().services;
|
||||
const { ObservabilityPageTemplate, config } = usePluginContext();
|
||||
|
||||
const { sloId } = useParams<{ sloId: string | undefined }>();
|
||||
|
||||
const { hasAtLeast } = useLicense();
|
||||
const hasRightLicense = hasAtLeast('platinum');
|
||||
|
||||
useBreadcrumbs([
|
||||
{
|
||||
href: http.basePath.prepend(paths.observability.slos),
|
||||
href: basePath.prepend(paths.observability.slos),
|
||||
text: i18n.translate('xpack.observability.breadcrumbs.sloEditLinkText', {
|
||||
defaultMessage: 'SLOs',
|
||||
}),
|
||||
|
@ -40,6 +46,10 @@ export function SloEditPage() {
|
|||
return <PageNotFound />;
|
||||
}
|
||||
|
||||
if (hasRightLicense === false) {
|
||||
navigateToUrl(basePath.prepend(paths.observability.slos));
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { ComponentStory } from '@storybook/react';
|
||||
|
||||
import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator';
|
||||
import { SloListWelcomePrompt as Component } from './slo_list_welcome_prompt';
|
||||
|
||||
export default {
|
||||
component: Component,
|
||||
title: 'app/SLO/ListPage/SloListWelcomePrompt',
|
||||
decorators: [KibanaReactStorybookDecorator],
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof Component> = () => <Component />;
|
||||
|
||||
export const SloListWelcomePrompt = Template.bind({});
|
|
@ -6,11 +6,21 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiPageTemplate, EuiButton, EuiTitle, EuiLink, EuiImage } from '@elastic/eui';
|
||||
import {
|
||||
EuiPageTemplate,
|
||||
EuiButton,
|
||||
EuiTitle,
|
||||
EuiLink,
|
||||
EuiImage,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { paths } from '../../../config';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { useLicense } from '../../../hooks/use_license';
|
||||
import { paths } from '../../../config';
|
||||
import illustration from './assets/illustration.svg';
|
||||
|
||||
export function SloListWelcomePrompt() {
|
||||
|
@ -19,6 +29,10 @@ export function SloListWelcomePrompt() {
|
|||
http: { basePath },
|
||||
} = useKibana().services;
|
||||
|
||||
const { hasAtLeast } = useLicense();
|
||||
|
||||
const hasRightLicense = hasAtLeast('platinum');
|
||||
|
||||
const handleClickCreateSlo = () => {
|
||||
navigateToUrl(basePath.prepend(paths.observability.sloCreate));
|
||||
};
|
||||
|
@ -58,20 +72,92 @@ export function SloListWelcomePrompt() {
|
|||
'Easily report the uptime and reliability of your services to stakeholders with real-time insights.',
|
||||
})}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{i18n.translate('xpack.observability.slos.sloList.welcomePrompt.messageParagraph3', {
|
||||
defaultMessage: 'To get started, create your first SLO.',
|
||||
})}
|
||||
</p>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<EuiButton color="primary" fill onClick={handleClickCreateSlo}>
|
||||
{i18n.translate('xpack.observability.slos.sloList.welcomePrompt.buttonLabel', {
|
||||
defaultMessage: 'Create first SLO',
|
||||
})}
|
||||
</EuiButton>
|
||||
<>
|
||||
{hasRightLicense ? (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xxs">
|
||||
<span>
|
||||
{i18n.translate(
|
||||
'xpack.observability.slos.sloList.welcomePrompt.getStartedMessage',
|
||||
{
|
||||
defaultMessage: 'To get started, create your first SLO.',
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<span>
|
||||
<EuiButton fill color="primary" onClick={handleClickCreateSlo}>
|
||||
{i18n.translate(
|
||||
'xpack.observability.slos.sloList.welcomePrompt.buttonLabel',
|
||||
{
|
||||
defaultMessage: 'Create SLO',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xxs">
|
||||
<span>
|
||||
{i18n.translate(
|
||||
'xpack.observability.slos.sloList.welcomePrompt.needLicenseMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'You need an Elastic Cloud subscription or Platinum license to use SLOs.',
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row">
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
fill
|
||||
href="https://www.elastic.co/cloud/elasticsearch-service/signup"
|
||||
target="_blank"
|
||||
data-test-subj="slosPageWelcomePromptSignupForCloudButton"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.observability.slos.sloList.welcomePrompt.signupForCloud',
|
||||
{
|
||||
defaultMessage: 'Sign up for Elastic Cloud',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
href="https://www.elastic.co/subscriptions"
|
||||
target="_blank"
|
||||
data-test-subj="slosPageWelcomePromptSignupForLicenseButton"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.observability.slos.sloList.welcomePrompt.signupForLicense',
|
||||
{
|
||||
defaultMessage: 'Sign up for license',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
|
|
|
@ -8,29 +8,44 @@
|
|||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
|
||||
import { ConfigSchema } from '../../plugin';
|
||||
import { Subset } from '../../typings';
|
||||
import { kibanaStartMock } from '../../utils/kibana_react.mock';
|
||||
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 { SlosPage } from '.';
|
||||
import { emptySloList, sloList } from '../../data/slo';
|
||||
import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list';
|
||||
import type { ConfigSchema } from '../../plugin';
|
||||
import type { Subset } from '../../typings';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../hooks/slo/use_fetch_slo_list');
|
||||
|
||||
jest.mock('../../utils/kibana_react');
|
||||
jest.mock('../../hooks/use_breadcrumbs');
|
||||
jest.mock('../../hooks/use_license');
|
||||
jest.mock('../../hooks/slo/use_fetch_slo_list');
|
||||
|
||||
const mockUseKibanaReturnValue = kibanaStartMock.startContract();
|
||||
|
||||
jest.mock('../../utils/kibana_react', () => ({
|
||||
useKibana: jest.fn(() => mockUseKibanaReturnValue),
|
||||
}));
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mock;
|
||||
const useLicenseMock = useLicense as jest.Mock;
|
||||
const useFetchSloListMock = useFetchSloList as jest.Mock;
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
|
||||
const mockKibana = () => {
|
||||
useKibanaMock.mockReturnValue({
|
||||
services: {
|
||||
application: { navigateToUrl: mockNavigate },
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const config: Subset<ConfigSchema> = {
|
||||
unsafe: {
|
||||
slo: { enabled: true },
|
||||
|
@ -40,38 +55,62 @@ const config: Subset<ConfigSchema> = {
|
|||
describe('SLOs Page', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockKibana();
|
||||
});
|
||||
|
||||
it('renders the not found page when the feature flag is not enabled', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ loading: false, sloList: emptySloList });
|
||||
describe('when the feature flag is not enabled', () => {
|
||||
it('renders the not found page ', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ loading: false, sloList: emptySloList });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SlosPage />, { unsafe: { slo: { enabled: false } } });
|
||||
render(<SlosPage />, { unsafe: { slo: { enabled: false } } });
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeTruthy();
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the feature flag is enabled', () => {
|
||||
it('renders nothing when the API is loading', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ loading: true, sloList: emptySloList });
|
||||
describe('when the incorrect license is found', () => {
|
||||
it('renders the welcome prompt with subscription buttons', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ loading: false, sloList: emptySloList });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
|
||||
|
||||
const { container } = render(<SlosPage />, config);
|
||||
render(<SlosPage />, config);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy();
|
||||
expect(screen.queryByTestId('slosPageWelcomePromptSignupForCloudButton')).toBeTruthy();
|
||||
expect(screen.queryByTestId('slosPageWelcomePromptSignupForLicenseButton')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the SLOs Welcome Prompt when the API has finished loading and there are no results', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ loading: false, sloList: emptySloList });
|
||||
render(<SlosPage />, config);
|
||||
describe('when the correct license is found', () => {
|
||||
it('renders nothing when the API is loading', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ loading: true, sloList: emptySloList });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy();
|
||||
});
|
||||
const { container } = render(<SlosPage />, config);
|
||||
|
||||
it('renders the SLOs page when the API has finished loading and there are results', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ loading: false, sloList });
|
||||
render(<SlosPage />, config);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('slosPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloList')).toBeTruthy();
|
||||
it('renders the SLOs Welcome Prompt when the API has finished loading and there are no results', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ loading: false, sloList: emptySloList });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SlosPage />, config);
|
||||
|
||||
expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders the SLOs page when the API has finished loading and there are results', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ loading: false, sloList });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SlosPage />, config);
|
||||
|
||||
expect(screen.queryByTestId('slosPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloList')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,17 +9,17 @@ import React from 'react';
|
|||
import { EuiButton } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { ObservabilityAppServices } from '../../application/types';
|
||||
import { paths } from '../../config';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { isSloFeatureEnabled } from './helpers/is_slo_feature_enabled';
|
||||
import { SLOS_BREADCRUMB_TEXT, SLOS_PAGE_TITLE } from './translations';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { useLicense } from '../../hooks/use_license';
|
||||
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
|
||||
import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list';
|
||||
import { SloList } from './components/slo_list';
|
||||
import { SloListWelcomePrompt } from './components/slo_list_welcome_prompt';
|
||||
import PageNotFound from '../404';
|
||||
import { paths } from '../../config';
|
||||
import { isSloFeatureEnabled } from './helpers/is_slo_feature_enabled';
|
||||
import type { ObservabilityAppServices } from '../../application/types';
|
||||
|
||||
export function SlosPage() {
|
||||
const {
|
||||
|
@ -28,6 +28,8 @@ export function SlosPage() {
|
|||
} = useKibana<ObservabilityAppServices>().services;
|
||||
const { ObservabilityPageTemplate, config } = usePluginContext();
|
||||
|
||||
const { hasAtLeast } = useLicense();
|
||||
|
||||
const {
|
||||
loading,
|
||||
sloList: { total },
|
||||
|
@ -36,7 +38,9 @@ export function SlosPage() {
|
|||
useBreadcrumbs([
|
||||
{
|
||||
href: basePath.prepend(paths.observability.slos),
|
||||
text: SLOS_BREADCRUMB_TEXT,
|
||||
text: i18n.translate('xpack.observability.breadcrumbs.slosLinkText', {
|
||||
defaultMessage: 'SLOs',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
|
@ -52,14 +56,16 @@ export function SlosPage() {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (total === 0) {
|
||||
if (total === 0 || !hasAtLeast('platinum')) {
|
||||
return <SloListWelcomePrompt />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ObservabilityPageTemplate
|
||||
pageHeader={{
|
||||
pageTitle: SLOS_PAGE_TITLE,
|
||||
pageTitle: i18n.translate('xpack.observability.slosPageTitle', {
|
||||
defaultMessage: 'SLOs',
|
||||
}),
|
||||
rightSideItems: [
|
||||
<EuiButton color="primary" fill onClick={handleClickCreateSlo}>
|
||||
{i18n.translate('xpack.observability.slos.sloList.pageHeader.createNewButtonLabel', {
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SLOS_PAGE_TITLE = i18n.translate('xpack.observability.slosPageTitle', {
|
||||
defaultMessage: 'SLOs',
|
||||
});
|
||||
|
||||
export const SLOS_BREADCRUMB_TEXT = i18n.translate('xpack.observability.breadcrumbs.slosLinkText', {
|
||||
defaultMessage: 'SLOs',
|
||||
});
|
|
@ -40,6 +40,7 @@ import {
|
|||
import { SecurityPluginStart } from '@kbn/security-plugin/public';
|
||||
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
|
||||
import { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
|
||||
import { RuleDetailsLocatorDefinition } from './locators/rule_details';
|
||||
import { observabilityAppId, observabilityFeatureId, casesPath } from '../common';
|
||||
import { createLazyObservabilityPageTemplate } from './components/shared';
|
||||
|
@ -89,21 +90,22 @@ export interface ObservabilityPublicPluginsSetup {
|
|||
}
|
||||
|
||||
export interface ObservabilityPublicPluginsStart {
|
||||
usageCollection: UsageCollectionSetup;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
cases: CasesUiStart;
|
||||
embeddable: EmbeddableStart;
|
||||
home?: HomePublicPluginStart;
|
||||
share: SharePluginStart;
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
data: DataPublicPluginStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
lens: LensPublicStart;
|
||||
discover: DiscoverStart;
|
||||
ruleTypeRegistry: RuleTypeRegistryContract;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
security: SecurityPluginStart;
|
||||
embeddable: EmbeddableStart;
|
||||
guidedOnboarding: GuidedOnboardingPluginStart;
|
||||
lens: LensPublicStart;
|
||||
licensing: LicensingPluginStart;
|
||||
ruleTypeRegistry: RuleTypeRegistryContract;
|
||||
security: SecurityPluginStart;
|
||||
share: SharePluginStart;
|
||||
spaces: SpacesPluginStart;
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
usageCollection: UsageCollectionSetup;
|
||||
home?: HomePublicPluginStart;
|
||||
}
|
||||
|
||||
export type ObservabilityPublicStart = ReturnType<Plugin['start']>;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { badRequest } from '@hapi/boom';
|
||||
import {
|
||||
createSLOParamsSchema,
|
||||
deleteSLOParamsSchema,
|
||||
|
@ -29,8 +30,9 @@ import {
|
|||
KQLCustomTransformGenerator,
|
||||
TransformGenerator,
|
||||
} from '../../services/slo/transform_generators';
|
||||
import { IndicatorTypes } from '../../domain/models';
|
||||
import { createObservabilityServerRoute } from '../create_observability_server_route';
|
||||
import type { IndicatorTypes } from '../../domain/models';
|
||||
import type { ObservabilityRequestHandlerContext } from '../../types';
|
||||
|
||||
const transformGenerators: Record<IndicatorTypes, TransformGenerator> = {
|
||||
'sli.apm.transactionDuration': new ApmTransactionDurationTransformGenerator(),
|
||||
|
@ -38,6 +40,10 @@ const transformGenerators: Record<IndicatorTypes, TransformGenerator> = {
|
|||
'sli.kql.custom': new KQLCustomTransformGenerator(),
|
||||
};
|
||||
|
||||
const isLicenseAtLeastPlatinum = async (context: ObservabilityRequestHandlerContext) => {
|
||||
return (await context.licensing).license.hasAtLeast('platinum');
|
||||
};
|
||||
|
||||
const createSLORoute = createObservabilityServerRoute({
|
||||
endpoint: 'POST /api/observability/slos',
|
||||
options: {
|
||||
|
@ -45,6 +51,10 @@ const createSLORoute = createObservabilityServerRoute({
|
|||
},
|
||||
params: createSLOParamsSchema,
|
||||
handler: async ({ context, params, logger }) => {
|
||||
if (!isLicenseAtLeastPlatinum(context)) {
|
||||
throw badRequest('Platinum license or higher is needed to make use of this feature.');
|
||||
}
|
||||
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
|
||||
|
@ -66,6 +76,10 @@ const updateSLORoute = createObservabilityServerRoute({
|
|||
},
|
||||
params: updateSLOParamsSchema,
|
||||
handler: async ({ context, params, logger }) => {
|
||||
if (!isLicenseAtLeastPlatinum(context)) {
|
||||
throw badRequest('Platinum license or higher is needed to make use of this feature.');
|
||||
}
|
||||
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
|
||||
|
@ -86,6 +100,10 @@ const deleteSLORoute = createObservabilityServerRoute({
|
|||
},
|
||||
params: deleteSLOParamsSchema,
|
||||
handler: async ({ context, params, logger }) => {
|
||||
if (!isLicenseAtLeastPlatinum(context)) {
|
||||
throw badRequest('Platinum license or higher is needed to make use of this feature.');
|
||||
}
|
||||
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
|
||||
|
@ -105,6 +123,10 @@ const getSLORoute = createObservabilityServerRoute({
|
|||
},
|
||||
params: getSLOParamsSchema,
|
||||
handler: async ({ context, params }) => {
|
||||
if (!isLicenseAtLeastPlatinum(context)) {
|
||||
throw badRequest('Platinum license or higher is needed to make use of this feature.');
|
||||
}
|
||||
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClient);
|
||||
|
@ -124,6 +146,10 @@ const findSLORoute = createObservabilityServerRoute({
|
|||
},
|
||||
params: findSLOParamsSchema,
|
||||
handler: async ({ context, params }) => {
|
||||
if (!isLicenseAtLeastPlatinum(context)) {
|
||||
throw badRequest('Platinum license or higher is needed to make use of this feature.');
|
||||
}
|
||||
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClient);
|
||||
|
|
|
@ -67,7 +67,6 @@
|
|||
"@kbn/share-plugin",
|
||||
"@kbn/core-notifications-browser",
|
||||
"@kbn/slo-schema",
|
||||
"@kbn/core-http-server-internal",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue