mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* allow users importing data if they are authorized * rename props * rename types * hide import timeline btn if unauthorized
This commit is contained in:
parent
3486d44286
commit
b863c0fb80
11 changed files with 115 additions and 38 deletions
|
@ -21,7 +21,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { ImportRulesResponse, ImportRulesProps } from '../../containers/detection_engine/rules';
|
||||
import { ImportDataResponse, ImportDataProps } from '../../containers/detection_engine/rules';
|
||||
import {
|
||||
displayErrorToast,
|
||||
displaySuccessToast,
|
||||
|
@ -37,7 +37,7 @@ interface ImportDataModalProps {
|
|||
errorMessage: string;
|
||||
failedDetailed: (id: string, statusCode: number, message: string) => string;
|
||||
importComplete: () => void;
|
||||
importData: (arg: ImportRulesProps) => Promise<ImportRulesResponse>;
|
||||
importData: (arg: ImportDataProps) => Promise<ImportDataResponse>;
|
||||
showCheckBox: boolean;
|
||||
showModal: boolean;
|
||||
submitBtnText: string;
|
||||
|
@ -75,7 +75,7 @@ export const ImportDataModalComponent = ({
|
|||
closeModal();
|
||||
}, [setIsImporting, setSelectedFiles, closeModal]);
|
||||
|
||||
const importRulesCallback = useCallback(async () => {
|
||||
const importDataCallback = useCallback(async () => {
|
||||
if (selectedFiles != null) {
|
||||
setIsImporting(true);
|
||||
const abortCtrl = new AbortController();
|
||||
|
@ -152,7 +152,7 @@ export const ImportDataModalComponent = ({
|
|||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={handleCloseModal}>{i18n.CANCEL_BUTTON}</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
onClick={importRulesCallback}
|
||||
onClick={importDataCallback}
|
||||
disabled={selectedFiles == null || isImporting}
|
||||
fill
|
||||
>
|
||||
|
|
|
@ -54,7 +54,7 @@ interface OwnProps<TCache = object> {
|
|||
export type OpenTimelineOwnProps = OwnProps &
|
||||
Pick<
|
||||
OpenTimelineProps,
|
||||
'defaultPageSize' | 'title' | 'importCompleteToggle' | 'setImportCompleteToggle'
|
||||
'defaultPageSize' | 'title' | 'importDataModalToggle' | 'setImportDataModalToggle'
|
||||
> &
|
||||
PropsFromRedux;
|
||||
|
||||
|
@ -77,9 +77,9 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
|
|||
defaultPageSize,
|
||||
hideActions = [],
|
||||
isModal = false,
|
||||
importCompleteToggle,
|
||||
importDataModalToggle,
|
||||
onOpenTimeline,
|
||||
setImportCompleteToggle,
|
||||
setImportDataModalToggle,
|
||||
timeline,
|
||||
title,
|
||||
updateTimeline,
|
||||
|
@ -269,7 +269,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
|
|||
defaultPageSize={defaultPageSize}
|
||||
isLoading={loading}
|
||||
itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap}
|
||||
importCompleteToggle={importCompleteToggle}
|
||||
importDataModalToggle={importDataModalToggle}
|
||||
onAddTimelinesToFavorites={undefined}
|
||||
onDeleteSelected={onDeleteSelected}
|
||||
onlyFavorites={onlyFavorites}
|
||||
|
@ -284,7 +284,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
|
|||
query={search}
|
||||
refetch={refetch}
|
||||
searchResults={timelines}
|
||||
setImportCompleteToggle={setImportCompleteToggle}
|
||||
setImportDataModalToggle={setImportDataModalToggle}
|
||||
selectedItems={selectedItems}
|
||||
sortDirection={sortDirection}
|
||||
sortField={sortField}
|
||||
|
|
|
@ -33,7 +33,7 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
|
|||
defaultPageSize,
|
||||
isLoading,
|
||||
itemIdToExpandedNotesRowMap,
|
||||
importCompleteToggle,
|
||||
importDataModalToggle,
|
||||
onAddTimelinesToFavorites,
|
||||
onDeleteSelected,
|
||||
onlyFavorites,
|
||||
|
@ -50,7 +50,7 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
|
|||
searchResults,
|
||||
selectedItems,
|
||||
sortDirection,
|
||||
setImportCompleteToggle,
|
||||
setImportDataModalToggle,
|
||||
sortField,
|
||||
title,
|
||||
totalSearchResultsCount,
|
||||
|
@ -103,18 +103,18 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
|
|||
}, [refetch]);
|
||||
|
||||
const handleCloseModal = useCallback(() => {
|
||||
if (setImportCompleteToggle != null) {
|
||||
setImportCompleteToggle(false);
|
||||
if (setImportDataModalToggle != null) {
|
||||
setImportDataModalToggle(false);
|
||||
}
|
||||
}, [setImportCompleteToggle]);
|
||||
}, [setImportDataModalToggle]);
|
||||
const handleComplete = useCallback(() => {
|
||||
if (setImportCompleteToggle != null) {
|
||||
setImportCompleteToggle(false);
|
||||
if (setImportDataModalToggle != null) {
|
||||
setImportDataModalToggle(false);
|
||||
}
|
||||
if (refetch != null) {
|
||||
refetch();
|
||||
}
|
||||
}, [setImportCompleteToggle, refetch]);
|
||||
}, [setImportDataModalToggle, refetch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -136,7 +136,7 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
|
|||
importData={importTimelines}
|
||||
successMessage={i18n.SUCCESSFULLY_IMPORTED_TIMELINES}
|
||||
showCheckBox={false}
|
||||
showModal={importCompleteToggle ?? false}
|
||||
showModal={importDataModalToggle ?? false}
|
||||
submitBtnText={i18n.IMPORT_TIMELINE_BTN_TITLE}
|
||||
subtitle={i18n.INITIAL_PROMPT_TEXT}
|
||||
title={i18n.IMPORT_TIMELINE}
|
||||
|
|
|
@ -156,4 +156,72 @@ describe('#getActionsColumns', () => {
|
|||
|
||||
expect(onOpenTimeline).toBeCalledWith({ duplicate: true, timelineId: 'saved-timeline-11' });
|
||||
});
|
||||
|
||||
test('it renders the export icon when enableExportTimelineDownloader is including the action export', () => {
|
||||
const testProps: TimelinesTableProps = {
|
||||
...getMockTimelinesTableProps(mockResults),
|
||||
actionTimelineToShow: ['export'],
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable {...testProps} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="export-timeline"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('it renders No export icon when export is not included in the action ', () => {
|
||||
const testProps: TimelinesTableProps = {
|
||||
...getMockTimelinesTableProps(mockResults),
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable {...testProps} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="export-timeline"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('it renders a disabled the export button if the timeline does not have a saved object id', () => {
|
||||
const missingSavedObjectId: OpenTimelineResult[] = [
|
||||
omit('savedObjectId', { ...mockResults[0] }),
|
||||
];
|
||||
|
||||
const testProps: TimelinesTableProps = {
|
||||
...getMockTimelinesTableProps(missingSavedObjectId),
|
||||
actionTimelineToShow: ['export'],
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable {...testProps} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
const props = wrapper
|
||||
.find('[data-test-subj="export-timeline"]')
|
||||
.first()
|
||||
.props() as EuiButtonIconProps;
|
||||
expect(props.isDisabled).toBe(true);
|
||||
});
|
||||
|
||||
test('it invokes enableExportTimelineDownloader with the expected params when the button is clicked', () => {
|
||||
const enableExportTimelineDownloader = jest.fn();
|
||||
const testProps: TimelinesTableProps = {
|
||||
...getMockTimelinesTableProps(mockResults),
|
||||
actionTimelineToShow: ['export'],
|
||||
enableExportTimelineDownloader,
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable {...testProps} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="export-timeline"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(enableExportTimelineDownloader).toBeCalledWith(mockResults[0]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -55,6 +55,7 @@ export const getActionsColumns = ({
|
|||
},
|
||||
enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null,
|
||||
description: i18n.EXPORT_SELECTED,
|
||||
'data-test-subj': 'export-timeline',
|
||||
};
|
||||
|
||||
const deleteTimelineColumn = {
|
||||
|
|
|
@ -121,7 +121,7 @@ export interface OpenTimelineProps {
|
|||
/** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */
|
||||
itemIdToExpandedNotesRowMap: Record<string, JSX.Element>;
|
||||
/** Display import timelines modal*/
|
||||
importCompleteToggle?: boolean;
|
||||
importDataModalToggle?: boolean;
|
||||
/** If this callback is specified, a "Favorite Selected" button will be displayed, and this callback will be invoked when the button is clicked */
|
||||
onAddTimelinesToFavorites?: OnAddTimelinesToFavorites;
|
||||
/** If this callback is specified, a "Delete Selected" button will be displayed, and this callback will be invoked when the button is clicked */
|
||||
|
@ -153,7 +153,7 @@ export interface OpenTimelineProps {
|
|||
/** the currently-selected timelines in the table */
|
||||
selectedItems: OpenTimelineResult[];
|
||||
/** Toggle export timelines modal*/
|
||||
setImportCompleteToggle?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setImportDataModalToggle?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
/** the requested sort direction of the query results */
|
||||
sortDirection: 'asc' | 'desc';
|
||||
/** the requested field to sort on */
|
||||
|
|
|
@ -15,10 +15,10 @@ import {
|
|||
Rule,
|
||||
FetchRuleProps,
|
||||
BasicFetchProps,
|
||||
ImportRulesProps,
|
||||
ImportDataProps,
|
||||
ExportDocumentsProps,
|
||||
RuleStatusResponse,
|
||||
ImportRulesResponse,
|
||||
ImportDataResponse,
|
||||
PrePackagedRulesStatusResponse,
|
||||
BulkRuleResponse,
|
||||
} from './types';
|
||||
|
@ -204,11 +204,11 @@ export const importRules = async ({
|
|||
fileToImport,
|
||||
overwrite = false,
|
||||
signal,
|
||||
}: ImportRulesProps): Promise<ImportRulesResponse> => {
|
||||
}: ImportDataProps): Promise<ImportDataResponse> => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileToImport);
|
||||
|
||||
return KibanaServices.get().http.fetch<ImportRulesResponse>(
|
||||
return KibanaServices.get().http.fetch<ImportDataResponse>(
|
||||
`${DETECTION_ENGINE_RULES_URL}/_import`,
|
||||
{
|
||||
method: 'POST',
|
||||
|
|
|
@ -194,7 +194,7 @@ export interface BasicFetchProps {
|
|||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface ImportRulesProps {
|
||||
export interface ImportDataProps {
|
||||
fileToImport: File;
|
||||
overwrite?: boolean;
|
||||
signal: AbortSignal;
|
||||
|
@ -208,7 +208,7 @@ export interface ImportRulesResponseError {
|
|||
};
|
||||
}
|
||||
|
||||
export interface ImportRulesResponse {
|
||||
export interface ImportDataResponse {
|
||||
success: boolean;
|
||||
success_count: number;
|
||||
errors: ImportRulesResponseError[];
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ImportRulesProps, ImportRulesResponse } from '../../detection_engine/rules';
|
||||
import { ImportDataProps, ImportDataResponse } from '../../detection_engine/rules';
|
||||
import { KibanaServices } from '../../../lib/kibana';
|
||||
import { TIMELINE_IMPORT_URL, TIMELINE_EXPORT_URL } from '../../../../common/constants';
|
||||
import { ExportSelectedData } from '../../../components/generic_downloader';
|
||||
|
@ -13,11 +13,11 @@ export const importTimelines = async ({
|
|||
fileToImport,
|
||||
overwrite = false,
|
||||
signal,
|
||||
}: ImportRulesProps): Promise<ImportRulesResponse> => {
|
||||
}: ImportDataProps): Promise<ImportDataResponse> => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileToImport);
|
||||
|
||||
return KibanaServices.get().http.fetch<ImportRulesResponse>(`${TIMELINE_IMPORT_URL}`, {
|
||||
return KibanaServices.get().http.fetch<ImportDataResponse>(`${TIMELINE_IMPORT_URL}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': undefined },
|
||||
query: { overwrite },
|
||||
|
|
|
@ -14,6 +14,7 @@ import { StatefulOpenTimeline } from '../../components/open_timeline';
|
|||
import { WrapperPage } from '../../components/wrapper_page';
|
||||
import { SpyRoute } from '../../utils/route/spy_routes';
|
||||
import * as i18n from './translations';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
|
||||
const TimelinesContainer = styled.div`
|
||||
width: 100%;
|
||||
|
@ -28,17 +29,24 @@ type OwnProps = TimelinesProps;
|
|||
export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10;
|
||||
|
||||
const TimelinesPageComponent: React.FC<OwnProps> = ({ apolloClient }) => {
|
||||
const [importCompleteToggle, setImportCompleteToggle] = useState<boolean>(false);
|
||||
const [importDataModalToggle, setImportDataModalToggle] = useState<boolean>(false);
|
||||
const onImportTimelineBtnClick = useCallback(() => {
|
||||
setImportCompleteToggle(true);
|
||||
}, [setImportCompleteToggle]);
|
||||
setImportDataModalToggle(true);
|
||||
}, [setImportDataModalToggle]);
|
||||
|
||||
const uiCapabilities = useKibana().services.application.capabilities;
|
||||
const capabilitiesCanUserCRUD: boolean =
|
||||
typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<WrapperPage>
|
||||
<HeaderPage border title={i18n.PAGE_TITLE}>
|
||||
<EuiButton iconType="indexOpen" onClick={onImportTimelineBtnClick}>
|
||||
{i18n.ALL_TIMELINES_IMPORT_TIMELINE_TITLE}
|
||||
</EuiButton>
|
||||
{capabilitiesCanUserCRUD && (
|
||||
<EuiButton iconType="indexOpen" onClick={onImportTimelineBtnClick}>
|
||||
{i18n.ALL_TIMELINES_IMPORT_TIMELINE_TITLE}
|
||||
</EuiButton>
|
||||
)}
|
||||
</HeaderPage>
|
||||
|
||||
<TimelinesContainer>
|
||||
|
@ -46,8 +54,8 @@ const TimelinesPageComponent: React.FC<OwnProps> = ({ apolloClient }) => {
|
|||
apolloClient={apolloClient}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
isModal={false}
|
||||
importCompleteToggle={importCompleteToggle}
|
||||
setImportCompleteToggle={setImportCompleteToggle}
|
||||
importDataModalToggle={importDataModalToggle && capabilitiesCanUserCRUD}
|
||||
setImportDataModalToggle={setImportDataModalToggle}
|
||||
title={i18n.ALL_TIMELINES_PANEL_TITLE}
|
||||
/>
|
||||
</TimelinesContainer>
|
||||
|
|
|
@ -175,7 +175,7 @@ export default function({ getService }: FtrProviderContext) {
|
|||
expect(version).to.not.be.empty();
|
||||
});
|
||||
|
||||
it.skip('Update a timeline with a new title', async () => {
|
||||
it('Update a timeline with a new title', async () => {
|
||||
const titleToSaved = 'hello title';
|
||||
const response = await createBasicTimeline(client, titleToSaved);
|
||||
const { savedObjectId, version } = response.data && response.data.persistTimeline.timeline;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue