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 7 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
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.4.0.0",
"opensearchDashboardsVersion": "2.4.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 } 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 [timeFileds, setTimeFields] = useState<string[]>([]);
djindjic marked this conversation as resolved.
Show resolved Hide resolved
const [creatingIndexInProgress, setCreatingIndexInProgress] = useState<boolean>(false);
const [createdIndex, setCreatedIndex] = useState<{ id?: string; title: string }>();

const getTimeFields = async (name: string): Promise<string[]> => {
amsiglan marked this conversation as resolved.
Show resolved Hide resolved
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 [];
});
};

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">
<EuiButton
djindjic marked this conversation as resolved.
Show resolved Hide resolved
fill
onClick={() => {
created(createdIndex?.id || '');
}}
>
View surrounding documents
</EuiButton>
</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 }) => {
setSubmitting(false);
amsiglan marked this conversation as resolved.
Show resolved Hide resolved
if (!indexPatternsService) {
return;
}
try {
setCreatingIndexInProgress(true);
const newIndex = await indexPatternsService.createAndSave({
title: values.name,
timeFieldName: values.timeField,
});
setCreatedIndex({ id: newIndex.id, title: newIndex.title });
} catch (e) {
console.warn(e);
}
setCreatingIndexInProgress(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 fileds = await getTimeFields(e.target.value);
setTimeFields(fileds);
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={timeFileds.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
isLoading={creatingIndexInProgress}
fill
onClick={() => props.handleSubmit()}
>
Create index pattern
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</Form>
)}
</Formik>
);
};
68 changes: 60 additions & 8 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,
EuiSpacer,
EuiText,
EuiTitle,
Expand All @@ -29,21 +33,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 @@ -55,6 +62,7 @@ export default class FindingDetailsFlyout extends Component<
this.state = {
loading: false,
ruleViewerFlyoutData: null,
isCreateIndexPatternModalVisible: false,
};
}

Expand Down Expand Up @@ -217,12 +225,16 @@ export default class FindingDetailsFlyout extends Component<
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
href={
indexPatternId
? `discover#/context/${indexPatternId}/${related_doc_ids[0]}`
: `#${ROUTES.FINDINGS}`
}
target={indexPatternId ? '_blank' : undefined}
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 +278,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 +346,7 @@ export default class FindingDetailsFlyout extends Component<
ruleTableItem={this.state.ruleViewerFlyoutData}
/>
)}

{this.createIndexPatternModal()}
<EuiFlyoutHeader hasBorder={true}>
<EuiFlexGroup justifyContent="flexStart" alignItems="center">
<EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { FieldValueSelectionFilterConfigType } from '@elastic/eui/src/components
import dateMath from '@elastic/datemath';
import { capitalizeFirstLetter, renderTime } from '../../../../utils/helpers';
import { DEFAULT_EMPTY_DATA } from '../../../../utils/constants';
import { DetectorsService, OpenSearchService } from '../../../../services';
import { DetectorsService, OpenSearchService, IndexPatternsService } from '../../../../services';
import FindingDetailsFlyout from '../FindingDetailsFlyout';
import { Finding } from '../../models/interfaces';
import CreateAlertFlyout from '../CreateAlertFlyout';
Expand All @@ -39,6 +39,7 @@ interface FindingsTableProps extends RouteComponentProps {
onRefresh: () => void;
onFindingsFiltered: (findings: FindingItemType[]) => void;
hasNotificationsPlugin: boolean;
indexPatternsService?: IndexPatternsService;
}

interface FindingsTableState {
Expand Down Expand Up @@ -100,6 +101,7 @@ export default class FindingsTable extends Component<FindingsTableProps, Finding
finding={finding}
closeFlyout={this.closeFlyout}
allRules={this.props.rules}
indexPatternsService={this.props.indexPatternsService}
/>
),
flyoutOpen: true,
Expand Down
Loading