[Exploratory View] Add to case button (#112463)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
50134cbec6
commit
bbf3d4b9ca
|
@ -20,6 +20,7 @@ import { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public'
|
|||
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
|
||||
import { LensPublicStart } from '../../../lens/public';
|
||||
import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public';
|
||||
import { CasesUiStart } from '../../../cases/public';
|
||||
|
||||
export interface ObservabilityAppServices {
|
||||
http: HttpStart;
|
||||
|
@ -36,4 +37,5 @@ export interface ObservabilityAppServices {
|
|||
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
lens: LensPublicStart;
|
||||
cases: CasesUiStart;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { render } from '../rtl_helpers';
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
import { AddToCaseAction } from './add_to_case_action';
|
||||
|
||||
describe('AddToCaseAction', function () {
|
||||
it('should render properly', async function () {
|
||||
const { findByText } = render(
|
||||
<AddToCaseAction
|
||||
lensAttributes={{ title: 'Performance distribution' } as any}
|
||||
timeRange={{ to: 'now', from: 'now-5m' }}
|
||||
/>
|
||||
);
|
||||
expect(await findByText('Add to case')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should be able to click add to case button', async function () {
|
||||
const initSeries = {
|
||||
data: {
|
||||
'uptime-pings-histogram': {
|
||||
dataType: 'synthetics' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { findByText, core } = render(
|
||||
<AddToCaseAction
|
||||
lensAttributes={{ title: 'Performance distribution' } as any}
|
||||
timeRange={{ to: 'now', from: 'now-5m' }}
|
||||
/>,
|
||||
{ initSeries }
|
||||
);
|
||||
fireEvent.click(await findByText('Add to case'));
|
||||
|
||||
expect(core?.cases?.getAllCasesSelectorModal).toHaveBeenCalledTimes(1);
|
||||
expect(core?.cases?.getAllCasesSelectorModal).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
createCaseNavigation: expect.objectContaining({ href: '/app/observability/cases/create' }),
|
||||
owner: ['observability'],
|
||||
userCanCrud: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback } from 'react';
|
||||
import { toMountPoint, useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ObservabilityAppServices } from '../../../../application/types';
|
||||
import { AllCasesSelectorModalProps } from '../../../../../../cases/public';
|
||||
import { TypedLensByValueInput } from '../../../../../../lens/public';
|
||||
import { useAddToCase } from '../hooks/use_add_to_case';
|
||||
import { Case, SubCase } from '../../../../../../cases/common';
|
||||
import { observabilityFeatureId } from '../../../../../common';
|
||||
|
||||
export interface AddToCaseProps {
|
||||
timeRange: { from: string; to: string };
|
||||
lensAttributes: TypedLensByValueInput['attributes'] | null;
|
||||
}
|
||||
|
||||
export function AddToCaseAction({ lensAttributes, timeRange }: AddToCaseProps) {
|
||||
const kServices = useKibana<ObservabilityAppServices>().services;
|
||||
|
||||
const { cases, http } = kServices;
|
||||
|
||||
const getToastText = useCallback(
|
||||
(theCase) => toMountPoint(<CaseToastText theCase={theCase} basePath={http.basePath.get()} />),
|
||||
[http.basePath]
|
||||
);
|
||||
|
||||
const { createCaseUrl, goToCreateCase, onCaseClicked, isCasesOpen, setIsCasesOpen, isSaving } =
|
||||
useAddToCase({
|
||||
lensAttributes,
|
||||
getToastText,
|
||||
timeRange,
|
||||
});
|
||||
|
||||
const getAllCasesSelectorModalProps: AllCasesSelectorModalProps = {
|
||||
createCaseNavigation: {
|
||||
href: createCaseUrl,
|
||||
onClick: goToCreateCase,
|
||||
},
|
||||
onRowClick: onCaseClicked,
|
||||
userCanCrud: true,
|
||||
owner: [observabilityFeatureId],
|
||||
onClose: () => {
|
||||
setIsCasesOpen(false);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiButton
|
||||
isLoading={isSaving}
|
||||
fullWidth={false}
|
||||
isDisabled={lensAttributes === null}
|
||||
onClick={() => {
|
||||
if (lensAttributes) {
|
||||
setIsCasesOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.observability.expView.heading.addToCase', {
|
||||
defaultMessage: 'Add to case',
|
||||
})}
|
||||
</EuiButton>
|
||||
{isCasesOpen &&
|
||||
lensAttributes &&
|
||||
cases.getAllCasesSelectorModal(getAllCasesSelectorModalProps)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CaseToastText({ theCase, basePath }: { theCase: Case | SubCase; basePath: string }) {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiFlexItem>
|
||||
<EuiLink href={`${basePath}/app/observability/cases/${theCase.id}`} target="_blank">
|
||||
{i18n.translate('xpack.observability.expView.heading.addToCase.notification.viewCase', {
|
||||
defaultMessage: 'View case',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -14,6 +14,7 @@ import { DataViewLabels } from '../configurations/constants';
|
|||
import { ObservabilityAppServices } from '../../../../application/types';
|
||||
import { useSeriesStorage } from '../hooks/use_series_storage';
|
||||
import { combineTimeRanges } from '../exploratory_view';
|
||||
import { AddToCaseAction } from './add_to_case_action';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
|
@ -56,6 +57,9 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
|
|||
</h2>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AddToCaseAction lensAttributes={lensAttributes} timeRange={timeRange} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
iconType="lensApp"
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { useAddToCase } from './use_add_to_case';
|
||||
import React, { useEffect } from 'react';
|
||||
import { render } from '../rtl_helpers';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
|
||||
describe('useAddToCase', function () {
|
||||
function setupTestComponent() {
|
||||
const setData = jest.fn();
|
||||
function TestComponent() {
|
||||
const getToastText = jest.fn();
|
||||
|
||||
const result = useAddToCase({
|
||||
lensAttributes: { title: 'Test lens attributes' } as any,
|
||||
timeRange: { to: 'now', from: 'now-5m' },
|
||||
getToastText,
|
||||
});
|
||||
useEffect(() => {
|
||||
setData(result);
|
||||
}, [result]);
|
||||
|
||||
return (
|
||||
<span>
|
||||
<EuiButton onClick={result.goToCreateCase}>Add new case</EuiButton>
|
||||
<EuiButton onClick={() => result.onCaseClicked({ id: 'test' } as any)}>
|
||||
On case click
|
||||
</EuiButton>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const renderSetup = render(<TestComponent />);
|
||||
|
||||
return { setData, ...renderSetup };
|
||||
}
|
||||
it('should return expected result', async function () {
|
||||
const { setData, core, findByText } = setupTestComponent();
|
||||
|
||||
expect(setData).toHaveBeenLastCalledWith({
|
||||
createCaseUrl: '/app/observability/cases/create',
|
||||
goToCreateCase: expect.any(Function),
|
||||
isCasesOpen: false,
|
||||
isSaving: false,
|
||||
onCaseClicked: expect.any(Function),
|
||||
setIsCasesOpen: expect.any(Function),
|
||||
});
|
||||
fireEvent.click(await findByText('Add new case'));
|
||||
|
||||
expect(core.application?.navigateToApp).toHaveBeenCalledTimes(1);
|
||||
expect(core.application?.navigateToApp).toHaveBeenCalledWith('observability', {
|
||||
path: '/cases/create',
|
||||
});
|
||||
|
||||
fireEvent.click(await findByText('On case click'));
|
||||
|
||||
expect(core.http?.post).toHaveBeenCalledTimes(1);
|
||||
expect(core.http?.post).toHaveBeenCalledWith('/api/cases/test/comments', {
|
||||
body: '{"comment":"!{lens{\\"attributes\\":{\\"title\\":\\"Test lens attributes\\"},\\"timeRange\\":{\\"to\\":\\"now\\",\\"from\\":\\"now-5m\\"}}}","type":"user","owner":"observability"}',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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, useMemo, useState } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { HttpSetup, MountPoint } from 'kibana/public';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { Case, SubCase } from '../../../../../../cases/common';
|
||||
import { TypedLensByValueInput } from '../../../../../../lens/public';
|
||||
import { AddToCaseProps } from '../header/add_to_case_action';
|
||||
import { observabilityFeatureId } from '../../../../../common';
|
||||
|
||||
const appendSearch = (search?: string) =>
|
||||
isEmpty(search) ? '' : `${search?.startsWith('?') ? search : `?${search}`}`;
|
||||
|
||||
const getCreateCaseUrl = (search?: string | null) =>
|
||||
`/cases/create${appendSearch(search ?? undefined)}`;
|
||||
|
||||
async function addToCase(
|
||||
http: HttpSetup,
|
||||
theCase: Case | SubCase,
|
||||
attributes: TypedLensByValueInput['attributes'],
|
||||
timeRange: { from: string; to: string }
|
||||
) {
|
||||
const apiPath = `/api/cases/${theCase?.id}/comments`;
|
||||
|
||||
const vizPayload = {
|
||||
attributes,
|
||||
timeRange,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
comment: `!{lens${JSON.stringify(vizPayload)}}`,
|
||||
type: 'user',
|
||||
owner: observabilityFeatureId,
|
||||
};
|
||||
|
||||
return http.post(apiPath, { body: JSON.stringify(payload) });
|
||||
}
|
||||
|
||||
export const useAddToCase = ({
|
||||
lensAttributes,
|
||||
getToastText,
|
||||
timeRange,
|
||||
}: AddToCaseProps & { getToastText: (thaCase: Case | SubCase) => MountPoint<HTMLElement> }) => {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isCasesOpen, setIsCasesOpen] = useState(false);
|
||||
|
||||
const {
|
||||
http,
|
||||
application: { navigateToApp, getUrlForApp },
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const createCaseUrl = useMemo(
|
||||
() => getUrlForApp(observabilityFeatureId) + getCreateCaseUrl(),
|
||||
[getUrlForApp]
|
||||
);
|
||||
|
||||
const goToCreateCase = useCallback(
|
||||
async (ev) => {
|
||||
ev.preventDefault();
|
||||
return navigateToApp(observabilityFeatureId, {
|
||||
path: getCreateCaseUrl(),
|
||||
});
|
||||
},
|
||||
[navigateToApp]
|
||||
);
|
||||
|
||||
const onCaseClicked = useCallback(
|
||||
(theCase?: Case | SubCase) => {
|
||||
if (theCase && lensAttributes) {
|
||||
setIsCasesOpen(false);
|
||||
setIsSaving(true);
|
||||
addToCase(http, theCase, lensAttributes, timeRange).then(
|
||||
() => {
|
||||
setIsSaving(false);
|
||||
toasts.addSuccess(
|
||||
{
|
||||
title: i18n.translate(
|
||||
'xpack.observability.expView.heading.addToCase.notification',
|
||||
{
|
||||
defaultMessage: 'Successfully added visualization to the case: {caseTitle}',
|
||||
values: { caseTitle: theCase.title },
|
||||
}
|
||||
),
|
||||
text: getToastText(theCase),
|
||||
},
|
||||
{
|
||||
toastLifeTimeMs: 10000,
|
||||
}
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
toasts.addError(error, {
|
||||
title: i18n.translate(
|
||||
'xpack.observability.expView.heading.addToCase.notification.error',
|
||||
{
|
||||
defaultMessage: 'Failed to add visualization to the selected case.',
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
navigateToApp(observabilityFeatureId, {
|
||||
path: getCreateCaseUrl(),
|
||||
openInNewTab: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[getToastText, http, lensAttributes, navigateToApp, timeRange, toasts]
|
||||
);
|
||||
|
||||
return {
|
||||
createCaseUrl,
|
||||
goToCreateCase,
|
||||
onCaseClicked,
|
||||
isSaving,
|
||||
isCasesOpen,
|
||||
setIsCasesOpen,
|
||||
};
|
||||
};
|
|
@ -39,6 +39,7 @@ import { createStubIndexPattern } from '../../../../../../../src/plugins/data/co
|
|||
import { AppDataType, UrlFilter } from './types';
|
||||
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
|
||||
import { ListItem } from '../../../hooks/use_values_list';
|
||||
import { casesPluginMock } from '../../../../../cases/public/mocks';
|
||||
|
||||
interface KibanaProps {
|
||||
services?: KibanaServices;
|
||||
|
@ -118,6 +119,7 @@ export const mockCore: () => Partial<CoreStart & ObservabilityPublicPluginsStart
|
|||
},
|
||||
lens: lensPluginMock.createStartContract(),
|
||||
data: dataPluginMock.createStartContract(),
|
||||
cases: casesPluginMock.createStartContract(),
|
||||
};
|
||||
|
||||
return core;
|
||||
|
|
Loading…
Reference in a new issue