mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
feat(slo): scaffold slo details page (#146919)
This commit is contained in:
parent
9998b328fc
commit
a0b08dae24
11 changed files with 361 additions and 1 deletions
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { SLO } from '../../../typings';
|
||||
|
||||
export interface Props {
|
||||
slo: SLO | undefined;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function PageTitle(props: Props) {
|
||||
const { isLoading, slo } = props;
|
||||
if (isLoading) {
|
||||
return <EuiLoadingSpinner data-test-subj="loadingTitle" />;
|
||||
}
|
||||
|
||||
return <>{slo && slo.name}</>;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { SloDetails as Component, Props } from './slo_details';
|
||||
import { anSLO } from '../mocks/slo';
|
||||
|
||||
export default {
|
||||
component: Component,
|
||||
title: 'app/SLO/DetailsPage/SloDetails',
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof Component> = (props: Props) => <Component {...props} />;
|
||||
|
||||
const defaultProps: Props = {
|
||||
slo: anSLO,
|
||||
};
|
||||
|
||||
export const SloDetails = Template.bind({});
|
||||
SloDetails.args = defaultProps;
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { SLO } from '../../../typings';
|
||||
|
||||
export interface Props {
|
||||
slo: SLO;
|
||||
}
|
||||
|
||||
export function SloDetails(props: Props) {
|
||||
const { slo } = props;
|
||||
return <pre data-test-subj="sloDetails">{JSON.stringify(slo, null, 2)}</pre>;
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useDataFetcher } from '../../../hooks/use_data_fetcher';
|
||||
import { SLO } from '../../../typings';
|
||||
|
||||
interface UseFetchSloDetailsResponse {
|
||||
loading: boolean;
|
||||
slo: SLO | undefined;
|
||||
}
|
||||
|
||||
function useFetchSloDetails(sloId: string): UseFetchSloDetailsResponse {
|
||||
const params = useMemo(() => ({ sloId }), [sloId]);
|
||||
const shouldExecuteApiCall = useCallback(
|
||||
(apiCallParams: { sloId: string }) => params.sloId === apiCallParams.sloId,
|
||||
[params]
|
||||
);
|
||||
|
||||
const { loading, data: slo } = useDataFetcher<{ sloId: string }, SLO | undefined>({
|
||||
paramsForApiCall: params,
|
||||
initialDataState: undefined,
|
||||
executeApiCall: fetchSlo,
|
||||
shouldExecuteApiCall,
|
||||
});
|
||||
|
||||
return { loading, slo };
|
||||
}
|
||||
|
||||
const fetchSlo = async (
|
||||
params: { sloId: string },
|
||||
abortController: AbortController,
|
||||
http: HttpSetup
|
||||
): Promise<SLO | undefined> => {
|
||||
try {
|
||||
const response = await http.get<Record<string, unknown>>(
|
||||
`/api/observability/slos/${params.sloId}`,
|
||||
{
|
||||
query: {},
|
||||
signal: abortController.signal,
|
||||
}
|
||||
);
|
||||
if (response !== undefined) {
|
||||
return toSLO(response);
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore error for retrieving slos
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
function toSLO(result: any): SLO {
|
||||
return {
|
||||
id: String(result.id),
|
||||
name: String(result.name),
|
||||
objective: { target: Number(result.objective.target) },
|
||||
summary: {
|
||||
sliValue: Number(result.summary.sli_value),
|
||||
errorBudget: {
|
||||
remaining: Number(result.summary.error_budget.remaining),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type { UseFetchSloDetailsResponse };
|
||||
export { useFetchSloDetails };
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen } 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 { render } from '../../utils/test_helper';
|
||||
import { SloDetailsPage } from '.';
|
||||
import { useFetchSloDetails } from './hooks/use_fetch_slo_details';
|
||||
import { anSLO } from './mocks/slo';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./hooks/use_fetch_slo_details');
|
||||
jest.mock('../../utils/kibana_react');
|
||||
jest.mock('../../hooks/use_breadcrumbs');
|
||||
|
||||
const useFetchSloDetailsMock = useFetchSloDetails as jest.Mock;
|
||||
const useParamsMock = useParams as jest.Mock;
|
||||
const useKibanaMock = useKibana as jest.Mock;
|
||||
const mockKibana = () => {
|
||||
useKibanaMock.mockReturnValue({
|
||||
services: {
|
||||
...kibanaStartMock.startContract(),
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const config: Subset<ConfigSchema> = {
|
||||
unsafe: {
|
||||
slo: { enabled: true },
|
||||
},
|
||||
};
|
||||
|
||||
describe('SLO Details Page', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
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 } } });
|
||||
|
||||
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);
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders the loading spiner when fetching the SLO', async () => {
|
||||
useParamsMock.mockReturnValue(anSLO.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ loading: true, slo: undefined });
|
||||
render(<SloDetailsPage />, config);
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeFalsy();
|
||||
expect(screen.queryByTestId('loadingTitle')).toBeTruthy();
|
||||
expect(screen.queryByTestId('loadingDetails')).toBeTruthy();
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
expect(screen.queryByTestId('sloDetailsPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloDetails')).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { 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 { 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 PageNotFound from '../404';
|
||||
import { isSloFeatureEnabled } from '../slos/helpers';
|
||||
import { SLOS_BREADCRUMB_TEXT } from '../slos/translations';
|
||||
import { SloDetailsPathParams } from './types';
|
||||
import { useFetchSloDetails } from './hooks/use_fetch_slo_details';
|
||||
import { SLO } from '../../typings';
|
||||
import { SloDetails } from './components/slo_details';
|
||||
import { SLO_DETAILS_BREADCRUMB_TEXT } from './translations';
|
||||
import { PageTitle } from './components/page_title';
|
||||
|
||||
export function SloDetailsPage() {
|
||||
const { http } = useKibana<ObservabilityAppServices>().services;
|
||||
const { ObservabilityPageTemplate, config } = usePluginContext();
|
||||
const { sloId } = useParams<SloDetailsPathParams>();
|
||||
|
||||
const { loading, slo } = useFetchSloDetails(sloId);
|
||||
useBreadcrumbs(getBreadcrumbs(http.basePath, slo));
|
||||
|
||||
const isSloNotFound = !loading && slo === undefined;
|
||||
if (!isSloFeatureEnabled(config) || isSloNotFound) {
|
||||
return <PageNotFound />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ObservabilityPageTemplate
|
||||
pageHeader={{
|
||||
pageTitle: <PageTitle isLoading={loading} slo={slo} />,
|
||||
rightSideItems: [],
|
||||
bottomBorder: true,
|
||||
}}
|
||||
data-test-subj="sloDetailsPage"
|
||||
>
|
||||
{loading && <EuiLoadingSpinner data-test-subj="loadingDetails" />}
|
||||
{!loading && <SloDetails slo={slo!} />}
|
||||
</ObservabilityPageTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function getBreadcrumbs(basePath: IBasePath, slo: SLO | undefined): EuiBreadcrumbProps[] {
|
||||
return [
|
||||
{
|
||||
href: basePath.prepend(paths.observability.slos),
|
||||
text: SLOS_BREADCRUMB_TEXT,
|
||||
},
|
||||
{
|
||||
text: slo?.name ?? SLO_DETAILS_BREADCRUMB_TEXT,
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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 { SLO } from '../../../typings';
|
||||
|
||||
export const anSLO: SLO = {
|
||||
id: '2f17deb0-725a-11ed-ab7c-4bb641cfc57e',
|
||||
name: 'SLO latency service log',
|
||||
objective: {
|
||||
target: 0.98,
|
||||
},
|
||||
summary: {
|
||||
sliValue: 0.990097,
|
||||
errorBudget: {
|
||||
remaining: 0.504831,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 interface SloDetailsPathParams {
|
||||
sloId: string;
|
||||
}
|
|
@ -12,7 +12,7 @@ import { SloList as Component } from './slo_list';
|
|||
|
||||
export default {
|
||||
component: Component,
|
||||
title: 'app/SLOs/SloList',
|
||||
title: 'app/SLO/ListPage/SloList',
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import { AlertingPages } from '../config';
|
|||
import { AlertDetails } from '../pages/alert_details';
|
||||
import { DatePickerContextProvider } from '../context/date_picker_context';
|
||||
import { SlosPage } from '../pages/slos';
|
||||
import { SloDetailsPage } from '../pages/slo_details';
|
||||
|
||||
export type RouteParams<T extends keyof typeof routes> = DecodeParams<typeof routes[T]['params']>;
|
||||
|
||||
|
@ -137,4 +138,11 @@ export const routes = {
|
|||
params: {},
|
||||
exact: true,
|
||||
},
|
||||
'/slos/:sloId': {
|
||||
handler: () => {
|
||||
return <SloDetailsPage />;
|
||||
},
|
||||
params: {},
|
||||
exact: true,
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue