Skip to content

Commit

Permalink
Create index pattern - modal popup (opensearch-project#366) (opensear…
Browse files Browse the repository at this point in the history
…ch-project#397)

* create index pattern modal

Signed-off-by: Aleksandar Djindjic <[email protected]>

* validate index pattern name and fill timeFields

Signed-off-by: Aleksandar Djindjic <[email protected]>

* reuse SavedObjectsService from dashboards core

Signed-off-by: Aleksandar Djindjic <[email protected]>

* take title and time field from the form

Signed-off-by: Aleksandar Djindjic <[email protected]>

* reuse index patterns service from core dashboards

Signed-off-by: Aleksandar Djindjic <[email protected]>

* improve ux for create index pattern

Signed-off-by: Aleksandar Djindjic <[email protected]>

* rename props

Signed-off-by: Aleksandar Djindjic <[email protected]>

* submit button loading state from formik submiting

Signed-off-by: Aleksandar Djindjic <[email protected]>

* typos and minor changes

Signed-off-by: Aleksandar Djindjic <[email protected]>

* indexPatternsService marked as mandatory dep

Signed-off-by: Aleksandar Djindjic <[email protected]>

* better typing on injected dependencies

Signed-off-by: Aleksandar Djindjic <[email protected]>

* cypress test for create index pattern

Signed-off-by: Aleksandar Djindjic <[email protected]>

* align cypress test with new convention

Signed-off-by: Aleksandar Djindjic <[email protected]>

* useCallback event handler wrapper

Signed-off-by: Aleksandar Djindjic <[email protected]>

---------

Signed-off-by: Aleksandar Djindjic <[email protected]>
(cherry picked from commit 2f08f22)

Co-authored-by: Aleksandar Djindjic <[email protected]>
Signed-off-by: AWSHurneyt <[email protected]>
  • Loading branch information
2 people authored and AWSHurneyt committed Feb 22, 2023
1 parent d120d59 commit 21140df
Show file tree
Hide file tree
Showing 12 changed files with 370 additions and 17 deletions.
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(
`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');
}}
></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

0 comments on commit 21140df

Please sign in to comment.