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

Create index pattern - modal popup #366

Merged
Merged
Show file tree
Hide file tree
Changes from 15 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
29 changes: 29 additions & 0 deletions cypress/integration/4_findings.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,35 @@ describe('Findings', () => {
cy.get('.euiFlexItem--flexGrowZero > .euiButtonIcon').click({ force: true });
});

it('displays finding details and create an index pattern from flyout', () => {
// filter table to show only sample_detector findings
cy.get(`input[placeholder="Search findings"]`).ospSearch('sample_detector');

// Click findingId to trigger Finding details flyout
cy.getTableFirstRow('[data-test-subj="finding-details-flyout-button"]').then(($el) => {
cy.get($el).click({ force: true });
});

cy.get('[data-test-subj="finding-details-flyout-view-surrounding-documents"]')
.contains('View surrounding documents')
.click({ force: true });

cy.contains('Create index pattern to view documents');

cy.get(
`[data-test-subj="index_pattern_time_field_dropdown"] [data-test-subj="comboBoxSearchInput"]`
).type('EventTime');

cy.get('[data-test-subj="index_pattern_form_submit_button"]')
.contains('Create index pattern')
.click({ force: true });

cy.contains('cypress-test-windows* has been successfully created');

// Close Flyout
cy.get('.euiFlexItem--flexGrowZero > .euiButtonIcon').click({ force: true });
});

it('allows user to view details about rules that were triggered', () => {
// filter table to show only sample_detector findings
cy.get(`input[placeholder="Search findings"]`).ospSearch('sample_detector');
Expand Down
2 changes: 1 addition & 1 deletion opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "2.5.0.0",
"opensearchDashboardsVersion": "2.5.0",
"configPath": ["opensearch_security_analytics"],
"requiredPlugins": [],
"requiredPlugins": ["data"],
"server": true,
"ui": true
}
2 changes: 2 additions & 0 deletions public/models/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
IndexService,
RuleService,
NotificationsService,
IndexPatternsService,
} from '../services';

export interface BrowserServices {
Expand All @@ -23,6 +24,7 @@ export interface BrowserServices {
alertService: AlertsService;
ruleService: RuleService;
notificationsService: NotificationsService;
indexPatternsService: IndexPatternsService;
}

export interface RuleOptions {
Expand Down
210 changes: 210 additions & 0 deletions public/pages/Findings/components/CreateIndexPatternForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useEffect, useState, useCallback } from 'react';
import { Formik, Form, FormikErrors } from 'formik';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiFieldText,
EuiButton,
EuiSpacer,
EuiComboBox,
EuiText,
EuiCallOut,
} from '@elastic/eui';
import { IndexPatternsService } from '../../../services';

const ILLEGAL_CHARACTERS = [' ', '\\', '/', '?', '"', '<', '>', '|'];

const containsIllegalCharacters = (pattern: string) => {
return ILLEGAL_CHARACTERS.some((char) => pattern.includes(char));
};

export interface CreateIndexPatternFormModel {
name: string;
timeField: string;
}

export interface CreateIndexPatternFormProps {
initialValue: {
name: string;
};
created: (values: string) => void;
close: () => void;
indexPatternsService: IndexPatternsService;
}

export const CreateIndexPatternForm: React.FC<CreateIndexPatternFormProps> = ({
initialValue,
created,
close,
indexPatternsService,
}) => {
const [timeFields, setTimeFields] = useState<string[]>([]);
const [createdIndex, setCreatedIndex] = useState<{ id?: string; title: string }>();

const getTimeFields = useCallback(
async (name: string): Promise<string[]> => {
if (!indexPatternsService) {
return [];
}

return indexPatternsService
.getFieldsForWildcard({
pattern: `${name}`,
metaFields: ['_source', '_id', '_type', '_index', '_score'],
params: {},
})
.then((res) => {
return res.filter((f) => f.type === 'date').map((f) => f.name);
})
.catch(() => {
return [];
});
},
[initialValue]
);

useEffect(() => {
getTimeFields(initialValue.name).then((fields) => {
setTimeFields(fields);
});
}, [initialValue.name]);

return createdIndex ? (
<>
<EuiCallOut title={`${createdIndex?.title} has been successfully created`} color="success">
<p>You may now view surrounding documents within the index</p>
</EuiCallOut>
<EuiSpacer />
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
fill
onClick={() => {
created(createdIndex?.id || '');
}}
>
View surrounding documents
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
) : (
<Formik
initialValues={{ ...initialValue, timeField: '' }}
validate={(values) => {
const errors: FormikErrors<CreateIndexPatternFormModel> = {};

if (!values.name) {
errors.name = 'Index patter name is required';
}

if (!values.timeField) {
errors.timeField = 'Time field is required';
}

if (containsIllegalCharacters(values.name)) {
errors.name =
'A index pattern cannot contain spaces or the characters: , /, ?, ", <, >, |';
}

return errors;
}}
onSubmit={async (values, { setSubmitting }) => {
try {
const newIndex = await indexPatternsService.createAndSave({
title: values.name,
timeFieldName: values.timeField,
});
setCreatedIndex({ id: newIndex.id, title: newIndex.title });
} catch (e) {
console.warn(e);
}
setSubmitting(false);
}}
>
{(props) => (
<Form>
<EuiText>
An index pattern is required to view all surrounding documents within the index. Create
an index pattern to continue.
</EuiText>
<EuiSpacer />
<EuiFormRow
label={
<EuiText size={'s'}>
<strong>Specify index pattern name</strong>
</EuiText>
}
isInvalid={props.touched.name && !!props.errors?.name}
error={props.errors.name}
>
<EuiFieldText
isInvalid={props.touched.name && !!props.errors.name}
placeholder="Enter index pattern name"
data-test-subj={'index_pattern_name_field'}
onChange={async (e) => {
props.handleChange('name')(e);
const fields = await getTimeFields(e.target.value);
setTimeFields(fields);
props.setFieldValue('timeField', '');
}}
onBlur={props.handleBlur('name')}
value={props.values.name}
/>
</EuiFormRow>

<EuiFormRow
label={
<EuiText size={'s'}>
<strong>Time filed</strong>
</EuiText>
}
isInvalid={props.touched.timeField && !!props.errors?.timeField}
error={props.errors.timeField}
>
<EuiComboBox
isInvalid={props.touched.timeField && !!props.errors.timeField}
placeholder="Select a time field"
data-test-subj={'index_pattern_time_field_dropdown'}
options={timeFields.map((field: string) => ({ value: field, label: field }))}
singleSelection={{ asPlainText: true }}
onChange={(e) => {
props.handleChange('timeField')(e[0]?.value ? e[0].value : '');
}}
onBlur={props.handleBlur('timeField')}
selectedOptions={
props.values.timeField
? [{ value: props.values.timeField, label: props.values.timeField }]
: []
}
/>
</EuiFormRow>

<EuiSpacer />

<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton onClick={() => close()}>Cancel</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj={'index_pattern_form_submit_button'}
isLoading={props.isSubmitting}
fill
onClick={() => props.handleSubmit()}
>
Create index pattern
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</Form>
)}
</Formik>
);
};
71 changes: 62 additions & 9 deletions public/pages/Findings/components/FindingDetailsFlyout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import {
EuiFormRow,
EuiHorizontalRule,
EuiLink,
EuiModal,
EuiModalBody,
EuiModalHeader,
EuiModalHeaderTitle,
EuiPanel,
EuiSpacer,
EuiText,
Expand All @@ -30,21 +34,24 @@ import { Finding, Query } from '../models/interfaces';
import { RuleViewerFlyout } from '../../Rules/components/RuleViewerFlyout/RuleViewerFlyout';
import { RuleSource } from '../../../../server/models/interfaces';
import { RuleItemInfoBase } from '../../Rules/models/types';
import { OpenSearchService } from '../../../services';
import { OpenSearchService, IndexPatternsService } from '../../../services';
import { RuleTableItem } from '../../Rules/utils/helpers';
import { CreateIndexPatternForm } from './CreateIndexPatternForm';

interface FindingDetailsFlyoutProps {
finding: Finding;
backButton?: React.ReactNode;
allRules: { [id: string]: RuleSource };
opensearchService: OpenSearchService;
indexPatternsService: IndexPatternsService;
closeFlyout: () => void;
}

interface FindingDetailsFlyoutState {
loading: boolean;
ruleViewerFlyoutData: RuleTableItem | null;
indexPatternId?: string;
isCreateIndexPatternModalVisible: boolean;
}

export default class FindingDetailsFlyout extends Component<
Expand All @@ -56,6 +63,7 @@ export default class FindingDetailsFlyout extends Component<
this.state = {
loading: false,
ruleViewerFlyoutData: null,
isCreateIndexPatternModalVisible: false,
};
}

Expand Down Expand Up @@ -215,14 +223,19 @@ export default class FindingDetailsFlyout extends Component<
<h3>Documents</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ display: 'none' }}>
<EuiFlexItem grow={false}>
<EuiButton
href={
indexPatternId
? `discover#/context/${indexPatternId}/${related_doc_ids[0]}`
: `#${ROUTES.FINDINGS}`
}
target={indexPatternId ? '_blank' : undefined}
data-test-subj={'finding-details-flyout-view-surrounding-documents'}
onClick={() => {
if (indexPatternId) {
window.open(
amsiglan marked this conversation as resolved.
Show resolved Hide resolved
`discover#/context/${indexPatternId}/${related_doc_ids[0]}`,
'_blank'
);
} else {
this.setState({ ...this.state, isCreateIndexPatternModalVisible: true });
}
}}
>
View surrounding documents
</EuiButton>
Expand Down Expand Up @@ -266,6 +279,46 @@ export default class FindingDetailsFlyout extends Component<
);
}

createIndexPatternModal() {
const {
finding: { related_doc_ids },
} = this.props;
if (this.state.isCreateIndexPatternModalVisible) {
return (
<EuiModal
style={{ width: 800 }}
onClose={() => this.setState({ ...this.state, isCreateIndexPatternModalVisible: false })}
>
<EuiModalHeader>
<EuiModalHeaderTitle>
<h1>Create index pattern to view documents</h1>
</EuiModalHeaderTitle>
</EuiModalHeader>

<EuiModalBody>
<CreateIndexPatternForm
indexPatternsService={this.props.indexPatternsService}
initialValue={{
name: this.props.finding.detector._source.inputs[0].detector_input.indices[0] + '*',
}}
close={() =>
this.setState({ ...this.state, isCreateIndexPatternModalVisible: false })
}
created={(indexPatternId) => {
this.setState({
...this.state,
indexPatternId,
isCreateIndexPatternModalVisible: false,
});
window.open(`discover#/context/${indexPatternId}/${related_doc_ids[0]}`, '_blank');
amsiglan marked this conversation as resolved.
Show resolved Hide resolved
}}
></CreateIndexPatternForm>
</EuiModalBody>
</EuiModal>
);
}
}

render() {
const {
finding: {
Expand Down Expand Up @@ -294,7 +347,7 @@ export default class FindingDetailsFlyout extends Component<
ruleTableItem={this.state.ruleViewerFlyoutData}
/>
)}

{this.createIndexPatternModal()}
<EuiFlyoutHeader hasBorder={true}>
<EuiFlexGroup justifyContent="flexStart" alignItems="center">
<EuiFlexItem>
Expand Down
Loading