mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Osquery] Fix scheduled query status (#106600)
This commit is contained in:
parent
562c3cc67c
commit
90152edeaa
26 changed files with 1323 additions and 532 deletions
|
@ -348,7 +348,7 @@
|
|||
"react-moment-proptypes": "^1.7.0",
|
||||
"react-monaco-editor": "^0.41.2",
|
||||
"react-popper-tooltip": "^2.10.1",
|
||||
"react-query": "^3.18.1",
|
||||
"react-query": "^3.21.0",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-resizable": "^1.7.5",
|
||||
"react-resize-detector": "^4.2.0",
|
||||
|
|
|
@ -7,12 +7,19 @@
|
|||
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { PLUGIN_ID } from '../../../fleet/common';
|
||||
import { pagePathGetters } from '../../../fleet/public';
|
||||
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
|
||||
import { useAgentPolicy } from './use_agent_policy';
|
||||
|
||||
const StyledEuiLink = styled(EuiLink)`
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
interface AgentsPolicyLinkProps {
|
||||
policyId: string;
|
||||
}
|
||||
|
@ -46,10 +53,9 @@ const AgentsPolicyLinkComponent: React.FC<AgentsPolicyLinkProps> = ({ policyId }
|
|||
);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @elastic/eui/href-or-on-click
|
||||
<EuiLink href={href} onClick={handleClick}>
|
||||
<StyledEuiLink href={href} onClick={handleClick}>
|
||||
{data?.name ?? policyId}
|
||||
</EuiLink>
|
||||
</StyledEuiLink>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -36,6 +36,10 @@ export const useAgentPolicy = ({ policyId, skip, silent }: UseAgentPolicy) => {
|
|||
defaultMessage: 'Error while fetching agent policy details',
|
||||
}),
|
||||
}),
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: Infinity,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { map } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { AGENT_SAVED_OBJECT_TYPE, Agent } from '../../../fleet/common';
|
||||
import { useErrorToast } from '../common/hooks/use_error_toast';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
|
||||
interface UseAgentPolicyAgentIdsProps {
|
||||
agentPolicyId: string | undefined;
|
||||
silent?: boolean;
|
||||
skip?: boolean;
|
||||
}
|
||||
|
||||
export const useAgentPolicyAgentIds = ({
|
||||
agentPolicyId,
|
||||
silent,
|
||||
skip,
|
||||
}: UseAgentPolicyAgentIdsProps) => {
|
||||
const { http } = useKibana().services;
|
||||
const setErrorToast = useErrorToast();
|
||||
|
||||
return useQuery<{ agents: Agent[] }, unknown, string[]>(
|
||||
['agentPolicyAgentIds', agentPolicyId],
|
||||
() => {
|
||||
const kuery = `${AGENT_SAVED_OBJECT_TYPE}.policy_id:${agentPolicyId}`;
|
||||
|
||||
return http.get(`/internal/osquery/fleet_wrapper/agents`, {
|
||||
query: {
|
||||
kuery,
|
||||
perPage: 10000,
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
select: (data) => map(data?.agents, 'id') || ([] as string[]),
|
||||
enabled: !skip || !agentPolicyId,
|
||||
onSuccess: () => setErrorToast(),
|
||||
onError: (error) =>
|
||||
!silent &&
|
||||
setErrorToast(error as Error, {
|
||||
title: i18n.translate('xpack.osquery.agents.fetchError', {
|
||||
defaultMessage: 'Error while fetching agents',
|
||||
}),
|
||||
}),
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
);
|
||||
};
|
|
@ -22,5 +22,9 @@ export const useOsqueryIntegrationStatus = () => {
|
|||
defaultMessage: 'Error while fetching osquery integration',
|
||||
}),
|
||||
}),
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EuiCodeEditor } from '@elastic/eui';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import 'brace/theme/tomorrow';
|
||||
|
||||
import './osquery_mode.ts';
|
||||
|
@ -26,22 +27,19 @@ interface OsqueryEditorProps {
|
|||
}
|
||||
|
||||
const OsqueryEditorComponent: React.FC<OsqueryEditorProps> = ({ defaultValue, onChange }) => {
|
||||
const editorValue = useRef(defaultValue ?? '');
|
||||
const [editorValue, setEditorValue] = useState(defaultValue ?? '');
|
||||
|
||||
const handleChange = useCallback((newValue: string) => {
|
||||
editorValue.current = newValue;
|
||||
}, []);
|
||||
useDebounce(() => onChange(editorValue.replaceAll('\n', ' ').replaceAll(' ', ' ')), 500, [
|
||||
editorValue,
|
||||
]);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
onChange(editorValue.current.replaceAll('\n', ' ').replaceAll(' ', ' '));
|
||||
}, [onChange]);
|
||||
useEffect(() => setEditorValue(defaultValue), [defaultValue]);
|
||||
|
||||
return (
|
||||
<EuiCodeEditor
|
||||
onBlur={onBlur}
|
||||
value={defaultValue}
|
||||
value={editorValue}
|
||||
mode="osquery"
|
||||
onChange={handleChange}
|
||||
onChange={setEditorValue}
|
||||
theme="tomorrow"
|
||||
name="osquery_editor"
|
||||
setOptions={EDITOR_SET_OPTIONS}
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import deepMerge from 'deepmerge';
|
||||
|
||||
|
@ -114,7 +114,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
),
|
||||
});
|
||||
|
||||
const { submit } = form;
|
||||
const { setFieldValue, submit } = form;
|
||||
|
||||
const actionId = useMemo(() => data?.actions[0].action_id, [data?.actions]);
|
||||
const agentIds = useMemo(() => data?.actions[0].agents, [data?.actions]);
|
||||
|
@ -253,6 +253,15 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
[queryFieldStepContent, resultsStepContent]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue?.agentSelection) {
|
||||
setFieldValue('agentSelection', defaultValue?.agentSelection);
|
||||
}
|
||||
if (defaultValue?.query) {
|
||||
setFieldValue('query', defaultValue?.query);
|
||||
}
|
||||
}, [defaultValue, setFieldValue]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form form={form}>{singleAgentMode ? singleAgentForm : <EuiSteps steps={formSteps} />}</Form>
|
||||
|
|
|
@ -31,7 +31,7 @@ import {
|
|||
ViewResultsInDiscoverAction,
|
||||
ViewResultsInLensAction,
|
||||
ViewResultsActionButtonType,
|
||||
} from '../scheduled_query_groups/scheduled_query_group_queries_table';
|
||||
} from '../scheduled_query_groups/scheduled_query_group_queries_status_table';
|
||||
import { useActionResultsPrivileges } from '../action_results/use_action_privileges';
|
||||
import { OSQUERY_INTEGRATION_NAME } from '../../common';
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import moment from 'moment-timezone';
|
||||
import {
|
||||
EuiInMemoryTable,
|
||||
EuiButton,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { startCase } from 'lodash';
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
@ -26,7 +27,7 @@ const AddScheduledQueryGroupPageComponent = () => {
|
|||
|
||||
return {
|
||||
name: osqueryIntegration.name,
|
||||
title: osqueryIntegration.title,
|
||||
title: osqueryIntegration.title ?? startCase(osqueryIntegration.name),
|
||||
version: osqueryIntegration.version,
|
||||
};
|
||||
}, [osqueryIntegration]);
|
||||
|
|
|
@ -24,10 +24,11 @@ import styled from 'styled-components';
|
|||
import { useKibana, useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useScheduledQueryGroup } from '../../../scheduled_query_groups/use_scheduled_query_group';
|
||||
import { ScheduledQueryGroupQueriesTable } from '../../../scheduled_query_groups/scheduled_query_group_queries_table';
|
||||
import { ScheduledQueryGroupQueriesStatusTable } from '../../../scheduled_query_groups/scheduled_query_group_queries_status_table';
|
||||
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
|
||||
import { AgentsPolicyLink } from '../../../agent_policies/agents_policy_link';
|
||||
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
|
||||
import { useAgentPolicyAgentIds } from '../../../agents/use_agent_policy_agent_ids';
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 0;
|
||||
|
@ -44,6 +45,10 @@ const ScheduledQueryGroupDetailsPageComponent = () => {
|
|||
);
|
||||
|
||||
const { data } = useScheduledQueryGroup({ scheduledQueryGroupId });
|
||||
const { data: agentIds } = useAgentPolicyAgentIds({
|
||||
agentPolicyId: data?.policy_id,
|
||||
skip: !data,
|
||||
});
|
||||
|
||||
useBreadcrumbs('scheduled_query_group_details', { scheduledQueryGroupName: data?.name ?? '' });
|
||||
|
||||
|
@ -131,7 +136,13 @@ const ScheduledQueryGroupDetailsPageComponent = () => {
|
|||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn} rightColumn={RightColumn} rightColumnGrow={false}>
|
||||
{data && <ScheduledQueryGroupQueriesTable data={data.inputs[0].streams} />}
|
||||
{data && (
|
||||
<ScheduledQueryGroupQueriesStatusTable
|
||||
agentIds={agentIds}
|
||||
scheduledQueryGroupName={data.name}
|
||||
data={data.inputs[0].streams}
|
||||
/>
|
||||
)}
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -244,7 +244,7 @@ const ScheduledQueryGroupFormComponent: React.FC<ScheduledQueryGroupFormProps> =
|
|||
),
|
||||
});
|
||||
|
||||
const { submit } = form;
|
||||
const { setFieldValue, submit } = form;
|
||||
|
||||
const policyIdEuiFieldProps = useMemo(
|
||||
() => ({ isDisabled: !!defaultValue, options: agentPolicyOptions }),
|
||||
|
@ -276,6 +276,10 @@ const ScheduledQueryGroupFormComponent: React.FC<ScheduledQueryGroupFormProps> =
|
|||
};
|
||||
}, [agentPoliciesById, policyId]);
|
||||
|
||||
const handleNameChange = useCallback((newName: string) => setFieldValue('name', newName), [
|
||||
setFieldValue,
|
||||
]);
|
||||
|
||||
const handleSaveClick = useCallback(() => {
|
||||
if (currentPolicy.agentCount) {
|
||||
setShowConfirmationModal(true);
|
||||
|
@ -343,6 +347,7 @@ const ScheduledQueryGroupFormComponent: React.FC<ScheduledQueryGroupFormProps> =
|
|||
component={QueriesField}
|
||||
scheduledQueryGroupId={defaultValue?.id ?? null}
|
||||
integrationPackageVersion={integrationPackageVersion}
|
||||
handleNameChange={handleNameChange}
|
||||
/>
|
||||
|
||||
<CommonUseField path="enabled" component={GhostFormField} />
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mapKeys, kebabCase } from 'lodash';
|
||||
import { kebabCase } from 'lodash';
|
||||
import { EuiLink, EuiFormRow, EuiFilePicker, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback, useState, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -25,7 +25,7 @@ const ExamplePackLink = React.memo(() => (
|
|||
ExamplePackLink.displayName = 'ExamplePackLink';
|
||||
|
||||
interface OsqueryPackUploaderProps {
|
||||
onChange: (payload: Record<string, unknown>) => void;
|
||||
onChange: (payload: Record<string, unknown>, packName: string) => void;
|
||||
}
|
||||
|
||||
const OsqueryPackUploaderComponent: React.FC<OsqueryPackUploaderProps> = ({ onChange }) => {
|
||||
|
@ -61,12 +61,7 @@ const OsqueryPackUploaderComponent: React.FC<OsqueryPackUploaderProps> = ({ onCh
|
|||
return;
|
||||
}
|
||||
|
||||
const queriesJSON = mapKeys(
|
||||
parsedContent?.queries,
|
||||
(value, key) => `pack_${packName.current}_${key}`
|
||||
);
|
||||
|
||||
onChange(queriesJSON);
|
||||
onChange(parsedContent?.queries, packName.current);
|
||||
// @ts-expect-error update types
|
||||
filePickerRef.current?.removeFiles(new Event('fake'));
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer } from '@elastic/eui';
|
|||
import { produce } from 'immer';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { satisfies } from 'semver';
|
||||
|
||||
import {
|
||||
OsqueryManagerPackagePolicyInputStream,
|
||||
|
@ -23,6 +24,7 @@ import { OsqueryPackUploader } from './pack_uploader';
|
|||
import { getSupportedPlatforms } from '../queries/platforms/helpers';
|
||||
|
||||
interface QueriesFieldProps {
|
||||
handleNameChange: (name: string) => void;
|
||||
field: FieldHook<OsqueryManagerPackagePolicyInput[]>;
|
||||
integrationPackageVersion?: string | undefined;
|
||||
scheduledQueryGroupId: string;
|
||||
|
@ -82,6 +84,7 @@ const getNewStream = (payload: GetNewStreamProps) =>
|
|||
|
||||
const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({
|
||||
field,
|
||||
handleNameChange,
|
||||
integrationPackageVersion,
|
||||
scheduledQueryGroupId,
|
||||
}) => {
|
||||
|
@ -208,13 +211,18 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({
|
|||
}, [setValue, tableSelectedItems]);
|
||||
|
||||
const handlePackUpload = useCallback(
|
||||
(newQueries) => {
|
||||
(newQueries, packName) => {
|
||||
/* Osquery scheduled packs are supported since osquery_manager@0.5.0 */
|
||||
const isOsqueryPackSupported = integrationPackageVersion
|
||||
? satisfies(integrationPackageVersion, '>=0.5.0')
|
||||
: false;
|
||||
|
||||
setValue(
|
||||
produce((draft) => {
|
||||
forEach(newQueries, (newQuery, newQueryId) => {
|
||||
draft[0].streams.push(
|
||||
getNewStream({
|
||||
id: newQueryId,
|
||||
id: isOsqueryPackSupported ? newQueryId : `pack_${packName}_${newQueryId}`,
|
||||
interval: newQuery.interval,
|
||||
query: newQuery.query,
|
||||
version: newQuery.version,
|
||||
|
@ -227,8 +235,12 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({
|
|||
return draft;
|
||||
})
|
||||
);
|
||||
|
||||
if (isOsqueryPackSupported) {
|
||||
handleNameChange(packName);
|
||||
}
|
||||
},
|
||||
[scheduledQueryGroupId, setValue]
|
||||
[handleNameChange, integrationPackageVersion, scheduledQueryGroupId, setValue]
|
||||
);
|
||||
|
||||
const tableData = useMemo(() => (field.value.length ? field.value[0].streams : []), [
|
||||
|
@ -277,7 +289,6 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({
|
|||
<EuiSpacer />
|
||||
{field.value && field.value[0].streams?.length ? (
|
||||
<ScheduledQueryGroupQueriesTable
|
||||
editMode={true}
|
||||
data={tableData}
|
||||
onEditClick={handleEditClick}
|
||||
onDeleteClick={handleDeleteClick}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { produce } from 'immer';
|
||||
import { find, sortBy, isArray, map } from 'lodash';
|
||||
import { find, orderBy, sortedUniqBy, isArray, map } from 'lodash';
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
|
@ -78,7 +78,10 @@ const typeMap = {
|
|||
|
||||
const StyledFieldIcon = styled(FieldIcon)`
|
||||
width: 32px;
|
||||
padding: 0 4px;
|
||||
|
||||
> svg {
|
||||
padding: 0 6px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledFieldSpan = styled.span`
|
||||
|
@ -88,7 +91,15 @@ const StyledFieldSpan = styled.span`
|
|||
|
||||
// align the icon to the inputs
|
||||
const StyledButtonWrapper = styled.div`
|
||||
margin-top: 32px;
|
||||
margin-top: 30px;
|
||||
`;
|
||||
|
||||
const ECSFieldColumn = styled(EuiFlexGroup)`
|
||||
max-width: 100%;
|
||||
`;
|
||||
|
||||
const ECSFieldWrapper = styled(EuiFlexItem)`
|
||||
max-width: calc(100% - 66px);
|
||||
`;
|
||||
|
||||
const singleSelection = { asPlainText: true };
|
||||
|
@ -163,7 +174,7 @@ export const ECSComboboxField: React.FC<ECSComboboxFieldProps> = ({
|
|||
size="l"
|
||||
type={
|
||||
// @ts-expect-error update types
|
||||
selectedOptions[0]?.value?.type === 'keyword' ? 'string' : selectedOptions[0]?.value?.type
|
||||
typeMap[selectedOptions[0]?.value?.type] ?? selectedOptions[0]?.value?.type
|
||||
}
|
||||
/>
|
||||
),
|
||||
|
@ -220,7 +231,7 @@ export const OsqueryColumnField: React.FC<OsqueryColumnFieldProps> = ({
|
|||
idAria,
|
||||
...rest
|
||||
}) => {
|
||||
const { setErrors, setValue } = field;
|
||||
const { setValue } = field;
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
const describedByIds = useMemo(() => (idAria ? [idAria] : []), [idAria]);
|
||||
const [selectedOptions, setSelected] = useState<
|
||||
|
@ -250,25 +261,6 @@ export const OsqueryColumnField: React.FC<OsqueryColumnFieldProps> = ({
|
|||
[]
|
||||
);
|
||||
|
||||
const onCreateOsqueryOption = useCallback(
|
||||
(searchValue = []) => {
|
||||
const normalizedSearchValue = searchValue.trim().toLowerCase();
|
||||
|
||||
if (!normalizedSearchValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOption = {
|
||||
label: searchValue,
|
||||
};
|
||||
|
||||
// Select the option.
|
||||
setSelected([newOption]);
|
||||
setValue(newOption.label);
|
||||
},
|
||||
[setValue, setSelected]
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newSelectedOptions) => {
|
||||
setSelected(newSelectedOptions);
|
||||
|
@ -285,7 +277,7 @@ export const OsqueryColumnField: React.FC<OsqueryColumnFieldProps> = ({
|
|||
|
||||
return selectedOption ? [selectedOption] : [{ label: field.value }];
|
||||
});
|
||||
}, [euiFieldProps?.options, setSelected, field.value, setErrors]);
|
||||
}, [euiFieldProps?.options, setSelected, field.value]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
|
@ -302,7 +294,6 @@ export const OsqueryColumnField: React.FC<OsqueryColumnFieldProps> = ({
|
|||
singleSelection={singleSelection}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={handleChange}
|
||||
onCreateOption={onCreateOsqueryOption}
|
||||
renderOption={renderOsqueryOption}
|
||||
rowHeight={32}
|
||||
isClearable
|
||||
|
@ -513,7 +504,7 @@ export const ECSMappingEditorForm = forwardRef<ECSMappingEditorFormRef, ECSMappi
|
|||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<EuiFlexGroup alignItems="flexStart">
|
||||
<EuiFlexGroup alignItems="flexStart" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="value.field"
|
||||
|
@ -526,15 +517,15 @@ export const ECSMappingEditorForm = forwardRef<ECSMappingEditorFormRef, ECSMappi
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="flexStart">
|
||||
<ECSFieldColumn alignItems="flexStart" gutterSize="s" wrap>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledButtonWrapper>
|
||||
<EuiIcon type="arrowRight" />
|
||||
</StyledButtonWrapper>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<ECSFieldWrapper>
|
||||
<CommonUseField path="key" component={ECSComboboxField} />
|
||||
</EuiFlexItem>
|
||||
</ECSFieldWrapper>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledButtonWrapper>
|
||||
{defaultValue ? (
|
||||
|
@ -564,7 +555,7 @@ export const ECSMappingEditorForm = forwardRef<ECSMappingEditorFormRef, ECSMappi
|
|||
)}
|
||||
</StyledButtonWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</ECSFieldColumn>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
|
@ -634,6 +625,11 @@ export const ECSMappingEditorField = ({ field, query, fieldRef }: ECSMappingEdit
|
|||
return currentValue;
|
||||
}
|
||||
|
||||
const tablesOrderMap = ast?.from?.reduce((acc, table, index) => {
|
||||
acc[table.as ?? table.table] = index;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const astOsqueryTables: Record<string, OsqueryColumn[]> = ast?.from?.reduce((acc, table) => {
|
||||
const osqueryTable = find(osquerySchema, ['name', table.table]);
|
||||
|
||||
|
@ -665,6 +661,7 @@ export const ECSMappingEditorField = ({ field, query, fieldRef }: ECSMappingEdit
|
|||
name: osqueryColumn.name,
|
||||
description: osqueryColumn.description,
|
||||
table: tableName,
|
||||
tableOrder: tablesOrderMap[tableName],
|
||||
suggestion_label: osqueryColumn.name,
|
||||
},
|
||||
}));
|
||||
|
@ -678,13 +675,14 @@ export const ECSMappingEditorField = ({ field, query, fieldRef }: ECSMappingEdit
|
|||
isArray(ast?.columns) &&
|
||||
ast?.columns
|
||||
?.map((column) => {
|
||||
if (column.expr.column === '*') {
|
||||
if (column.expr.column === '*' && astOsqueryTables[column.expr.table]) {
|
||||
return astOsqueryTables[column.expr.table].map((osqueryColumn) => ({
|
||||
label: osqueryColumn.name,
|
||||
value: {
|
||||
name: osqueryColumn.name,
|
||||
description: osqueryColumn.description,
|
||||
table: column.expr.table,
|
||||
tableOrder: tablesOrderMap[column.expr.table],
|
||||
suggestion_label: `${osqueryColumn.name}`,
|
||||
},
|
||||
}));
|
||||
|
@ -706,6 +704,7 @@ export const ECSMappingEditorField = ({ field, query, fieldRef }: ECSMappingEdit
|
|||
name: osqueryColumn.name,
|
||||
description: osqueryColumn.description,
|
||||
table: column.expr.table,
|
||||
tableOrder: tablesOrderMap[column.expr.table],
|
||||
suggestion_label: `${label}`,
|
||||
},
|
||||
},
|
||||
|
@ -717,8 +716,12 @@ export const ECSMappingEditorField = ({ field, query, fieldRef }: ECSMappingEdit
|
|||
})
|
||||
.flat();
|
||||
|
||||
// @ts-expect-error update types
|
||||
return sortBy(suggestions, 'value.suggestion_label');
|
||||
// Remove column duplicates by keeping the column from the table that appears last in the query
|
||||
return sortedUniqBy(
|
||||
// @ts-expect-error update types
|
||||
orderBy(suggestions, ['value.suggestion_label', 'value.tableOrder'], ['asc', 'desc']),
|
||||
'label'
|
||||
);
|
||||
});
|
||||
}, [query]);
|
||||
|
||||
|
@ -766,6 +769,10 @@ export const ECSMappingEditorField = ({ field, query, fieldRef }: ECSMappingEdit
|
|||
return draft;
|
||||
})
|
||||
);
|
||||
|
||||
if (formRefs.current[key]) {
|
||||
delete formRefs.current[key];
|
||||
}
|
||||
}
|
||||
},
|
||||
[setValue]
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutFooter,
|
||||
EuiPortal,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
|
@ -117,138 +116,145 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
[isFieldSupported, setFieldValue, reset]
|
||||
);
|
||||
|
||||
/* Avoids accidental closing of the flyout when the user clicks outside of the flyout */
|
||||
const maskProps = useMemo(() => ({ onClick: () => ({}) }), []);
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiFlyout size="m" ownFocus onClose={onClose} aria-labelledby="flyoutTitle">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h2 id="flyoutTitle">
|
||||
{isEditMode ? (
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduleQueryGroup.queryFlyoutForm.editFormTitle"
|
||||
defaultMessage="Edit query"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduleQueryGroup.queryFlyoutForm.addFormTitle"
|
||||
defaultMessage="Attach next query"
|
||||
/>
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<Form form={form}>
|
||||
{!isEditMode ? (
|
||||
<>
|
||||
<SavedQueriesDropdown onChange={handleSetQueryValue} />
|
||||
<EuiSpacer />
|
||||
</>
|
||||
) : null}
|
||||
<CommonUseField path="id" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="query" component={CodeEditorField} />
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="interval"
|
||||
// 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.scheduledQueryGroup.queryFlyoutForm.versionFieldOptionalLabel"
|
||||
defaultMessage="(optional)"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{
|
||||
isDisabled: !isFieldSupported,
|
||||
noSuggestions: false,
|
||||
singleSelection: { asPlainText: true },
|
||||
placeholder: i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queriesTable.osqueryVersionAllLabel',
|
||||
{
|
||||
defaultMessage: 'ALL',
|
||||
}
|
||||
),
|
||||
options: ALL_OSQUERY_VERSIONS_OPTIONS,
|
||||
onCreateOption: undefined,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="platform"
|
||||
component={PlatformCheckBoxGroupField}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{ disabled: !isFieldSupported }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="ecs_mapping"
|
||||
component={ECSMappingEditorField}
|
||||
query={query}
|
||||
fieldRef={ecsFieldRef}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Form>
|
||||
{!isFieldSupported ? (
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduleQueryGroup.queryFlyoutForm.unsupportedPlatformAndVersionFieldsCalloutTitle"
|
||||
defaultMessage="Platform and version fields are available from {version}"
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
values={{ version: `osquery_manager@0.3.0` }}
|
||||
/>
|
||||
}
|
||||
iconType="pin"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ManageIntegrationLink />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiCallOut>
|
||||
<EuiFlyout
|
||||
size="m"
|
||||
onClose={onClose}
|
||||
aria-labelledby="flyoutTitle"
|
||||
outsideClickCloses={false}
|
||||
maskProps={maskProps}
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h2 id="flyoutTitle">
|
||||
{isEditMode ? (
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduleQueryGroup.queryFlyoutForm.editFormTitle"
|
||||
defaultMessage="Edit query"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduleQueryGroup.queryFlyoutForm.addFormTitle"
|
||||
defaultMessage="Attach next query"
|
||||
/>
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<Form form={form}>
|
||||
{!isEditMode ? (
|
||||
<>
|
||||
<SavedQueriesDropdown onChange={handleSetQueryValue} />
|
||||
<EuiSpacer />
|
||||
</>
|
||||
) : null}
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryGroup.queryFlyoutForm.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<CommonUseField path="id" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="query" component={CodeEditorField} />
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="interval"
|
||||
// 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.scheduledQueryGroup.queryFlyoutForm.versionFieldOptionalLabel"
|
||||
defaultMessage="(optional)"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{
|
||||
isDisabled: !isFieldSupported,
|
||||
noSuggestions: false,
|
||||
singleSelection: { asPlainText: true },
|
||||
placeholder: i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queriesTable.osqueryVersionAllLabel',
|
||||
{
|
||||
defaultMessage: 'ALL',
|
||||
}
|
||||
),
|
||||
options: ALL_OSQUERY_VERSIONS_OPTIONS,
|
||||
onCreateOption: undefined,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton onClick={submit} fill>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryGroup.queryFlyoutForm.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="platform"
|
||||
component={PlatformCheckBoxGroupField}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{ disabled: !isFieldSupported }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="ecs_mapping"
|
||||
component={ECSMappingEditorField}
|
||||
query={query}
|
||||
fieldRef={ecsFieldRef}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Form>
|
||||
{!isFieldSupported ? (
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduleQueryGroup.queryFlyoutForm.unsupportedPlatformAndVersionFieldsCalloutTitle"
|
||||
defaultMessage="Platform and version fields are available from {version}"
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
values={{ version: `osquery_manager@0.3.0` }}
|
||||
/>
|
||||
}
|
||||
iconType="pin"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ManageIntegrationLink />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiCallOut>
|
||||
) : null}
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryGroup.queryFlyoutForm.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton onClick={submit} fill>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryGroup.queryFlyoutForm.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -63,7 +63,23 @@ export const useScheduledQueryGroupQueryForm = ({
|
|||
options: {
|
||||
stripEmptyFields: false,
|
||||
},
|
||||
defaultValue,
|
||||
defaultValue: defaultValue || {
|
||||
id: {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
query: {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
interval: {
|
||||
type: 'integer',
|
||||
value: '3600',
|
||||
},
|
||||
ecs_mapping: {
|
||||
value: {},
|
||||
},
|
||||
},
|
||||
// @ts-expect-error update types
|
||||
serializer: (payload) =>
|
||||
produce(payload, (draft) => {
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* 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 { EuiInMemoryTable, EuiCodeBlock, EuiToolTip, EuiButtonIcon } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { encode } from 'rison-node';
|
||||
import { stringify } from 'querystring';
|
||||
|
||||
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
|
||||
import { AgentIdToName } from '../agents/agent_id_to_name';
|
||||
import { useScheduledQueryGroupQueryErrors } from './use_scheduled_query_group_query_errors';
|
||||
|
||||
const VIEW_IN_LOGS = i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queriesTable.viewLogsErrorsActionAriaLabel',
|
||||
{
|
||||
defaultMessage: 'View in Logs',
|
||||
}
|
||||
);
|
||||
|
||||
interface ViewErrorsInLogsActionProps {
|
||||
actionId: string;
|
||||
agentId: string;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
const ViewErrorsInLogsActionComponent: React.FC<ViewErrorsInLogsActionProps> = ({
|
||||
actionId,
|
||||
agentId,
|
||||
timestamp,
|
||||
}) => {
|
||||
const navigateToApp = useKibana().services.application.navigateToApp;
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event) => {
|
||||
const openInNewTab = !(!isModifiedEvent(event) && isLeftClickEvent(event));
|
||||
|
||||
event.preventDefault();
|
||||
const queryString = stringify({
|
||||
logPosition: encode({
|
||||
end: timestamp,
|
||||
streamLive: false,
|
||||
}),
|
||||
logFilter: encode({
|
||||
expression: `elastic_agent.id:${agentId} and (data_stream.dataset:elastic_agent or data_stream.dataset:elastic_agent.osquerybeat) and "${actionId}"`,
|
||||
kind: 'kuery',
|
||||
}),
|
||||
});
|
||||
|
||||
navigateToApp('logs', {
|
||||
path: `stream?${queryString}`,
|
||||
openInNewTab,
|
||||
});
|
||||
},
|
||||
[actionId, agentId, navigateToApp, timestamp]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiToolTip content={VIEW_IN_LOGS}>
|
||||
<EuiButtonIcon iconType="search" onClick={handleClick} aria-label={VIEW_IN_LOGS} />
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
export const ViewErrorsInLogsAction = React.memo(ViewErrorsInLogsActionComponent);
|
||||
|
||||
interface ScheduledQueryErrorsTableProps {
|
||||
actionId: string;
|
||||
agentIds?: string[];
|
||||
interval: number;
|
||||
}
|
||||
|
||||
const renderErrorMessage = (error: string) => (
|
||||
<EuiCodeBlock fontSize="s" paddingSize="none" transparentBackground>
|
||||
{error}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
|
||||
const ScheduledQueryErrorsTableComponent: React.FC<ScheduledQueryErrorsTableProps> = ({
|
||||
actionId,
|
||||
agentIds,
|
||||
interval,
|
||||
}) => {
|
||||
const { data: lastErrorsData } = useScheduledQueryGroupQueryErrors({
|
||||
actionId,
|
||||
agentIds,
|
||||
interval,
|
||||
});
|
||||
|
||||
const renderAgentIdColumn = useCallback((agentId) => <AgentIdToName agentId={agentId} />, []);
|
||||
|
||||
const renderLogsErrorsAction = useCallback(
|
||||
(item) => (
|
||||
<ViewErrorsInLogsAction
|
||||
actionId={actionId}
|
||||
agentId={item?.fields['elastic_agent.id'][0]}
|
||||
timestamp={item?.fields['event.ingested'][0]}
|
||||
/>
|
||||
),
|
||||
[actionId]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'fields.@timestamp',
|
||||
name: '@timestamp',
|
||||
width: '220px',
|
||||
},
|
||||
{
|
||||
field: 'fields["elastic_agent.id"][0]',
|
||||
name: i18n.translate('xpack.osquery.scheduledQueryErrorsTable.agentIdColumnTitle', {
|
||||
defaultMessage: 'Agent Id',
|
||||
}),
|
||||
truncateText: true,
|
||||
render: renderAgentIdColumn,
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
field: 'fields.message[0]',
|
||||
name: i18n.translate('xpack.osquery.scheduledQueryErrorsTable.errorColumnTitle', {
|
||||
defaultMessage: 'Error',
|
||||
}),
|
||||
render: renderErrorMessage,
|
||||
},
|
||||
{
|
||||
width: '50px',
|
||||
actions: [
|
||||
{
|
||||
render: renderLogsErrorsAction,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[renderAgentIdColumn, renderLogsErrorsAction]
|
||||
);
|
||||
|
||||
// @ts-expect-error update types
|
||||
return <EuiInMemoryTable items={lastErrorsData?.hits} columns={columns} pagination={true} />;
|
||||
};
|
||||
|
||||
export const ScheduledQueryErrorsTable = React.memo(ScheduledQueryErrorsTableComponent);
|
|
@ -0,0 +1,644 @@
|
|||
/*
|
||||
* 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 { get } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButtonEmpty,
|
||||
EuiCodeBlock,
|
||||
EuiButtonIcon,
|
||||
EuiToolTip,
|
||||
EuiLoadingSpinner,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiNotificationBadge,
|
||||
EuiSpacer,
|
||||
EuiPanel,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import {
|
||||
TypedLensByValueInput,
|
||||
PersistedIndexPatternLayer,
|
||||
PieVisualizationState,
|
||||
} from '../../../lens/public';
|
||||
import { FilterStateStore } from '../../../../../src/plugins/data/common';
|
||||
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
|
||||
import { OsqueryManagerPackagePolicyInputStream } from '../../common/types';
|
||||
import { ScheduledQueryErrorsTable } from './scheduled_query_errors_table';
|
||||
import { useScheduledQueryGroupQueryLastResults } from './use_scheduled_query_group_query_last_results';
|
||||
import { useScheduledQueryGroupQueryErrors } from './use_scheduled_query_group_query_errors';
|
||||
|
||||
const VIEW_IN_DISCOVER = i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queriesTable.viewDiscoverResultsActionAriaLabel',
|
||||
{
|
||||
defaultMessage: 'View in Discover',
|
||||
}
|
||||
);
|
||||
|
||||
const VIEW_IN_LENS = i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queriesTable.viewLensResultsActionAriaLabel',
|
||||
{
|
||||
defaultMessage: 'View in Lens',
|
||||
}
|
||||
);
|
||||
|
||||
export enum ViewResultsActionButtonType {
|
||||
icon = 'icon',
|
||||
button = 'button',
|
||||
}
|
||||
|
||||
interface ViewResultsInDiscoverActionProps {
|
||||
actionId: string;
|
||||
agentIds?: string[];
|
||||
buttonType: ViewResultsActionButtonType;
|
||||
endDate?: string;
|
||||
startDate?: string;
|
||||
}
|
||||
|
||||
function getLensAttributes(
|
||||
actionId: string,
|
||||
agentIds?: string[]
|
||||
): TypedLensByValueInput['attributes'] {
|
||||
const dataLayer: PersistedIndexPatternLayer = {
|
||||
columnOrder: ['8690befd-fd69-4246-af4a-dd485d2a3b38', 'ed999e9d-204c-465b-897f-fe1a125b39ed'],
|
||||
columns: {
|
||||
'8690befd-fd69-4246-af4a-dd485d2a3b38': {
|
||||
sourceField: 'type',
|
||||
isBucketed: true,
|
||||
dataType: 'string',
|
||||
scale: 'ordinal',
|
||||
operationType: 'terms',
|
||||
label: 'Top values of type',
|
||||
params: {
|
||||
otherBucket: true,
|
||||
size: 5,
|
||||
missingBucket: false,
|
||||
orderBy: {
|
||||
columnId: 'ed999e9d-204c-465b-897f-fe1a125b39ed',
|
||||
type: 'column',
|
||||
},
|
||||
orderDirection: 'desc',
|
||||
},
|
||||
},
|
||||
'ed999e9d-204c-465b-897f-fe1a125b39ed': {
|
||||
sourceField: 'Records',
|
||||
isBucketed: false,
|
||||
dataType: 'number',
|
||||
scale: 'ratio',
|
||||
operationType: 'count',
|
||||
label: 'Count of records',
|
||||
},
|
||||
},
|
||||
incompleteColumns: {},
|
||||
};
|
||||
|
||||
const xyConfig: PieVisualizationState = {
|
||||
shape: 'pie',
|
||||
layers: [
|
||||
{
|
||||
layerType: 'data',
|
||||
legendDisplay: 'default',
|
||||
nestedLegend: false,
|
||||
layerId: 'layer1',
|
||||
metric: 'ed999e9d-204c-465b-897f-fe1a125b39ed',
|
||||
numberDisplay: 'percent',
|
||||
groups: ['8690befd-fd69-4246-af4a-dd485d2a3b38'],
|
||||
categoryDisplay: 'default',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const agentIdsQuery = {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: agentIds?.map((agentId) => ({ match_phrase: { 'agent.id': agentId } })),
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
visualizationType: 'lnsPie',
|
||||
title: `Action ${actionId} results`,
|
||||
references: [
|
||||
{
|
||||
id: 'logs-*',
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: 'logs-*',
|
||||
name: 'indexpattern-datasource-layer-layer1',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
name: 'filter-index-pattern-0',
|
||||
id: 'logs-*',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
state: {
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
layers: {
|
||||
layer1: dataLayer,
|
||||
},
|
||||
},
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
$state: { store: FilterStateStore.APP_STATE },
|
||||
meta: {
|
||||
indexRefName: 'filter-index-pattern-0',
|
||||
negate: false,
|
||||
alias: null,
|
||||
disabled: false,
|
||||
params: {
|
||||
query: actionId,
|
||||
},
|
||||
type: 'phrase',
|
||||
key: 'action_id',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
action_id: actionId,
|
||||
},
|
||||
},
|
||||
},
|
||||
...(agentIdsQuery
|
||||
? [
|
||||
{
|
||||
$state: { store: FilterStateStore.APP_STATE },
|
||||
meta: {
|
||||
alias: 'agent IDs',
|
||||
disabled: false,
|
||||
indexRefName: 'filter-index-pattern-0',
|
||||
key: 'query',
|
||||
negate: false,
|
||||
type: 'custom',
|
||||
value: JSON.stringify(agentIdsQuery),
|
||||
},
|
||||
query: agentIdsQuery,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
query: { language: 'kuery', query: '' },
|
||||
visualization: xyConfig,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ViewResultsInLensActionComponent: React.FC<ViewResultsInDiscoverActionProps> = ({
|
||||
actionId,
|
||||
agentIds,
|
||||
buttonType,
|
||||
endDate,
|
||||
startDate,
|
||||
}) => {
|
||||
const lensService = useKibana().services.lens;
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event) => {
|
||||
const openInNewTab = !(!isModifiedEvent(event) && isLeftClickEvent(event));
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
lensService?.navigateToPrefilledEditor(
|
||||
{
|
||||
id: '',
|
||||
timeRange: {
|
||||
from: startDate ?? 'now-1d',
|
||||
to: endDate ?? 'now',
|
||||
mode: startDate || endDate ? 'absolute' : 'relative',
|
||||
},
|
||||
attributes: getLensAttributes(actionId, agentIds),
|
||||
},
|
||||
{
|
||||
openInNewTab,
|
||||
}
|
||||
);
|
||||
},
|
||||
[actionId, agentIds, endDate, lensService, startDate]
|
||||
);
|
||||
|
||||
if (buttonType === ViewResultsActionButtonType.button) {
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
iconType="lensApp"
|
||||
onClick={handleClick}
|
||||
disabled={!lensService?.canUseEditor()}
|
||||
>
|
||||
{VIEW_IN_LENS}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiToolTip content={VIEW_IN_LENS}>
|
||||
<EuiButtonIcon
|
||||
iconType="lensApp"
|
||||
disabled={!lensService?.canUseEditor()}
|
||||
onClick={handleClick}
|
||||
aria-label={VIEW_IN_LENS}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
export const ViewResultsInLensAction = React.memo(ViewResultsInLensActionComponent);
|
||||
|
||||
const ViewResultsInDiscoverActionComponent: React.FC<ViewResultsInDiscoverActionProps> = ({
|
||||
actionId,
|
||||
agentIds,
|
||||
buttonType,
|
||||
endDate,
|
||||
startDate,
|
||||
}) => {
|
||||
const urlGenerator = useKibana().services.discover?.urlGenerator;
|
||||
const [discoverUrl, setDiscoverUrl] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const getDiscoverUrl = async () => {
|
||||
if (!urlGenerator?.createUrl) return;
|
||||
|
||||
const agentIdsQuery = agentIds?.length
|
||||
? {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: agentIds.map((agentId) => ({ match_phrase: { 'agent.id': agentId } })),
|
||||
},
|
||||
}
|
||||
: null;
|
||||
|
||||
const newUrl = await urlGenerator.createUrl({
|
||||
indexPatternId: 'logs-*',
|
||||
filters: [
|
||||
{
|
||||
meta: {
|
||||
index: 'logs-*',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'action_id',
|
||||
params: { query: actionId },
|
||||
},
|
||||
query: { match_phrase: { action_id: actionId } },
|
||||
$state: { store: FilterStateStore.APP_STATE },
|
||||
},
|
||||
...(agentIdsQuery
|
||||
? [
|
||||
{
|
||||
$state: { store: FilterStateStore.APP_STATE },
|
||||
meta: {
|
||||
alias: 'agent IDs',
|
||||
disabled: false,
|
||||
index: 'logs-*',
|
||||
key: 'query',
|
||||
negate: false,
|
||||
type: 'custom',
|
||||
value: JSON.stringify(agentIdsQuery),
|
||||
},
|
||||
query: agentIdsQuery,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
refreshInterval: {
|
||||
pause: true,
|
||||
value: 0,
|
||||
},
|
||||
timeRange:
|
||||
startDate && endDate
|
||||
? {
|
||||
to: endDate,
|
||||
from: startDate,
|
||||
mode: 'absolute',
|
||||
}
|
||||
: {
|
||||
to: 'now',
|
||||
from: 'now-1d',
|
||||
mode: 'relative',
|
||||
},
|
||||
});
|
||||
setDiscoverUrl(newUrl);
|
||||
};
|
||||
getDiscoverUrl();
|
||||
}, [actionId, agentIds, endDate, startDate, urlGenerator]);
|
||||
|
||||
if (buttonType === ViewResultsActionButtonType.button) {
|
||||
return (
|
||||
<EuiButtonEmpty size="xs" iconType="discoverApp" href={discoverUrl}>
|
||||
{VIEW_IN_DISCOVER}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiToolTip content={VIEW_IN_DISCOVER}>
|
||||
<EuiButtonIcon iconType="discoverApp" href={discoverUrl} aria-label={VIEW_IN_DISCOVER} />
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
export const ViewResultsInDiscoverAction = React.memo(ViewResultsInDiscoverActionComponent);
|
||||
|
||||
interface ScheduledQueryExpandedContentProps {
|
||||
actionId: string;
|
||||
agentIds?: string[];
|
||||
interval: number;
|
||||
}
|
||||
|
||||
const ScheduledQueryExpandedContent = React.memo<ScheduledQueryExpandedContentProps>(
|
||||
({ actionId, agentIds, interval }) => (
|
||||
<EuiFlexGroup direction="column" gutterSize="xl">
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel paddingSize="s" hasBorder hasShadow={false}>
|
||||
<ScheduledQueryErrorsTable actionId={actionId} agentIds={agentIds} interval={interval} />
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="m" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)
|
||||
);
|
||||
|
||||
ScheduledQueryExpandedContent.displayName = 'ScheduledQueryExpandedContent';
|
||||
|
||||
interface ScheduledQueryLastResultsProps {
|
||||
actionId: string;
|
||||
agentIds: string[];
|
||||
queryId: string;
|
||||
interval: number;
|
||||
toggleErrors: (payload: { queryId: string; interval: number }) => void;
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
const ScheduledQueryLastResults: React.FC<ScheduledQueryLastResultsProps> = ({
|
||||
actionId,
|
||||
agentIds,
|
||||
queryId,
|
||||
interval,
|
||||
toggleErrors,
|
||||
expanded,
|
||||
}) => {
|
||||
const { data: lastResultsData, isFetched } = useScheduledQueryGroupQueryLastResults({
|
||||
actionId,
|
||||
agentIds,
|
||||
interval,
|
||||
});
|
||||
|
||||
const { data: errorsData, isFetched: errorsFetched } = useScheduledQueryGroupQueryErrors({
|
||||
actionId,
|
||||
agentIds,
|
||||
interval,
|
||||
});
|
||||
|
||||
const handleErrorsToggle = useCallback(() => toggleErrors({ queryId, interval }), [
|
||||
queryId,
|
||||
interval,
|
||||
toggleErrors,
|
||||
]);
|
||||
|
||||
if (!isFetched || !errorsFetched) {
|
||||
return <EuiLoadingSpinner />;
|
||||
}
|
||||
|
||||
if (!lastResultsData) {
|
||||
return <>{'-'}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={4}>
|
||||
{lastResultsData.first_event_ingested_time?.value ? (
|
||||
<EuiToolTip content={lastResultsData.first_event_ingested_time?.value}>
|
||||
<>{moment(lastResultsData.first_event_ingested_time?.value).fromNow()}</>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={4}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiNotificationBadge color="subdued">
|
||||
{lastResultsData?.doc_count ?? 0}
|
||||
</EuiNotificationBadge>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{'Documents'}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={4}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiNotificationBadge color="subdued">
|
||||
{lastResultsData?.unique_agents?.value ?? 0}
|
||||
</EuiNotificationBadge>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{'Agents'}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={5}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiNotificationBadge color={errorsData?.total ? 'accent' : 'subdued'}>
|
||||
{errorsData?.total ?? 0}
|
||||
</EuiNotificationBadge>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>{'Errors'}</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
isDisabled={!errorsData?.total}
|
||||
onClick={handleErrorsToggle}
|
||||
iconType={expanded ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const getPackActionId = (actionId: string, packName: string) => `pack_${packName}_${actionId}`;
|
||||
|
||||
interface ScheduledQueryGroupQueriesStatusTableProps {
|
||||
agentIds?: string[];
|
||||
data: OsqueryManagerPackagePolicyInputStream[];
|
||||
scheduledQueryGroupName: string;
|
||||
}
|
||||
|
||||
const ScheduledQueryGroupQueriesStatusTableComponent: React.FC<ScheduledQueryGroupQueriesStatusTableProps> = ({
|
||||
agentIds,
|
||||
data,
|
||||
scheduledQueryGroupName,
|
||||
}) => {
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<
|
||||
Record<string, ReturnType<typeof ScheduledQueryExpandedContent>>
|
||||
>({});
|
||||
|
||||
const renderQueryColumn = useCallback(
|
||||
(query: string) => (
|
||||
<EuiCodeBlock language="sql" fontSize="s" paddingSize="none" transparentBackground>
|
||||
{query}
|
||||
</EuiCodeBlock>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const toggleErrors = useCallback(
|
||||
({ queryId, interval }: { queryId: string; interval: number }) => {
|
||||
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
|
||||
if (itemIdToExpandedRowMapValues[queryId]) {
|
||||
delete itemIdToExpandedRowMapValues[queryId];
|
||||
} else {
|
||||
itemIdToExpandedRowMapValues[queryId] = (
|
||||
<ScheduledQueryExpandedContent
|
||||
actionId={getPackActionId(queryId, scheduledQueryGroupName)}
|
||||
agentIds={agentIds}
|
||||
interval={interval}
|
||||
/>
|
||||
);
|
||||
}
|
||||
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
|
||||
},
|
||||
[agentIds, itemIdToExpandedRowMap, scheduledQueryGroupName]
|
||||
);
|
||||
|
||||
const renderLastResultsColumn = useCallback(
|
||||
(item) => (
|
||||
<ScheduledQueryLastResults
|
||||
// @ts-expect-error update types
|
||||
agentIds={agentIds}
|
||||
queryId={item.vars.id.value}
|
||||
actionId={getPackActionId(item.vars.id.value, scheduledQueryGroupName)}
|
||||
interval={item.vars?.interval.value}
|
||||
toggleErrors={toggleErrors}
|
||||
expanded={!!itemIdToExpandedRowMap[item.vars.id.value]}
|
||||
/>
|
||||
),
|
||||
[agentIds, itemIdToExpandedRowMap, scheduledQueryGroupName, toggleErrors]
|
||||
);
|
||||
|
||||
const renderDiscoverResultsAction = useCallback(
|
||||
(item) => (
|
||||
<ViewResultsInDiscoverAction
|
||||
actionId={getPackActionId(item.vars?.id.value, scheduledQueryGroupName)}
|
||||
agentIds={agentIds}
|
||||
buttonType={ViewResultsActionButtonType.icon}
|
||||
/>
|
||||
),
|
||||
[agentIds, scheduledQueryGroupName]
|
||||
);
|
||||
|
||||
const renderLensResultsAction = useCallback(
|
||||
(item) => (
|
||||
<ViewResultsInLensAction
|
||||
actionId={item.vars?.id.value}
|
||||
agentIds={agentIds}
|
||||
buttonType={ViewResultsActionButtonType.icon}
|
||||
/>
|
||||
),
|
||||
[agentIds]
|
||||
);
|
||||
|
||||
const getItemId = useCallback(
|
||||
(item: OsqueryManagerPackagePolicyInputStream) => get('vars.id.value', item),
|
||||
[]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'vars.id.value',
|
||||
name: i18n.translate('xpack.osquery.scheduledQueryGroup.queriesTable.idColumnTitle', {
|
||||
defaultMessage: 'ID',
|
||||
}),
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
field: 'vars.interval.value',
|
||||
name: i18n.translate('xpack.osquery.scheduledQueryGroup.queriesTable.intervalColumnTitle', {
|
||||
defaultMessage: 'Interval (s)',
|
||||
}),
|
||||
width: '80px',
|
||||
},
|
||||
{
|
||||
field: 'vars.query.value',
|
||||
name: i18n.translate('xpack.osquery.scheduledQueryGroup.queriesTable.queryColumnTitle', {
|
||||
defaultMessage: 'Query',
|
||||
}),
|
||||
render: renderQueryColumn,
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queriesTable.lastResultsColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Last results',
|
||||
}
|
||||
),
|
||||
render: renderLastResultsColumn,
|
||||
},
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queriesTable.viewResultsColumnTitle',
|
||||
{
|
||||
defaultMessage: 'View results',
|
||||
}
|
||||
),
|
||||
width: '90px',
|
||||
actions: [
|
||||
{
|
||||
render: renderDiscoverResultsAction,
|
||||
},
|
||||
{
|
||||
render: renderLensResultsAction,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
renderQueryColumn,
|
||||
renderLastResultsColumn,
|
||||
renderDiscoverResultsAction,
|
||||
renderLensResultsAction,
|
||||
]
|
||||
);
|
||||
|
||||
const sorting = useMemo(
|
||||
() => ({
|
||||
sort: {
|
||||
field: 'vars.id.value' as keyof OsqueryManagerPackagePolicyInputStream,
|
||||
direction: 'asc' as const,
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiBasicTable<OsqueryManagerPackagePolicyInputStream>
|
||||
items={data}
|
||||
itemId={getItemId}
|
||||
columns={columns}
|
||||
sorting={sorting}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
isExpandable
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScheduledQueryGroupQueriesStatusTable = React.memo(
|
||||
ScheduledQueryGroupQueriesStatusTableComponent
|
||||
);
|
|
@ -6,288 +6,15 @@
|
|||
*/
|
||||
|
||||
import { get } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButtonEmpty,
|
||||
EuiCodeBlock,
|
||||
EuiButtonIcon,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { EuiBasicTable, EuiCodeBlock, EuiButtonIcon } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
TypedLensByValueInput,
|
||||
PersistedIndexPatternLayer,
|
||||
PieVisualizationState,
|
||||
} from '../../../lens/public';
|
||||
import { FilterStateStore } from '../../../../../src/plugins/data/common';
|
||||
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
|
||||
import { PlatformIcons } from './queries/platforms';
|
||||
import { OsqueryManagerPackagePolicyInputStream } from '../../common/types';
|
||||
|
||||
const VIEW_IN_DISCOVER = i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queriesTable.viewDiscoverResultsActionAriaLabel',
|
||||
{
|
||||
defaultMessage: 'View in Discover',
|
||||
}
|
||||
);
|
||||
|
||||
const VIEW_IN_LENS = i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queriesTable.viewLensResultsActionAriaLabel',
|
||||
{
|
||||
defaultMessage: 'View in Lens',
|
||||
}
|
||||
);
|
||||
|
||||
export enum ViewResultsActionButtonType {
|
||||
icon = 'icon',
|
||||
button = 'button',
|
||||
}
|
||||
|
||||
interface ViewResultsInDiscoverActionProps {
|
||||
actionId: string;
|
||||
buttonType: ViewResultsActionButtonType;
|
||||
endDate?: string;
|
||||
startDate?: string;
|
||||
}
|
||||
|
||||
function getLensAttributes(actionId: string): TypedLensByValueInput['attributes'] {
|
||||
const dataLayer: PersistedIndexPatternLayer = {
|
||||
columnOrder: ['8690befd-fd69-4246-af4a-dd485d2a3b38', 'ed999e9d-204c-465b-897f-fe1a125b39ed'],
|
||||
columns: {
|
||||
'8690befd-fd69-4246-af4a-dd485d2a3b38': {
|
||||
sourceField: 'type',
|
||||
isBucketed: true,
|
||||
dataType: 'string',
|
||||
scale: 'ordinal',
|
||||
operationType: 'terms',
|
||||
label: 'Top values of type',
|
||||
params: {
|
||||
otherBucket: true,
|
||||
size: 5,
|
||||
missingBucket: false,
|
||||
orderBy: {
|
||||
columnId: 'ed999e9d-204c-465b-897f-fe1a125b39ed',
|
||||
type: 'column',
|
||||
},
|
||||
orderDirection: 'desc',
|
||||
},
|
||||
},
|
||||
'ed999e9d-204c-465b-897f-fe1a125b39ed': {
|
||||
sourceField: 'Records',
|
||||
isBucketed: false,
|
||||
dataType: 'number',
|
||||
scale: 'ratio',
|
||||
operationType: 'count',
|
||||
label: 'Count of records',
|
||||
},
|
||||
},
|
||||
incompleteColumns: {},
|
||||
};
|
||||
|
||||
const xyConfig: PieVisualizationState = {
|
||||
shape: 'pie',
|
||||
layers: [
|
||||
{
|
||||
legendDisplay: 'default',
|
||||
nestedLegend: false,
|
||||
layerId: 'layer1',
|
||||
layerType: 'data',
|
||||
metric: 'ed999e9d-204c-465b-897f-fe1a125b39ed',
|
||||
numberDisplay: 'percent',
|
||||
groups: ['8690befd-fd69-4246-af4a-dd485d2a3b38'],
|
||||
categoryDisplay: 'default',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return {
|
||||
visualizationType: 'lnsPie',
|
||||
title: `Action ${actionId} results`,
|
||||
references: [
|
||||
{
|
||||
id: 'logs-*',
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: 'logs-*',
|
||||
name: 'indexpattern-datasource-layer-layer1',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
name: 'filter-index-pattern-0',
|
||||
id: 'logs-*',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
state: {
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
layers: {
|
||||
layer1: dataLayer,
|
||||
},
|
||||
},
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
$state: { store: FilterStateStore.APP_STATE },
|
||||
meta: {
|
||||
indexRefName: 'filter-index-pattern-0',
|
||||
negate: false,
|
||||
alias: null,
|
||||
disabled: false,
|
||||
params: {
|
||||
query: actionId,
|
||||
},
|
||||
type: 'phrase',
|
||||
key: 'action_id',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
action_id: actionId,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
query: { language: 'kuery', query: '' },
|
||||
visualization: xyConfig,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ViewResultsInLensActionComponent: React.FC<ViewResultsInDiscoverActionProps> = ({
|
||||
actionId,
|
||||
buttonType,
|
||||
endDate,
|
||||
startDate,
|
||||
}) => {
|
||||
const lensService = useKibana().services.lens;
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event) => {
|
||||
const openInNewTab = !(!isModifiedEvent(event) && isLeftClickEvent(event));
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
lensService?.navigateToPrefilledEditor(
|
||||
{
|
||||
id: '',
|
||||
timeRange: {
|
||||
from: startDate ?? 'now-1d',
|
||||
to: endDate ?? 'now',
|
||||
mode: startDate || endDate ? 'absolute' : 'relative',
|
||||
},
|
||||
attributes: getLensAttributes(actionId),
|
||||
},
|
||||
{
|
||||
openInNewTab,
|
||||
}
|
||||
);
|
||||
},
|
||||
[actionId, endDate, lensService, startDate]
|
||||
);
|
||||
|
||||
if (buttonType === ViewResultsActionButtonType.button) {
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
iconType="lensApp"
|
||||
onClick={handleClick}
|
||||
disabled={!lensService?.canUseEditor()}
|
||||
>
|
||||
{VIEW_IN_LENS}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiToolTip content={VIEW_IN_LENS}>
|
||||
<EuiButtonIcon
|
||||
iconType="lensApp"
|
||||
disabled={!lensService?.canUseEditor()}
|
||||
onClick={handleClick}
|
||||
aria-label={VIEW_IN_LENS}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
export const ViewResultsInLensAction = React.memo(ViewResultsInLensActionComponent);
|
||||
|
||||
const ViewResultsInDiscoverActionComponent: React.FC<ViewResultsInDiscoverActionProps> = ({
|
||||
actionId,
|
||||
buttonType,
|
||||
endDate,
|
||||
startDate,
|
||||
}) => {
|
||||
const urlGenerator = useKibana().services.discover?.urlGenerator;
|
||||
const [discoverUrl, setDiscoverUrl] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const getDiscoverUrl = async () => {
|
||||
if (!urlGenerator?.createUrl) return;
|
||||
|
||||
const newUrl = await urlGenerator.createUrl({
|
||||
indexPatternId: 'logs-*',
|
||||
filters: [
|
||||
{
|
||||
meta: {
|
||||
index: 'logs-*',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'action_id',
|
||||
params: { query: actionId },
|
||||
},
|
||||
query: { match_phrase: { action_id: actionId } },
|
||||
$state: { store: FilterStateStore.APP_STATE },
|
||||
},
|
||||
],
|
||||
refreshInterval: {
|
||||
pause: true,
|
||||
value: 0,
|
||||
},
|
||||
timeRange:
|
||||
startDate && endDate
|
||||
? {
|
||||
to: endDate,
|
||||
from: startDate,
|
||||
mode: 'absolute',
|
||||
}
|
||||
: {
|
||||
to: 'now',
|
||||
from: 'now-1d',
|
||||
mode: 'relative',
|
||||
},
|
||||
});
|
||||
setDiscoverUrl(newUrl);
|
||||
};
|
||||
getDiscoverUrl();
|
||||
}, [actionId, endDate, startDate, urlGenerator]);
|
||||
|
||||
if (buttonType === ViewResultsActionButtonType.button) {
|
||||
return (
|
||||
<EuiButtonEmpty size="xs" iconType="discoverApp" href={discoverUrl}>
|
||||
{VIEW_IN_DISCOVER}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiToolTip content={VIEW_IN_DISCOVER}>
|
||||
<EuiButtonIcon iconType="discoverApp" href={discoverUrl} aria-label={VIEW_IN_DISCOVER} />
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
export const ViewResultsInDiscoverAction = React.memo(ViewResultsInDiscoverActionComponent);
|
||||
|
||||
interface ScheduledQueryGroupQueriesTableProps {
|
||||
data: OsqueryManagerPackagePolicyInputStream[];
|
||||
editMode?: boolean;
|
||||
onDeleteClick?: (item: OsqueryManagerPackagePolicyInputStream) => void;
|
||||
onEditClick?: (item: OsqueryManagerPackagePolicyInputStream) => void;
|
||||
selectedItems?: OsqueryManagerPackagePolicyInputStream[];
|
||||
|
@ -296,7 +23,6 @@ interface ScheduledQueryGroupQueriesTableProps {
|
|||
|
||||
const ScheduledQueryGroupQueriesTableComponent: React.FC<ScheduledQueryGroupQueriesTableProps> = ({
|
||||
data,
|
||||
editMode = false,
|
||||
onDeleteClick,
|
||||
onEditClick,
|
||||
selectedItems,
|
||||
|
@ -370,26 +96,6 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC<ScheduledQueryGroupQuer
|
|||
[]
|
||||
);
|
||||
|
||||
const renderDiscoverResultsAction = useCallback(
|
||||
(item) => (
|
||||
<ViewResultsInDiscoverAction
|
||||
actionId={item.vars?.id.value}
|
||||
buttonType={ViewResultsActionButtonType.icon}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const renderLensResultsAction = useCallback(
|
||||
(item) => (
|
||||
<ViewResultsInLensAction
|
||||
actionId={item.vars?.id.value}
|
||||
buttonType={ViewResultsActionButtonType.icon}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
@ -428,42 +134,23 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC<ScheduledQueryGroupQuer
|
|||
render: renderVersionColumn,
|
||||
},
|
||||
{
|
||||
name: editMode
|
||||
? i18n.translate('xpack.osquery.scheduledQueryGroup.queriesTable.actionsColumnTitle', {
|
||||
defaultMessage: 'Actions',
|
||||
})
|
||||
: i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queriesTable.viewResultsColumnTitle',
|
||||
{
|
||||
defaultMessage: 'View results',
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.osquery.scheduledQueryGroup.queriesTable.actionsColumnTitle', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
width: '120px',
|
||||
actions: editMode
|
||||
? [
|
||||
{
|
||||
render: renderEditAction,
|
||||
},
|
||||
{
|
||||
render: renderDeleteAction,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
render: renderDiscoverResultsAction,
|
||||
},
|
||||
{
|
||||
render: renderLensResultsAction,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
render: renderEditAction,
|
||||
},
|
||||
{
|
||||
render: renderDeleteAction,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
editMode,
|
||||
renderDeleteAction,
|
||||
renderDiscoverResultsAction,
|
||||
renderEditAction,
|
||||
renderLensResultsAction,
|
||||
renderPlatformColumn,
|
||||
renderQueryColumn,
|
||||
renderVersionColumn,
|
||||
|
@ -499,8 +186,8 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC<ScheduledQueryGroupQuer
|
|||
itemId={itemId}
|
||||
columns={columns}
|
||||
sorting={sorting}
|
||||
selection={editMode ? selection : undefined}
|
||||
isSelectable={editMode}
|
||||
selection={selection}
|
||||
isSelectable
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { EuiInMemoryTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui';
|
||||
import moment from 'moment';
|
||||
import moment from 'moment-timezone';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
|
|
@ -33,6 +33,10 @@ export const useScheduledQueryGroup = ({
|
|||
keepPreviousData: true,
|
||||
enabled: !skip || !scheduledQueryGroupId,
|
||||
select: (response) => response.item,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: Infinity,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 { useQuery } from 'react-query';
|
||||
import { SortDirection } from '../../../../../src/plugins/data/common';
|
||||
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
|
||||
interface UseScheduledQueryGroupQueryErrorsProps {
|
||||
actionId: string;
|
||||
agentIds?: string[];
|
||||
interval: number;
|
||||
skip?: boolean;
|
||||
}
|
||||
|
||||
export const useScheduledQueryGroupQueryErrors = ({
|
||||
actionId,
|
||||
agentIds,
|
||||
interval,
|
||||
skip = false,
|
||||
}: UseScheduledQueryGroupQueryErrorsProps) => {
|
||||
const data = useKibana().services.data;
|
||||
|
||||
return useQuery(
|
||||
['scheduledQueryErrors', { actionId, interval }],
|
||||
async () => {
|
||||
const indexPattern = await data.indexPatterns.find('logs-*');
|
||||
const searchSource = await data.search.searchSource.create({
|
||||
index: indexPattern[0],
|
||||
fields: ['*'],
|
||||
sort: [
|
||||
{
|
||||
'@timestamp': SortDirection.desc,
|
||||
},
|
||||
],
|
||||
query: {
|
||||
// @ts-expect-error update types
|
||||
bool: {
|
||||
should: agentIds?.map((agentId) => ({
|
||||
match_phrase: {
|
||||
'elastic_agent.id': agentId,
|
||||
},
|
||||
})),
|
||||
minimum_should_match: 1,
|
||||
filter: [
|
||||
{
|
||||
match_phrase: {
|
||||
message: 'Error',
|
||||
},
|
||||
},
|
||||
{
|
||||
match_phrase: {
|
||||
'data_stream.dataset': 'elastic_agent.osquerybeat',
|
||||
},
|
||||
},
|
||||
{
|
||||
match_phrase: {
|
||||
message: actionId,
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: `now-${interval * 2}s`,
|
||||
lte: 'now',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
size: 1000,
|
||||
});
|
||||
|
||||
return searchSource.fetch$().toPromise();
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
enabled: !!(!skip && actionId && interval && agentIds?.length),
|
||||
select: (response) => response.rawResponse.hits ?? [],
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: Infinity,
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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 { useQuery } from 'react-query';
|
||||
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
|
||||
interface UseScheduledQueryGroupQueryLastResultsProps {
|
||||
actionId: string;
|
||||
agentIds?: string[];
|
||||
interval: number;
|
||||
skip?: boolean;
|
||||
}
|
||||
|
||||
export const useScheduledQueryGroupQueryLastResults = ({
|
||||
actionId,
|
||||
agentIds,
|
||||
interval,
|
||||
skip = false,
|
||||
}: UseScheduledQueryGroupQueryLastResultsProps) => {
|
||||
const data = useKibana().services.data;
|
||||
|
||||
return useQuery(
|
||||
['scheduledQueryLastResults', { actionId }],
|
||||
async () => {
|
||||
const indexPattern = await data.indexPatterns.find('logs-*');
|
||||
const searchSource = await data.search.searchSource.create({
|
||||
index: indexPattern[0],
|
||||
size: 0,
|
||||
aggs: {
|
||||
runs: {
|
||||
terms: {
|
||||
field: 'response_id',
|
||||
order: { first_event_ingested_time: 'desc' },
|
||||
size: 1,
|
||||
},
|
||||
aggs: {
|
||||
first_event_ingested_time: { min: { field: '@timestamp' } },
|
||||
unique_agents: { cardinality: { field: 'agent.id' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
query: {
|
||||
// @ts-expect-error update types
|
||||
bool: {
|
||||
should: agentIds?.map((agentId) => ({
|
||||
match_phrase: {
|
||||
'agent.id': agentId,
|
||||
},
|
||||
})),
|
||||
minimum_should_match: 1,
|
||||
filter: [
|
||||
{
|
||||
match_phrase: {
|
||||
action_id: actionId,
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: `now-${interval * 2}s`,
|
||||
lte: 'now',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return searchSource.fetch$().toPromise();
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
enabled: !!(!skip && actionId && interval && agentIds?.length),
|
||||
// @ts-expect-error update types
|
||||
select: (response) => response.rawResponse.aggregations?.runs?.buckets[0] ?? [],
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: Infinity,
|
||||
}
|
||||
);
|
||||
};
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import uuid from 'uuid';
|
||||
import moment from 'moment';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { PLUGIN_ID } from '../../../common';
|
||||
import { IRouter } from '../../../../../../src/core/server';
|
||||
|
|
|
@ -23271,10 +23271,10 @@ react-popper@^2.2.4:
|
|||
react-fast-compare "^3.0.1"
|
||||
warning "^4.0.2"
|
||||
|
||||
react-query@^3.18.1:
|
||||
version "3.18.1"
|
||||
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.18.1.tgz#893b5475a7b4add099e007105317446f7a2cd310"
|
||||
integrity sha512-17lv3pQxU9n+cB5acUv0/cxNTjo9q8G+RsedC6Ax4V9D8xEM7Q5xf9xAbCPdEhDrrnzPjTls9fQEABKRSi7OJA==
|
||||
react-query@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.21.0.tgz#2e099a7906c38eeeb750e8b9b12121a21fa8d9ef"
|
||||
integrity sha512-5rY5J8OD9f4EdkytjSsdCO+pqbJWKwSIMETfh/UyxqyjLURHE0IhlB+IPNPrzzu/dzK0rRxi5p0IkcCdSfizDQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
broadcast-channel "^3.4.1"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue