mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Osquery] Refactor to React hooks form (#138501)
This commit is contained in:
parent
0212338e4e
commit
767cb6b1c1
47 changed files with 1274 additions and 1135 deletions
|
@ -6,14 +6,7 @@
|
|||
*/
|
||||
|
||||
import { isEmpty, reduce } from 'lodash';
|
||||
|
||||
export const convertECSMappingToArray = (ecsMapping: Record<string, object> | undefined) =>
|
||||
ecsMapping
|
||||
? Object.entries(ecsMapping).map((item) => ({
|
||||
key: item[0],
|
||||
value: item[1],
|
||||
}))
|
||||
: undefined;
|
||||
import type { ECSMapping } from './schemas';
|
||||
|
||||
export const convertECSMappingToObject = (
|
||||
ecsMapping: Array<{
|
||||
|
@ -23,7 +16,7 @@ export const convertECSMappingToObject = (
|
|||
value: string;
|
||||
};
|
||||
}>
|
||||
): Record<string, { field?: string; value?: string }> =>
|
||||
): ECSMapping =>
|
||||
reduce(
|
||||
ecsMapping,
|
||||
(acc, value) => {
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver';
|
||||
import { login } from '../../tasks/login';
|
||||
import {
|
||||
checkResults,
|
||||
findAndClickButton,
|
||||
findFormFieldByRowsLabelAndType,
|
||||
inputQuery,
|
||||
|
@ -80,7 +79,8 @@ describe('Alert Event Details', () => {
|
|||
cy.contains('1 agent selected.');
|
||||
inputQuery('select * from uptime;');
|
||||
submitQuery();
|
||||
checkResults();
|
||||
cy.contains('Results');
|
||||
cy.contains('Add to timeline investigation');
|
||||
cy.contains('Save for later').click();
|
||||
cy.contains('Save query');
|
||||
cy.get('.euiButtonEmpty--flushLeft').contains('Cancel').click();
|
||||
|
|
|
@ -32,10 +32,9 @@ describe('ALL - Edit saved query', () => {
|
|||
}).click();
|
||||
cy.contains('Custom key/value pairs.').should('exist');
|
||||
cy.contains('Hours of uptime').should('exist');
|
||||
cy.react('ECSComboboxFieldComponent', { props: { field: { value: 'labels' } } })
|
||||
.parents('[data-test-subj="ECSMappingEditorForm"]')
|
||||
.react('EuiButtonIcon', { props: { iconType: 'trash' } })
|
||||
.click();
|
||||
cy.react('ECSMappingEditorForm').within(() => {
|
||||
cy.react('EuiButtonIcon', { props: { iconType: 'trash' } }).click();
|
||||
});
|
||||
|
||||
cy.react('PlatformCheckBoxGroupField').within(() => {
|
||||
cy.react('EuiCheckbox', {
|
||||
|
|
|
@ -41,6 +41,28 @@ describe('ALL - Live Query', () => {
|
|||
runKbnArchiverScript(ArchiverMethod.UNLOAD, 'example_pack');
|
||||
});
|
||||
|
||||
it('should validate the form', () => {
|
||||
cy.contains('New live query').click();
|
||||
submitQuery();
|
||||
cy.contains('Agents is a required field');
|
||||
cy.contains('Query is a required field');
|
||||
selectAllAgents();
|
||||
inputQuery('select * from uptime; ');
|
||||
submitQuery();
|
||||
cy.contains('Agents is a required field').should('not.exist');
|
||||
cy.contains('Query is a required field').should('not.exist');
|
||||
checkResults();
|
||||
getAdvancedButton().click();
|
||||
typeInOsqueryFieldInput('days{downArrow}{enter}');
|
||||
submitQuery();
|
||||
cy.contains('ECS field is required.');
|
||||
typeInECSFieldInput('message{downArrow}{enter}');
|
||||
submitQuery();
|
||||
cy.contains('ECS field is required.').should('not.exist');
|
||||
|
||||
checkResults();
|
||||
});
|
||||
|
||||
it('should run query and enable ecs mapping', () => {
|
||||
const cmd = Cypress.platform === 'darwin' ? '{meta}{enter}' : '{ctrl}{enter}';
|
||||
cy.contains('New live query').click();
|
||||
|
@ -82,7 +104,7 @@ describe('ALL - Live Query', () => {
|
|||
cy.contains('New live query').click();
|
||||
selectAllAgents();
|
||||
cy.react('SavedQueriesDropdown').type('NOMAPPING{downArrow}{enter}');
|
||||
cy.getReact('SavedQueriesDropdown').getCurrentState().should('have.length', 1);
|
||||
// cy.getReact('SavedQueriesDropdown').getCurrentState().should('have.length', 1); // TODO do we need it?
|
||||
inputQuery('{selectall}{backspace}{selectall}{backspace}select * from users');
|
||||
cy.wait(1000);
|
||||
submitQuery();
|
||||
|
|
|
@ -77,6 +77,7 @@ describe('ALL - Packs', () => {
|
|||
cy.contains('Attach next query');
|
||||
inputQuery('select * from uptime');
|
||||
findFormFieldByRowsLabelAndType('ID', SAVED_QUERY_ID);
|
||||
cy.react('EuiFlyoutFooter').react('EuiButton').contains('Save').click();
|
||||
cy.contains('ID must be unique').should('exist');
|
||||
findFormFieldByRowsLabelAndType('ID', NEW_QUERY_NAME);
|
||||
cy.contains('ID must be unique').should('not.exist');
|
||||
|
@ -95,6 +96,7 @@ describe('ALL - Packs', () => {
|
|||
cy.contains('Attach next query');
|
||||
cy.contains('ID must be unique').should('not.exist');
|
||||
getSavedQueriesDropdown().type(`${SAVED_QUERY_ID}{downArrow}{enter}`);
|
||||
cy.react('EuiFlyoutFooter').react('EuiButton').contains('Save').click();
|
||||
cy.contains('ID must be unique').should('exist');
|
||||
cy.react('EuiFlyoutFooter').react('EuiButtonEmpty').contains('Cancel').click();
|
||||
});
|
||||
|
|
|
@ -20,7 +20,6 @@ describe('Admin', () => {
|
|||
cy.contains('New live query').click();
|
||||
selectAllAgents();
|
||||
inputQuery('select * from uptime; ');
|
||||
cy.wait(500);
|
||||
submitQuery();
|
||||
checkResults();
|
||||
});
|
||||
|
|
|
@ -94,6 +94,7 @@ describe('T1 Analyst - READ + runSavedQueries ', () => {
|
|||
cy.contains('New live query').click();
|
||||
selectAllAgents();
|
||||
cy.get(LIVE_QUERY_EDITOR).should('not.exist');
|
||||
cy.contains('Submit').should('be.disabled');
|
||||
submitQuery();
|
||||
cy.contains('Query is a required field');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -101,15 +101,15 @@ describe('T2 Analyst - READ + Write Live/Saved + runSavedQueries ', () => {
|
|||
it('to click the edit button and edit pack', () => {
|
||||
navigateTo('/app/osquery/saved_queries');
|
||||
cy.getBySel('pagination-button-next').click();
|
||||
|
||||
cy.react('CustomItemAction', {
|
||||
props: { index: 1, item: { attributes: { id: SAVED_QUERY_ID } } },
|
||||
}).click();
|
||||
cy.contains('Custom key/value pairs.').should('exist');
|
||||
cy.contains('Hours of uptime').should('exist');
|
||||
cy.react('ECSComboboxFieldComponent', { props: { field: { value: 'labels' } } })
|
||||
.parents('[data-test-subj="ECSMappingEditorForm"]')
|
||||
.react('EuiButtonIcon', { props: { iconType: 'trash' } })
|
||||
.click();
|
||||
cy.react('ECSMappingEditorForm').within(() => {
|
||||
cy.react('EuiButtonIcon', { props: { iconType: 'trash' } }).click();
|
||||
});
|
||||
cy.react('EuiButton').contains('Update query').click();
|
||||
cy.wait(5000);
|
||||
|
||||
|
|
|
@ -25,7 +25,10 @@ export const clearInputQuery = () =>
|
|||
|
||||
export const inputQuery = (query: string) => cy.get(LIVE_QUERY_EDITOR).type(query);
|
||||
|
||||
export const submitQuery = () => cy.contains('Submit').click();
|
||||
export const submitQuery = () => {
|
||||
cy.wait(1000); // wait for the validation to trigger - cypress is way faster than users ;)
|
||||
cy.contains('Submit').click();
|
||||
};
|
||||
|
||||
export const checkResults = () =>
|
||||
cy.getBySel('dataGridRowCell', { timeout: 120000 }).should('have.lengthOf.above', 0);
|
||||
|
|
|
@ -35,12 +35,13 @@ import { AGENT_GROUP_KEY } from './types';
|
|||
interface AgentsTableProps {
|
||||
agentSelection: AgentSelection;
|
||||
onChange: (payload: AgentSelection) => void;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const perPage = 10;
|
||||
const DEBOUNCE_DELAY = 300; // ms
|
||||
|
||||
const AgentsTableComponent: React.FC<AgentsTableProps> = ({ agentSelection, onChange }) => {
|
||||
const AgentsTableComponent: React.FC<AgentsTableProps> = ({ agentSelection, onChange, error }) => {
|
||||
// search related
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [modifyingSearch, setModifyingSearch] = useState<boolean>(false);
|
||||
|
@ -185,7 +186,7 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ agentSelection, onCh
|
|||
|
||||
return (
|
||||
<div>
|
||||
<EuiFormRow label={AGENT_SELECTION_LABEL} fullWidth>
|
||||
<EuiFormRow label={AGENT_SELECTION_LABEL} fullWidth isInvalid={!!error} error={error}>
|
||||
<EuiComboBox
|
||||
data-test-subj="agentSelection"
|
||||
placeholder={SELECT_AGENT_LABEL}
|
||||
|
|
|
@ -15,7 +15,6 @@ import type {
|
|||
} from '../../common/search_strategy';
|
||||
|
||||
import type { ESQuery } from '../../common/typed_json';
|
||||
import type { ArrayItem } from '../shared_imports';
|
||||
|
||||
export const createFilter = (filterQuery: ESQuery | string | undefined) =>
|
||||
isString(filterQuery) ? filterQuery : JSON.stringify(filterQuery);
|
||||
|
@ -44,7 +43,7 @@ export const getInspectResponse = <T extends FactoryQueryTypes>(
|
|||
response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response,
|
||||
});
|
||||
|
||||
export const prepareEcsFieldsToValidate = (ecsMapping: ArrayItem[]): string[] =>
|
||||
export const prepareEcsFieldsToValidate = (ecsMapping: Array<{ id: string }>): string[] =>
|
||||
ecsMapping
|
||||
?.map((_: unknown, index: number) => [
|
||||
`ecs_mapping[${index}].result.value`,
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import type { FormData, ValidationFunc } from '../shared_imports';
|
||||
import { fieldValidators } from '../shared_imports';
|
||||
|
||||
export const queryFieldValidation: ValidationFunc<FormData, string, string> =
|
||||
fieldValidators.emptyField(
|
||||
i18n.translate('xpack.osquery.pack.queryFlyoutForm.emptyQueryError', {
|
||||
defaultMessage: 'Query is a required field',
|
||||
})
|
||||
);
|
|
@ -7,12 +7,12 @@
|
|||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import 'brace/theme/tomorrow';
|
||||
|
||||
import type { EuiCodeEditorProps } from '../shared_imports';
|
||||
import { EuiCodeEditor } from '../shared_imports';
|
||||
|
||||
import './osquery_mode';
|
||||
import 'brace/theme/tomorrow';
|
||||
|
||||
const EDITOR_SET_OPTIONS = {
|
||||
enableBasicAutocompletion: true,
|
||||
|
|
12
x-pack/plugins/osquery/public/form/index.ts
Normal file
12
x-pack/plugins/osquery/public/form/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { VersionField } from './version_field';
|
||||
export { QueryDescriptionField } from './query_description_field';
|
||||
export { IntervalField } from './interval_field';
|
||||
export { QueryIdField } from './query_id_field';
|
||||
export type { FormField } from './types';
|
82
x-pack/plugins/osquery/public/form/interval_field.tsx
Normal file
82
x-pack/plugins/osquery/public/form/interval_field.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useController } from 'react-hook-form';
|
||||
import type { EuiFieldNumberProps } from '@elastic/eui';
|
||||
import { EuiFieldNumber, EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
const intervalFieldValidations = {
|
||||
required: {
|
||||
message: i18n.translate('xpack.osquery.pack.queryFlyoutForm.intervalFieldMinNumberError', {
|
||||
defaultMessage: 'A positive interval value is required',
|
||||
}),
|
||||
value: true,
|
||||
},
|
||||
min: {
|
||||
message: i18n.translate('xpack.osquery.pack.queryFlyoutForm.intervalFieldMinNumberError', {
|
||||
defaultMessage: 'A positive interval value is required',
|
||||
}),
|
||||
value: 1,
|
||||
},
|
||||
max: {
|
||||
message: i18n.translate('xpack.osquery.pack.queryFlyoutForm.intervalFieldMaxNumberError', {
|
||||
defaultMessage: 'An interval value must be lower than {than}',
|
||||
values: { than: 604800 },
|
||||
}),
|
||||
value: 604800,
|
||||
},
|
||||
};
|
||||
|
||||
interface IntervalFieldProps {
|
||||
euiFieldProps?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const IntervalFieldComponent = ({ euiFieldProps }: IntervalFieldProps) => {
|
||||
const {
|
||||
field: { onChange, value },
|
||||
fieldState: { error },
|
||||
} = useController({
|
||||
name: 'interval',
|
||||
defaultValue: 3600,
|
||||
rules: {
|
||||
...intervalFieldValidations,
|
||||
},
|
||||
});
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const numberValue = e.target.valueAsNumber ? e.target.valueAsNumber : 0;
|
||||
onChange(numberValue);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
const hasError = useMemo(() => !!error?.message, [error?.message]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.osquery.pack.queryFlyoutForm.intervalFieldLabel', {
|
||||
defaultMessage: 'Interval (s)',
|
||||
})}
|
||||
error={error?.message}
|
||||
isInvalid={hasError}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldNumber
|
||||
isInvalid={hasError}
|
||||
value={value as EuiFieldNumberProps['value']}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
type="number"
|
||||
data-test-subj="input"
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const IntervalField = React.memo(IntervalFieldComponent);
|
|
@ -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.
|
||||
*/
|
||||
import React, { useMemo } from 'react';
|
||||
import { useController } from 'react-hook-form';
|
||||
import { EuiFieldText, EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface QueryDescriptionFieldProps {
|
||||
euiFieldProps?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const QueryDescriptionFieldComponentn = ({ euiFieldProps }: QueryDescriptionFieldProps) => {
|
||||
const {
|
||||
field: { onChange, value, name: fieldName },
|
||||
fieldState: { error },
|
||||
} = useController({
|
||||
name: 'description',
|
||||
defaultValue: '',
|
||||
});
|
||||
|
||||
const hasError = useMemo(() => !!error?.message, [error?.message]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.osquery.pack.form.descriptionFieldLabel', {
|
||||
defaultMessage: 'Description (optional)',
|
||||
})}
|
||||
error={error?.message}
|
||||
isInvalid={hasError}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
isInvalid={hasError}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
name={fieldName}
|
||||
fullWidth
|
||||
data-test-subj="input"
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const QueryDescriptionField = React.memo(QueryDescriptionFieldComponentn);
|
52
x-pack/plugins/osquery/public/form/query_id_field.tsx
Normal file
52
x-pack/plugins/osquery/public/form/query_id_field.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React, { useMemo } from 'react';
|
||||
import { useController } from 'react-hook-form';
|
||||
import { EuiFieldText, EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createFormIdFieldValidations } from '../packs/queries/validations';
|
||||
|
||||
interface QueryIdFieldProps {
|
||||
idSet?: Set<string>;
|
||||
euiFieldProps?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const QueryIdFieldComponentn = ({ idSet, euiFieldProps }: QueryIdFieldProps) => {
|
||||
const {
|
||||
field: { onChange, value, name: fieldName },
|
||||
fieldState: { error },
|
||||
} = useController({
|
||||
name: 'id',
|
||||
defaultValue: '',
|
||||
rules: idSet && createFormIdFieldValidations(idSet),
|
||||
});
|
||||
|
||||
const hasError = useMemo(() => !!error?.message, [error?.message]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.osquery.pack.queryFlyoutForm.idFieldLabel', {
|
||||
defaultMessage: 'ID',
|
||||
})}
|
||||
error={error?.message}
|
||||
isInvalid={hasError}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
isInvalid={hasError}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
name={fieldName}
|
||||
fullWidth
|
||||
data-test-subj="input"
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const QueryIdField = React.memo(QueryIdFieldComponentn);
|
28
x-pack/plugins/osquery/public/form/types.ts
Normal file
28
x-pack/plugins/osquery/public/form/types.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface FormField<T> {
|
||||
name: string;
|
||||
onChange: (data: T) => void;
|
||||
value: T;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
export interface FormFieldProps<T> {
|
||||
name: string;
|
||||
label: string | Element;
|
||||
labelAppend?: ReactNode;
|
||||
helpText?: string | (() => React.ReactNode);
|
||||
idAria?: string;
|
||||
euiFieldProps?: Record<string, unknown>;
|
||||
defaultValue?: T;
|
||||
required?: boolean;
|
||||
rules?: Record<string, unknown>;
|
||||
}
|
88
x-pack/plugins/osquery/public/form/version_field.tsx
Normal file
88
x-pack/plugins/osquery/public/form/version_field.tsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { EuiFormRow, EuiComboBox, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { useController } from 'react-hook-form';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
interface VersionFieldProps {
|
||||
euiFieldProps?: Record<string, unknown>;
|
||||
}
|
||||
const VersionFieldComponent = ({ euiFieldProps = {} }: VersionFieldProps) => {
|
||||
const {
|
||||
field: { onChange, value },
|
||||
fieldState: { error },
|
||||
} = useController({
|
||||
name: 'version',
|
||||
defaultValue: [],
|
||||
rules: {},
|
||||
});
|
||||
|
||||
const onCreateComboOption = useCallback(
|
||||
(newValue: string) => {
|
||||
const result = [...(value as string[]), newValue];
|
||||
|
||||
onChange(result);
|
||||
},
|
||||
[onChange, value]
|
||||
);
|
||||
|
||||
const onComboChange = useCallback(
|
||||
(options: EuiComboBoxOptionOption[]) => {
|
||||
onChange(options.map((option) => option.label));
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
const hasError = useMemo(() => !!error?.message, [error?.message]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.pack.queryFlyoutForm.versionFieldLabel"
|
||||
defaultMessage="Minimum Osquery version"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
labelAppend={
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.queryFlyoutForm.versionFieldOptionalLabel"
|
||||
defaultMessage="(optional)"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
}
|
||||
error={error?.message}
|
||||
isInvalid={hasError}
|
||||
fullWidth
|
||||
>
|
||||
<EuiComboBox
|
||||
isInvalid={hasError}
|
||||
noSuggestions
|
||||
placeholder={i18n.translate('xpack.osquery.comboBoxField.placeHolderText', {
|
||||
defaultMessage: 'Type and then hit "ENTER"',
|
||||
})}
|
||||
selectedOptions={value.map((v: string) => ({ label: v }))}
|
||||
onCreateOption={onCreateComboOption}
|
||||
onChange={onComboChange}
|
||||
fullWidth
|
||||
data-test-subj="input"
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const VersionField = React.memo(VersionFieldComponent);
|
|
@ -5,27 +5,47 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import type { FieldHook } from '../../shared_imports';
|
||||
import React from 'react';
|
||||
import { useController } from 'react-hook-form';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AgentsTable } from '../../agents/agents_table';
|
||||
import type { AgentSelection } from '../../agents/types';
|
||||
|
||||
interface AgentsTableFieldProps {
|
||||
field: FieldHook<AgentSelection>;
|
||||
}
|
||||
const checkAgentsLength = (agentsSelection: AgentSelection) => {
|
||||
if (!isEmpty(agentsSelection)) {
|
||||
const isValid = !!(
|
||||
agentsSelection.allAgentsSelected ||
|
||||
agentsSelection.agents?.length ||
|
||||
agentsSelection.platformsSelected?.length ||
|
||||
agentsSelection.policiesSelected?.length
|
||||
);
|
||||
|
||||
const AgentsTableFieldComponent: React.FC<AgentsTableFieldProps> = ({ field }) => {
|
||||
const { value, setValue } = field;
|
||||
const handleChange = useCallback(
|
||||
(props) => {
|
||||
if (props !== value) {
|
||||
return setValue(props);
|
||||
}
|
||||
return !isValid
|
||||
? i18n.translate('xpack.osquery.pack.queryFlyoutForm.osqueryAgentsMissingErrorMessage', {
|
||||
defaultMessage: 'Agents is a required field',
|
||||
})
|
||||
: undefined;
|
||||
}
|
||||
|
||||
return i18n.translate('xpack.osquery.pack.queryFlyoutForm.osqueryAgentsMissingErrorMessage', {
|
||||
defaultMessage: 'Agents is a required field',
|
||||
});
|
||||
};
|
||||
|
||||
const AgentsTableFieldComponent: React.FC<{}> = () => {
|
||||
const {
|
||||
field: { onChange, value },
|
||||
fieldState: { error },
|
||||
} = useController({
|
||||
name: 'agentSelection',
|
||||
rules: {
|
||||
validate: checkAgentsLength,
|
||||
},
|
||||
[value, setValue]
|
||||
);
|
||||
defaultValue: {},
|
||||
});
|
||||
|
||||
return <AgentsTable agentSelection={value} onChange={handleChange} />;
|
||||
return <AgentsTable agentSelection={value} onChange={onChange} error={error?.message} />;
|
||||
};
|
||||
|
||||
export const AgentsTableField = React.memo(AgentsTableFieldComponent);
|
||||
|
|
|
@ -19,27 +19,47 @@ import {
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useForm as useHookForm, FormProvider } from 'react-hook-form';
|
||||
|
||||
import { pickBy, isEmpty, map, find } from 'lodash';
|
||||
import { isEmpty, map, find, pickBy } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { SavedQuerySOFormData } from '../../saved_queries/form/use_saved_query_form';
|
||||
import type {
|
||||
EcsMappingFormField,
|
||||
EcsMappingSerialized,
|
||||
} from '../../packs/queries/ecs_mapping_editor_field';
|
||||
import { defaultEcsFormData } from '../../packs/queries/ecs_mapping_editor_field';
|
||||
import { convertECSMappingToObject } from '../../../common/schemas/common/utils';
|
||||
import type { FormData } from '../../shared_imports';
|
||||
import { UseField, Form, 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 { SavedQueryFlyout } from '../../saved_queries';
|
||||
import { ECSMappingEditorField } from '../../packs/queries/lazy_ecs_mapping_editor_field';
|
||||
import { SavedQueriesDropdown } from '../../saved_queries/saved_queries_dropdown';
|
||||
import { liveQueryFormSchema } from './schema';
|
||||
import { usePacks } from '../../packs/use_packs';
|
||||
import { PackQueriesStatusTable } from './pack_queries_status_table';
|
||||
import { useCreateLiveQuery } from '../use_create_live_query_action';
|
||||
import { useLiveQueryDetails } from '../../actions/use_live_query_details';
|
||||
import type { AgentSelection } from '../../agents/types';
|
||||
import { LiveQueryQueryField } from './live_query_query_field';
|
||||
import { AgentsTableField } from './agents_table_field';
|
||||
import { PacksComboBoxField } from './packs_combobox_field';
|
||||
import { savedQueryDataSerializer } from '../../saved_queries/form/use_saved_query_form';
|
||||
|
||||
const FORM_ID = 'liveQueryForm';
|
||||
export interface LiveQueryFormFields {
|
||||
query?: string;
|
||||
agentSelection: AgentSelection;
|
||||
savedQueryId?: string | null;
|
||||
ecs_mapping: EcsMappingFormField[];
|
||||
packId: string[];
|
||||
}
|
||||
|
||||
interface DefaultLiveQueryFormFields {
|
||||
query?: string;
|
||||
agentSelection?: AgentSelection;
|
||||
savedQueryId?: string | null;
|
||||
ecs_mapping?: EcsMappingSerialized;
|
||||
packId?: string;
|
||||
}
|
||||
|
||||
const StyledEuiCard = styled(EuiCard)`
|
||||
padding: 16px 92px 16px 16px !important;
|
||||
|
@ -86,12 +106,10 @@ const StyledEuiAccordion = styled(EuiAccordion)`
|
|||
}
|
||||
`;
|
||||
|
||||
const GhostFormField = () => <></>;
|
||||
|
||||
type FormType = 'simple' | 'steps';
|
||||
|
||||
interface LiveQueryFormProps {
|
||||
defaultValue?: Partial<FormData>;
|
||||
defaultValue?: DefaultLiveQueryFormFields;
|
||||
onSuccess?: () => void;
|
||||
queryField?: boolean;
|
||||
ecsMappingField?: boolean;
|
||||
|
@ -118,6 +136,22 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
[permissions]
|
||||
);
|
||||
|
||||
const hooksForm = useHookForm<LiveQueryFormFields>({
|
||||
defaultValues: {
|
||||
ecs_mapping: [defaultEcsFormData],
|
||||
},
|
||||
});
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
resetField,
|
||||
clearErrors,
|
||||
getFieldState,
|
||||
register,
|
||||
formState: { isSubmitting, errors },
|
||||
} = hooksForm;
|
||||
|
||||
const canRunSingleQuery = useMemo(
|
||||
() =>
|
||||
!!(
|
||||
|
@ -133,6 +167,8 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
const [queryType, setQueryType] = useState<string>('query');
|
||||
const [isLive, setIsLive] = useState(false);
|
||||
|
||||
const queryState = getFieldState('query');
|
||||
const watchedValues = watch();
|
||||
const handleShowSaveQueryFlyout = useCallback(() => setShowSavedQueryFlyout(true), []);
|
||||
const handleCloseSaveQueryFlyout = useCallback(() => setShowSavedQueryFlyout(false), []);
|
||||
|
||||
|
@ -150,75 +186,22 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
isLive,
|
||||
});
|
||||
|
||||
const { form } = useForm({
|
||||
id: FORM_ID,
|
||||
schema: liveQueryFormSchema,
|
||||
onSubmit: async (formData, isValid) => {
|
||||
if (isValid) {
|
||||
try {
|
||||
// @ts-expect-error update types
|
||||
await mutateAsync(formData);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
options: {
|
||||
stripEmptyFields: false,
|
||||
},
|
||||
serializer: ({
|
||||
savedQueryId,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
ecs_mapping,
|
||||
packId,
|
||||
...formData
|
||||
}) =>
|
||||
pickBy(
|
||||
{
|
||||
...formData,
|
||||
pack_id: packId?.length ? packId[0] : undefined,
|
||||
saved_query_id: savedQueryId,
|
||||
ecs_mapping: convertECSMappingToObject(ecs_mapping),
|
||||
},
|
||||
(value) => !isEmpty(value)
|
||||
),
|
||||
});
|
||||
|
||||
const { updateFieldValues, setFieldValue, submit, isSubmitting } = form;
|
||||
|
||||
const actionId = useMemo(() => liveQueryDetails?.action_id, [liveQueryDetails?.action_id]);
|
||||
const agentIds = useMemo(() => liveQueryDetails?.agents, [liveQueryDetails?.agents]);
|
||||
const [
|
||||
{ agentSelection, ecs_mapping: ecsMapping, query, savedQueryId, packId },
|
||||
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]);
|
||||
useEffect(() => {
|
||||
register('savedQueryId');
|
||||
}, [register]);
|
||||
|
||||
const agentSelected = useMemo(
|
||||
() =>
|
||||
agentSelection &&
|
||||
!!(
|
||||
agentSelection.allAgentsSelected ||
|
||||
agentSelection.agents?.length ||
|
||||
agentSelection.platformsSelected?.length ||
|
||||
agentSelection.policiesSelected?.length
|
||||
),
|
||||
[agentSelection]
|
||||
);
|
||||
|
||||
const queryValueProvided = useMemo(() => !!query?.length, [query]);
|
||||
const { packId } = watchedValues;
|
||||
|
||||
const queryStatus = useMemo(() => {
|
||||
if (isError || !form.getFields().query?.isValid) return 'danger';
|
||||
if (isError || queryState.invalid) return 'danger';
|
||||
if (isLoading) return 'loading';
|
||||
if (isSuccess) return 'complete';
|
||||
|
||||
return 'incomplete';
|
||||
}, [isError, isLoading, isSuccess, form]);
|
||||
}, [isError, isLoading, isSuccess, queryState]);
|
||||
|
||||
const resultsStatus = useMemo(
|
||||
() => (queryStatus === 'complete' ? 'incomplete' : 'disabled'),
|
||||
|
@ -228,39 +211,66 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
const handleSavedQueryChange = useCallback(
|
||||
(savedQuery) => {
|
||||
if (savedQuery) {
|
||||
updateFieldValues({
|
||||
query: savedQuery.query,
|
||||
savedQueryId: savedQuery.savedQueryId,
|
||||
ecs_mapping: savedQuery.ecs_mapping
|
||||
setValue('query', savedQuery.query);
|
||||
setValue('savedQueryId', savedQuery.savedQueryId);
|
||||
setValue(
|
||||
'ecs_mapping',
|
||||
!isEmpty(savedQuery.ecs_mapping)
|
||||
? map(savedQuery.ecs_mapping, (value, key) => ({
|
||||
key,
|
||||
result: {
|
||||
type: Object.keys(value)[0],
|
||||
value: Object.values(value)[0],
|
||||
value: Object.values(value)[0] as string,
|
||||
},
|
||||
}))
|
||||
: [],
|
||||
});
|
||||
: [defaultEcsFormData]
|
||||
);
|
||||
|
||||
if (!isEmpty(savedQuery.ecs_mapping)) {
|
||||
setAdvancedContentState('open');
|
||||
}
|
||||
} else {
|
||||
setFieldValue('savedQueryId', null);
|
||||
setValue('savedQueryId', null);
|
||||
}
|
||||
},
|
||||
[setFieldValue, updateFieldValues]
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
// not sure why, but submitOnCmdEnter doesn't have proper form values so I am passing them in manually
|
||||
async (values: LiveQueryFormFields = watchedValues) => {
|
||||
const serializedData = pickBy(
|
||||
{
|
||||
agentSelection: values.agentSelection,
|
||||
saved_query_id: values.savedQueryId,
|
||||
query: values.query,
|
||||
pack_id: packId?.length ? packId[0] : undefined,
|
||||
...(values.ecs_mapping
|
||||
? { ecs_mapping: convertECSMappingToObject(values.ecs_mapping) }
|
||||
: {}),
|
||||
},
|
||||
(value) => !isEmpty(value)
|
||||
);
|
||||
if (isEmpty(errors)) {
|
||||
try {
|
||||
// @ts-expect-error update types
|
||||
await mutateAsync(serializedData);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
[errors, mutateAsync, packId, watchedValues]
|
||||
);
|
||||
const commands = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'submitOnCmdEnter',
|
||||
bindKey: { win: 'ctrl+enter', mac: 'cmd+enter' },
|
||||
exec: () => submit(),
|
||||
// @ts-expect-error update types - explanation in onSubmit()
|
||||
exec: () => handleSubmit(onSubmit)(watchedValues),
|
||||
},
|
||||
],
|
||||
[submit]
|
||||
[handleSubmit, onSubmit, watchedValues]
|
||||
);
|
||||
|
||||
const queryComponentProps = useMemo(
|
||||
|
@ -270,9 +280,9 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
[commands]
|
||||
);
|
||||
|
||||
const flyoutFormDefaultValue = useMemo(
|
||||
() => ({ savedQueryId, query, ecs_mapping: serializedFormData.ecs_mapping }),
|
||||
[savedQueryId, serializedFormData.ecs_mapping, query]
|
||||
const serializedData: SavedQuerySOFormData = useMemo(
|
||||
() => savedQueryDataSerializer(watchedValues),
|
||||
[watchedValues]
|
||||
);
|
||||
|
||||
const handleToggle = useCallback((isOpen) => {
|
||||
|
@ -306,12 +316,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
{formType === 'steps' && queryType !== 'pack' && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
disabled={
|
||||
!permissions.writeSavedQueries ||
|
||||
!agentSelected ||
|
||||
!queryValueProvided ||
|
||||
resultsStatus === 'disabled'
|
||||
}
|
||||
disabled={!permissions.writeSavedQueries || resultsStatus === 'disabled'}
|
||||
onClick={handleShowSaveQueryFlyout}
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -324,15 +329,8 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
id="submit-button"
|
||||
disabled={
|
||||
!enabled ||
|
||||
!agentSelected ||
|
||||
(queryType === 'query' && !queryValueProvided) ||
|
||||
(queryType === 'pack' &&
|
||||
(!packId || !selectedPackData?.attributes.queries.length)) ||
|
||||
isSubmitting
|
||||
}
|
||||
onClick={submit}
|
||||
disabled={!enabled || isSubmitting}
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryForm.form.submitButtonLabel"
|
||||
|
@ -344,25 +342,22 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
</EuiFlexItem>
|
||||
),
|
||||
[
|
||||
agentSelected,
|
||||
enabled,
|
||||
formType,
|
||||
handleShowSaveQueryFlyout,
|
||||
isSubmitting,
|
||||
packId,
|
||||
permissions.writeSavedQueries,
|
||||
queryType,
|
||||
queryValueProvided,
|
||||
permissions.writeSavedQueries,
|
||||
resultsStatus,
|
||||
selectedPackData,
|
||||
submit,
|
||||
handleShowSaveQueryFlyout,
|
||||
enabled,
|
||||
isSubmitting,
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
]
|
||||
);
|
||||
|
||||
const queryFieldStepContent = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{queryField ? (
|
||||
{queryField && (
|
||||
<>
|
||||
{!isSavedQueryDisabled && (
|
||||
<>
|
||||
|
@ -372,20 +367,10 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
<UseField path="savedQueryId" component={GhostFormField} />
|
||||
<UseField
|
||||
path="query"
|
||||
component={LiveQueryQueryField}
|
||||
componentProps={queryComponentProps}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UseField path="savedQueryId" component={GhostFormField} />
|
||||
<UseField path="query" component={GhostFormField} />
|
||||
<LiveQueryQueryField {...queryComponentProps} queryType={queryType} />
|
||||
</>
|
||||
)}
|
||||
{ecsMappingField ? (
|
||||
{ecsMappingField && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<StyledEuiAccordion
|
||||
|
@ -398,20 +383,19 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
<ECSMappingEditorField euiFieldProps={ecsFieldProps} />
|
||||
</StyledEuiAccordion>
|
||||
</>
|
||||
) : (
|
||||
<UseField path="ecs_mapping" component={GhostFormField} />
|
||||
)}
|
||||
</>
|
||||
),
|
||||
[
|
||||
queryField,
|
||||
queryComponentProps,
|
||||
isSavedQueryDisabled,
|
||||
handleSavedQueryChange,
|
||||
queryComponentProps,
|
||||
queryType,
|
||||
ecsMappingField,
|
||||
advancedContentState,
|
||||
handleToggle,
|
||||
ecsFieldProps,
|
||||
isSavedQueryDisabled,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -422,7 +406,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
singleQueryDetails?.action_id ? (
|
||||
<ResultTabs
|
||||
actionId={singleQueryDetails?.action_id}
|
||||
ecsMapping={serializedFormData.ecs_mapping}
|
||||
ecsMapping={serializedData.ecs_mapping}
|
||||
endDate={singleQueryDetails?.expiration}
|
||||
agentIds={singleQueryDetails?.agents}
|
||||
addToTimeline={addToTimeline}
|
||||
|
@ -432,7 +416,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
singleQueryDetails?.action_id,
|
||||
singleQueryDetails?.expiration,
|
||||
singleQueryDetails?.agents,
|
||||
serializedFormData.ecs_mapping,
|
||||
serializedData.ecs_mapping,
|
||||
addToTimeline,
|
||||
]
|
||||
);
|
||||
|
@ -440,9 +424,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
useEffect(() => {
|
||||
if (defaultValue) {
|
||||
if (defaultValue.agentSelection) {
|
||||
updateFieldValues({
|
||||
agentSelection: defaultValue.agentSelection,
|
||||
});
|
||||
setValue('agentSelection', defaultValue.agentSelection);
|
||||
}
|
||||
|
||||
if (defaultValue?.packId && canRunPacks) {
|
||||
|
@ -451,19 +433,18 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
if (!isPackDataFetched) return;
|
||||
const selectedPackOption = find(packsData?.data, ['id', defaultValue.packId]);
|
||||
if (selectedPackOption) {
|
||||
updateFieldValues({
|
||||
packId: [defaultValue.packId],
|
||||
});
|
||||
setValue('packId', [defaultValue.packId]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (defaultValue?.query && canRunSingleQuery) {
|
||||
updateFieldValues({
|
||||
query: defaultValue.query,
|
||||
savedQueryId: defaultValue.savedQueryId,
|
||||
ecs_mapping: defaultValue.ecs_mapping
|
||||
setValue('query', defaultValue.query);
|
||||
setValue('savedQueryId', defaultValue.savedQueryId);
|
||||
setValue(
|
||||
'ecs_mapping',
|
||||
!isEmpty(defaultValue.ecs_mapping)
|
||||
? map(defaultValue.ecs_mapping, (value, key) => ({
|
||||
key,
|
||||
result: {
|
||||
|
@ -471,8 +452,8 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
value: Object.values(value)[0],
|
||||
},
|
||||
}))
|
||||
: undefined,
|
||||
});
|
||||
: [defaultEcsFormData]
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -485,14 +466,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
return setQueryType('pack');
|
||||
}
|
||||
}
|
||||
}, [
|
||||
canRunPacks,
|
||||
canRunSingleQuery,
|
||||
defaultValue,
|
||||
isPackDataFetched,
|
||||
packsData?.data,
|
||||
updateFieldValues,
|
||||
]);
|
||||
}, [canRunPacks, canRunSingleQuery, defaultValue, isPackDataFetched, packsData?.data, setValue]);
|
||||
|
||||
const queryCardSelectable = useMemo(
|
||||
() => ({
|
||||
|
@ -516,11 +490,20 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
setIsLive(() => !(liveQueryDetails?.status === 'completed'));
|
||||
}, [liveQueryDetails?.status]);
|
||||
|
||||
useEffect(() => cleanupLiveQuery(), [queryType, packId, cleanupLiveQuery]);
|
||||
useEffect(() => {
|
||||
cleanupLiveQuery();
|
||||
if (!defaultValue) {
|
||||
resetField('packId');
|
||||
resetField('query');
|
||||
resetField('ecs_mapping');
|
||||
resetField('savedQueryId');
|
||||
clearErrors();
|
||||
}
|
||||
}, [queryType, cleanupLiveQuery, resetField, setValue, clearErrors, defaultValue]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form form={form}>
|
||||
<FormProvider {...hooksForm}>
|
||||
<EuiFlexGroup direction="column">
|
||||
{queryField && (
|
||||
<EuiFlexItem>
|
||||
|
@ -572,25 +555,23 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{!hideAgentsField ? (
|
||||
{!hideAgentsField && (
|
||||
<EuiFlexItem>
|
||||
<UseField path="agentSelection" component={AgentsTableField} />
|
||||
<AgentsTableField />
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<UseField path="agentSelection" component={GhostFormField} />
|
||||
)}
|
||||
{queryType === 'pack' ? (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<UseField
|
||||
path="packId"
|
||||
component={PacksComboBoxField}
|
||||
<PacksComboBoxField
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{ packsData: packsData?.data }}
|
||||
fieldProps={{ packsData: packsData?.data }}
|
||||
queryType={queryType}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{submitButtonContent}
|
||||
<EuiSpacer />
|
||||
|
||||
{liveQueryDetails?.queries?.length ||
|
||||
selectedPackData?.attributes?.queries?.length ? (
|
||||
<>
|
||||
|
@ -598,6 +579,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
<PackQueriesStatusTable
|
||||
actionId={actionId}
|
||||
agentIds={agentIds}
|
||||
// @ts-expect-error version string !+ string[]
|
||||
data={liveQueryDetails?.queries ?? selectedPackData?.attributes?.queries}
|
||||
addToTimeline={addToTimeline}
|
||||
/>
|
||||
|
@ -613,12 +595,13 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
|
||||
{showSavedQueryFlyout ? (
|
||||
<SavedQueryFlyout
|
||||
isExternal={!!addToTimeline}
|
||||
onClose={handleCloseSaveQueryFlyout}
|
||||
defaultValue={flyoutFormDefaultValue}
|
||||
defaultValue={serializedData}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
|
|
|
@ -6,12 +6,15 @@
|
|||
*/
|
||||
|
||||
import { EuiCodeBlock, EuiFormRow } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { EuiCodeEditorProps, FieldHook } from '../../shared_imports';
|
||||
import { useController } from 'react-hook-form';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { EuiCodeEditorProps } from '../../shared_imports';
|
||||
import { OsqueryEditor } from '../../editor';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { MAX_QUERY_LENGTH } from '../../packs/queries/validations';
|
||||
|
||||
const StyledEuiCodeBlock = styled(EuiCodeBlock)`
|
||||
min-height: 100px;
|
||||
|
@ -19,30 +22,44 @@ const StyledEuiCodeBlock = styled(EuiCodeBlock)`
|
|||
|
||||
interface LiveQueryQueryFieldProps {
|
||||
disabled?: boolean;
|
||||
field: FieldHook<string>;
|
||||
commands?: EuiCodeEditorProps['commands'];
|
||||
queryType: string;
|
||||
}
|
||||
|
||||
const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({
|
||||
disabled,
|
||||
field,
|
||||
commands,
|
||||
queryType,
|
||||
}) => {
|
||||
const permissions = useKibana().services.application.capabilities.osquery;
|
||||
const { value, setValue, errors } = field;
|
||||
const error = errors[0]?.message;
|
||||
|
||||
const handleEditorChange = useCallback(
|
||||
(newValue) => {
|
||||
setValue(newValue);
|
||||
const {
|
||||
field: { onChange, value },
|
||||
fieldState: { error },
|
||||
} = useController({
|
||||
name: 'query',
|
||||
rules: {
|
||||
required: {
|
||||
message: i18n.translate('xpack.osquery.pack.queryFlyoutForm.emptyQueryError', {
|
||||
defaultMessage: 'Query is a required field',
|
||||
}),
|
||||
value: queryType === 'query',
|
||||
},
|
||||
maxLength: {
|
||||
message: i18n.translate('xpack.osquery.liveQuery.queryForm.largeQueryError', {
|
||||
defaultMessage: 'Query is too large (max {maxLength} characters)',
|
||||
values: { maxLength: MAX_QUERY_LENGTH },
|
||||
}),
|
||||
value: MAX_QUERY_LENGTH,
|
||||
},
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
defaultValue: '',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
isInvalid={typeof error === 'string'}
|
||||
error={error}
|
||||
isInvalid={!!error?.message}
|
||||
error={error?.message}
|
||||
fullWidth
|
||||
isDisabled={!permissions.writeLiveQueries || disabled}
|
||||
>
|
||||
|
@ -56,7 +73,7 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({
|
|||
{value}
|
||||
</StyledEuiCodeBlock>
|
||||
) : (
|
||||
<OsqueryEditor defaultValue={value} onChange={handleEditorChange} commands={commands} />
|
||||
<OsqueryEditor defaultValue={value} onChange={onChange} commands={commands} />
|
||||
)}
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
|
|
@ -12,8 +12,7 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui';
|
|||
import { EuiFormRow, EuiComboBox, EuiTextColor, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { FieldHook } from '../../shared_imports';
|
||||
import { VALIDATION_TYPES } from '../../shared_imports';
|
||||
import { useController } from 'react-hook-form';
|
||||
import type { PackSavedObject } from '../../packs/types';
|
||||
|
||||
const TextTruncate = styled.div`
|
||||
|
@ -21,13 +20,12 @@ const TextTruncate = styled.div`
|
|||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
field: FieldHook<string[]>;
|
||||
euiFieldProps?: {
|
||||
interface PackComboBoxFieldProps {
|
||||
fieldProps?: {
|
||||
packsData?: PackSavedObject[];
|
||||
};
|
||||
idAria?: string;
|
||||
[key: string]: unknown;
|
||||
queryType: string;
|
||||
}
|
||||
|
||||
interface PackOption {
|
||||
|
@ -36,48 +34,53 @@ interface PackOption {
|
|||
description?: string;
|
||||
}
|
||||
|
||||
export const PacksComboBoxField = ({ field, euiFieldProps = {}, idAria, ...rest }: Props) => {
|
||||
export const PacksComboBoxField = ({
|
||||
queryType,
|
||||
fieldProps = {},
|
||||
idAria,
|
||||
...rest
|
||||
}: PackComboBoxFieldProps) => {
|
||||
const {
|
||||
field: { value, onChange },
|
||||
fieldState,
|
||||
} = useController({
|
||||
name: 'packId',
|
||||
rules: {
|
||||
required: {
|
||||
message: i18n.translate(
|
||||
'xpack.osquery.pack.queryFlyoutForm.osqueryPackMissingErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Pack is a required field',
|
||||
}
|
||||
),
|
||||
value: queryType === 'pack',
|
||||
},
|
||||
},
|
||||
defaultValue: [],
|
||||
});
|
||||
const error = fieldState.error?.message;
|
||||
const [selectedOptions, setSelectedOptions] = useState<
|
||||
Array<EuiComboBoxOptionOption<PackOption>>
|
||||
>([]);
|
||||
// Errors for the comboBox value (the "array")
|
||||
const errorMessageField = field.getErrorsMessages();
|
||||
|
||||
// Errors for comboBox option added (the array "item")
|
||||
const errorMessageArrayItem = field.getErrorsMessages({
|
||||
validationType: VALIDATION_TYPES.ARRAY_ITEM,
|
||||
});
|
||||
|
||||
const isInvalid = field.errors.length
|
||||
? errorMessageField !== null || errorMessageArrayItem !== null
|
||||
: false;
|
||||
|
||||
// Concatenate error messages.
|
||||
const errorMessage =
|
||||
errorMessageField && errorMessageArrayItem
|
||||
? `${errorMessageField}, ${errorMessageArrayItem}`
|
||||
: errorMessageField
|
||||
? errorMessageField
|
||||
: errorMessageArrayItem;
|
||||
|
||||
const handlePackChange = useCallback(
|
||||
(newSelectedOptions) => {
|
||||
if (!newSelectedOptions.length) {
|
||||
setSelectedOptions(newSelectedOptions);
|
||||
field.setValue([]);
|
||||
onChange([]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedOptions(newSelectedOptions);
|
||||
field.setValue([newSelectedOptions[0].value?.id]);
|
||||
onChange([newSelectedOptions[0].value?.id]);
|
||||
},
|
||||
[field]
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const packOptions = useMemo<Array<EuiComboBoxOptionOption<PackOption>>>(
|
||||
() =>
|
||||
euiFieldProps?.packsData?.map((packSO) => ({
|
||||
fieldProps?.packsData?.map((packSO) => ({
|
||||
label: packSO.attributes.name ?? '',
|
||||
value: {
|
||||
id: packSO.id,
|
||||
|
@ -85,20 +88,11 @@ export const PacksComboBoxField = ({ field, euiFieldProps = {}, idAria, ...rest
|
|||
description: packSO.attributes.description,
|
||||
},
|
||||
})) ?? [],
|
||||
[euiFieldProps?.packsData]
|
||||
);
|
||||
|
||||
const onSearchComboChange = useCallback(
|
||||
(value: string) => {
|
||||
if (value !== undefined) {
|
||||
field.clearErrors(VALIDATION_TYPES.ARRAY_ITEM);
|
||||
}
|
||||
},
|
||||
[field]
|
||||
[fieldProps?.packsData]
|
||||
);
|
||||
|
||||
const renderOption = useCallback(
|
||||
({ value }) => (
|
||||
({ value: option }) => (
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
direction="column"
|
||||
|
@ -106,11 +100,11 @@ export const PacksComboBoxField = ({ field, euiFieldProps = {}, idAria, ...rest
|
|||
justifyContent="flexStart"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<strong>{value.name}</strong>
|
||||
<strong>{option?.name}</strong>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<TextTruncate>
|
||||
<EuiTextColor color="subdued">{value.description}</EuiTextColor>
|
||||
<EuiTextColor color="subdued">{option?.description}</EuiTextColor>
|
||||
</TextTruncate>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -119,22 +113,22 @@ export const PacksComboBoxField = ({ field, euiFieldProps = {}, idAria, ...rest
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (field.value.length) {
|
||||
const packOption = find(packOptions, ['value.id', field.value[0]]);
|
||||
if (value?.length) {
|
||||
const packOption = find(packOptions, ['value.id', value[0]]);
|
||||
|
||||
if (packOption) {
|
||||
setSelectedOptions([packOption]);
|
||||
}
|
||||
}
|
||||
}, [field.value, packOptions]);
|
||||
}, [value, packOptions]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
labelAppend={field.labelAppend}
|
||||
helpText={typeof field.helpText === 'function' ? field.helpText() : field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
label={i18n.translate('xpack.osquery.liveQuery.queryForm.packQueryTypeLabel', {
|
||||
defaultMessage: `Pack`,
|
||||
})}
|
||||
error={error}
|
||||
isInvalid={!!error}
|
||||
fullWidth
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-array-as-prop
|
||||
describedByIds={idAria ? [idAria] : undefined}
|
||||
|
@ -146,7 +140,6 @@ export const PacksComboBoxField = ({ field, euiFieldProps = {}, idAria, ...rest
|
|||
})}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={handlePackChange}
|
||||
onSearchChange={onSearchComboChange}
|
||||
data-test-subj="select-live-pack"
|
||||
fullWidth
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
|
@ -154,7 +147,7 @@ export const PacksComboBoxField = ({ field, euiFieldProps = {}, idAria, ...rest
|
|||
renderOption={renderOption}
|
||||
options={packOptions}
|
||||
rowHeight={60}
|
||||
{...euiFieldProps}
|
||||
{...fieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* 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: {
|
||||
defaultValue: '',
|
||||
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 },
|
||||
],
|
||||
},
|
||||
packId: {
|
||||
label: i18n.translate('xpack.osquery.packs.dropdown.searchFieldLabel', {
|
||||
defaultMessage: `Pack`,
|
||||
}),
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
defaultValue: [],
|
||||
},
|
||||
ecs_mapping: {
|
||||
defaultValue: [],
|
||||
type: FIELD_TYPES.JSON,
|
||||
},
|
||||
};
|
|
@ -10,6 +10,7 @@ import { EuiCode, EuiLoadingContent, EuiEmptyPrompt } from '@elastic/eui';
|
|||
import React, { useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type { EcsMappingSerialized } from '../packs/queries/ecs_mapping_editor_field';
|
||||
import { LiveQueryForm } from './form';
|
||||
import { useActionResultsPrivileges } from '../action_results/use_action_privileges';
|
||||
import { OSQUERY_INTEGRATION_NAME } from '../../common';
|
||||
|
@ -23,7 +24,7 @@ interface LiveQueryProps {
|
|||
onSuccess?: () => void;
|
||||
query?: string;
|
||||
savedQueryId?: string;
|
||||
ecs_mapping?: unknown;
|
||||
ecs_mapping?: EcsMappingSerialized;
|
||||
agentsField?: boolean;
|
||||
queryField?: boolean;
|
||||
ecsMappingField?: boolean;
|
||||
|
|
|
@ -93,6 +93,7 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({
|
|||
if (updatedQuery.ecs_mapping) {
|
||||
draft[showEditQueryFlyout].ecs_mapping = updatedQuery.ecs_mapping;
|
||||
} else {
|
||||
// @ts-expect-error update types
|
||||
delete draft[showEditQueryFlyout].ecs_mapping;
|
||||
}
|
||||
|
||||
|
@ -231,6 +232,7 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({
|
|||
{showEditQueryFlyout != null && showEditQueryFlyout >= 0 && (
|
||||
<QueryFlyout
|
||||
uniqueQueryIds={uniqueQueryIds}
|
||||
// @ts-expect-error update types
|
||||
defaultValue={field.value[showEditQueryFlyout]}
|
||||
onSave={handleEditQuery}
|
||||
onClose={handleHideEditFlyout}
|
||||
|
|
|
@ -39,27 +39,32 @@ import { i18n } from '@kbn/i18n';
|
|||
import styled from 'styled-components';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { prepareEcsFieldsToValidate } from '../../common/helpers';
|
||||
import { useController, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
||||
import type { FormField } from '../../form/types';
|
||||
import ECSSchema from '../../common/schemas/ecs/v8.4.0.json';
|
||||
import osquerySchema from '../../common/schemas/osquery/v5.4.0.json';
|
||||
|
||||
import { FieldIcon } from '../../common/lib/kibana';
|
||||
import type { FieldHook, ValidationFuncArg, ArrayItem, FormArrayField } from '../../shared_imports';
|
||||
import {
|
||||
FIELD_TYPES,
|
||||
getFieldValidityAndErrorMessage,
|
||||
useFormData,
|
||||
Field,
|
||||
getUseField,
|
||||
fieldValidators,
|
||||
UseMultiFields,
|
||||
UseArray,
|
||||
useFormContext,
|
||||
} from '../../shared_imports';
|
||||
import type { FormArrayField } from '../../shared_imports';
|
||||
import { OsqueryIcon } from '../../components/osquery_icon';
|
||||
import { removeMultilines } from '../../../common/utils/build_query/remove_multilines';
|
||||
import { prepareEcsFieldsToValidate } from '../../common/helpers';
|
||||
|
||||
export const CommonUseField = getUseField({ component: Field });
|
||||
export interface EcsMappingFormField {
|
||||
key: string;
|
||||
result: {
|
||||
type: string;
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type EcsMappingSerialized = Record<
|
||||
string,
|
||||
{
|
||||
field?: string;
|
||||
value?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
const typeMap = {
|
||||
binary: 'binary',
|
||||
|
@ -80,17 +85,6 @@ const typeMap = {
|
|||
constant_keyword: 'string',
|
||||
};
|
||||
|
||||
const StyledEuiSuperSelect = styled(EuiSuperSelect)`
|
||||
min-width: 70px;
|
||||
border-radius: 6px 0 0 6px;
|
||||
|
||||
.euiIcon {
|
||||
padding: 0;
|
||||
width: 18px;
|
||||
background: none;
|
||||
}
|
||||
`;
|
||||
|
||||
// @ts-expect-error update types
|
||||
const ResultComboBox = styled(EuiComboBox)`
|
||||
&.euiComboBox {
|
||||
|
@ -103,6 +97,17 @@ const ResultComboBox = styled(EuiComboBox)`
|
|||
}
|
||||
`;
|
||||
|
||||
const StyledEuiSuperSelect = styled(EuiSuperSelect)`
|
||||
min-width: 70px;
|
||||
border-radius: 6px 0 0 6px;
|
||||
|
||||
.euiIcon {
|
||||
padding: 0;
|
||||
width: 18px;
|
||||
background: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledFieldIcon = styled(FieldIcon)`
|
||||
width: 32px;
|
||||
|
||||
|
@ -144,31 +149,32 @@ const ECSSchemaOptions = ECSSchema.map((ecs) => ({
|
|||
|
||||
type ECSSchemaOption = typeof ECSSchemaOptions[0];
|
||||
|
||||
interface ECSComboboxFieldProps {
|
||||
field: FieldHook<string>;
|
||||
interface ECSComboboxFieldProps extends FormField<string> {
|
||||
euiFieldProps: EuiComboBoxProps<ECSSchemaOption>;
|
||||
idAria?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const ECSComboboxFieldComponent: React.FC<ECSComboboxFieldProps> = ({
|
||||
field,
|
||||
euiFieldProps = {},
|
||||
idAria,
|
||||
onChange,
|
||||
value,
|
||||
error,
|
||||
}) => {
|
||||
const { setValue } = field;
|
||||
const [selectedOptions, setSelected] = useState<Array<EuiComboBoxOptionOption<ECSSchemaOption>>>(
|
||||
[]
|
||||
);
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
const describedByIds = useMemo(() => (idAria ? [idAria] : []), [idAria]);
|
||||
const [formData] = useFormData();
|
||||
|
||||
const { ecs_mapping: watchedEcsMapping } = useWatch() as unknown as {
|
||||
ecs_mapping: EcsMappingFormField[];
|
||||
};
|
||||
const handleChange = useCallback(
|
||||
(newSelectedOptions) => {
|
||||
setSelected(newSelectedOptions);
|
||||
setValue(newSelectedOptions[0]?.label ?? '');
|
||||
onChange(newSelectedOptions[0]?.label ?? '');
|
||||
},
|
||||
[setValue]
|
||||
[onChange]
|
||||
);
|
||||
|
||||
// TODO: Create own component for this.
|
||||
|
@ -230,37 +236,36 @@ const ECSComboboxFieldComponent: React.FC<ECSComboboxFieldProps> = ({
|
|||
}, [selectedOptions]);
|
||||
|
||||
const availableECSSchemaOptions = useMemo(() => {
|
||||
const currentFormECSFieldValues = map(formData.ecs_mapping, 'key');
|
||||
const currentFormECSFieldValues = map(watchedEcsMapping, 'key');
|
||||
|
||||
return ECSSchemaOptions.filter(({ label }) => !currentFormECSFieldValues.includes(label));
|
||||
}, [formData.ecs_mapping]);
|
||||
}, [watchedEcsMapping]);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-expect-error update types
|
||||
setSelected(() => {
|
||||
if (!field.value.length) return [];
|
||||
if (!value?.length) return [];
|
||||
|
||||
const selectedOption = find(ECSSchemaOptions, ['label', field.value]);
|
||||
const selectedOption = find(ECSSchemaOptions, ['label', value]);
|
||||
|
||||
return selectedOption
|
||||
? [selectedOption]
|
||||
: [
|
||||
{
|
||||
label: field.value,
|
||||
label: value,
|
||||
value: {
|
||||
value: field.value,
|
||||
value,
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
}, [field.value]);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
helpText={helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
error={error}
|
||||
isInvalid={!!error}
|
||||
fullWidth
|
||||
describedByIds={describedByIds}
|
||||
isDisabled={euiFieldProps.isDisabled}
|
||||
|
@ -329,29 +334,72 @@ const OSQUERY_COLUMN_VALUE_TYPE_OPTIONS = [
|
|||
const EMPTY_ARRAY: EuiComboBoxOptionOption[] = [];
|
||||
|
||||
interface OsqueryColumnFieldProps {
|
||||
resultType: FieldHook<string>;
|
||||
resultValue: FieldHook<string | string[]>;
|
||||
euiFieldProps: EuiComboBoxProps<OsquerySchemaOption>;
|
||||
item: ArrayItem;
|
||||
item: EcsMappingFormField;
|
||||
index: number;
|
||||
idAria?: string;
|
||||
isLastItem: boolean;
|
||||
}
|
||||
|
||||
const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
||||
resultType,
|
||||
resultValue,
|
||||
euiFieldProps = {},
|
||||
euiFieldProps,
|
||||
idAria,
|
||||
item,
|
||||
index,
|
||||
isLastItem,
|
||||
}) => {
|
||||
const osqueryResultFieldValidator = (
|
||||
value: string,
|
||||
ecsMappingFormData: EcsMappingFormField[]
|
||||
): string | undefined => {
|
||||
const currentMapping = ecsMappingFormData[index];
|
||||
|
||||
if (!value.length && currentMapping.key.length) {
|
||||
return i18n.translate(
|
||||
'xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldRequiredErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Value field is required.',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (!value.length || currentMapping.result.type !== 'field') return;
|
||||
|
||||
const osqueryColumnExists = find(euiFieldProps.options, [
|
||||
'label',
|
||||
isArray(value) ? value[0] : value,
|
||||
]);
|
||||
|
||||
return !osqueryColumnExists
|
||||
? i18n.translate(
|
||||
'xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldValueMissingErrorMessage',
|
||||
{
|
||||
defaultMessage: 'The current query does not return a {columnName} field',
|
||||
values: {
|
||||
columnName: value,
|
||||
},
|
||||
}
|
||||
)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const { setValue } = useFormContext();
|
||||
const { ecs_mapping: watchedEcsMapping } = useWatch() as unknown as {
|
||||
ecs_mapping: EcsMappingFormField[];
|
||||
};
|
||||
|
||||
const { field: resultField, fieldState: resultFieldState } = useController({
|
||||
name: `ecs_mapping.${index}.result.value`,
|
||||
rules: {
|
||||
validate: (data) => osqueryResultFieldValidator(data, watchedEcsMapping),
|
||||
},
|
||||
defaultValue: '',
|
||||
});
|
||||
const itemPath = `ecs_mapping.${index}`;
|
||||
const resultValue = item.result;
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
const { setValue } = resultValue;
|
||||
const { value: typeValue, setValue: setType } = resultType;
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(resultValue);
|
||||
const [selectedOptions, setSelected] = useState<OsquerySchemaOption[]>([]);
|
||||
const describedByIds = useMemo(() => (idAria ? [idAria] : []), [idAria]);
|
||||
const [selectedOptions, setSelected] = useState<
|
||||
Array<EuiComboBoxOptionOption<OsquerySchemaOption>>
|
||||
>([]);
|
||||
const [formData] = useFormData();
|
||||
|
||||
const renderOsqueryOption = useCallback(
|
||||
(option, searchValue, contentClassName) => (
|
||||
|
@ -365,7 +413,6 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
|||
{option.value.suggestion_label}
|
||||
</StyledFieldSpan>
|
||||
</EuiFlexItem>
|
||||
|
||||
<DescriptionWrapper grow={false}>
|
||||
<StyledFieldSpan className="euiSuggestItem__description euiSuggestItem__description">
|
||||
{option.value.description}
|
||||
|
@ -376,37 +423,44 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
|||
[]
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
const handleKeyChange = useCallback(
|
||||
(newSelectedOptions) => {
|
||||
setSelected(newSelectedOptions);
|
||||
setValue(
|
||||
resultField.onChange(
|
||||
isArray(newSelectedOptions)
|
||||
? map(newSelectedOptions, 'label')
|
||||
: newSelectedOptions[0]?.label ?? ''
|
||||
);
|
||||
},
|
||||
[setValue, setSelected]
|
||||
[resultField]
|
||||
);
|
||||
|
||||
const isSingleSelection = useMemo(() => {
|
||||
const ecsKey = get(formData, item.path)?.key;
|
||||
if (ecsKey?.length && typeValue === 'value') {
|
||||
const ecsKeySchemaOption = find(ECSSchemaOptions, ['label', ecsKey]);
|
||||
const ecsData = get(watchedEcsMapping, `${index}`);
|
||||
if (ecsData?.key?.length && item.result.type === 'value') {
|
||||
const ecsKeySchemaOption = find(ECSSchemaOptions, ['label', ecsData?.key]);
|
||||
|
||||
return ecsKeySchemaOption?.value?.normalization !== 'array';
|
||||
}
|
||||
|
||||
return !!ecsKey?.length;
|
||||
}, [typeValue, formData, item.path]);
|
||||
if (!ecsData?.key?.length && isLastItem) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !!ecsData?.key?.length;
|
||||
}, [index, isLastItem, item.result.type, watchedEcsMapping]);
|
||||
|
||||
const onTypeChange = useCallback(
|
||||
(newType) => {
|
||||
if (newType !== typeValue) {
|
||||
setType(newType);
|
||||
setValue(newType === 'value' && isSingleSelection === false ? [] : '');
|
||||
if (newType !== item.result.type) {
|
||||
setValue(`${itemPath}.result.type`, newType);
|
||||
setValue(
|
||||
`${itemPath}.result.value`,
|
||||
newType === 'value' && isSingleSelection === false ? [] : ''
|
||||
);
|
||||
}
|
||||
},
|
||||
[typeValue, setType, setValue, isSingleSelection]
|
||||
[isSingleSelection, item.result.type, itemPath, setValue]
|
||||
);
|
||||
|
||||
const handleCreateOption = useCallback(
|
||||
|
@ -416,19 +470,19 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
|||
if (!trimmedNewOption.length) return;
|
||||
|
||||
if (isSingleSelection === false) {
|
||||
setValue([trimmedNewOption]);
|
||||
if (resultValue.value.length) {
|
||||
setValue([...castArray(resultValue.value), trimmedNewOption]);
|
||||
setValue(`${itemPath}.result.value`, [trimmedNewOption]);
|
||||
if (item.result.value.length) {
|
||||
setValue(`${itemPath}.result.value`, [...castArray(resultValue.value), trimmedNewOption]);
|
||||
} else {
|
||||
setValue([trimmedNewOption]);
|
||||
setValue(`${itemPath}.result.value`, [trimmedNewOption]);
|
||||
}
|
||||
|
||||
inputRef.current?.blur();
|
||||
} else {
|
||||
setValue(trimmedNewOption);
|
||||
setValue(`${itemPath}.result.value`, trimmedNewOption);
|
||||
}
|
||||
},
|
||||
[isSingleSelection, resultValue.value, setValue]
|
||||
[isSingleSelection, item.result.value.length, itemPath, resultValue.value, setValue]
|
||||
);
|
||||
|
||||
const Prepend = useMemo(
|
||||
|
@ -436,7 +490,7 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
|||
<StyledEuiSuperSelect
|
||||
disabled={euiFieldProps.isDisabled}
|
||||
options={OSQUERY_COLUMN_VALUE_TYPE_OPTIONS}
|
||||
valueOfSelected={typeValue || OSQUERY_COLUMN_VALUE_TYPE_OPTIONS[0].value}
|
||||
valueOfSelected={item.result.type || OSQUERY_COLUMN_VALUE_TYPE_OPTIONS[0].value}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
popoverProps={{
|
||||
panelStyle: {
|
||||
|
@ -446,29 +500,33 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
|||
onChange={onTypeChange}
|
||||
/>
|
||||
),
|
||||
[euiFieldProps.isDisabled, onTypeChange, typeValue]
|
||||
[euiFieldProps.isDisabled, item.result.type, onTypeChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSingleSelection && isArray(resultValue.value)) {
|
||||
setValue(resultValue.value.join(' '));
|
||||
setValue(`${itemPath}.result.value`, resultValue.value.join(' '));
|
||||
}
|
||||
|
||||
if (!isSingleSelection && !isArray(resultValue.value)) {
|
||||
setValue(resultValue.value.length ? [resultValue.value] : []);
|
||||
const value = resultValue.value.length ? [resultValue.value] : [];
|
||||
setValue(`${itemPath}.result.value`, value);
|
||||
}
|
||||
}, [isSingleSelection, resultValue.value, setValue]);
|
||||
}, [index, isSingleSelection, itemPath, resultValue, resultValue.value, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(() => {
|
||||
// @ts-expect-error hard to type to satisfy TS, but it represents proper types
|
||||
setSelected((_: OsquerySchemaOption[]): OsquerySchemaOption[] | Array<{ label: string }> => {
|
||||
if (!resultValue.value.length) return [];
|
||||
|
||||
// Static array values
|
||||
if (isArray(resultValue.value)) {
|
||||
return resultValue.value.map((value) => ({ label: value }));
|
||||
return resultValue.value.map((value) => ({ label: value })) as OsquerySchemaOption[];
|
||||
}
|
||||
|
||||
const selectedOption = find(euiFieldProps?.options, ['label', resultValue.value]);
|
||||
const selectedOption = find(euiFieldProps?.options, ['label', resultValue.value]) as
|
||||
| OsquerySchemaOption
|
||||
| undefined;
|
||||
|
||||
return selectedOption ? [selectedOption] : [{ label: resultValue.value }];
|
||||
});
|
||||
|
@ -476,10 +534,9 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
|||
|
||||
return (
|
||||
<EuiFormRow
|
||||
// @ts-expect-error update types
|
||||
helpText={selectedOptions[0]?.value?.description}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
error={resultFieldState.error?.message}
|
||||
isInvalid={!!resultFieldState.error?.message?.length}
|
||||
fullWidth
|
||||
describedByIds={describedByIds}
|
||||
isDisabled={euiFieldProps.isDisabled}
|
||||
|
@ -488,20 +545,26 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
|
|||
<EuiFlexItem grow={false}>{Prepend}</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<ResultComboBox
|
||||
onBlur={resultField.onBlur}
|
||||
value={resultField.value}
|
||||
name={resultField.name}
|
||||
error={resultFieldState.error?.message}
|
||||
// eslint-disable-next-line react/jsx-no-bind, react-perf/jsx-no-new-function-as-prop
|
||||
inputRef={(ref: HTMLInputElement) => {
|
||||
inputRef.current = ref;
|
||||
}}
|
||||
fullWidth
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={handleChange}
|
||||
onChange={handleKeyChange}
|
||||
onCreateOption={handleCreateOption}
|
||||
renderOption={renderOsqueryOption}
|
||||
rowHeight={32}
|
||||
isClearable
|
||||
{...euiFieldProps}
|
||||
singleSelection={isSingleSelection ? SINGLE_SELECTION : false}
|
||||
options={(typeValue === 'field' && euiFieldProps.options) || EMPTY_ARRAY}
|
||||
options={(item.result.type === 'field' && euiFieldProps.options) || EMPTY_ARRAY}
|
||||
idAria={idAria}
|
||||
helpText={selectedOptions[0]?.value?.description}
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -518,177 +581,76 @@ export interface ECSMappingEditorFieldProps {
|
|||
interface ECSMappingEditorFormProps {
|
||||
isDisabled?: boolean;
|
||||
osquerySchemaOptions: OsquerySchemaOption[];
|
||||
item: ArrayItem;
|
||||
isLastItem?: boolean;
|
||||
item: EcsMappingFormField;
|
||||
index: number;
|
||||
isLastItem: boolean;
|
||||
onAppend: (ecs_mapping: EcsMappingFormField[]) => void;
|
||||
onDelete?: FormArrayField['removeItem'];
|
||||
}
|
||||
|
||||
const ecsFieldValidator = (
|
||||
args: ValidationFuncArg<ECSMappingEditorFormData, ECSMappingEditorFormData['key']> & {
|
||||
customData: {
|
||||
value: {
|
||||
editForm: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
) => {
|
||||
const editForm: boolean = args.customData.value?.editForm;
|
||||
const rootPath = args.path.split('.')[0];
|
||||
|
||||
const fieldRequiredError = fieldValidators.emptyField(
|
||||
i18n.translate('xpack.osquery.pack.queryFlyoutForm.ecsFieldRequiredErrorMessage', {
|
||||
defaultMessage: 'ECS field is required.',
|
||||
})
|
||||
)(args);
|
||||
|
||||
if (
|
||||
fieldRequiredError &&
|
||||
// @ts-expect-error update types
|
||||
((!editForm && args.formData[`${rootPath}.result.value`]?.length) || editForm)
|
||||
) {
|
||||
return fieldRequiredError;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
export const defaultEcsFormData = {
|
||||
key: '',
|
||||
result: {
|
||||
type: 'field',
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
|
||||
const osqueryResultFieldValidator = async (
|
||||
args: ValidationFuncArg<ECSMappingEditorFormData, ECSMappingEditorFormData['value']['value']> & {
|
||||
customData: {
|
||||
value: {
|
||||
editForm: boolean;
|
||||
osquerySchemaOptions: OsquerySchemaOption[];
|
||||
};
|
||||
};
|
||||
}
|
||||
) => {
|
||||
const rootPath = args.path.split('.')[0];
|
||||
const { editForm, osquerySchemaOptions } = args.customData.value;
|
||||
const fieldRequiredError = fieldValidators.emptyField(
|
||||
i18n.translate('xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldRequiredErrorMessage', {
|
||||
defaultMessage: 'Value is required.',
|
||||
})
|
||||
)(args);
|
||||
|
||||
// @ts-expect-error update types
|
||||
if (fieldRequiredError && ((!editForm && args.formData[`${rootPath}.key`]?.length) || editForm)) {
|
||||
return fieldRequiredError;
|
||||
}
|
||||
|
||||
// @ts-expect-error update types
|
||||
if (!args.value?.length || args.formData[`${rootPath}.result.type`] !== 'field') return;
|
||||
|
||||
const osqueryColumnExists = find(osquerySchemaOptions, [
|
||||
'label',
|
||||
isArray(args.value) ? args.value[0] : args.value,
|
||||
]);
|
||||
|
||||
return !osqueryColumnExists
|
||||
? {
|
||||
code: 'ERR_FIELD_FORMAT',
|
||||
path: args.path,
|
||||
message: i18n.translate(
|
||||
'xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldValueMissingErrorMessage',
|
||||
{
|
||||
defaultMessage: 'The current query does not return a {columnName} field',
|
||||
values: {
|
||||
columnName: args.value,
|
||||
},
|
||||
}
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
};
|
||||
|
||||
interface ECSMappingEditorFormData {
|
||||
key: string;
|
||||
value: {
|
||||
field?: string;
|
||||
value?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const ECSMappingEditorForm: React.FC<ECSMappingEditorFormProps> = ({
|
||||
isDisabled,
|
||||
osquerySchemaOptions,
|
||||
item,
|
||||
isLastItem,
|
||||
index,
|
||||
onDelete,
|
||||
}) => {
|
||||
const ecsFieldValidator = (value: string, ecsMapping: EcsMappingFormField[]) => {
|
||||
const ecsCurrentMapping = ecsMapping[index].result.value;
|
||||
|
||||
return !value.length && ecsCurrentMapping.length
|
||||
? i18n.translate('xpack.osquery.pack.queryFlyoutForm.ecsFieldRequiredErrorMessage', {
|
||||
defaultMessage: 'ECS field is required.',
|
||||
})
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const { ecs_mapping: ecsMapping } = useWatch() as unknown as {
|
||||
ecs_mapping: EcsMappingFormField[];
|
||||
};
|
||||
const { field: ECSField, fieldState: ECSFieldState } = useController({
|
||||
name: `ecs_mapping.${index}.key`,
|
||||
rules: {
|
||||
validate: (value: string) => ecsFieldValidator(value, ecsMapping),
|
||||
},
|
||||
defaultValue: '',
|
||||
});
|
||||
|
||||
const MultiFields = useMemo(
|
||||
() => (
|
||||
<UseMultiFields
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
fields={{
|
||||
resultType: {
|
||||
path: `${item.path}.result.type`,
|
||||
config: {
|
||||
valueChangeDebounceTime: 300,
|
||||
defaultValue: OSQUERY_COLUMN_VALUE_TYPE_OPTIONS[0].value,
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
fieldsToValidateOnChange: [`${item.path}.key`, `${item.path}.result.value`],
|
||||
},
|
||||
},
|
||||
resultValue: {
|
||||
path: `${item.path}.result.value`,
|
||||
validationData: {
|
||||
osquerySchemaOptions,
|
||||
editForm: !isLastItem,
|
||||
},
|
||||
readDefaultValueOnForm: !item.isNew,
|
||||
config: {
|
||||
valueChangeDebounceTime: 300,
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
fieldsToValidateOnChange: [`${item.path}.key`, `${item.path}.result.value`],
|
||||
validations: [
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
validator: osqueryResultFieldValidator,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{(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,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</UseMultiFields>
|
||||
<div>
|
||||
<OsqueryColumnField
|
||||
item={item}
|
||||
index={index}
|
||||
isLastItem={isLastItem}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{
|
||||
// @ts-expect-error update types
|
||||
options: osquerySchemaOptions,
|
||||
isDisabled,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
[item, osquerySchemaOptions, isLastItem, isDisabled]
|
||||
[item, index, isLastItem, osquerySchemaOptions, isDisabled]
|
||||
);
|
||||
|
||||
const ecsComboBoxEuiFieldProps = useMemo(() => ({ isDisabled }), [isDisabled]);
|
||||
|
||||
const validationData = useMemo(() => ({ editForm: !isLastItem }), [isLastItem]);
|
||||
|
||||
const config = useMemo(
|
||||
() => ({
|
||||
valueChangeDebounceTime: 300,
|
||||
fieldsToValidateOnChange: [`${item.path}.key`, `${item.path}.result.value`],
|
||||
validations: [
|
||||
{
|
||||
validator: ecsFieldValidator,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[item.path]
|
||||
);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
if (onDelete) {
|
||||
onDelete(item.id);
|
||||
onDelete(index);
|
||||
}
|
||||
}, [item.id, onDelete]);
|
||||
}, [index, onDelete]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -696,14 +658,13 @@ export const ECSMappingEditorForm: React.FC<ECSMappingEditorFormProps> = ({
|
|||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="flexStart" gutterSize="s" wrap>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path={`${item.path}.key`}
|
||||
component={ECSComboboxField}
|
||||
<ECSComboboxField
|
||||
onChange={ECSField.onChange}
|
||||
onBlur={ECSField.onBlur}
|
||||
value={ECSField.value}
|
||||
name={ECSField.name}
|
||||
error={ECSFieldState.error?.message}
|
||||
euiFieldProps={ecsComboBoxEuiFieldProps}
|
||||
validationData={validationData}
|
||||
readDefaultValueOnForm={!item.isNew}
|
||||
// @ts-expect-error update types
|
||||
config={config}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -764,22 +725,26 @@ interface OsqueryColumn {
|
|||
|
||||
export const ECSMappingEditorField = React.memo(
|
||||
({ euiFieldProps }: ECSMappingEditorFieldProps) => {
|
||||
const lastItemPath = useRef<string>();
|
||||
const onAdd = useRef<FormArrayField['addItem']>();
|
||||
const itemsList = useRef<ArrayItem[]>([]);
|
||||
const [osquerySchemaOptions, setOsquerySchemaOptions] = useState<OsquerySchemaOption[]>([]);
|
||||
const [{ query, ...formData }, formDataSerializer, isMounted] = useFormData();
|
||||
const { trigger } = useFormContext();
|
||||
const { fields, append, remove } = useFieldArray<{ ecs_mapping: EcsMappingFormField[] }>({
|
||||
name: 'ecs_mapping',
|
||||
});
|
||||
|
||||
const { validateFields } = useFormContext();
|
||||
const itemsList = useRef<Array<{ id: string }>>([]);
|
||||
const [osquerySchemaOptions, setOsquerySchemaOptions] = useState<OsquerySchemaOption[]>([]);
|
||||
const { query, ...formData } = useWatch() as unknown as {
|
||||
query: string;
|
||||
ecs_mapping: EcsMappingFormField[];
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Additional 'suspended' validation of osquery ecs fields. fieldsToValidateOnChange doesn't work because it happens before the osquerySchema gets updated.
|
||||
const fieldsToValidate = prepareEcsFieldsToValidate(itemsList.current);
|
||||
const fieldsToValidate = prepareEcsFieldsToValidate(fields);
|
||||
// it is always at least 2 - empty fields
|
||||
if (fieldsToValidate.length > 2) {
|
||||
setTimeout(() => validateFields(fieldsToValidate), 0);
|
||||
setTimeout(async () => await trigger('ecs_mapping'), 0);
|
||||
}
|
||||
}, [query, validateFields]);
|
||||
}, [fields, query, trigger]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!query?.length) {
|
||||
|
@ -1013,32 +978,23 @@ export const ECSMappingEditorField = React.memo(
|
|||
}, [query]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isMounted) {
|
||||
if (!lastItemPath.current && onAdd.current) {
|
||||
onAdd.current();
|
||||
const ecsList = formData?.ecs_mapping;
|
||||
const lastEcs = formData?.ecs_mapping?.[itemsList?.current.length - 1];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (euiFieldProps?.isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemKey = get(formData, `${lastItemPath.current}.key`);
|
||||
|
||||
if (itemKey) {
|
||||
const serializedFormData = formDataSerializer();
|
||||
const itemValue =
|
||||
serializedFormData.ecs_mapping &&
|
||||
(serializedFormData.ecs_mapping[`${itemKey}`]?.field ||
|
||||
serializedFormData.ecs_mapping[`${itemKey}`]?.value);
|
||||
|
||||
if (itemValue && onAdd.current) {
|
||||
onAdd.current();
|
||||
}
|
||||
}
|
||||
// we skip appending on remove
|
||||
if (itemsList?.current?.length < ecsList?.length) {
|
||||
return;
|
||||
}
|
||||
}, [euiFieldProps?.isDisabled, formData, formDataSerializer, isMounted, onAdd]);
|
||||
|
||||
// // list contains ecs already, and the last item has values provided
|
||||
if (
|
||||
ecsList?.length === itemsList.current.length &&
|
||||
lastEcs?.key?.length &&
|
||||
lastEcs?.result?.value?.length
|
||||
) {
|
||||
return append(defaultEcsFormData);
|
||||
}
|
||||
}, [append, euiFieldProps?.isDisabled, formData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -1080,28 +1036,24 @@ export const ECSMappingEditorField = React.memo(
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<UseArray path="ecs_mapping">
|
||||
{({ items, addItem, removeItem }) => {
|
||||
lastItemPath.current = items[items.length - 1]?.path;
|
||||
onAdd.current = addItem;
|
||||
itemsList.current = items;
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map((item, index) => (
|
||||
<ECSMappingEditorForm
|
||||
key={item.id}
|
||||
osquerySchemaOptions={osquerySchemaOptions}
|
||||
item={item}
|
||||
isLastItem={index === items.length - 1}
|
||||
onDelete={removeItem}
|
||||
isDisabled={!!euiFieldProps?.isDisabled}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</UseArray>
|
||||
{fields.map((item, index, array) => {
|
||||
itemsList.current = array;
|
||||
|
||||
return (
|
||||
<div key={item.id}>
|
||||
<ECSMappingEditorForm
|
||||
osquerySchemaOptions={osquerySchemaOptions}
|
||||
item={item}
|
||||
index={index}
|
||||
onAppend={append}
|
||||
isLastItem={index === array.length - 1}
|
||||
onDelete={remove}
|
||||
isDisabled={!!euiFieldProps?.isDisabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -11,23 +11,22 @@ import type { EuiCheckboxGroupOption } from '@elastic/eui';
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiCheckboxGroup } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type { FieldHook } from '../../shared_imports';
|
||||
import { getFieldValidityAndErrorMessage } from '../../shared_imports';
|
||||
import { useController } from 'react-hook-form';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { FormFieldProps } from '../../form/types';
|
||||
import { PlatformIcon } from './platforms/platform_icon';
|
||||
|
||||
interface Props {
|
||||
field: FieldHook<string>;
|
||||
euiFieldProps?: Record<string, unknown>;
|
||||
idAria?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
type Props = Omit<FormFieldProps<string[]>, 'name' | 'label'>;
|
||||
|
||||
export const PlatformCheckBoxGroupField = ({
|
||||
field,
|
||||
euiFieldProps = {},
|
||||
idAria,
|
||||
...rest
|
||||
}: Props) => {
|
||||
export const PlatformCheckBoxGroupField = (props: Props) => {
|
||||
const { euiFieldProps = {}, idAria, helpText, ...rest } = props;
|
||||
const {
|
||||
field: { onChange, value },
|
||||
fieldState: { error },
|
||||
} = useController({
|
||||
name: 'platform',
|
||||
defaultValue: [],
|
||||
});
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
@ -82,17 +81,16 @@ export const PlatformCheckBoxGroupField = ({
|
|||
[]
|
||||
);
|
||||
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
const [checkboxIdToSelectedMap, setCheckboxIdToSelectedMap] = useState<Record<string, boolean>>(
|
||||
() =>
|
||||
(options as EuiCheckboxGroupOption[]).reduce((acc, option) => {
|
||||
acc[option.id] = isEmpty(field.value) ? true : field.value?.includes(option.id) ?? false;
|
||||
acc[option.id] = isEmpty(value) ? true : value?.includes(option.id) ?? false;
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>)
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
const handleChange = useCallback(
|
||||
(optionId: string) => {
|
||||
const newCheckboxIdToSelectedMap = {
|
||||
...checkboxIdToSelectedMap,
|
||||
|
@ -100,11 +98,13 @@ export const PlatformCheckBoxGroupField = ({
|
|||
};
|
||||
setCheckboxIdToSelectedMap(newCheckboxIdToSelectedMap);
|
||||
|
||||
field.setValue(() =>
|
||||
Object.keys(pickBy(newCheckboxIdToSelectedMap, (value) => value === true)).join(',')
|
||||
onChange(
|
||||
Object.keys(
|
||||
pickBy(newCheckboxIdToSelectedMap, (checkboxValue) => checkboxValue === true)
|
||||
).join(',')
|
||||
);
|
||||
},
|
||||
[checkboxIdToSelectedMap, field]
|
||||
[checkboxIdToSelectedMap, onChange]
|
||||
);
|
||||
|
||||
const describedByIds = useMemo(() => (idAria ? [idAria] : []), [idAria]);
|
||||
|
@ -112,19 +112,23 @@ export const PlatformCheckBoxGroupField = ({
|
|||
useEffect(() => {
|
||||
setCheckboxIdToSelectedMap(() =>
|
||||
(options as EuiCheckboxGroupOption[]).reduce((acc, option) => {
|
||||
acc[option.id] = isEmpty(field.value) ? true : field.value?.includes(option.id) ?? false;
|
||||
acc[option.id] = isEmpty(value) ? true : value?.includes(option.id) ?? false;
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>)
|
||||
);
|
||||
}, [field.value, options]);
|
||||
}, [value, options]);
|
||||
|
||||
const hasError = useMemo(() => !!error?.message, [error?.message]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
helpText={typeof field.helpText === 'function' ? field.helpText() : field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
label={i18n.translate('xpack.osquery.pack.queryFlyoutForm.platformFieldLabel', {
|
||||
defaultMessage: 'Platform',
|
||||
})}
|
||||
helpText={typeof helpText === 'function' ? helpText() : helpText}
|
||||
error={error?.message}
|
||||
isInvalid={hasError}
|
||||
fullWidth
|
||||
describedByIds={describedByIds}
|
||||
{...rest}
|
||||
|
@ -132,7 +136,7 @@ export const PlatformCheckBoxGroupField = ({
|
|||
<EuiCheckboxGroup
|
||||
idToSelectedMap={checkboxIdToSelectedMap}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
onChange={handleChange}
|
||||
data-test-subj="input"
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { map } from 'lodash';
|
||||
import {
|
||||
EuiFlyout,
|
||||
EuiTitle,
|
||||
|
@ -17,28 +16,33 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
|
||||
import { isEmpty, map } from 'lodash';
|
||||
import { QueryIdField, IntervalField } from '../../form';
|
||||
import { defaultEcsFormData } from './ecs_mapping_editor_field';
|
||||
import { CodeEditorField } from '../../saved_queries/form/code_editor_field';
|
||||
import { Form, getUseField, Field } from '../../shared_imports';
|
||||
import { PlatformCheckBoxGroupField } from './platform_checkbox_group_field';
|
||||
import { ALL_OSQUERY_VERSIONS_OPTIONS } from './constants';
|
||||
import type { UsePackQueryFormProps, PackQueryFormData } from './use_pack_query_form';
|
||||
import type {
|
||||
UsePackQueryFormProps,
|
||||
PackQueryFormData,
|
||||
PackSOQueryFormData,
|
||||
} from './use_pack_query_form';
|
||||
import { usePackQueryForm } from './use_pack_query_form';
|
||||
import { SavedQueriesDropdown } from '../../saved_queries/saved_queries_dropdown';
|
||||
import { ECSMappingEditorField } from './lazy_ecs_mapping_editor_field';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
import { VersionField } from '../../form';
|
||||
|
||||
interface QueryFlyoutProps {
|
||||
uniqueQueryIds: string[];
|
||||
defaultValue?: UsePackQueryFormProps['defaultValue'] | undefined;
|
||||
onSave: (payload: PackQueryFormData) => Promise<void>;
|
||||
onSave: (payload: PackSOQueryFormData) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
@ -50,45 +54,48 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
}) => {
|
||||
const permissions = useKibana().services.application.capabilities.osquery;
|
||||
const [isEditMode] = useState(!!defaultValue);
|
||||
const { form } = usePackQueryForm({
|
||||
const { serializer, idSet, ...hooksForm } = usePackQueryForm({
|
||||
uniqueQueryIds,
|
||||
defaultValue,
|
||||
handleSubmit: async (payload, isValid) =>
|
||||
new Promise((resolve) => {
|
||||
if (isValid) {
|
||||
onSave(payload);
|
||||
onClose();
|
||||
}
|
||||
|
||||
resolve();
|
||||
}),
|
||||
});
|
||||
|
||||
const { submit, isSubmitting, updateFieldValues } = form;
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
setValue,
|
||||
clearErrors,
|
||||
} = hooksForm;
|
||||
const onSubmit = (payload: PackQueryFormData) => {
|
||||
const serializedData: PackSOQueryFormData = serializer(payload);
|
||||
onSave(serializedData);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSetQueryValue = useCallback(
|
||||
(savedQuery) => {
|
||||
if (savedQuery) {
|
||||
updateFieldValues({
|
||||
id: savedQuery.id,
|
||||
query: savedQuery.query,
|
||||
description: savedQuery.description,
|
||||
platform: savedQuery.platform ? savedQuery.platform : 'linux,windows,darwin',
|
||||
version: savedQuery.version,
|
||||
interval: savedQuery.interval,
|
||||
// @ts-expect-error update types
|
||||
ecs_mapping:
|
||||
map(savedQuery.ecs_mapping, (value, key) => ({
|
||||
key,
|
||||
result: {
|
||||
type: Object.keys(value)[0],
|
||||
value: Object.values(value)[0],
|
||||
},
|
||||
})) ?? [],
|
||||
});
|
||||
clearErrors('id');
|
||||
setValue('id', savedQuery.id);
|
||||
setValue('query', savedQuery.query);
|
||||
// setValue('description', savedQuery.description); // TODO do we need it?
|
||||
setValue('platform', savedQuery.platform ? savedQuery.platform : 'linux,windows,darwin');
|
||||
setValue('version', savedQuery.version ? [savedQuery.version] : []);
|
||||
setValue('interval', savedQuery.interval);
|
||||
setValue(
|
||||
'ecs_mapping',
|
||||
!isEmpty(savedQuery.ecs_mapping)
|
||||
? map(savedQuery.ecs_mapping, (value, key) => ({
|
||||
key,
|
||||
result: {
|
||||
type: Object.keys(value)[0],
|
||||
value: Object.values(value)[0] as string,
|
||||
},
|
||||
}))
|
||||
: [defaultEcsFormData]
|
||||
);
|
||||
}
|
||||
},
|
||||
[updateFieldValues]
|
||||
[clearErrors, setValue]
|
||||
);
|
||||
/* Avoids accidental closing of the flyout when the user clicks outside of the flyout */
|
||||
const maskProps = useMemo(() => ({ onClick: () => ({}) }), []);
|
||||
|
@ -119,37 +126,25 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<Form form={form}>
|
||||
<FormProvider {...hooksForm}>
|
||||
{!isEditMode && permissions.readSavedQueries ? (
|
||||
<>
|
||||
<SavedQueriesDropdown onChange={handleSetQueryValue} />
|
||||
<EuiSpacer />
|
||||
</>
|
||||
) : null}
|
||||
<CommonUseField path="id" />
|
||||
<QueryIdField idSet={idSet} />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="query" component={CodeEditorField} />
|
||||
<CodeEditorField />
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="interval"
|
||||
<IntervalField
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{ append: 's' }}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<CommonUseField
|
||||
path="version"
|
||||
labelAppend={
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.queryFlyoutForm.versionFieldOptionalLabel"
|
||||
defaultMessage="(optional)"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
}
|
||||
<VersionField
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{
|
||||
noSuggestions: false,
|
||||
|
@ -163,7 +158,7 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField path="platform" component={PlatformCheckBoxGroupField} />
|
||||
<PlatformCheckBoxGroupField />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
|
@ -172,7 +167,7 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
<ECSMappingEditorField />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
|
@ -185,7 +180,7 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton isLoading={isSubmitting} onClick={submit} fill>
|
||||
<EuiButton isLoading={isSubmitting} onClick={handleSubmit(onSubmit)} fill>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.queryFlyoutForm.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { FIELD_TYPES } from '../../shared_imports';
|
||||
|
||||
import {
|
||||
createIdFieldValidations,
|
||||
intervalFieldValidations,
|
||||
queryFieldValidation,
|
||||
} from './validations';
|
||||
|
||||
export const createFormSchema = (ids: Set<string>) => ({
|
||||
id: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.osquery.pack.queryFlyoutForm.idFieldLabel', {
|
||||
defaultMessage: 'ID',
|
||||
}),
|
||||
validations: createIdFieldValidations(ids).map((validator) => ({ validator })),
|
||||
},
|
||||
description: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.osquery.pack.queryFlyoutForm.descriptionFieldLabel', {
|
||||
defaultMessage: 'Description (optional)',
|
||||
}),
|
||||
validations: [],
|
||||
},
|
||||
query: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.osquery.pack.queryFlyoutForm.queryFieldLabel', {
|
||||
defaultMessage: 'Query',
|
||||
}),
|
||||
validations: [{ validator: queryFieldValidation }],
|
||||
},
|
||||
interval: {
|
||||
defaultValue: 3600,
|
||||
type: FIELD_TYPES.NUMBER,
|
||||
label: i18n.translate('xpack.osquery.pack.queryFlyoutForm.intervalFieldLabel', {
|
||||
defaultMessage: 'Interval (s)',
|
||||
}),
|
||||
validations: intervalFieldValidations,
|
||||
},
|
||||
platform: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.osquery.pack.queryFlyoutForm.platformFieldLabel', {
|
||||
defaultMessage: 'Platform',
|
||||
}),
|
||||
validations: [],
|
||||
},
|
||||
version: {
|
||||
defaultValue: [],
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
label: (
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.pack.queryFlyoutForm.versionFieldLabel"
|
||||
defaultMessage="Minimum Osquery version"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) as unknown as string,
|
||||
validations: [],
|
||||
},
|
||||
ecs_mapping: {
|
||||
defaultValue: [],
|
||||
type: FIELD_TYPES.JSON,
|
||||
},
|
||||
});
|
|
@ -5,134 +5,113 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isArray, isEmpty, xor, map } from 'lodash';
|
||||
import uuid from 'uuid';
|
||||
import { isArray, isEmpty, map, xor } from 'lodash';
|
||||
|
||||
import { useForm as useHookForm } from 'react-hook-form';
|
||||
import type { Draft } from 'immer';
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import type { ECSMapping } from '../../../common/schemas/common';
|
||||
import { convertECSMappingToObject } from '../../../common/schemas/common/utils';
|
||||
import type { FormConfig } from '../../shared_imports';
|
||||
import { useForm } from '../../shared_imports';
|
||||
import { createFormSchema } from './schema';
|
||||
|
||||
const FORM_ID = 'editQueryFlyoutForm';
|
||||
import type { EcsMappingFormField } from './ecs_mapping_editor_field';
|
||||
import { defaultEcsFormData } from './ecs_mapping_editor_field';
|
||||
|
||||
export interface UsePackQueryFormProps {
|
||||
uniqueQueryIds: string[];
|
||||
defaultValue?: PackQueryFormData | undefined;
|
||||
handleSubmit: FormConfig<PackQueryFormData, PackQueryFormData>['onSubmit'];
|
||||
defaultValue?: PackSOQueryFormData | undefined;
|
||||
}
|
||||
|
||||
export interface PackSOQueryFormData {
|
||||
id: string;
|
||||
query: string;
|
||||
interval: number;
|
||||
interval: string;
|
||||
platform?: string | undefined;
|
||||
version?: string | undefined;
|
||||
ecs_mapping?: PackQuerySOECSMapping[] | undefined;
|
||||
ecs_mapping?: PackQuerySOECSMapping[];
|
||||
}
|
||||
|
||||
export type PackQuerySOECSMapping = Array<{ field: string; value: string }>;
|
||||
|
||||
export interface PackQueryFormData {
|
||||
id?: string;
|
||||
id: string;
|
||||
description?: string;
|
||||
query: string;
|
||||
interval?: number;
|
||||
interval: number;
|
||||
platform?: string | undefined;
|
||||
version?: string | undefined;
|
||||
ecs_mapping?: ECSMapping;
|
||||
version?: string[] | undefined;
|
||||
ecs_mapping: EcsMappingFormField[];
|
||||
}
|
||||
|
||||
export type PackQueryECSMapping = Record<
|
||||
string,
|
||||
{
|
||||
field?: string;
|
||||
value?: string;
|
||||
}
|
||||
>;
|
||||
const deserializer = (payload: PackSOQueryFormData): PackQueryFormData =>
|
||||
({
|
||||
id: payload.id,
|
||||
query: payload.query,
|
||||
interval: payload.interval ? parseInt(payload.interval, 10) : 3600,
|
||||
platform: payload.platform,
|
||||
version: payload.version ? [payload.version] : [],
|
||||
ecs_mapping: !isEmpty(payload.ecs_mapping)
|
||||
? !isArray(payload.ecs_mapping)
|
||||
? map(payload.ecs_mapping as unknown as PackQuerySOECSMapping, (value, key) => ({
|
||||
key,
|
||||
result: {
|
||||
type: Object.keys(value)[0],
|
||||
value: Object.values(value)[0],
|
||||
},
|
||||
}))
|
||||
: payload.ecs_mapping
|
||||
: [defaultEcsFormData],
|
||||
} as PackQueryFormData);
|
||||
|
||||
export const usePackQueryForm = ({
|
||||
uniqueQueryIds,
|
||||
defaultValue,
|
||||
handleSubmit,
|
||||
}: UsePackQueryFormProps) => {
|
||||
const serializer = (payload: PackQueryFormData): PackSOQueryFormData =>
|
||||
// @ts-expect-error update types
|
||||
produce<PackQueryFormData>(payload, (draft: Draft<PackSOQueryFormData>) => {
|
||||
if (isArray(draft.platform)) {
|
||||
if (draft.platform.length) {
|
||||
draft.platform.join(',');
|
||||
} else {
|
||||
delete draft.platform;
|
||||
}
|
||||
}
|
||||
|
||||
if (isArray(draft.version)) {
|
||||
if (!draft.version.length) {
|
||||
delete draft.version;
|
||||
} else {
|
||||
draft.version = draft.version[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (draft.interval) {
|
||||
draft.interval = draft.interval + '';
|
||||
}
|
||||
|
||||
if (isEmpty(draft.ecs_mapping)) {
|
||||
delete draft.ecs_mapping;
|
||||
} else {
|
||||
// @ts-expect-error update types
|
||||
draft.ecs_mapping = convertECSMappingToObject(payload.ecs_mapping);
|
||||
}
|
||||
|
||||
return draft;
|
||||
});
|
||||
|
||||
export const usePackQueryForm = ({ uniqueQueryIds, defaultValue }: UsePackQueryFormProps) => {
|
||||
const idSet = useMemo<Set<string>>(
|
||||
() => new Set<string>(xor(uniqueQueryIds, defaultValue?.id ? [defaultValue.id] : [])),
|
||||
[uniqueQueryIds, defaultValue]
|
||||
);
|
||||
const formSchema = useMemo<ReturnType<typeof createFormSchema>>(
|
||||
() => createFormSchema(idSet),
|
||||
[idSet]
|
||||
);
|
||||
|
||||
return useForm<PackSOQueryFormData, PackQueryFormData>({
|
||||
id: FORM_ID + uuid.v4(),
|
||||
onSubmit: async (formData, isValid) => {
|
||||
if (isValid && handleSubmit) {
|
||||
// @ts-expect-error update types
|
||||
return handleSubmit(formData, isValid);
|
||||
}
|
||||
},
|
||||
options: {
|
||||
stripEmptyFields: true,
|
||||
},
|
||||
// @ts-expect-error update types
|
||||
defaultValue: defaultValue || {
|
||||
id: '',
|
||||
query: '',
|
||||
interval: 3600,
|
||||
ecs_mapping: [],
|
||||
},
|
||||
// @ts-expect-error update types
|
||||
serializer: (payload) =>
|
||||
produce(payload, (draft) => {
|
||||
if (isArray(draft.platform)) {
|
||||
draft.platform.join(',');
|
||||
}
|
||||
|
||||
if (isArray(draft.version)) {
|
||||
if (!draft.version.length) {
|
||||
delete draft.version;
|
||||
} else {
|
||||
draft.version = draft.version[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmpty(draft.ecs_mapping)) {
|
||||
delete draft.ecs_mapping;
|
||||
} else {
|
||||
// @ts-expect-error update types
|
||||
draft.ecs_mapping = convertECSMappingToObject(payload.ecs_mapping);
|
||||
}
|
||||
|
||||
return draft;
|
||||
}),
|
||||
// @ts-expect-error update types
|
||||
deserializer: (payload) => {
|
||||
if (!payload) return {} as PackQueryFormData;
|
||||
|
||||
return {
|
||||
id: payload.id,
|
||||
query: payload.query,
|
||||
interval: payload.interval,
|
||||
platform: payload.platform,
|
||||
version: payload.version ? [payload.version] : [],
|
||||
ecs_mapping: !isArray(payload.ecs_mapping)
|
||||
? map(payload.ecs_mapping, (value, key) => ({
|
||||
key,
|
||||
result: {
|
||||
// @ts-expect-error update types
|
||||
type: Object.keys(value)[0],
|
||||
// @ts-expect-error update types
|
||||
value: Object.values(value)[0],
|
||||
},
|
||||
}))
|
||||
: payload.ecs_mapping,
|
||||
};
|
||||
},
|
||||
// @ts-expect-error update types
|
||||
schema: formSchema,
|
||||
});
|
||||
return {
|
||||
serializer,
|
||||
idSet,
|
||||
...useHookForm<PackQueryFormData>({
|
||||
defaultValues: defaultValue
|
||||
? deserializer(defaultValue)
|
||||
: {
|
||||
id: '',
|
||||
query: '',
|
||||
interval: 3600,
|
||||
ecs_mapping: [defaultEcsFormData],
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -6,12 +6,11 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { FormData, ValidationFunc } from '../../shared_imports';
|
||||
|
||||
import type { FormData, ValidationConfig, ValidationFunc } from '../../shared_imports';
|
||||
import { fieldValidators } from '../../shared_imports';
|
||||
export { queryFieldValidation } from '../../common/validations';
|
||||
|
||||
export const MAX_QUERY_LENGTH = 2000;
|
||||
const idPattern = /^[a-zA-Z0-9-_]+$/;
|
||||
// still used in Packs
|
||||
export const idSchemaValidation: ValidationFunc<FormData, string, string> = ({ value }) => {
|
||||
const valueIsValid = idPattern.test(value);
|
||||
if (!valueIsValid) {
|
||||
|
@ -23,47 +22,39 @@ export const idSchemaValidation: ValidationFunc<FormData, string, string> = ({ v
|
|||
}
|
||||
};
|
||||
|
||||
export const idHookSchemaValidation = (value: string) => {
|
||||
const valueIsValid = idPattern.test(value);
|
||||
|
||||
if (!valueIsValid) {
|
||||
return i18n.translate('xpack.osquery.pack.queryFlyoutForm.invalidIdError', {
|
||||
defaultMessage: 'Characters must be alphanumeric, _, or -',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createUniqueIdValidation = (ids: Set<string>) => {
|
||||
const uniqueIdCheck: ValidationFunc<FormData, string, string> = ({ value }) => {
|
||||
const uniqueIdCheck = (value: string) => {
|
||||
if (ids.has(value)) {
|
||||
return {
|
||||
message: i18n.translate('xpack.osquery.pack.queryFlyoutForm.uniqueIdError', {
|
||||
defaultMessage: 'ID must be unique',
|
||||
}),
|
||||
};
|
||||
return i18n.translate('xpack.osquery.pack.queryFlyoutForm.uniqueIdError', {
|
||||
defaultMessage: 'ID must be unique',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return uniqueIdCheck;
|
||||
};
|
||||
|
||||
export const createIdFieldValidations = (ids: Set<string>) => [
|
||||
fieldValidators.emptyField(
|
||||
i18n.translate('xpack.osquery.pack.queryFlyoutForm.emptyIdError', {
|
||||
export const createFormIdFieldValidations = (ids: Set<string>) => ({
|
||||
required: {
|
||||
message: i18n.translate('xpack.osquery.pack.queryFlyoutForm.emptyIdError', {
|
||||
defaultMessage: 'ID is required',
|
||||
})
|
||||
),
|
||||
idSchemaValidation,
|
||||
createUniqueIdValidation(ids),
|
||||
];
|
||||
}),
|
||||
value: true,
|
||||
},
|
||||
validate: (text: string) => {
|
||||
const isPatternValid = idHookSchemaValidation(text);
|
||||
const isUnique = createUniqueIdValidation(ids)(text);
|
||||
|
||||
export const intervalFieldValidations: Array<ValidationConfig<FormData, string, number>> = [
|
||||
{
|
||||
validator: fieldValidators.numberGreaterThanField({
|
||||
than: 0,
|
||||
message: i18n.translate('xpack.osquery.pack.queryFlyoutForm.intervalFieldMinNumberError', {
|
||||
defaultMessage: 'A positive interval value is required',
|
||||
}),
|
||||
}),
|
||||
return isPatternValid || isUnique;
|
||||
},
|
||||
{
|
||||
validator: fieldValidators.numberSmallerThanField({
|
||||
than: 604800,
|
||||
message: ({ than }) =>
|
||||
i18n.translate('xpack.osquery.pack.queryFlyoutForm.intervalFieldMaxNumberError', {
|
||||
defaultMessage: 'An interval value must be lower than {than}',
|
||||
values: { than },
|
||||
}),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
|
|
@ -6,24 +6,6 @@
|
|||
*/
|
||||
import type { SavedObject } from '@kbn/core/public';
|
||||
import type { PackQueryFormData } from './queries/use_pack_query_form';
|
||||
import type { PackQueryECSMapping } from './queries/use_pack_query_form';
|
||||
|
||||
export interface IQueryPayload {
|
||||
attributes?: {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PackItemQuery {
|
||||
id: string;
|
||||
name: string;
|
||||
interval: number;
|
||||
query: string;
|
||||
platform?: string;
|
||||
version?: string;
|
||||
ecs_mapping?: PackQueryECSMapping[];
|
||||
}
|
||||
|
||||
export type PackSavedObject = SavedObject<{
|
||||
name: string;
|
||||
|
|
|
@ -28,6 +28,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react';
|
||||
|
||||
import { pagePathGetters } from '@kbn/fleet-plugin/public';
|
||||
import type { ECSMapping } from '../../common/schemas/common';
|
||||
import { useAllResults } from './use_all_results';
|
||||
import type { ResultEdges } from '../../common/search_strategy';
|
||||
import { Direction } from '../../common/search_strategy';
|
||||
|
@ -48,7 +49,7 @@ interface ResultsTableComponentProps {
|
|||
actionId: string;
|
||||
selectedAgent?: string;
|
||||
agentIds?: string[];
|
||||
ecsMapping?: Record<string, string>;
|
||||
ecsMapping?: ECSMapping;
|
||||
endDate?: string;
|
||||
startDate?: string;
|
||||
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
|
||||
|
@ -186,10 +187,8 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
|||
if (!ecsMapping) return;
|
||||
|
||||
return reduce(
|
||||
(acc, [key, value]) => {
|
||||
// @ts-expect-error update types
|
||||
(acc: Record<string, string[]>, [key, value]) => {
|
||||
if (value?.field) {
|
||||
// @ts-expect-error update types
|
||||
acc[value?.field] = [...(acc[value?.field] ?? []), key];
|
||||
}
|
||||
|
||||
|
@ -202,7 +201,6 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
|||
|
||||
const getHeaderDisplay = useCallback(
|
||||
(columnName: string) => {
|
||||
// @ts-expect-error update types
|
||||
if (ecsMappingConfig && ecsMappingConfig[columnName]) {
|
||||
return (
|
||||
<>
|
||||
|
@ -217,12 +215,9 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
|||
/>
|
||||
{`:`}
|
||||
<ul>
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
ecsMappingConfig[columnName].map((fieldName) => (
|
||||
<li key={fieldName}>{fieldName}</li>
|
||||
))
|
||||
}
|
||||
{ecsMappingConfig[columnName].map((fieldName) => (
|
||||
<li key={fieldName}>{fieldName}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -16,14 +16,17 @@ import {
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type { PackQueryFormData } from '../../../packs/queries/use_pack_query_form';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { Form } from '../../../shared_imports';
|
||||
import { SavedQueryForm } from '../../../saved_queries/form';
|
||||
import type {
|
||||
SavedQueryFormData,
|
||||
SavedQuerySOFormData,
|
||||
} from '../../../saved_queries/form/use_saved_query_form';
|
||||
import { useSavedQueryForm } from '../../../saved_queries/form/use_saved_query_form';
|
||||
|
||||
interface EditSavedQueryFormProps {
|
||||
defaultValue?: PackQueryFormData;
|
||||
defaultValue?: SavedQuerySOFormData;
|
||||
handleSubmit: (payload: unknown) => Promise<void>;
|
||||
viewMode?: boolean;
|
||||
}
|
||||
|
@ -35,15 +38,28 @@ const EditSavedQueryFormComponent: React.FC<EditSavedQueryFormProps> = ({
|
|||
}) => {
|
||||
const savedQueryListProps = useRouterNavigate('saved_queries');
|
||||
|
||||
const { form } = useSavedQueryForm({
|
||||
const hooksForm = useSavedQueryForm({
|
||||
defaultValue,
|
||||
handleSubmit,
|
||||
});
|
||||
const { submit, isSubmitting } = form;
|
||||
|
||||
const {
|
||||
serializer,
|
||||
idSet,
|
||||
handleSubmit: formSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = hooksForm;
|
||||
|
||||
const onSubmit = (payload: SavedQueryFormData) => {
|
||||
const serializedData = serializer(payload);
|
||||
try {
|
||||
handleSubmit(serializedData);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<SavedQueryForm viewMode={viewMode} hasPlayground />
|
||||
<FormProvider {...hooksForm}>
|
||||
<SavedQueryForm viewMode={viewMode} hasPlayground idSet={idSet} />
|
||||
{!viewMode && (
|
||||
<>
|
||||
<EuiBottomBar>
|
||||
|
@ -65,7 +81,7 @@ const EditSavedQueryFormComponent: React.FC<EditSavedQueryFormProps> = ({
|
|||
fill
|
||||
size="m"
|
||||
iconType="save"
|
||||
onClick={submit}
|
||||
onClick={formSubmit(onSubmit)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.editSavedQuery.form.updateQueryButtonLabel"
|
||||
|
@ -82,7 +98,7 @@ const EditSavedQueryFormComponent: React.FC<EditSavedQueryFormProps> = ({
|
|||
<EuiSpacer size="xxl" />
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { EuiTabbedContent, EuiNotificationBadge } from '@elastic/eui';
|
|||
import React, { useMemo } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import type { ECSMapping } from '../../../../common/schemas/common';
|
||||
import { ResultsTable } from '../../../results/results_table';
|
||||
import { ActionResultsSummary } from '../../../action_results/action_results_summary';
|
||||
|
||||
|
@ -16,7 +17,7 @@ interface ResultTabsProps {
|
|||
actionId: string;
|
||||
agentIds?: string[];
|
||||
startDate?: string;
|
||||
ecsMapping?: Record<string, string>;
|
||||
ecsMapping?: ECSMapping;
|
||||
failedAgentsCount?: number;
|
||||
endDate?: string;
|
||||
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => ReactElement;
|
||||
|
|
|
@ -15,15 +15,19 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
|
||||
import type { PackQueryFormData } from '../../../packs/queries/use_pack_query_form';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { Form } from '../../../shared_imports';
|
||||
import { SavedQueryForm } from '../../../saved_queries/form';
|
||||
import type {
|
||||
SavedQuerySOFormData,
|
||||
SavedQueryFormData,
|
||||
} from '../../../saved_queries/form/use_saved_query_form';
|
||||
import { useSavedQueryForm } from '../../../saved_queries/form/use_saved_query_form';
|
||||
|
||||
interface NewSavedQueryFormProps {
|
||||
defaultValue?: PackQueryFormData;
|
||||
defaultValue?: SavedQuerySOFormData;
|
||||
handleSubmit: (payload: unknown) => Promise<void>;
|
||||
}
|
||||
|
||||
|
@ -33,15 +37,24 @@ const NewSavedQueryFormComponent: React.FC<NewSavedQueryFormProps> = ({
|
|||
}) => {
|
||||
const savedQueryListProps = useRouterNavigate('saved_queries');
|
||||
|
||||
const { form } = useSavedQueryForm({
|
||||
const hooksForm = useSavedQueryForm({
|
||||
defaultValue,
|
||||
handleSubmit,
|
||||
});
|
||||
const { submit, isSubmitting, isValid } = form;
|
||||
const {
|
||||
serializer,
|
||||
idSet,
|
||||
handleSubmit: formSubmit,
|
||||
formState: { isSubmitting, errors },
|
||||
} = hooksForm;
|
||||
|
||||
const onSubmit = (payload: SavedQueryFormData) => {
|
||||
const serializedData = serializer(payload);
|
||||
handleSubmit(serializedData);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<SavedQueryForm hasPlayground isValid={isValid} />
|
||||
<FormProvider {...hooksForm}>
|
||||
<SavedQueryForm hasPlayground isValid={isEmpty(errors)} idSet={idSet} />
|
||||
<EuiBottomBar>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -61,7 +74,7 @@ const NewSavedQueryFormComponent: React.FC<NewSavedQueryFormProps> = ({
|
|||
fill
|
||||
size="m"
|
||||
iconType="save"
|
||||
onClick={submit}
|
||||
onClick={formSubmit(onSubmit)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.addSavedQuery.form.saveQueryButtonLabel"
|
||||
|
@ -76,7 +89,7 @@ const NewSavedQueryFormComponent: React.FC<NewSavedQueryFormProps> = ({
|
|||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -10,9 +10,11 @@ import { EuiCodeBlock, EuiFormRow } from '@elastic/eui';
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useController } from 'react-hook-form';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { MAX_QUERY_LENGTH } from '../../packs/queries/validations';
|
||||
import { OsquerySchemaLink } from '../../components/osquery_schema_link';
|
||||
import { OsqueryEditor } from '../../editor';
|
||||
import type { FieldHook } from '../../shared_imports';
|
||||
|
||||
const StyledEuiCodeBlock = styled(EuiCodeBlock)`
|
||||
min-height: 100px;
|
||||
|
@ -20,20 +22,47 @@ const StyledEuiCodeBlock = styled(EuiCodeBlock)`
|
|||
|
||||
interface CodeEditorFieldProps {
|
||||
euiFieldProps?: Record<string, unknown>;
|
||||
field: FieldHook<string>;
|
||||
labelAppend?: string;
|
||||
helpText?: string;
|
||||
}
|
||||
|
||||
const CodeEditorFieldComponent: React.FC<CodeEditorFieldProps> = ({ euiFieldProps, field }) => {
|
||||
const { value, label, labelAppend, helpText, setValue, errors } = field;
|
||||
const error = errors[0]?.message;
|
||||
const CodeEditorFieldComponent: React.FC<CodeEditorFieldProps> = ({
|
||||
euiFieldProps,
|
||||
labelAppend,
|
||||
helpText,
|
||||
}) => {
|
||||
const {
|
||||
field: { onChange, value },
|
||||
fieldState: { error },
|
||||
} = useController({
|
||||
name: 'query',
|
||||
rules: {
|
||||
required: {
|
||||
message: i18n.translate('xpack.osquery.pack.queryFlyoutForm.emptyQueryError', {
|
||||
defaultMessage: 'Query is a required field',
|
||||
}),
|
||||
value: true,
|
||||
},
|
||||
maxLength: {
|
||||
message: i18n.translate('xpack.osquery.liveQuery.queryForm.largeQueryError', {
|
||||
defaultMessage: 'Query is too large (max {maxLength} characters)',
|
||||
values: { maxLength: MAX_QUERY_LENGTH },
|
||||
}),
|
||||
value: MAX_QUERY_LENGTH,
|
||||
},
|
||||
},
|
||||
defaultValue: '',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={label}
|
||||
label={i18n.translate('xpack.osquery.savedQuery.queryEditorLabel', {
|
||||
defaultMessage: 'Query',
|
||||
})}
|
||||
labelAppend={!isEmpty(labelAppend) ? labelAppend : <OsquerySchemaLink />}
|
||||
helpText={helpText}
|
||||
isInvalid={typeof error === 'string'}
|
||||
error={error}
|
||||
isInvalid={!!error?.message}
|
||||
error={error?.message}
|
||||
fullWidth
|
||||
>
|
||||
{euiFieldProps?.isDisabled ? (
|
||||
|
@ -46,7 +75,7 @@ const CodeEditorFieldComponent: React.FC<CodeEditorFieldProps> = ({ euiFieldProp
|
|||
{value}
|
||||
</StyledEuiCodeBlock>
|
||||
) : (
|
||||
<OsqueryEditor defaultValue={value} onChange={setValue} />
|
||||
<OsqueryEditor defaultValue={value} onChange={onChange} />
|
||||
)}
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
|
|
@ -17,25 +17,25 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { ALL_OSQUERY_VERSIONS_OPTIONS } from '../../packs/queries/constants';
|
||||
import { IntervalField, QueryIdField, QueryDescriptionField, VersionField } from '../../form';
|
||||
import { PlatformCheckBoxGroupField } from '../../packs/queries/platform_checkbox_group_field';
|
||||
import { Field, getUseField, UseField } from '../../shared_imports';
|
||||
import { CodeEditorField } from './code_editor_field';
|
||||
import { ALL_OSQUERY_VERSIONS_OPTIONS } from '../../packs/queries/constants';
|
||||
import { ECSMappingEditorField } from '../../packs/queries/lazy_ecs_mapping_editor_field';
|
||||
import { PlaygroundFlyout } from './playground_flyout';
|
||||
|
||||
export const CommonUseField = getUseField({ component: Field });
|
||||
import { CodeEditorField } from './code_editor_field';
|
||||
|
||||
interface SavedQueryFormProps {
|
||||
viewMode?: boolean;
|
||||
hasPlayground?: boolean;
|
||||
isValid?: boolean;
|
||||
idSet?: Set<string>;
|
||||
}
|
||||
|
||||
const SavedQueryFormComponent: React.FC<SavedQueryFormProps> = ({
|
||||
viewMode,
|
||||
hasPlayground,
|
||||
isValid,
|
||||
idSet,
|
||||
}) => {
|
||||
const [playgroundVisible, setPlaygroundVisible] = useState(false);
|
||||
|
||||
|
@ -77,11 +77,11 @@ const SavedQueryFormComponent: React.FC<SavedQueryFormProps> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<CommonUseField path="id" euiFieldProps={euiFieldProps} />
|
||||
<QueryIdField idSet={idSet} euiFieldProps={euiFieldProps} />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="description" euiFieldProps={euiFieldProps} />
|
||||
<QueryDescriptionField euiFieldProps={euiFieldProps} />
|
||||
<EuiSpacer />
|
||||
<UseField path="query" component={CodeEditorField} euiFieldProps={euiFieldProps} />
|
||||
<CodeEditorField euiFieldProps={euiFieldProps} />
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
|
@ -119,16 +119,12 @@ const SavedQueryFormComponent: React.FC<SavedQueryFormProps> = ({
|
|||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField path="interval" euiFieldProps={intervalEuiFieldProps} />
|
||||
<IntervalField euiFieldProps={intervalEuiFieldProps} />
|
||||
<EuiSpacer size="m" />
|
||||
<CommonUseField path="version" euiFieldProps={versionEuiFieldProps} />
|
||||
<VersionField euiFieldProps={versionEuiFieldProps} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="platform"
|
||||
component={PlatformCheckBoxGroupField}
|
||||
euiFieldProps={euiFieldProps}
|
||||
/>
|
||||
<PlatformCheckBoxGroupField euiFieldProps={euiFieldProps} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{playgroundVisible && (
|
||||
|
|
|
@ -10,8 +10,8 @@ import React, { useMemo } from 'react';
|
|||
import styled from 'styled-components';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { LiveQuery } from '../../live_queries';
|
||||
import { useFormData } from '../../shared_imports';
|
||||
|
||||
const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)`
|
||||
&.euiFlyoutHeader.euiFlyoutHeader--hasBorder {
|
||||
|
@ -26,11 +26,13 @@ interface PlaygroundFlyoutProps {
|
|||
}
|
||||
|
||||
const PlaygroundFlyoutComponent: React.FC<PlaygroundFlyoutProps> = ({ enabled, onClose }) => {
|
||||
const [{ query, ecs_mapping: ecsMapping, id }, formDataSerializer] = useFormData();
|
||||
|
||||
// @ts-expect-error update types
|
||||
const { serializer, watch } = useFormContext();
|
||||
const watchedValues = watch();
|
||||
const { query, ecs_mapping: ecsMapping, id } = watchedValues;
|
||||
/* recalculate the form data when ecs_mapping changes */
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const serializedFormData = useMemo(() => formDataSerializer(), [ecsMapping, formDataSerializer]);
|
||||
const serializedFormData = useMemo(() => serializer(watchedValues), [ecsMapping]);
|
||||
|
||||
return (
|
||||
<EuiFlyout type="push" size="m" onClose={onClose}>
|
||||
|
|
|
@ -5,105 +5,98 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useForm as useHookForm } from 'react-hook-form';
|
||||
import { isArray, isEmpty, map } from 'lodash';
|
||||
import uuid from 'uuid';
|
||||
import { produce } from 'immer';
|
||||
import type { Draft } from 'immer';
|
||||
import produce from 'immer';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { ECSMapping } from '../../../common/schemas/common';
|
||||
import { convertECSMappingToObject } from '../../../common/schemas/common/utils';
|
||||
import { useForm } from '../../shared_imports';
|
||||
import { createFormSchema } from '../../packs/queries/schema';
|
||||
import type {
|
||||
PackQueryECSMapping,
|
||||
PackQueryFormData,
|
||||
} from '../../packs/queries/use_pack_query_form';
|
||||
import type { EcsMappingFormField } from '../../packs/queries/ecs_mapping_editor_field';
|
||||
import { defaultEcsFormData } from '../../packs/queries/ecs_mapping_editor_field';
|
||||
import { useSavedQueries } from '../use_saved_queries';
|
||||
|
||||
const SAVED_QUERY_FORM_ID = 'savedQueryForm';
|
||||
|
||||
interface ReturnFormData {
|
||||
export interface SavedQuerySOFormData {
|
||||
id?: string;
|
||||
description?: string;
|
||||
query: string;
|
||||
query?: string;
|
||||
interval?: string;
|
||||
platform?: string;
|
||||
version?: string | undefined;
|
||||
ecs_mapping?: ECSMapping | undefined;
|
||||
}
|
||||
|
||||
export interface SavedQueryFormData {
|
||||
id?: string;
|
||||
description?: string;
|
||||
query?: string;
|
||||
interval?: number;
|
||||
platform?: string;
|
||||
version?: string[];
|
||||
ecs_mapping?: PackQueryECSMapping[] | undefined;
|
||||
ecs_mapping: EcsMappingFormField[];
|
||||
}
|
||||
|
||||
interface UseSavedQueryFormProps {
|
||||
defaultValue?: PackQueryFormData;
|
||||
handleSubmit: (payload: unknown) => Promise<void>;
|
||||
defaultValue?: SavedQuerySOFormData;
|
||||
}
|
||||
|
||||
export const useSavedQueryForm = ({ defaultValue, handleSubmit }: UseSavedQueryFormProps) => {
|
||||
const deserializer = (payload: SavedQuerySOFormData): SavedQueryFormData => ({
|
||||
id: payload.id,
|
||||
description: payload.description,
|
||||
query: payload.query,
|
||||
interval: payload.interval ? parseInt(payload.interval, 10) : 3600,
|
||||
platform: payload.platform,
|
||||
version: payload.version ? [payload.version] : [],
|
||||
ecs_mapping: !isEmpty(payload.ecs_mapping)
|
||||
? (map(payload.ecs_mapping, (value, key: string) => ({
|
||||
key,
|
||||
result: {
|
||||
type: Object.keys(value)[0],
|
||||
value: Object.values(value)[0],
|
||||
},
|
||||
})) as unknown as EcsMappingFormField[])
|
||||
: [defaultEcsFormData],
|
||||
});
|
||||
|
||||
export const savedQueryDataSerializer = (payload: SavedQueryFormData): SavedQuerySOFormData =>
|
||||
// @ts-expect-error update types
|
||||
produce<SavedQueryFormData>(payload, (draft: Draft<SavedQuerySOFormData>) => {
|
||||
if (isArray(draft.version)) {
|
||||
if (!draft.version.length) {
|
||||
draft.version = '';
|
||||
} else {
|
||||
draft.version = draft.version[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (isArray(draft.platform) && !draft.platform.length) {
|
||||
delete draft.platform;
|
||||
}
|
||||
|
||||
draft.ecs_mapping = convertECSMappingToObject(payload.ecs_mapping);
|
||||
|
||||
if (draft.interval) {
|
||||
draft.interval = draft.interval + '';
|
||||
}
|
||||
|
||||
return draft;
|
||||
});
|
||||
|
||||
export const useSavedQueryForm = ({ defaultValue }: UseSavedQueryFormProps) => {
|
||||
const { data } = useSavedQueries({});
|
||||
const ids: string[] = useMemo<string[]>(() => map(data, 'attributes.id') ?? [], [data]);
|
||||
const ids: string[] = useMemo<string[]>(() => map(data?.data, 'attributes.id') ?? [], [data]);
|
||||
const idSet = useMemo<Set<string>>(() => {
|
||||
const res = new Set<string>(ids);
|
||||
if (defaultValue && defaultValue.id) res.delete(defaultValue.id);
|
||||
|
||||
return res;
|
||||
}, [ids, defaultValue]);
|
||||
const formSchema = useMemo(() => createFormSchema(idSet), [idSet]);
|
||||
|
||||
return useForm<PackQueryFormData, ReturnFormData>({
|
||||
id: SAVED_QUERY_FORM_ID + uuid.v4(),
|
||||
schema: formSchema,
|
||||
onSubmit: async (formData, isValid) => {
|
||||
if (isValid) {
|
||||
try {
|
||||
await handleSubmit(formData);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
defaultValue,
|
||||
// @ts-expect-error update types
|
||||
serializer: (payload) =>
|
||||
produce(payload, (draft) => {
|
||||
if (isArray(draft.version)) {
|
||||
if (!draft.version.length) {
|
||||
// @ts-expect-error update types
|
||||
draft.version = '';
|
||||
} else {
|
||||
// @ts-expect-error update types
|
||||
draft.version = draft.version[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmpty(payload.ecs_mapping)) {
|
||||
delete draft.ecs_mapping;
|
||||
} else {
|
||||
// @ts-expect-error update types
|
||||
draft.ecs_mapping = convertECSMappingToObject(payload.ecs_mapping);
|
||||
}
|
||||
|
||||
// @ts-expect-error update types
|
||||
draft.interval = draft.interval + '';
|
||||
|
||||
return draft;
|
||||
}),
|
||||
deserializer: (payload) => {
|
||||
if (!payload) return {} as ReturnFormData;
|
||||
|
||||
return {
|
||||
id: payload.id,
|
||||
description: payload.description,
|
||||
query: payload.query,
|
||||
interval: payload.interval ?? 3600,
|
||||
platform: payload.platform,
|
||||
version: payload.version ? [payload.version] : [],
|
||||
ecs_mapping: (!isEmpty(payload.ecs_mapping)
|
||||
? map(payload.ecs_mapping, (value, key: string) => ({
|
||||
key,
|
||||
result: {
|
||||
type: Object.keys(value)[0],
|
||||
value: Object.values(value)[0],
|
||||
},
|
||||
}))
|
||||
: ([] as PackQueryECSMapping[])) as PackQueryECSMapping[],
|
||||
};
|
||||
},
|
||||
});
|
||||
return {
|
||||
serializer: savedQueryDataSerializer,
|
||||
idSet,
|
||||
...useHookForm<SavedQueryFormData>({
|
||||
defaultValues: defaultValue ? deserializer(defaultValue) : {},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -9,11 +9,11 @@ import { find } from 'lodash/fp';
|
|||
import { EuiCodeBlock, EuiFormRow, EuiComboBox, EuiTextColor } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { QUERIES_DROPDOWN_LABEL, QUERIES_DROPDOWN_SEARCH_FIELD_LABEL } from './constants';
|
||||
import { OsquerySchemaLink } from '../components/osquery_schema_link';
|
||||
|
||||
import { useSavedQueries } from './use_saved_queries';
|
||||
import { useFormData } from '../shared_imports';
|
||||
import type { SavedQuerySO } from '../routes/saved_queries/list';
|
||||
|
||||
const TextTruncate = styled.div`
|
||||
|
@ -49,10 +49,9 @@ const SavedQueriesDropdownComponent: React.FC<SavedQueriesDropdownProps> = ({
|
|||
disabled,
|
||||
onChange,
|
||||
}) => {
|
||||
const savedQueryId = useWatch({ name: 'savedQueryId' });
|
||||
const [selectedOptions, setSelectedOptions] = useState<SelectedOption[]>([]);
|
||||
|
||||
const [{ savedQueryId }] = useFormData();
|
||||
|
||||
const { data } = useSavedQueries({});
|
||||
|
||||
const queryOptions = useMemo(
|
||||
|
|
|
@ -17,17 +17,18 @@ import {
|
|||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { Form } from '../shared_imports';
|
||||
import type { SavedQuerySOFormData, SavedQueryFormData } from './form/use_saved_query_form';
|
||||
import { useSavedQueryForm } from './form/use_saved_query_form';
|
||||
import { SavedQueryForm } from './form';
|
||||
import { useCreateSavedQuery } from './use_create_saved_query';
|
||||
import type { PackQueryFormData } from '../packs/queries/use_pack_query_form';
|
||||
|
||||
interface AddQueryFlyoutProps {
|
||||
defaultValue: PackQueryFormData;
|
||||
defaultValue: SavedQuerySOFormData;
|
||||
onClose: () => void;
|
||||
isExternal?: boolean;
|
||||
}
|
||||
|
@ -41,18 +42,24 @@ const SavedQueryFlyoutComponent: React.FC<AddQueryFlyoutProps> = ({
|
|||
}) => {
|
||||
const createSavedQueryMutation = useCreateSavedQuery({ withRedirect: false });
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (payload) => {
|
||||
await createSavedQueryMutation.mutateAsync(payload).then(() => onClose());
|
||||
},
|
||||
[createSavedQueryMutation, onClose]
|
||||
);
|
||||
|
||||
const { form } = useSavedQueryForm({
|
||||
const hooksForm = useSavedQueryForm({
|
||||
defaultValue,
|
||||
handleSubmit,
|
||||
});
|
||||
const { submit, isSubmitting } = form;
|
||||
const {
|
||||
serializer,
|
||||
idSet,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = hooksForm;
|
||||
const onSubmit = useCallback(
|
||||
async (payload: SavedQueryFormData) => {
|
||||
const serializedData = serializer(payload);
|
||||
// TODO CHECK THIS
|
||||
// @ts-expect-error update types
|
||||
await createSavedQueryMutation.mutateAsync(serializedData).then(() => onClose());
|
||||
},
|
||||
[createSavedQueryMutation, onClose, serializer]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
|
@ -74,9 +81,9 @@ const SavedQueryFlyoutComponent: React.FC<AddQueryFlyoutProps> = ({
|
|||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<Form form={form}>
|
||||
<SavedQueryForm />
|
||||
</Form>
|
||||
<FormProvider {...hooksForm}>
|
||||
<SavedQueryForm idSet={idSet} />
|
||||
</FormProvider>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
|
@ -89,7 +96,7 @@ const SavedQueryFlyoutComponent: React.FC<AddQueryFlyoutProps> = ({
|
|||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton isLoading={isSubmitting} onClick={submit} fill>
|
||||
<EuiButton isLoading={isSubmitting} onClick={handleSubmit(onSubmit)} fill>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.pack.queryFlyoutForm.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
|
|
|
@ -40,7 +40,7 @@ export const useUpdateSavedQuery = ({ savedQueryId }: UseUpdateSavedQueryProps)
|
|||
toastMessage: error.body.message,
|
||||
});
|
||||
},
|
||||
onSuccess: (payload: SavedQuerySO) => {
|
||||
onSuccess: (payload: { data: SavedQuerySO }) => {
|
||||
queryClient.invalidateQueries([SAVED_QUERIES_ID]);
|
||||
queryClient.invalidateQueries([SAVED_QUERY_ID, { savedQueryId }]);
|
||||
navigateToApp(PLUGIN_ID, { path: pagePathGetters.saved_queries() });
|
||||
|
@ -48,7 +48,7 @@ export const useUpdateSavedQuery = ({ savedQueryId }: UseUpdateSavedQueryProps)
|
|||
i18n.translate('xpack.osquery.editSavedQuery.successToastMessageText', {
|
||||
defaultMessage: 'Successfully updated "{savedQueryName}" query',
|
||||
values: {
|
||||
savedQueryName: payload.attributes?.id ?? '',
|
||||
savedQueryName: payload.data.attributes?.id ?? '',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -23657,7 +23657,7 @@
|
|||
"xpack.osquery.pack.queriesTable.deleteActionAriaLabel": "Supprimer {queryName}",
|
||||
"xpack.osquery.pack.queriesTable.editActionAriaLabel": "Modifier {queryName}",
|
||||
"xpack.osquery.pack.queryFlyoutForm.intervalFieldMaxNumberError": "La valeur d'intervalle doit être inférieure à {than}",
|
||||
"xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldValueMissingErrorMessage": "La recherche en cours ne retourne pas de champ {columnName}",
|
||||
"xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldRequiredErrorMessage": "Valeur obligatoire.",
|
||||
"xpack.osquery.pack.table.activatedSuccessToastMessageText": "Le pack \"{packName}\" a bien été activé.",
|
||||
"xpack.osquery.pack.table.deactivatedSuccessToastMessageText": "Le pack \"{packName}\" a bien été désactivé.",
|
||||
"xpack.osquery.pack.table.deleteQueriesButtonLabel": "Supprimer {queriesCount, plural, one {# recherche} other {# recherches}}",
|
||||
|
@ -23820,7 +23820,6 @@
|
|||
"xpack.osquery.pack.queriesTable.viewResultsColumnTitle": "Afficher les résultats",
|
||||
"xpack.osquery.pack.queryFlyoutForm.cancelButtonLabel": "Annuler",
|
||||
"xpack.osquery.pack.queryFlyoutForm.deleteECSMappingRowButtonAriaLabel": "Supprimer la ligne de mapping ECS",
|
||||
"xpack.osquery.pack.queryFlyoutForm.descriptionFieldLabel": "Description (facultative)",
|
||||
"xpack.osquery.pack.queryFlyoutForm.ecsFieldRequiredErrorMessage": "Le champ ECS est requis.",
|
||||
"xpack.osquery.pack.queryFlyoutForm.emptyIdError": "L'ID est requis",
|
||||
"xpack.osquery.pack.queryFlyoutForm.emptyQueryError": "La recherche est requise",
|
||||
|
@ -23830,12 +23829,11 @@
|
|||
"xpack.osquery.pack.queryFlyoutForm.invalidIdError": "Les caractères doivent être alphanumériques, _ ou -",
|
||||
"xpack.osquery.pack.queryFlyoutForm.mappingEcsFieldLabel": "Champ ECS",
|
||||
"xpack.osquery.pack.queryFlyoutForm.mappingValueFieldLabel": "Valeur",
|
||||
"xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldRequiredErrorMessage": "Valeur obligatoire.",
|
||||
"xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldValueMissingErrorMessage": "La recherche en cours ne retourne pas de champ {columnName}",
|
||||
"xpack.osquery.pack.queryFlyoutForm.platformFieldLabel": "Plateforme",
|
||||
"xpack.osquery.pack.queryFlyoutForm.platformLinusLabel": "macOS",
|
||||
"xpack.osquery.pack.queryFlyoutForm.platformMacOSLabel": "Linux",
|
||||
"xpack.osquery.pack.queryFlyoutForm.platformWindowsLabel": "Windows",
|
||||
"xpack.osquery.pack.queryFlyoutForm.queryFieldLabel": "Recherche",
|
||||
"xpack.osquery.pack.queryFlyoutForm.saveButtonLabel": "Enregistrer",
|
||||
"xpack.osquery.pack.queryFlyoutForm.uniqueIdError": "L'ID doit être unique",
|
||||
"xpack.osquery.pack.queryFlyoutForm.versionFieldLabel": "Version Osquery minimale",
|
||||
|
@ -23851,7 +23849,6 @@
|
|||
"xpack.osquery.packList.prePackagedPacks.emptyPromptTitle.emptyPromptMessage": "Un pack est un ensemble de requêtes pouvant être planifié. Chargez des packs prédéfinis ou créez vos propres packs.",
|
||||
"xpack.osquery.packList.prePackagedPacks.loadButtonLabel": "Charger les pack prédéfinis Elastic",
|
||||
"xpack.osquery.packList.prePackagedPacks.updateButtonLabel": "Mettre à jour les pack prédéfinis Elastic",
|
||||
"xpack.osquery.packs.dropdown.searchFieldLabel": "Pack",
|
||||
"xpack.osquery.packs.dropdown.searchFieldPlaceholder": "Rechercher un pack à exécuter",
|
||||
"xpack.osquery.packs.table.activeColumnTitle": "Actif",
|
||||
"xpack.osquery.packs.table.createdByColumnTitle": "Créé par",
|
||||
|
|
|
@ -23637,7 +23637,7 @@
|
|||
"xpack.osquery.pack.queriesTable.deleteActionAriaLabel": "{queryName}を削除",
|
||||
"xpack.osquery.pack.queriesTable.editActionAriaLabel": "{queryName}を編集",
|
||||
"xpack.osquery.pack.queryFlyoutForm.intervalFieldMaxNumberError": "間隔値は{than}よりも小さくなければなりません",
|
||||
"xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldValueMissingErrorMessage": "現在のクエリは{columnName}フィールドを返しません",
|
||||
"xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldRequiredErrorMessage": "値が必要です。",
|
||||
"xpack.osquery.pack.table.activatedSuccessToastMessageText": "\"{packName}\"が正常にアクティブ化されました",
|
||||
"xpack.osquery.pack.table.deactivatedSuccessToastMessageText": "\"{packName}\"が正常に非アクティブ化されました",
|
||||
"xpack.osquery.pack.table.deleteQueriesButtonLabel": "{queriesCount, plural, other {# 個のクエリ}}を削除",
|
||||
|
@ -23800,7 +23800,6 @@
|
|||
"xpack.osquery.pack.queriesTable.viewResultsColumnTitle": "結果を表示",
|
||||
"xpack.osquery.pack.queryFlyoutForm.cancelButtonLabel": "キャンセル",
|
||||
"xpack.osquery.pack.queryFlyoutForm.deleteECSMappingRowButtonAriaLabel": "ECSマッピング行を削除",
|
||||
"xpack.osquery.pack.queryFlyoutForm.descriptionFieldLabel": "説明(オプション)",
|
||||
"xpack.osquery.pack.queryFlyoutForm.ecsFieldRequiredErrorMessage": "ECSフィールドは必須です。",
|
||||
"xpack.osquery.pack.queryFlyoutForm.emptyIdError": "IDが必要です",
|
||||
"xpack.osquery.pack.queryFlyoutForm.emptyQueryError": "クエリは必須フィールドです",
|
||||
|
@ -23810,12 +23809,11 @@
|
|||
"xpack.osquery.pack.queryFlyoutForm.invalidIdError": "文字は英数字、_、または-でなければなりません",
|
||||
"xpack.osquery.pack.queryFlyoutForm.mappingEcsFieldLabel": "ECSフィールド",
|
||||
"xpack.osquery.pack.queryFlyoutForm.mappingValueFieldLabel": "値",
|
||||
"xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldRequiredErrorMessage": "値が必要です。",
|
||||
"xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldValueMissingErrorMessage": "現在のクエリは{columnName}フィールドを返しません",
|
||||
"xpack.osquery.pack.queryFlyoutForm.platformFieldLabel": "プラットフォーム",
|
||||
"xpack.osquery.pack.queryFlyoutForm.platformLinusLabel": "macOS",
|
||||
"xpack.osquery.pack.queryFlyoutForm.platformMacOSLabel": "Linux",
|
||||
"xpack.osquery.pack.queryFlyoutForm.platformWindowsLabel": "Windows",
|
||||
"xpack.osquery.pack.queryFlyoutForm.queryFieldLabel": "クエリ",
|
||||
"xpack.osquery.pack.queryFlyoutForm.saveButtonLabel": "保存",
|
||||
"xpack.osquery.pack.queryFlyoutForm.uniqueIdError": "IDは一意でなければなりません",
|
||||
"xpack.osquery.pack.queryFlyoutForm.versionFieldLabel": "最低Osqueryバージョン",
|
||||
|
@ -23831,7 +23829,6 @@
|
|||
"xpack.osquery.packList.prePackagedPacks.emptyPromptTitle.emptyPromptMessage": "パックはスケジュールできるクエリのセットです。事前構築済みパックを読み込むか、独自のパックを作成します。",
|
||||
"xpack.osquery.packList.prePackagedPacks.loadButtonLabel": "Elastic事前構築済みパックを読み込む",
|
||||
"xpack.osquery.packList.prePackagedPacks.updateButtonLabel": "Elastic事前構築済みパックの更新",
|
||||
"xpack.osquery.packs.dropdown.searchFieldLabel": "パック",
|
||||
"xpack.osquery.packs.dropdown.searchFieldPlaceholder": "実行するパックを検索",
|
||||
"xpack.osquery.packs.table.activeColumnTitle": "アクティブ",
|
||||
"xpack.osquery.packs.table.createdByColumnTitle": "作成者",
|
||||
|
|
|
@ -23665,7 +23665,7 @@
|
|||
"xpack.osquery.pack.queriesTable.deleteActionAriaLabel": "删除 {queryName}",
|
||||
"xpack.osquery.pack.queriesTable.editActionAriaLabel": "编辑 {queryName}",
|
||||
"xpack.osquery.pack.queryFlyoutForm.intervalFieldMaxNumberError": "间隔值必须小于 {than}",
|
||||
"xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldValueMissingErrorMessage": "当前查询不返回 {columnName} 字段",
|
||||
"xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldRequiredErrorMessage": "“值”必填。",
|
||||
"xpack.osquery.pack.table.activatedSuccessToastMessageText": "已成功激活“{packName}”包",
|
||||
"xpack.osquery.pack.table.deactivatedSuccessToastMessageText": "已成功停用“{packName}”包",
|
||||
"xpack.osquery.pack.table.deleteQueriesButtonLabel": "删除 {queriesCount, plural, other {# 个查询}}",
|
||||
|
@ -23828,7 +23828,6 @@
|
|||
"xpack.osquery.pack.queriesTable.viewResultsColumnTitle": "查看结果",
|
||||
"xpack.osquery.pack.queryFlyoutForm.cancelButtonLabel": "取消",
|
||||
"xpack.osquery.pack.queryFlyoutForm.deleteECSMappingRowButtonAriaLabel": "删除 ECS 映射行",
|
||||
"xpack.osquery.pack.queryFlyoutForm.descriptionFieldLabel": "描述(可选)",
|
||||
"xpack.osquery.pack.queryFlyoutForm.ecsFieldRequiredErrorMessage": "ECS 字段必填。",
|
||||
"xpack.osquery.pack.queryFlyoutForm.emptyIdError": "“ID”必填",
|
||||
"xpack.osquery.pack.queryFlyoutForm.emptyQueryError": "“查询”是必填字段",
|
||||
|
@ -23838,12 +23837,11 @@
|
|||
"xpack.osquery.pack.queryFlyoutForm.invalidIdError": "字符必须是数字字母、_ 或 -",
|
||||
"xpack.osquery.pack.queryFlyoutForm.mappingEcsFieldLabel": "ECS 字段",
|
||||
"xpack.osquery.pack.queryFlyoutForm.mappingValueFieldLabel": "值",
|
||||
"xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldRequiredErrorMessage": "“值”必填。",
|
||||
"xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldValueMissingErrorMessage": "当前查询不返回 {columnName} 字段",
|
||||
"xpack.osquery.pack.queryFlyoutForm.platformFieldLabel": "平台",
|
||||
"xpack.osquery.pack.queryFlyoutForm.platformLinusLabel": "macOS",
|
||||
"xpack.osquery.pack.queryFlyoutForm.platformMacOSLabel": "Linux",
|
||||
"xpack.osquery.pack.queryFlyoutForm.platformWindowsLabel": "Windows",
|
||||
"xpack.osquery.pack.queryFlyoutForm.queryFieldLabel": "查询",
|
||||
"xpack.osquery.pack.queryFlyoutForm.saveButtonLabel": "保存",
|
||||
"xpack.osquery.pack.queryFlyoutForm.uniqueIdError": "ID 必须唯一",
|
||||
"xpack.osquery.pack.queryFlyoutForm.versionFieldLabel": "最低 Osquery 版本",
|
||||
|
@ -23859,7 +23857,6 @@
|
|||
"xpack.osquery.packList.prePackagedPacks.emptyPromptTitle.emptyPromptMessage": "包是您可以计划的一组查询。加载预构建包或创建您自己的预构建包。",
|
||||
"xpack.osquery.packList.prePackagedPacks.loadButtonLabel": "加载 Elastic 预构建包",
|
||||
"xpack.osquery.packList.prePackagedPacks.updateButtonLabel": "更新 Elastic 预构建包",
|
||||
"xpack.osquery.packs.dropdown.searchFieldLabel": "包",
|
||||
"xpack.osquery.packs.dropdown.searchFieldPlaceholder": "搜索要运行的包",
|
||||
"xpack.osquery.packs.table.activeColumnTitle": "活动",
|
||||
"xpack.osquery.packs.table.createdByColumnTitle": "创建者",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue