Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[SLOs] Configuration inspect api and flyout #173723

Merged
merged 7 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const createSLOParamsSchema = t.type({
settings: optionalSettingsSchema,
tags: tagsSchema,
groupBy: allOrAnyString,
revision: t.number,
}),
]),
});
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/ingest_pipelines/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ import { IngestPipelinesPlugin } from './plugin';
export function plugin() {
return new IngestPipelinesPlugin();
}

export { INGEST_PIPELINES_APP_LOCATOR, INGEST_PIPELINES_PAGES } from './locator';
export type { IngestPipelinesListParams } from './locator';
3 changes: 2 additions & 1 deletion x-pack/plugins/observability/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
"unifiedSearch",
"stackAlerts",
"spaces",
"embeddable"
"embeddable",
"ingestPipelines"
],
"extraPublicDirs": [
"common"
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/observability/public/application/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export const renderApp = ({
>
<PluginContext.Provider
value={{
isDev,
config,
appMountParameters,
observabilityRuleTypeRegistry,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { ObservabilityRuleTypeRegistry } from '../../rules/create_observabi
import type { ConfigSchema } from '../../plugin';

export interface PluginContextValue {
isDev?: boolean;
config: ConfigSchema;
appMountParameters: AppMountParameters;
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry;
Expand Down
41 changes: 41 additions & 0 deletions x-pack/plugins/observability/public/hooks/slo/use_inspect_slo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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 { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import type { FindSLOResponse, SLOResponse } from '@kbn/slo-schema';
import { QueryKey, useMutation } from '@tanstack/react-query';
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { CreateSLOInput } from '@kbn/slo-schema';
import { useKibana } from '../../utils/kibana_react';

type ServerError = IHttpFetchError<ResponseErrorBody>;

interface SLOInspectResponse {
slo: SLOResponse;
pipeline: Record<string, any>;
rollUpTransform: TransformPutTransformRequest;
summaryTransform: TransformPutTransformRequest;
temporaryDoc: Record<string, any>;
}

export function useInspectSlo() {
const { http } = useKibana().services;

return useMutation<
SLOInspectResponse,
ServerError,
{ slo: CreateSLOInput },
{ previousData?: FindSLOResponse; queryKey?: QueryKey }
>(
['inspectSlo'],
({ slo }) => {
const body = JSON.stringify(slo);
return http.post<SLOInspectResponse>(`/internal/api/observability/slos/_inspect`, { body });
},
{}
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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 { InPortal } from 'react-reverse-portal';
import { GetSLOResponse } from '@kbn/slo-schema';
import { CreateSLOForm } from '../../types';
import { SLOInspectWrapper } from './slo_inspect';
import { InspectSLOPortalNode } from '../../slo_edit';

export interface SloInspectPortalProps {
getValues: () => CreateSLOForm;
trigger: () => Promise<boolean>;
slo?: GetSLOResponse;
}
export function InspectSLOPortal(props: SloInspectPortalProps) {
return (
<InPortal node={InspectSLOPortalNode}>
<SLOInspectWrapper {...props} />
</InPortal>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
/*
* 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';

import React, { ReactNode, useState } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useFetcher } from '@kbn/observability-shared-plugin/public';
import {
EuiFlyout,
EuiButton,
EuiCodeBlock,
EuiFlyoutHeader,
EuiTitle,
EuiFlyoutFooter,
EuiSpacer,
EuiFlyoutBody,
EuiToolTip,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
EuiAccordion,
EuiButtonIcon,
} from '@elastic/eui';
import {
INGEST_PIPELINES_APP_LOCATOR,
INGEST_PIPELINES_PAGES,
IngestPipelinesListParams,
} from '@kbn/ingest-pipelines-plugin/public';
import { SloInspectPortalProps } from './inspect_slo_portal';
import { ObservabilityPublicPluginsStart } from '../../../..';
import { useInspectSlo } from '../../../../hooks/slo/use_inspect_slo';
import { transformCreateSLOFormToCreateSLOInput } from '../../helpers/process_slo_form_values';
import { enableInspectEsQueries } from '../../../../../common';
import { usePluginContext } from '../../../../hooks/use_plugin_context';

export function SLOInspectWrapper(props: SloInspectPortalProps) {
const {
services: { uiSettings },
} = useKibana();

const { isDev } = usePluginContext();

const isInspectorEnabled = uiSettings?.get<boolean>(enableInspectEsQueries);

return isDev || isInspectorEnabled ? <SLOInspect {...props} /> : null;
}

function SLOInspect({ getValues, trigger, slo }: SloInspectPortalProps) {
const { share, http } = useKibana<ObservabilityPublicPluginsStart>().services;
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
const { mutateAsync: inspectSlo, data, isLoading } = useInspectSlo();

const { data: sloData } = useFetcher(async () => {
if (!isFlyoutVisible) {
return;
}
const isValid = await trigger();
if (!isValid) {
return;
}
const sloForm = transformCreateSLOFormToCreateSLOInput(getValues());
inspectSlo({ slo: { ...sloForm, id: slo?.id, revision: slo?.revision } });
return sloForm;
}, [isFlyoutVisible, trigger, getValues, inspectSlo, slo]);

const { data: pipeLineUrl } = useFetcher(async () => {
const ingestPipeLocator = share.url.locators.get<IngestPipelinesListParams>(
INGEST_PIPELINES_APP_LOCATOR
);
const ingestPipeLineId = data?.pipeline?.id;
return ingestPipeLocator?.getUrl({
pipelineId: ingestPipeLineId,
page: INGEST_PIPELINES_PAGES.LIST,
});
}, [data?.pipeline?.id, share.url.locators]);

const closeFlyout = () => {
setIsFlyoutVisible(false);
setIsInspecting(false);
};

const [isInspecting, setIsInspecting] = useState(false);
const onButtonClick = () => {
trigger().then((isValid) => {
if (isValid) {
setIsInspecting(() => !isInspecting);
setIsFlyoutVisible(() => !isFlyoutVisible);
}
});
};

let flyout;

if (isFlyoutVisible) {
flyout = (
<EuiFlyout ownFocus onClose={closeFlyout} aria-labelledby="flyoutTitle">
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2 id="flyoutTitle">{CONFIG_LABEL}</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{isLoading && <LoadingState />}
<EuiSpacer size="m" />
{data && (
<>
<CodeBlockAccordion
id="slo"
label={i18n.translate(
'xpack.observability.sLOInspect.codeBlockAccordion.sloConfigurationLabel',
{ defaultMessage: 'SLO configuration' }
)}
json={data.slo}
/>
<EuiSpacer size="s" />
<CodeBlockAccordion
id="rollUpTransform"
label={i18n.translate(
'xpack.observability.sLOInspect.codeBlockAccordion.rollupTransformLabel',
{ defaultMessage: 'Rollup transform' }
)}
json={data.rollUpTransform}
extraAction={
<EuiButtonIcon
iconType="link"
data-test-subj="o11ySLOInspectDetailsButton"
href={http?.basePath.prepend('/app/management/data/transform')}
/>
}
/>
<EuiSpacer size="s" />

<CodeBlockAccordion
id="summaryTransform"
label={i18n.translate(
'xpack.observability.sLOInspect.codeBlockAccordion.summaryTransformLabel',
{ defaultMessage: 'Summary transform' }
)}
json={data.summaryTransform}
extraAction={
<EuiButtonIcon
iconType="link"
data-test-subj="o11ySLOInspectDetailsButton"
href={http?.basePath.prepend('/app/management/data/transform')}
/>
}
/>
<EuiSpacer size="s" />

<CodeBlockAccordion
id="pipeline"
label={i18n.translate(
'xpack.observability.sLOInspect.codeBlockAccordion.ingestPipelineLabel',
{ defaultMessage: 'SLO Ingest pipeline' }
)}
extraAction={
<EuiButtonIcon
iconType="link"
data-test-subj="o11ySLOInspectDetailsButton"
href={pipeLineUrl}
/>
}
json={data.pipeline}
/>
<EuiSpacer size="s" />

<CodeBlockAccordion
id="temporaryDoc"
label={i18n.translate(
'xpack.observability.sLOInspect.codeBlockAccordion.temporaryDocumentLabel',
{ defaultMessage: 'Temporary document' }
)}
json={data.temporaryDoc}
/>
</>
)}
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiButton
data-test-subj="syntheticsMonitorInspectCloseButton"
onClick={closeFlyout}
fill
>
{i18n.translate('xpack.observability.sLOInspect.closeButtonLabel', {
defaultMessage: 'Close',
})}
</EuiButton>
</EuiFlyoutFooter>
</EuiFlyout>
);
}
return (
<>
<EuiToolTip
content={sloData ? VIEW_FORMATTED_CONFIG_LABEL : VALID_CONFIG_LABEL}
repositionOnScroll
>
<EuiButton
data-test-subj="syntheticsMonitorInspectShowFlyoutExampleButton"
onClick={onButtonClick}
iconType="inspect"
iconSide="left"
>
{SLO_INSPECT_LABEL}
</EuiButton>
</EuiToolTip>

{flyout}
</>
);
}

function CodeBlockAccordion({
id,
label,
json,
extraAction,
}: {
id: string;
label: string;
json: any;
extraAction?: ReactNode;
}) {
return (
<EuiAccordion
id={id}
extraAction={extraAction}
buttonContent={
<EuiTitle size="xs">
<h3>{label}</h3>
</EuiTitle>
}
>
<EuiCodeBlock language="json" fontSize="m" paddingSize="m" isCopyable={true}>
{JSON.stringify(json, null, 2)}
</EuiCodeBlock>
</EuiAccordion>
);
}

export function LoadingState() {
return (
<EuiFlexGroup alignItems="center" justifyContent="center" style={{ height: '100%' }}>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />
</EuiFlexItem>
</EuiFlexGroup>
);
}

const SLO_INSPECT_LABEL = i18n.translate('xpack.observability.sLOInspect.sLOInspectButtonLabel', {
defaultMessage: 'SLO Inspect',
});

const VIEW_FORMATTED_CONFIG_LABEL = i18n.translate(
'xpack.observability.slo.viewFormattedResourcesConfigsButtonLabel',
{ defaultMessage: 'View formatted resources configs for SLO' }
);

const VALID_CONFIG_LABEL = i18n.translate('xpack.observability.slo.formattedConfigLabel.valid', {
defaultMessage: 'Only valid form configurations can be inspected.',
});

const CONFIG_LABEL = i18n.translate('xpack.observability.monitorInspect.configLabel', {
defaultMessage: 'SLO Configurations',
});
Loading