mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Osquery] Fix ECS mapping editor issues (#132307)
This commit is contained in:
parent
d638b188dc
commit
df38c6fc46
11 changed files with 180 additions and 101 deletions
|
@ -4,7 +4,8 @@
|
|||
"execTimeout": 120000,
|
||||
"pageLoadTimeout": 12000,
|
||||
"retries": {
|
||||
"runMode": 0
|
||||
"runMode": 1,
|
||||
"openMode": 0
|
||||
},
|
||||
"screenshotsFolder": "../../../target/kibana-osquery/cypress/screenshots",
|
||||
"trashAssetsBeforeRuns": false,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver';
|
||||
import { login } from '../../tasks/login';
|
||||
import { navigateTo } from '../../tasks/navigation';
|
||||
import {
|
||||
|
@ -24,11 +25,19 @@ import { getAdvancedButton } from '../../screens/integrations';
|
|||
import { ROLES } from '../../test';
|
||||
|
||||
describe('ALL - Live Query', () => {
|
||||
before(() => {
|
||||
runKbnArchiverScript(ArchiverMethod.LOAD, 'ecs_mapping_1');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
login(ROLES.soc_manager);
|
||||
navigateTo('/app/osquery');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
runKbnArchiverScript(ArchiverMethod.UNLOAD, 'ecs_mapping_1');
|
||||
});
|
||||
|
||||
it('should run query and enable ecs mapping', () => {
|
||||
const cmd = Cypress.platform === 'darwin' ? '{meta}{enter}' : '{ctrl}{enter}';
|
||||
cy.contains('New live query').click();
|
||||
|
@ -65,4 +74,22 @@ describe('ALL - Live Query', () => {
|
|||
.react('EuiIconTip', { props: { type: 'indexMapping' } })
|
||||
.should('exist');
|
||||
});
|
||||
|
||||
it('should run customized saved query', () => {
|
||||
cy.contains('New live query').click();
|
||||
selectAllAgents();
|
||||
cy.react('SavedQueriesDropdown').type('NOMAPPING{downArrow}{enter}');
|
||||
cy.getReact('SavedQueriesDropdown').getCurrentState().should('have.length', 1);
|
||||
inputQuery('{selectall}{backspace}{selectall}{backspace}select * from users');
|
||||
cy.wait(1000);
|
||||
submitQuery();
|
||||
checkResults();
|
||||
navigateTo('/app/osquery');
|
||||
cy.react('EuiButtonIcon', { props: { iconType: 'play' } })
|
||||
.eq(0)
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
||||
cy.react('ReactAce', { props: { value: 'select * from users' } }).should('exist');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -110,7 +110,7 @@ describe('ALL - Packs', () => {
|
|||
`pack_${PACK_NAME}_${SAVED_QUERY_ID}`
|
||||
);
|
||||
});
|
||||
it('by clicking in Lens button', () => {
|
||||
it.skip('by clicking in Lens button', () => {
|
||||
let lensUrl = '';
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'open')
|
||||
|
@ -160,8 +160,8 @@ describe('ALL - Packs', () => {
|
|||
.contains(/^Save and deploy changes$/)
|
||||
.click();
|
||||
cy.contains(`${PACK_NAME}`).click();
|
||||
cy.contains(`${PACK_NAME} details`);
|
||||
cy.contains(/^No items found/);
|
||||
cy.contains(`${PACK_NAME} details`).should('exist');
|
||||
cy.contains(/^No items found/).should('exist');
|
||||
});
|
||||
|
||||
it('enable changing saved queries and ecs_mappings', () => {
|
||||
|
@ -171,12 +171,13 @@ describe('ALL - Packs', () => {
|
|||
findAndClickButton('Add query');
|
||||
|
||||
getSavedQueriesDropdown().type('Multiple {downArrow} {enter}');
|
||||
cy.contains('Custom key/value pairs');
|
||||
cy.contains('Days of uptime');
|
||||
cy.contains('List of keywords used to tag each');
|
||||
cy.contains('Seconds of uptime');
|
||||
cy.contains('Client network address.');
|
||||
cy.contains('Total uptime seconds');
|
||||
cy.contains('Custom key/value pairs').should('exist');
|
||||
cy.contains('Days of uptime').should('exist');
|
||||
cy.contains('List of keywords used to tag each').should('exist');
|
||||
cy.contains('Seconds of uptime').should('exist');
|
||||
cy.contains('Client network address.').should('exist');
|
||||
cy.contains('Total uptime seconds').should('exist');
|
||||
cy.getBySel('ECSMappingEditorForm').should('have.length', 4);
|
||||
|
||||
getSavedQueriesDropdown().type('NOMAPPING {downArrow} {enter}');
|
||||
cy.contains('Custom key/value pairs').should('not.exist');
|
||||
|
@ -185,17 +186,19 @@ describe('ALL - Packs', () => {
|
|||
cy.contains('Seconds of uptime').should('not.exist');
|
||||
cy.contains('Client network address.').should('not.exist');
|
||||
cy.contains('Total uptime seconds').should('not.exist');
|
||||
cy.getBySel('ECSMappingEditorForm').should('have.length', 1);
|
||||
|
||||
getSavedQueriesDropdown().type('ONE_MAPPING {downArrow} {enter}');
|
||||
cy.contains('Name of the continent');
|
||||
cy.contains('Seconds of uptime');
|
||||
cy.contains('Name of the continent').should('exist');
|
||||
cy.contains('Seconds of uptime').should('exist');
|
||||
cy.getBySel('ECSMappingEditorForm').should('have.length', 2);
|
||||
|
||||
findAndClickButton('Save');
|
||||
cy.react('CustomItemAction', {
|
||||
props: { index: 0, item: { id: 'ONE_MAPPING_CHANGED' } },
|
||||
}).click();
|
||||
cy.contains('Name of the continent');
|
||||
cy.contains('Seconds of uptime');
|
||||
cy.contains('Name of the continent').should('exist');
|
||||
cy.contains('Seconds of uptime').should('exist');
|
||||
});
|
||||
|
||||
it('to click delete button', () => {
|
||||
|
@ -231,7 +234,7 @@ describe('ALL - Packs', () => {
|
|||
|
||||
cy.getBySel('toastCloseButton').click();
|
||||
cy.contains(REMOVING_PACK).click();
|
||||
cy.contains(`${REMOVING_PACK} details`);
|
||||
cy.contains(`${REMOVING_PACK} details`).should('exist');
|
||||
findAndClickButton('Edit');
|
||||
cy.react('EuiComboBoxInput', { props: { value: AGENT_NAME } }).should('exist');
|
||||
|
||||
|
@ -246,7 +249,7 @@ describe('ALL - Packs', () => {
|
|||
closeModalIfVisible();
|
||||
navigateTo('app/osquery/packs');
|
||||
cy.contains(REMOVING_PACK).click();
|
||||
cy.contains(`${REMOVING_PACK} details`);
|
||||
cy.contains(`${REMOVING_PACK} details`).should('exist');
|
||||
cy.wait(1000);
|
||||
findAndClickButton('Edit');
|
||||
cy.react('EuiComboBoxInput', { props: { value: '' } }).should('exist');
|
||||
|
|
|
@ -20,6 +20,9 @@ export const selectAllAgents = () => {
|
|||
cy.contains('1 agent selected.');
|
||||
};
|
||||
|
||||
export const clearInputQuery = () =>
|
||||
cy.get(LIVE_QUERY_EDITOR).click().type(`{selectall}{backspace}`);
|
||||
|
||||
export const inputQuery = (query: string) => cy.get(LIVE_QUERY_EDITOR).type(query);
|
||||
|
||||
export const submitQuery = () => cy.contains('Submit').click();
|
||||
|
|
|
@ -37,8 +37,6 @@ export const useAgentPolicy = ({ policyId, skip, silent }: UseAgentPolicy) => {
|
|||
defaultMessage: 'Error while fetching agent policy details',
|
||||
}),
|
||||
}),
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
EuiAccordion,
|
||||
EuiAccordionProps,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
|
@ -23,17 +22,16 @@ import styled from 'styled-components';
|
|||
|
||||
import { pickBy, isEmpty, map } from 'lodash';
|
||||
import { convertECSMappingToObject } from '../../../common/schemas/common/utils';
|
||||
import { UseField, Form, FormData, useForm, useFormData, FIELD_TYPES } from '../../shared_imports';
|
||||
import { UseField, Form, FormData, useForm, useFormData } from '../../shared_imports';
|
||||
import { AgentsTableField } from './agents_table_field';
|
||||
import { LiveQueryQueryField } from './live_query_query_field';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { ResultTabs } from '../../routes/saved_queries/edit/tabs';
|
||||
import { queryFieldValidation } from '../../common/validations';
|
||||
import { fieldValidators } from '../../shared_imports';
|
||||
import { SavedQueryFlyout } from '../../saved_queries';
|
||||
import { useErrorToast } from '../../common/hooks/use_error_toast';
|
||||
import { ECSMappingEditorField } from '../../packs/queries/lazy_ecs_mapping_editor_field';
|
||||
import { SavedQueriesDropdown } from '../../saved_queries/saved_queries_dropdown';
|
||||
import { liveQueryFormSchema } from './schema';
|
||||
|
||||
const FORM_ID = 'liveQueryForm';
|
||||
|
||||
|
@ -44,8 +42,6 @@ const StyledEuiAccordion = styled(EuiAccordion)`
|
|||
}
|
||||
`;
|
||||
|
||||
export const MAX_QUERY_LENGTH = 2000;
|
||||
|
||||
const GhostFormField = () => <></>;
|
||||
|
||||
type FormType = 'simple' | 'steps';
|
||||
|
@ -100,46 +96,9 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
}
|
||||
);
|
||||
|
||||
const formSchema = {
|
||||
agentSelection: {
|
||||
defaultValue: {
|
||||
agents: [],
|
||||
allAgentsSelected: false,
|
||||
platformsSelected: [],
|
||||
policiesSelected: [],
|
||||
},
|
||||
type: FIELD_TYPES.JSON,
|
||||
validations: [],
|
||||
},
|
||||
savedQueryId: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
validations: [],
|
||||
},
|
||||
query: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
validations: [
|
||||
{
|
||||
validator: fieldValidators.maxLengthField({
|
||||
length: MAX_QUERY_LENGTH,
|
||||
message: i18n.translate('xpack.osquery.liveQuery.queryForm.largeQueryError', {
|
||||
defaultMessage: 'Query is too large (max {maxLength} characters)',
|
||||
values: { maxLength: MAX_QUERY_LENGTH },
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{ validator: queryFieldValidation },
|
||||
],
|
||||
},
|
||||
ecs_mapping: {
|
||||
defaultValue: [],
|
||||
type: FIELD_TYPES.JSON,
|
||||
validations: [],
|
||||
},
|
||||
};
|
||||
|
||||
const { form } = useForm({
|
||||
id: FORM_ID,
|
||||
schema: formSchema,
|
||||
schema: liveQueryFormSchema,
|
||||
onSubmit: async (formData, isValid) => {
|
||||
if (isValid) {
|
||||
try {
|
||||
|
@ -181,11 +140,14 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
|
||||
const actionId = useMemo(() => data?.actions[0].action_id, [data?.actions]);
|
||||
const agentIds = useMemo(() => data?.actions[0].agents, [data?.actions]);
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const [{ agentSelection, ecs_mapping, query, savedQueryId }] = useFormData({
|
||||
form,
|
||||
watch: ['agentSelection', 'ecs_mapping', 'query', 'savedQueryId'],
|
||||
});
|
||||
const [{ agentSelection, ecs_mapping: ecsMapping, query, savedQueryId }, formDataSerializer] =
|
||||
useFormData({
|
||||
form,
|
||||
});
|
||||
|
||||
/* recalculate the form data when ecs_mapping changes */
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const serializedFormData = useMemo(() => formDataSerializer(), [ecsMapping, formDataSerializer]);
|
||||
|
||||
const agentSelected = useMemo(
|
||||
() =>
|
||||
|
@ -260,8 +222,8 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
);
|
||||
|
||||
const flyoutFormDefaultValue = useMemo(
|
||||
() => ({ savedQueryId, query, ecs_mapping }),
|
||||
[savedQueryId, ecs_mapping, query]
|
||||
() => ({ savedQueryId, query, ecs_mapping: serializedFormData.ecs_mapping }),
|
||||
[savedQueryId, serializedFormData.ecs_mapping, query]
|
||||
);
|
||||
|
||||
const handleToggle = useCallback((isOpen) => {
|
||||
|
|
49
x-pack/plugins/osquery/public/live_queries/form/schema.ts
Normal file
49
x-pack/plugins/osquery/public/live_queries/form/schema.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const MAX_QUERY_LENGTH = 2000;
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FIELD_TYPES } from '../../shared_imports';
|
||||
import { queryFieldValidation } from '../../common/validations';
|
||||
import { fieldValidators } from '../../shared_imports';
|
||||
|
||||
export const liveQueryFormSchema = {
|
||||
agentSelection: {
|
||||
defaultValue: {
|
||||
agents: [],
|
||||
allAgentsSelected: false,
|
||||
platformsSelected: [],
|
||||
policiesSelected: [],
|
||||
},
|
||||
type: FIELD_TYPES.JSON,
|
||||
validations: [],
|
||||
},
|
||||
savedQueryId: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
validations: [],
|
||||
},
|
||||
query: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
validations: [
|
||||
{
|
||||
validator: fieldValidators.maxLengthField({
|
||||
length: MAX_QUERY_LENGTH,
|
||||
message: i18n.translate('xpack.osquery.liveQuery.queryForm.largeQueryError', {
|
||||
defaultMessage: 'Query is too large (max {maxLength} characters)',
|
||||
values: { maxLength: MAX_QUERY_LENGTH },
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{ validator: queryFieldValidation },
|
||||
],
|
||||
},
|
||||
ecs_mapping: {
|
||||
defaultValue: [],
|
||||
type: FIELD_TYPES.JSON,
|
||||
},
|
||||
};
|
|
@ -136,7 +136,7 @@ const ECSFieldWrapper = styled(EuiFlexItem)`
|
|||
max-width: 100%;
|
||||
`;
|
||||
|
||||
const singleSelection = { asPlainText: true };
|
||||
const SINGLE_SELECTION = { asPlainText: true };
|
||||
|
||||
const ECSSchemaOptions = ECSSchema.map((ecs) => ({
|
||||
label: ecs.field,
|
||||
|
@ -162,6 +162,7 @@ const ECSComboboxFieldComponent: React.FC<ECSComboboxFieldProps> = ({
|
|||
);
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
const describedByIds = useMemo(() => (idAria ? [idAria] : []), [idAria]);
|
||||
const [formData] = useFormData();
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newSelectedOptions) => {
|
||||
|
@ -229,6 +230,12 @@ const ECSComboboxFieldComponent: React.FC<ECSComboboxFieldProps> = ({
|
|||
return text;
|
||||
}, [selectedOptions]);
|
||||
|
||||
const availableECSSchemaOptions = useMemo(() => {
|
||||
const currentFormECSFieldValues = map(formData.ecs_mapping, 'key');
|
||||
|
||||
return ECSSchemaOptions.filter(({ label }) => !currentFormECSFieldValues.includes(label));
|
||||
}, [formData.ecs_mapping]);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-expect-error update types
|
||||
setSelected(() => {
|
||||
|
@ -236,7 +243,16 @@ const ECSComboboxFieldComponent: React.FC<ECSComboboxFieldProps> = ({
|
|||
|
||||
const selectedOption = find(ECSSchemaOptions, ['label', field.value]);
|
||||
|
||||
return selectedOption ? [selectedOption] : [];
|
||||
return selectedOption
|
||||
? [selectedOption]
|
||||
: [
|
||||
{
|
||||
label: field.value,
|
||||
value: {
|
||||
value: field.value,
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
}, [field.value]);
|
||||
|
||||
|
@ -253,9 +269,9 @@ const ECSComboboxFieldComponent: React.FC<ECSComboboxFieldProps> = ({
|
|||
<EuiComboBox
|
||||
prepend={prepend}
|
||||
fullWidth
|
||||
singleSelection={singleSelection}
|
||||
singleSelection={SINGLE_SELECTION}
|
||||
// @ts-expect-error update types
|
||||
options={ECSSchemaOptions}
|
||||
options={availableECSSchemaOptions}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={handleChange}
|
||||
data-test-subj="ECS-field-input"
|
||||
|
@ -317,6 +333,7 @@ interface OsqueryColumnFieldProps {
|
|||
resultType: FieldHook<string>;
|
||||
resultValue: FieldHook<string | string[]>;
|
||||
euiFieldProps: EuiComboBoxProps<OsquerySchemaOption>;
|
||||
item: ArrayItem;
|
||||
idAria?: string;
|
||||
}
|
||||
|
||||
|
@ -325,6 +342,7 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
|||
resultValue,
|
||||
euiFieldProps = {},
|
||||
idAria,
|
||||
item,
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
const { setValue } = resultValue;
|
||||
|
@ -334,6 +352,7 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
|||
const [selectedOptions, setSelected] = useState<
|
||||
Array<EuiComboBoxOptionOption<OsquerySchemaOption>>
|
||||
>([]);
|
||||
const [formData] = useFormData();
|
||||
|
||||
const renderOsqueryOption = useCallback(
|
||||
(option, searchValue, contentClassName) => (
|
||||
|
@ -370,14 +389,25 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
|||
[setValue, setSelected]
|
||||
);
|
||||
|
||||
const isSingleSelection = useMemo(() => {
|
||||
const ecsKey = get(formData, item.path)?.key;
|
||||
if (ecsKey?.length && typeValue === 'value') {
|
||||
const ecsKeySchemaOption = find(ECSSchemaOptions, ['label', ecsKey]);
|
||||
|
||||
return ecsKeySchemaOption?.value?.normalization !== 'array';
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [typeValue, formData, item.path]);
|
||||
|
||||
const onTypeChange = useCallback(
|
||||
(newType) => {
|
||||
if (newType !== typeValue) {
|
||||
setType(newType);
|
||||
setValue(newType === 'value' && euiFieldProps.singleSelection === false ? [] : '');
|
||||
setValue(newType === 'value' && isSingleSelection === false ? [] : '');
|
||||
}
|
||||
},
|
||||
[typeValue, setType, setValue, euiFieldProps.singleSelection]
|
||||
[typeValue, setType, setValue, isSingleSelection]
|
||||
);
|
||||
|
||||
const handleCreateOption = useCallback(
|
||||
|
@ -386,7 +416,7 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
|||
|
||||
if (!trimmedNewOption.length) return;
|
||||
|
||||
if (euiFieldProps.singleSelection === false) {
|
||||
if (isSingleSelection === false) {
|
||||
setValue([trimmedNewOption]);
|
||||
if (resultValue.value.length) {
|
||||
setValue([...castArray(resultValue.value), trimmedNewOption]);
|
||||
|
@ -399,7 +429,7 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
|||
setValue(trimmedNewOption);
|
||||
}
|
||||
},
|
||||
[euiFieldProps.singleSelection, resultValue.value, setValue]
|
||||
[isSingleSelection, resultValue.value, setValue]
|
||||
);
|
||||
|
||||
const Prepend = useMemo(
|
||||
|
@ -421,14 +451,14 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (euiFieldProps?.singleSelection && isArray(resultValue.value)) {
|
||||
if (isSingleSelection && isArray(resultValue.value)) {
|
||||
setValue(resultValue.value.join(' '));
|
||||
}
|
||||
|
||||
if (!euiFieldProps?.singleSelection && !isArray(resultValue.value)) {
|
||||
if (!isSingleSelection && !isArray(resultValue.value)) {
|
||||
setValue(resultValue.value.length ? [resultValue.value] : []);
|
||||
}
|
||||
}, [euiFieldProps?.singleSelection, resultValue.value, setValue]);
|
||||
}, [isSingleSelection, resultValue.value, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(() => {
|
||||
|
@ -471,6 +501,7 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
|||
rowHeight={32}
|
||||
isClearable
|
||||
{...euiFieldProps}
|
||||
singleSelection={isSingleSelection ? SINGLE_SELECTION : false}
|
||||
options={(typeValue === 'field' && euiFieldProps.options) || EMPTY_ARRAY}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -566,7 +597,6 @@ const osqueryResultFieldValidator = async (
|
|||
},
|
||||
}
|
||||
),
|
||||
__isBlocking__: false,
|
||||
}
|
||||
: undefined;
|
||||
};
|
||||
|
@ -586,8 +616,6 @@ export const ECSMappingEditorForm: React.FC<ECSMappingEditorFormProps> = ({
|
|||
isLastItem,
|
||||
onDelete,
|
||||
}) => {
|
||||
const multipleValuesField = useRef(false);
|
||||
|
||||
const MultiFields = useMemo(
|
||||
() => (
|
||||
<UseMultiFields
|
||||
|
@ -625,19 +653,18 @@ export const ECSMappingEditorForm: React.FC<ECSMappingEditorFormProps> = ({
|
|||
{(fields) => (
|
||||
<OsqueryColumnField
|
||||
{...fields}
|
||||
item={item}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{
|
||||
// @ts-expect-error update types
|
||||
options: osquerySchemaOptions,
|
||||
isDisabled,
|
||||
// @ts-expect-error update types
|
||||
singleSelection: !multipleValuesField.current ? { asPlainText: true } : false,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</UseMultiFields>
|
||||
),
|
||||
[item.path, osquerySchemaOptions, isLastItem, isDisabled]
|
||||
[item, osquerySchemaOptions, isLastItem, isDisabled]
|
||||
);
|
||||
|
||||
const ecsComboBoxEuiFieldProps = useMemo(() => ({ isDisabled }), [isDisabled]);
|
||||
|
@ -738,7 +765,7 @@ export const ECSMappingEditorField = React.memo(
|
|||
({ euiFieldProps }: ECSMappingEditorFieldProps) => {
|
||||
const lastItemPath = useRef<string>();
|
||||
const onAdd = useRef<FormArrayField['addItem']>();
|
||||
const osquerySchemaOptions = useRef<OsquerySchemaOption[]>([]);
|
||||
const [osquerySchemaOptions, setOsquerySchemaOptions] = useState<OsquerySchemaOption[]>([]);
|
||||
const [{ query, ...formData }, formDataSerializer, isMounted] = useFormData();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -917,10 +944,13 @@ export const ECSMappingEditorField = React.memo(
|
|||
.flat();
|
||||
|
||||
// Remove column duplicates by keeping the column from the table that appears last in the query
|
||||
osquerySchemaOptions.current = sortedUniqBy(
|
||||
const newOptions = sortedUniqBy(
|
||||
orderBy(suggestions, ['value.suggestion_label', 'value.tableOrder'], ['asc', 'desc']),
|
||||
'label'
|
||||
);
|
||||
setOsquerySchemaOptions((prevValue) =>
|
||||
!deepEqual(prevValue, newOptions) ? newOptions : prevValue
|
||||
);
|
||||
}, [query]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
@ -999,7 +1029,7 @@ export const ECSMappingEditorField = React.memo(
|
|||
{items.map((item, index) => (
|
||||
<ECSMappingEditorForm
|
||||
key={item.id}
|
||||
osquerySchemaOptions={osquerySchemaOptions.current}
|
||||
osquerySchemaOptions={osquerySchemaOptions}
|
||||
item={item}
|
||||
isLastItem={index === items.length - 1}
|
||||
onDelete={removeItem}
|
||||
|
|
|
@ -73,6 +73,5 @@ export const createFormSchema = (ids: Set<string>) => ({
|
|||
ecs_mapping: {
|
||||
defaultValue: [],
|
||||
type: FIELD_TYPES.JSON,
|
||||
validations: [],
|
||||
},
|
||||
});
|
||||
|
|
|
@ -26,9 +26,7 @@ interface PlaygroundFlyoutProps {
|
|||
}
|
||||
|
||||
const PlaygroundFlyoutComponent: React.FC<PlaygroundFlyoutProps> = ({ enabled, onClose }) => {
|
||||
const [{ query, ecs_mapping: ecsMapping, id }, formDataSerializer] = useFormData({
|
||||
watch: ['query', 'ecs_mapping', 'savedQueryId'],
|
||||
});
|
||||
const [{ query, ecs_mapping: ecsMapping, id }, formDataSerializer] = useFormData();
|
||||
|
||||
/* recalculate the form data when ecs_mapping changes */
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
|
@ -40,11 +40,22 @@ interface SavedQueriesDropdownProps {
|
|||
) => void;
|
||||
}
|
||||
|
||||
interface SelectedOption {
|
||||
label: string;
|
||||
value: {
|
||||
savedQueryId: string;
|
||||
id: string;
|
||||
description: string;
|
||||
query: string;
|
||||
ecs_mapping: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
const SavedQueriesDropdownComponent: React.FC<SavedQueriesDropdownProps> = ({
|
||||
disabled,
|
||||
onChange,
|
||||
}) => {
|
||||
const [selectedOptions, setSelectedOptions] = useState([]);
|
||||
const [selectedOptions, setSelectedOptions] = useState<SelectedOption[]>([]);
|
||||
|
||||
const [{ savedQueryId }] = useFormData();
|
||||
|
||||
|
@ -109,17 +120,13 @@ const SavedQueriesDropdownComponent: React.FC<SavedQueriesDropdownProps> = ({
|
|||
const savedQueryOption = find(['value.savedQueryId', savedQueryId], queryOptions);
|
||||
|
||||
if (savedQueryOption) {
|
||||
handleSavedQueryChange([savedQueryOption]);
|
||||
setSelectedOptions([savedQueryOption]);
|
||||
}
|
||||
}
|
||||
}, [savedQueryId, handleSavedQueryChange, queryOptions]);
|
||||
}, [savedQueryId, queryOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedOptions.length &&
|
||||
// @ts-expect-error update types
|
||||
selectedOptions[0].value.savedQueryId !== savedQueryId
|
||||
) {
|
||||
if (selectedOptions.length && selectedOptions[0].value.savedQueryId !== savedQueryId) {
|
||||
setSelectedOptions([]);
|
||||
}
|
||||
}, [savedQueryId, selectedOptions]);
|
||||
|
@ -152,4 +159,6 @@ const SavedQueriesDropdownComponent: React.FC<SavedQueriesDropdownProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
SavedQueriesDropdownComponent.displayName = 'SavedQueriesDropdown';
|
||||
|
||||
export const SavedQueriesDropdown = React.memo(SavedQueriesDropdownComponent);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue