mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Osquery] Fix 7.14 UX issues (#104257)
This commit is contained in:
parent
6d9d1db6ac
commit
fe6eb09936
34 changed files with 625 additions and 435 deletions
|
@ -351,7 +351,7 @@
|
|||
"react-moment-proptypes": "^1.7.0",
|
||||
"react-monaco-editor": "^0.41.2",
|
||||
"react-popper-tooltip": "^2.10.1",
|
||||
"react-query": "^3.13.10",
|
||||
"react-query": "^3.18.1",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-resizable": "^1.7.5",
|
||||
"react-resize-detector": "^4.2.0",
|
||||
|
|
|
@ -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 { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Direction } from '../../common/search_strategy';
|
||||
import { AgentStatusBar } from './action_agents_status_bar';
|
||||
import { ActionAgentsStatusBadges } from './action_agents_status_badges';
|
||||
import { useActionResults } from './use_action_results';
|
||||
|
||||
interface ActionAgentsStatusProps {
|
||||
actionId: string;
|
||||
expirationDate?: string;
|
||||
agentIds?: string[];
|
||||
}
|
||||
|
||||
const ActionAgentsStatusComponent: React.FC<ActionAgentsStatusProps> = ({
|
||||
actionId,
|
||||
expirationDate,
|
||||
agentIds,
|
||||
}) => {
|
||||
const [isLive, setIsLive] = useState(true);
|
||||
const expired = useMemo(() => (!expirationDate ? false : new Date(expirationDate) < new Date()), [
|
||||
expirationDate,
|
||||
]);
|
||||
const {
|
||||
// @ts-expect-error update types
|
||||
data: { aggregations },
|
||||
} = useActionResults({
|
||||
actionId,
|
||||
activePage: 0,
|
||||
agentIds,
|
||||
limit: 0,
|
||||
direction: Direction.asc,
|
||||
sortField: '@timestamp',
|
||||
isLive,
|
||||
});
|
||||
|
||||
const agentStatus = useMemo(() => {
|
||||
const notRespondedCount = !agentIds?.length ? 0 : agentIds.length - aggregations.totalResponded;
|
||||
|
||||
return {
|
||||
success: aggregations.successful,
|
||||
pending: notRespondedCount,
|
||||
failed: aggregations.failed,
|
||||
};
|
||||
}, [agentIds?.length, aggregations.failed, aggregations.successful, aggregations.totalResponded]);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
setIsLive(() => {
|
||||
if (!agentIds?.length || expired) return false;
|
||||
|
||||
return !!(aggregations.totalResponded !== agentIds?.length);
|
||||
}),
|
||||
[agentIds?.length, aggregations.totalResponded, expired]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiText size="xs" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryActionResults.summary.agentsQueriedLabelText"
|
||||
defaultMessage="Queried {count, plural, one {# agent} other {# agents}}"
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
values={{ count: agentIds?.length }}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ActionAgentsStatusBadges expired={expired} agentStatus={agentStatus} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<AgentStatusBar agentStatus={agentStatus} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ActionAgentsStatus = React.memo(ActionAgentsStatusComponent);
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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, EuiHealth, EuiNotificationBadge, EuiFlexItem } from '@elastic/eui';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import {
|
||||
AGENT_STATUSES,
|
||||
getColorForAgentStatus,
|
||||
getLabelForAgentStatus,
|
||||
} from './services/agent_status';
|
||||
import type { ActionAgentStatus } from './types';
|
||||
|
||||
export const ActionAgentsStatusBadges = memo<{
|
||||
agentStatus: { [k in ActionAgentStatus]: number };
|
||||
expired: boolean;
|
||||
}>(({ agentStatus, expired }) => (
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
{AGENT_STATUSES.map((status) => (
|
||||
<EuiFlexItem key={status} grow={false}>
|
||||
<AgentStatusBadge expired={expired} status={status} count={agentStatus[status] || 0} />
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
));
|
||||
|
||||
ActionAgentsStatusBadges.displayName = 'ActionAgentsStatusBadges';
|
||||
|
||||
const AgentStatusBadge = memo<{ expired: boolean; status: ActionAgentStatus; count: number }>(
|
||||
({ expired, status, count }) => (
|
||||
<>
|
||||
<EuiHealth color={getColorForAgentStatus(status)}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>{getLabelForAgentStatus(status, expired)}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiNotificationBadge size="s" color="subdued">
|
||||
{count}
|
||||
</EuiNotificationBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiHealth>
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
AgentStatusBadge.displayName = 'AgentStatusBadge';
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 styled from 'styled-components';
|
||||
import { EuiColorPaletteDisplay } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AGENT_STATUSES, getColorForAgentStatus } from './services/agent_status';
|
||||
import type { ActionAgentStatus } from './types';
|
||||
|
||||
const StyledEuiColorPaletteDisplay = styled(EuiColorPaletteDisplay)`
|
||||
&.osquery-action-agent-status-bar {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
&:after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const AgentStatusBar: React.FC<{
|
||||
agentStatus: { [k in ActionAgentStatus]: number };
|
||||
}> = ({ agentStatus }) => {
|
||||
const palette = useMemo(() => {
|
||||
let stop = 0;
|
||||
return AGENT_STATUSES.reduce((acc, status) => {
|
||||
stop += agentStatus[status] || 0;
|
||||
acc.push({
|
||||
stop,
|
||||
color: getColorForAgentStatus(status),
|
||||
});
|
||||
return acc;
|
||||
}, [] as Array<{ stop: number; color: string }>);
|
||||
}, [agentStatus]);
|
||||
return (
|
||||
<StyledEuiColorPaletteDisplay
|
||||
className="osquery-action-agent-status-bar"
|
||||
size="s"
|
||||
palette={palette}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -8,20 +8,8 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiLink,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiCard,
|
||||
EuiTextColor,
|
||||
EuiSpacer,
|
||||
EuiDescriptionList,
|
||||
EuiInMemoryTable,
|
||||
EuiCodeBlock,
|
||||
EuiProgress,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiLink, EuiInMemoryTable, EuiCodeBlock } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { PLUGIN_ID } from '../../../fleet/common';
|
||||
import { pagePathGetters } from '../../../fleet/public';
|
||||
|
@ -30,15 +18,10 @@ import { useAllResults } from '../results/use_all_results';
|
|||
import { Direction } from '../../common/search_strategy';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
|
||||
const StyledEuiCard = styled(EuiCard)`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
interface ActionResultsSummaryProps {
|
||||
actionId: string;
|
||||
expirationDate: Date;
|
||||
expirationDate?: string;
|
||||
agentIds?: string[];
|
||||
isLive?: boolean;
|
||||
}
|
||||
|
||||
const renderErrorMessage = (error: string) => (
|
||||
|
@ -51,14 +34,16 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
|
|||
actionId,
|
||||
expirationDate,
|
||||
agentIds,
|
||||
isLive,
|
||||
}) => {
|
||||
const getUrlForApp = useKibana().services.application.getUrlForApp;
|
||||
// @ts-expect-error update types
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
// @ts-expect-error update types
|
||||
const [pageSize, setPageSize] = useState(50);
|
||||
const expired = useMemo(() => expirationDate < new Date(), [expirationDate]);
|
||||
const expired = useMemo(() => (!expirationDate ? false : new Date(expirationDate) < new Date()), [
|
||||
expirationDate,
|
||||
]);
|
||||
const [isLive, setIsLive] = useState(true);
|
||||
const {
|
||||
// @ts-expect-error update types
|
||||
data: { aggregations, edges },
|
||||
|
@ -69,7 +54,7 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
|
|||
limit: pageSize,
|
||||
direction: Direction.asc,
|
||||
sortField: '@timestamp',
|
||||
isLive: !expired && isLive,
|
||||
isLive,
|
||||
});
|
||||
|
||||
const { data: logsResults } = useAllResults({
|
||||
|
@ -82,64 +67,15 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
|
|||
direction: Direction.asc,
|
||||
},
|
||||
],
|
||||
isLive: !expired && isLive,
|
||||
isLive,
|
||||
});
|
||||
|
||||
const notRespondedCount = useMemo(() => {
|
||||
if (!agentIds || !aggregations.totalResponded) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return agentIds.length - aggregations.totalResponded;
|
||||
}, [aggregations.totalResponded, agentIds]);
|
||||
|
||||
const listItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: i18n.translate(
|
||||
'xpack.osquery.liveQueryActionResults.summary.agentsQueriedLabelText',
|
||||
{
|
||||
defaultMessage: 'Agents queried',
|
||||
}
|
||||
),
|
||||
description: agentIds?.length,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.osquery.liveQueryActionResults.summary.successfulLabelText', {
|
||||
defaultMessage: 'Successful',
|
||||
}),
|
||||
description: aggregations.successful,
|
||||
},
|
||||
{
|
||||
title: expired
|
||||
? i18n.translate('xpack.osquery.liveQueryActionResults.summary.expiredLabelText', {
|
||||
defaultMessage: 'Expired',
|
||||
})
|
||||
: i18n.translate('xpack.osquery.liveQueryActionResults.summary.pendingLabelText', {
|
||||
defaultMessage: 'Not yet responded',
|
||||
}),
|
||||
description: notRespondedCount,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.osquery.liveQueryActionResults.summary.failedLabelText', {
|
||||
defaultMessage: 'Failed',
|
||||
}),
|
||||
description: (
|
||||
<EuiTextColor color={aggregations.failed ? 'danger' : 'default'}>
|
||||
{aggregations.failed}
|
||||
</EuiTextColor>
|
||||
),
|
||||
},
|
||||
],
|
||||
[agentIds, aggregations.failed, aggregations.successful, notRespondedCount, expired]
|
||||
);
|
||||
|
||||
const renderAgentIdColumn = useCallback(
|
||||
(agentId) => (
|
||||
<EuiLink
|
||||
className="eui-textTruncate"
|
||||
href={getUrlForApp(PLUGIN_ID, {
|
||||
path: `#` + pagePathGetters.agent_details({ agentId }),
|
||||
path: `#` + pagePathGetters.agent_details({ agentId })[1],
|
||||
})}
|
||||
target="_blank"
|
||||
>
|
||||
|
@ -236,30 +172,26 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
|
|||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledEuiCard title="" description="" textAlign="left">
|
||||
{!expired && notRespondedCount ? <EuiProgress size="xs" position="absolute" /> : null}
|
||||
<EuiDescriptionList
|
||||
compressed
|
||||
textStyle="reverse"
|
||||
type="responsiveColumn"
|
||||
listItems={listItems}
|
||||
/>
|
||||
</StyledEuiCard>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
useEffect(() => {
|
||||
setIsLive(() => {
|
||||
if (!agentIds?.length || expired) return false;
|
||||
|
||||
{edges.length ? (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiInMemoryTable items={edges} columns={columns} pagination={pagination} />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
const uniqueAgentsRepliedCount =
|
||||
// @ts-expect-error update types
|
||||
logsResults?.rawResponse.aggregations?.unique_agents.value ?? 0;
|
||||
|
||||
return !!(uniqueAgentsRepliedCount !== agentIds?.length - aggregations.failed);
|
||||
});
|
||||
}, [
|
||||
agentIds?.length,
|
||||
aggregations.failed,
|
||||
expired,
|
||||
logsResults?.rawResponse.aggregations?.unique_agents,
|
||||
]);
|
||||
|
||||
return edges.length ? (
|
||||
<EuiInMemoryTable loading={isLive} items={edges} columns={columns} pagination={pagination} />
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const ActionResultsSummary = React.memo(ActionResultsSummaryComponent);
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { euiPaletteColorBlindBehindText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import type { ActionAgentStatus } from '../types';
|
||||
|
||||
const visColors = euiPaletteColorBlindBehindText();
|
||||
const colorToHexMap = {
|
||||
default: '#d3dae6',
|
||||
primary: visColors[1],
|
||||
secondary: visColors[0],
|
||||
accent: visColors[2],
|
||||
warning: visColors[5],
|
||||
danger: visColors[9],
|
||||
};
|
||||
|
||||
export const AGENT_STATUSES: ActionAgentStatus[] = ['success', 'pending', 'failed'];
|
||||
|
||||
export function getColorForAgentStatus(agentStatus: ActionAgentStatus): string {
|
||||
switch (agentStatus) {
|
||||
case 'success':
|
||||
return colorToHexMap.secondary;
|
||||
case 'pending':
|
||||
return colorToHexMap.default;
|
||||
case 'failed':
|
||||
return colorToHexMap.danger;
|
||||
default:
|
||||
throw new Error(`Unsupported action agent status ${agentStatus}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function getLabelForAgentStatus(agentStatus: ActionAgentStatus, expired: boolean): string {
|
||||
switch (agentStatus) {
|
||||
case 'success':
|
||||
return i18n.translate('xpack.osquery.liveQueryActionResults.summary.successfulLabelText', {
|
||||
defaultMessage: 'Successful',
|
||||
});
|
||||
case 'pending':
|
||||
return expired
|
||||
? i18n.translate('xpack.osquery.liveQueryActionResults.summary.expiredLabelText', {
|
||||
defaultMessage: 'Expired',
|
||||
})
|
||||
: i18n.translate('xpack.osquery.liveQueryActionResults.summary.pendingLabelText', {
|
||||
defaultMessage: 'Not yet responded',
|
||||
});
|
||||
case 'failed':
|
||||
return i18n.translate('xpack.osquery.liveQueryActionResults.summary.failedLabelText', {
|
||||
defaultMessage: 'Failed',
|
||||
});
|
||||
default:
|
||||
throw new Error(`Unsupported action agent status ${agentStatus}`);
|
||||
}
|
||||
}
|
8
x-pack/plugins/osquery/public/action_results/types.ts
Normal file
8
x-pack/plugins/osquery/public/action_results/types.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 type ActionAgentStatus = 'success' | 'pending' | 'failed';
|
|
@ -83,7 +83,7 @@ export const useActionResults = ({
|
|||
|
||||
const totalResponded =
|
||||
// @ts-expect-error update types
|
||||
responseData.rawResponse?.aggregations?.aggs.responses_by_action_id?.doc_count;
|
||||
responseData.rawResponse?.aggregations?.aggs.responses_by_action_id?.doc_count ?? 0;
|
||||
const aggsBuckets =
|
||||
// @ts-expect-error update types
|
||||
responseData.rawResponse?.aggregations?.aggs.responses_by_action_id?.responses.buckets;
|
||||
|
@ -120,7 +120,7 @@ export const useActionResults = ({
|
|||
failed: 0,
|
||||
},
|
||||
},
|
||||
refetchInterval: isLive ? 1000 : false,
|
||||
refetchInterval: isLive ? 5000 : false,
|
||||
keepPreviousData: true,
|
||||
enabled: !skip && !!agentIds?.length,
|
||||
onSuccess: () => setErrorToast(),
|
||||
|
|
|
@ -27,7 +27,7 @@ const AgentsPolicyLinkComponent: React.FC<AgentsPolicyLinkProps> = ({ policyId }
|
|||
const href = useMemo(
|
||||
() =>
|
||||
getUrlForApp(PLUGIN_ID, {
|
||||
path: `#` + pagePathGetters.policy_details({ policyId }),
|
||||
path: `#` + pagePathGetters.policy_details({ policyId })[1],
|
||||
}),
|
||||
[getUrlForApp, policyId]
|
||||
);
|
||||
|
@ -38,7 +38,7 @@ const AgentsPolicyLinkComponent: React.FC<AgentsPolicyLinkProps> = ({ policyId }
|
|||
event.preventDefault();
|
||||
|
||||
return navigateToApp(PLUGIN_ID, {
|
||||
path: `#` + pagePathGetters.policy_details({ policyId }),
|
||||
path: `#` + pagePathGetters.policy_details({ policyId })[1],
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -30,7 +30,6 @@ export const useAgentPolicies = () => {
|
|||
}),
|
||||
{
|
||||
initialData: { items: [], total: 0, page: 1, perPage: 100 },
|
||||
placeholderData: [],
|
||||
keepPreviousData: true,
|
||||
select: (response) => response.items,
|
||||
onSuccess: () => setErrorToast(),
|
||||
|
|
|
@ -57,7 +57,7 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
|
|||
return getUrlForApp(PLUGIN_ID, {
|
||||
path:
|
||||
`#` +
|
||||
pagePathGetters.policy_details({ policyId: policy?.policy_id }) +
|
||||
pagePathGetters.policy_details({ policyId: policy?.policy_id })[1] +
|
||||
'?openEnrollmentFlyout=true',
|
||||
});
|
||||
}, [getUrlForApp, policy?.policy_id]);
|
||||
|
|
|
@ -13,4 +13,4 @@ import { OsqueryPlugin } from './plugin';
|
|||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new OsqueryPlugin(initializerContext);
|
||||
}
|
||||
export { OsqueryPluginSetup, OsqueryPluginStart } from './types';
|
||||
export type { OsqueryPluginSetup, OsqueryPluginStart } from './types';
|
||||
|
|
|
@ -1,30 +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 { EuiCodeBlock, EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useActionDetails } from '../../actions/use_action_details';
|
||||
import { ResultsTable } from '../../results/results_table';
|
||||
|
||||
const QueryAgentResultsComponent = () => {
|
||||
const { actionId, agentId } = useParams<{ actionId: string; agentId: string }>();
|
||||
const { data } = useActionDetails({ actionId });
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiCodeBlock language="sql" fontSize="m" paddingSize="m">
|
||||
{data?.actionDetails._source?.data?.query}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer />
|
||||
<ResultsTable actionId={actionId} selectedAgent={agentId} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const QueryAgentResults = React.memo(QueryAgentResultsComponent);
|
|
@ -18,6 +18,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import deepMerge from 'deepmerge';
|
||||
|
||||
import { UseField, Form, FormData, useForm, useFormData, FIELD_TYPES } from '../../shared_imports';
|
||||
import { AgentsTableField } from './agents_table_field';
|
||||
|
@ -33,12 +34,19 @@ const FORM_ID = 'liveQueryForm';
|
|||
|
||||
export const MAX_QUERY_LENGTH = 2000;
|
||||
|
||||
const GhostFormField = () => <></>;
|
||||
|
||||
interface LiveQueryFormProps {
|
||||
agentId?: string | undefined;
|
||||
defaultValue?: Partial<FormData> | undefined;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ defaultValue, onSuccess }) => {
|
||||
const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
||||
agentId,
|
||||
defaultValue,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const { http } = useKibana().services;
|
||||
const [showSavedQueryFlyout, setShowSavedQueryFlyout] = useState(false);
|
||||
const setErrorToast = useErrorToast();
|
||||
|
@ -71,8 +79,6 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ defaultValue, on
|
|||
}
|
||||
);
|
||||
|
||||
const expirationDate = useMemo(() => new Date(data?.actions[0].expiration), [data?.actions]);
|
||||
|
||||
const formSchema = {
|
||||
query: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
|
@ -100,9 +106,18 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ defaultValue, on
|
|||
options: {
|
||||
stripEmptyFields: false,
|
||||
},
|
||||
defaultValue: defaultValue ?? {
|
||||
query: '',
|
||||
},
|
||||
defaultValue: deepMerge(
|
||||
{
|
||||
agentSelection: {
|
||||
agents: [],
|
||||
allAgentsSelected: false,
|
||||
platformsSelected: [],
|
||||
policiesSelected: [],
|
||||
},
|
||||
query: '',
|
||||
},
|
||||
defaultValue ?? {}
|
||||
),
|
||||
});
|
||||
|
||||
const { submit } = form;
|
||||
|
@ -147,6 +162,59 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ defaultValue, on
|
|||
|
||||
const flyoutFormDefaultValue = useMemo(() => ({ query }), [query]);
|
||||
|
||||
const queryFieldStepContent = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<UseField
|
||||
path="query"
|
||||
component={LiveQueryQueryField}
|
||||
componentProps={queryComponentProps}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
{!agentId && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
disabled={!agentSelected || !queryValueProvided || resultsStatus === 'disabled'}
|
||||
onClick={handleShowSaveQueryFlout}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryForm.form.saveForLaterButtonLabel"
|
||||
defaultMessage="Save for later"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton disabled={!agentSelected || !queryValueProvided} onClick={submit}>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryForm.form.submitButtonLabel"
|
||||
defaultMessage="Submit"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
),
|
||||
[
|
||||
agentId,
|
||||
agentSelected,
|
||||
handleShowSaveQueryFlout,
|
||||
queryComponentProps,
|
||||
queryValueProvided,
|
||||
resultsStatus,
|
||||
submit,
|
||||
]
|
||||
);
|
||||
|
||||
const resultsStepContent = useMemo(
|
||||
() =>
|
||||
actionId ? (
|
||||
<ResultTabs actionId={actionId} endDate={data?.actions[0].expiration} agentIds={agentIds} />
|
||||
) : null,
|
||||
[actionId, agentIds, data?.actions]
|
||||
);
|
||||
|
||||
const formSteps: EuiContainedStepProps[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
@ -160,73 +228,34 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ defaultValue, on
|
|||
title: i18n.translate('xpack.osquery.liveQueryForm.steps.queryStepHeading', {
|
||||
defaultMessage: 'Enter query',
|
||||
}),
|
||||
children: (
|
||||
<>
|
||||
<UseField
|
||||
path="query"
|
||||
component={LiveQueryQueryField}
|
||||
componentProps={queryComponentProps}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
disabled={!agentSelected || !queryValueProvided || resultsStatus === 'disabled'}
|
||||
onClick={handleShowSaveQueryFlout}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryForm.form.saveForLaterButtonLabel"
|
||||
defaultMessage="Save for later"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton disabled={!agentSelected || !queryValueProvided} onClick={submit}>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryForm.form.submitButtonLabel"
|
||||
defaultMessage="Submit"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
),
|
||||
children: queryFieldStepContent,
|
||||
status: queryStatus,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.osquery.liveQueryForm.steps.resultsStepHeading', {
|
||||
defaultMessage: 'Check results',
|
||||
}),
|
||||
children: actionId ? (
|
||||
<ResultTabs
|
||||
actionId={actionId}
|
||||
expirationDate={expirationDate}
|
||||
agentIds={agentIds}
|
||||
isLive={true}
|
||||
/>
|
||||
) : null,
|
||||
children: resultsStepContent,
|
||||
status: resultsStatus,
|
||||
},
|
||||
],
|
||||
[
|
||||
actionId,
|
||||
agentIds,
|
||||
agentSelected,
|
||||
handleShowSaveQueryFlout,
|
||||
queryComponentProps,
|
||||
queryStatus,
|
||||
queryValueProvided,
|
||||
expirationDate,
|
||||
resultsStatus,
|
||||
submit,
|
||||
]
|
||||
[agentSelected, queryFieldStepContent, queryStatus, resultsStepContent, resultsStatus]
|
||||
);
|
||||
|
||||
const singleAgentForm = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup direction="column">
|
||||
<UseField path="agentSelection" component={GhostFormField} />
|
||||
<EuiFlexItem>{queryFieldStepContent}</EuiFlexItem>
|
||||
<EuiFlexItem>{resultsStepContent}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[queryFieldStepContent, resultsStepContent]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form form={form}>
|
||||
<EuiSteps steps={formSteps} />
|
||||
</Form>
|
||||
<Form form={form}>{agentId ? singleAgentForm : <EuiSteps steps={formSteps} />}</Form>
|
||||
{showSavedQueryFlyout ? (
|
||||
<SavedQueryFlyout
|
||||
onClose={handleCloseSaveQueryFlout}
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
LazyOsqueryManagedPolicyEditExtension,
|
||||
LazyOsqueryManagedCustomButtonExtension,
|
||||
} from './fleet_integration';
|
||||
import { getLazyOsqueryAction } from './shared_components';
|
||||
|
||||
export function toggleOsqueryPlugin(
|
||||
updater$: Subject<AppUpdater>,
|
||||
|
@ -160,7 +161,14 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
}));
|
||||
}
|
||||
|
||||
return {};
|
||||
return {
|
||||
OsqueryAction: getLazyOsqueryAction({
|
||||
...core,
|
||||
...plugins,
|
||||
storage: this.storage,
|
||||
kibanaVersion: this.kibanaVersion,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
|
|
|
@ -1,78 +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 { EuiTabbedContent, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { ResultsTable } from '../../results/results_table';
|
||||
import { ActionResultsSummary } from '../../action_results/action_results_summary';
|
||||
|
||||
interface ResultTabsProps {
|
||||
actionId: string;
|
||||
agentIds?: string[];
|
||||
expirationDate: Date;
|
||||
isLive?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
||||
actionId,
|
||||
agentIds,
|
||||
expirationDate,
|
||||
endDate,
|
||||
isLive,
|
||||
startDate,
|
||||
}) => {
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'status',
|
||||
name: 'Status',
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<ActionResultsSummary
|
||||
expirationDate={expirationDate}
|
||||
actionId={actionId}
|
||||
agentIds={agentIds}
|
||||
isLive={isLive}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'results',
|
||||
name: 'Results',
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<ResultsTable
|
||||
actionId={actionId}
|
||||
agentIds={agentIds}
|
||||
isLive={isLive}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
[actionId, agentIds, endDate, isLive, startDate, expirationDate]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiTabbedContent
|
||||
tabs={tabs}
|
||||
initialSelectedTab={tabs[0]}
|
||||
autoFocus="selected"
|
||||
expand={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ResultTabs = React.memo(ResultTabsComponent);
|
|
@ -14,6 +14,8 @@ import {
|
|||
EuiDataGridColumn,
|
||||
EuiLink,
|
||||
EuiLoadingContent,
|
||||
EuiProgress,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react';
|
||||
|
@ -37,17 +39,16 @@ interface ResultsTableComponentProps {
|
|||
selectedAgent?: string;
|
||||
agentIds?: string[];
|
||||
endDate?: string;
|
||||
isLive?: boolean;
|
||||
startDate?: string;
|
||||
}
|
||||
|
||||
const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
||||
actionId,
|
||||
agentIds,
|
||||
isLive,
|
||||
startDate,
|
||||
endDate,
|
||||
}) => {
|
||||
const [isLive, setIsLive] = useState(true);
|
||||
const {
|
||||
// @ts-expect-error update types
|
||||
data: { aggregations },
|
||||
|
@ -60,13 +61,13 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
|||
sortField: '@timestamp',
|
||||
isLive,
|
||||
});
|
||||
|
||||
const expired = useMemo(() => (!endDate ? false : new Date(endDate) < new Date()), [endDate]);
|
||||
const { getUrlForApp } = useKibana().services.application;
|
||||
|
||||
const getFleetAppUrl = useCallback(
|
||||
(agentId) =>
|
||||
getUrlForApp('fleet', {
|
||||
path: `#` + pagePathGetters.agent_details({ agentId }),
|
||||
path: `#` + pagePathGetters.agent_details({ agentId })[1],
|
||||
}),
|
||||
[getUrlForApp]
|
||||
);
|
||||
|
@ -216,29 +217,56 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
|
|||
[actionId, endDate, startDate]
|
||||
);
|
||||
|
||||
if (!aggregations.totalResponded) {
|
||||
useEffect(
|
||||
() =>
|
||||
setIsLive(() => {
|
||||
if (!agentIds?.length || expired) return false;
|
||||
|
||||
const uniqueAgentsRepliedCount =
|
||||
// @ts-expect-error-type
|
||||
allResultsData?.rawResponse.aggregations?.unique_agents.value ?? 0;
|
||||
|
||||
return !!(uniqueAgentsRepliedCount !== agentIds?.length - aggregations.failed);
|
||||
}),
|
||||
[
|
||||
agentIds?.length,
|
||||
aggregations.failed,
|
||||
// @ts-expect-error-type
|
||||
allResultsData?.rawResponse.aggregations?.unique_agents.value,
|
||||
expired,
|
||||
]
|
||||
);
|
||||
|
||||
if (!isFetched) {
|
||||
return <EuiLoadingContent lines={5} />;
|
||||
}
|
||||
|
||||
if (aggregations.totalResponded && isFetched && !allResultsData?.edges.length) {
|
||||
return <EuiCallOut title={generateEmptyDataMessage(aggregations.totalResponded)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
// @ts-expect-error update types
|
||||
<DataContext.Provider value={allResultsData?.edges}>
|
||||
<EuiDataGrid
|
||||
aria-label="Osquery results"
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
rowCount={allResultsData?.totalCount ?? 0}
|
||||
renderCellValue={renderCellValue}
|
||||
sorting={tableSorting}
|
||||
pagination={tablePagination}
|
||||
height="500px"
|
||||
toolbarVisibility={toolbarVisibility}
|
||||
/>
|
||||
</DataContext.Provider>
|
||||
<>
|
||||
{isLive && <EuiProgress color="primary" size="xs" />}
|
||||
|
||||
{isFetched && !allResultsData?.edges.length ? (
|
||||
<>
|
||||
<EuiCallOut title={generateEmptyDataMessage(aggregations.totalResponded)} />
|
||||
<EuiSpacer />
|
||||
</>
|
||||
) : (
|
||||
// @ts-expect-error update types
|
||||
<DataContext.Provider value={allResultsData?.edges}>
|
||||
<EuiDataGrid
|
||||
aria-label="Osquery results"
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
rowCount={allResultsData?.totalCount ?? 0}
|
||||
renderCellValue={renderCellValue}
|
||||
sorting={tableSorting}
|
||||
pagination={tablePagination}
|
||||
height="500px"
|
||||
toolbarVisibility={toolbarVisibility}
|
||||
/>
|
||||
</DataContext.Provider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
|
|||
export const generateEmptyDataMessage = (agentsResponded: number): string => {
|
||||
return i18n.translate('xpack.osquery.results.multipleAgentsResponded', {
|
||||
defaultMessage:
|
||||
'{agentsResponded, plural, one {# agent has} other {# agents have}} responded, but no osquery data has been reported.',
|
||||
'{agentsResponded, plural, one {# agent has} other {# agents have}} responded, no osquery data has been reported.',
|
||||
values: { agentsResponded },
|
||||
});
|
||||
};
|
||||
|
|
|
@ -78,7 +78,7 @@ export const useAllResults = ({
|
|||
};
|
||||
},
|
||||
{
|
||||
refetchInterval: isLive ? 1000 : false,
|
||||
refetchInterval: isLive ? 5000 : false,
|
||||
enabled: !skip,
|
||||
onSuccess: () => setErrorToast(),
|
||||
onError: (error: Error) =>
|
||||
|
|
|
@ -6,54 +6,24 @@
|
|||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiTextColor,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiCodeBlock,
|
||||
EuiSpacer,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
} from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiCodeBlock, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Direction } from '../../../../common/search_strategy';
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useActionResults } from '../../../action_results/use_action_results';
|
||||
import { useActionDetails } from '../../../actions/use_action_details';
|
||||
import { ResultTabs } from '../../saved_queries/edit/tabs';
|
||||
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
|
||||
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 0;
|
||||
height: 100%;
|
||||
border-left: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
`;
|
||||
|
||||
const LiveQueryDetailsPageComponent = () => {
|
||||
const { actionId } = useParams<{ actionId: string }>();
|
||||
useBreadcrumbs('live_query_details', { liveQueryId: actionId });
|
||||
const liveQueryListProps = useRouterNavigate('live_queries');
|
||||
|
||||
const { data } = useActionDetails({ actionId });
|
||||
const expirationDate = useMemo(() => new Date(data?.actionDetails._source.expiration), [
|
||||
data?.actionDetails,
|
||||
]);
|
||||
const expired = useMemo(() => expirationDate < new Date(), [expirationDate]);
|
||||
const { data: actionResultsData } = useActionResults({
|
||||
actionId,
|
||||
activePage: 0,
|
||||
limit: 0,
|
||||
direction: Direction.asc,
|
||||
sortField: '@timestamp',
|
||||
});
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
|
@ -82,72 +52,14 @@ const LiveQueryDetailsPageComponent = () => {
|
|||
[liveQueryListProps]
|
||||
);
|
||||
|
||||
const failed = useMemo(() => {
|
||||
let result = actionResultsData?.aggregations.failed;
|
||||
if (expired) {
|
||||
result = '-';
|
||||
if (data?.actionDetails?.fields?.agents && actionResultsData?.aggregations) {
|
||||
result =
|
||||
data.actionDetails.fields.agents.length - actionResultsData.aggregations.successful;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, [expired, actionResultsData?.aggregations, data?.actionDetails?.fields?.agents]);
|
||||
|
||||
const RightColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup justifyContent="flexEnd" direction="row">
|
||||
<EuiFlexItem grow={false} key="rows_count">
|
||||
<></>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} key="rows_count_divider">
|
||||
<Divider />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} key="agents_count">
|
||||
{/* eslint-disable-next-line react-perf/jsx-no-new-object-as-prop */}
|
||||
<EuiDescriptionList compressed textStyle="reverse" style={{ textAlign: 'right' }}>
|
||||
<EuiDescriptionListTitle className="eui-textNoWrap">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryDetails.kpis.agentsQueriedLabelText"
|
||||
defaultMessage="Agents queried"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription className="eui-textNoWrap">
|
||||
{data?.actionDetails?.fields?.agents?.length ?? '0'}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} key="agents_count_divider">
|
||||
<Divider />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} key="agents_failed_count">
|
||||
{/* eslint-disable-next-line react-perf/jsx-no-new-object-as-prop */}
|
||||
<EuiDescriptionList compressed textStyle="reverse" style={{ textAlign: 'right' }}>
|
||||
<EuiDescriptionListTitle className="eui-textNoWrap">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryDetails.kpis.agentsFailedCountLabelText"
|
||||
defaultMessage="Agents failed"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription className="eui-textNoWrap">
|
||||
<EuiTextColor color={failed ? 'danger' : 'default'}>{failed}</EuiTextColor>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[data?.actionDetails?.fields?.agents?.length, failed]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn} rightColumn={RightColumn} rightColumnGrow={false}>
|
||||
<WithHeaderLayout leftColumn={LeftColumn} rightColumnGrow={false}>
|
||||
<EuiCodeBlock language="sql" fontSize="m" paddingSize="m">
|
||||
{data?.actionDetails._source?.data?.query}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer />
|
||||
<ResultTabs
|
||||
actionId={actionId}
|
||||
expirationDate={expirationDate}
|
||||
agentIds={data?.actionDetails?.fields?.agents}
|
||||
startDate={get(data, ['actionDetails', 'fields', '@timestamp', '0'])}
|
||||
endDate={get(data, 'actionDetails.fields.expiration[0]')}
|
||||
|
|
|
@ -33,7 +33,7 @@ const EditSavedQueryPageComponent = () => {
|
|||
const updateSavedQueryMutation = useUpdateSavedQuery({ savedQueryId });
|
||||
const deleteSavedQueryMutation = useDeleteSavedQuery({ savedQueryId });
|
||||
|
||||
useBreadcrumbs('saved_query_edit', { savedQueryId: savedQueryDetails?.attributes?.id ?? '' });
|
||||
useBreadcrumbs('saved_query_edit', { savedQueryName: savedQueryDetails?.attributes?.id ?? '' });
|
||||
|
||||
const handleCloseDeleteConfirmationModal = useCallback(() => {
|
||||
setIsDeleteModalVisible(false);
|
||||
|
|
|
@ -10,12 +10,11 @@ import React, { useMemo } from 'react';
|
|||
|
||||
import { ResultsTable } from '../../../results/results_table';
|
||||
import { ActionResultsSummary } from '../../../action_results/action_results_summary';
|
||||
import { ActionAgentsStatus } from '../../../action_results/action_agents_status';
|
||||
|
||||
interface ResultTabsProps {
|
||||
actionId: string;
|
||||
agentIds?: string[];
|
||||
expirationDate: Date;
|
||||
isLive?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}
|
||||
|
@ -24,8 +23,6 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
|||
actionId,
|
||||
agentIds,
|
||||
endDate,
|
||||
expirationDate,
|
||||
isLive,
|
||||
startDate,
|
||||
}) => {
|
||||
const tabs = useMemo(
|
||||
|
@ -39,7 +36,6 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
|||
<ResultsTable
|
||||
actionId={actionId}
|
||||
agentIds={agentIds}
|
||||
isLive={isLive}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
|
@ -55,23 +51,26 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
|
|||
<ActionResultsSummary
|
||||
actionId={actionId}
|
||||
agentIds={agentIds}
|
||||
expirationDate={expirationDate}
|
||||
isLive={isLive}
|
||||
expirationDate={endDate}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
[actionId, agentIds, endDate, expirationDate, isLive, startDate]
|
||||
[actionId, agentIds, endDate, startDate]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiTabbedContent
|
||||
tabs={tabs}
|
||||
initialSelectedTab={tabs[0]}
|
||||
autoFocus="selected"
|
||||
expand={false}
|
||||
/>
|
||||
<>
|
||||
<ActionAgentsStatus actionId={actionId} agentIds={agentIds} expirationDate={endDate} />
|
||||
<EuiSpacer size="s" />
|
||||
<EuiTabbedContent
|
||||
tabs={tabs}
|
||||
initialSelectedTab={tabs[0]}
|
||||
autoFocus="selected"
|
||||
expand={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { ALL_OSQUERY_VERSIONS_OPTIONS } from '../../scheduled_query_groups/queries/constants';
|
||||
|
@ -57,7 +58,12 @@ const SavedQueryFormComponent = () => (
|
|||
euiFieldProps={{
|
||||
noSuggestions: false,
|
||||
singleSelection: { asPlainText: true },
|
||||
placeholder: ALL_OSQUERY_VERSIONS_OPTIONS[0].label,
|
||||
placeholder: i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queriesTable.osqueryVersionAllLabel',
|
||||
{
|
||||
defaultMessage: 'ALL',
|
||||
}
|
||||
),
|
||||
options: ALL_OSQUERY_VERSIONS_OPTIONS,
|
||||
onCreateOption: undefined,
|
||||
}}
|
||||
|
|
|
@ -40,7 +40,7 @@ export const useSavedQueries = ({
|
|||
{
|
||||
keepPreviousData: true,
|
||||
// Refetch the data every 10 seconds
|
||||
refetchInterval: isLive ? 10000 : false,
|
||||
refetchInterval: isLive ? 5000 : false,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -18,9 +18,10 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiDescribedFormGroup,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { satisfies } from 'semver';
|
||||
|
||||
|
@ -128,10 +129,7 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
<EuiSpacer />
|
||||
<CommonUseField path="query" component={CodeEditorField} />
|
||||
<EuiSpacer />
|
||||
<EuiDescribedFormGroup
|
||||
title={<h3>Set heading level based on context</h3>}
|
||||
description={'Will be wrapped in a small, subdued EuiText block.'}
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="interval"
|
||||
|
@ -141,12 +139,27 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
<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: ALL_OSQUERY_VERSIONS_OPTIONS[0].label,
|
||||
placeholder: i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queriesTable.osqueryVersionAllLabel',
|
||||
{
|
||||
defaultMessage: 'ALL',
|
||||
}
|
||||
),
|
||||
options: ALL_OSQUERY_VERSIONS_OPTIONS,
|
||||
onCreateOption: undefined,
|
||||
}}
|
||||
|
@ -160,7 +173,7 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
euiFieldProps={{ disabled: !isFieldSupported }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiDescribedFormGroup>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
</Form>
|
||||
{!isFieldSupported ? (
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -65,14 +65,6 @@ export const formSchema = {
|
|||
defaultMessage="Minimum Osquery version"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryGroup.queryFlyoutForm.versionFieldOptionalLabel"
|
||||
defaultMessage="(optional)"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) as unknown) as string,
|
||||
validations: [],
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 { getLazyOsqueryAction } from './lazy_osquery_action';
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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, { lazy, Suspense } from 'react';
|
||||
|
||||
// @ts-expect-error update types
|
||||
export const getLazyOsqueryAction = (services) => (props) => {
|
||||
const OsqueryAction = lazy(() => import('./osquery_action'));
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<OsqueryAction services={services} {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 { EuiErrorBoundary, EuiLoadingContent } from '@elastic/eui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { QueryClientProvider } from 'react-query';
|
||||
import { KibanaContextProvider, useKibana } from '../../common/lib/kibana';
|
||||
|
||||
import { LiveQueryForm } from '../../live_queries/form';
|
||||
import { queryClient } from '../../query_client';
|
||||
|
||||
interface OsqueryActionProps {
|
||||
hostId?: string | undefined;
|
||||
}
|
||||
|
||||
const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({ hostId }) => {
|
||||
const [agentId, setAgentId] = useState<string>();
|
||||
const { indexPatterns, search } = useKibana().services.data;
|
||||
|
||||
useEffect(() => {
|
||||
if (hostId) {
|
||||
const findAgent = async () => {
|
||||
const searchSource = await search.searchSource.create();
|
||||
const indexPattern = await indexPatterns.find('.fleet-agents');
|
||||
|
||||
searchSource.setField('index', indexPattern[0]);
|
||||
searchSource.setField('filter', [
|
||||
{
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
negate: false,
|
||||
key: 'local_metadata.host.id',
|
||||
value: hostId,
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'local_metadata.host.id': hostId,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
negate: false,
|
||||
key: 'active',
|
||||
value: 'true',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
active: 'true',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const response = await searchSource.fetch$().toPromise();
|
||||
|
||||
if (response.rawResponse.hits.hits.length && response.rawResponse.hits.hits[0]._id) {
|
||||
setAgentId(response.rawResponse.hits.hits[0]._id);
|
||||
}
|
||||
};
|
||||
|
||||
findAgent();
|
||||
}
|
||||
});
|
||||
|
||||
if (!agentId) {
|
||||
return <EuiLoadingContent lines={10} />;
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
<LiveQueryForm defaultValue={{ agentSelection: { agents: [agentId] } }} agentId={agentId} />
|
||||
);
|
||||
};
|
||||
|
||||
export const OsqueryAction = React.memo(OsqueryActionComponent);
|
||||
|
||||
// @ts-expect-error update types
|
||||
const OsqueryActionWrapperComponent = ({ services, ...props }) => (
|
||||
<KibanaContextProvider services={services}>
|
||||
<EuiErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<OsqueryAction {...props} />
|
||||
</QueryClientProvider>
|
||||
</EuiErrorBoundary>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
const OsqueryActionWrapper = React.memo(OsqueryActionWrapperComponent);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { OsqueryActionWrapper as default };
|
|
@ -16,11 +16,13 @@ import {
|
|||
TriggersAndActionsUIPublicPluginSetup,
|
||||
TriggersAndActionsUIPublicPluginStart,
|
||||
} from '../../triggers_actions_ui/public';
|
||||
import { getLazyOsqueryAction } from './shared_components';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface OsqueryPluginSetup {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface OsqueryPluginStart {}
|
||||
export interface OsqueryPluginStart {
|
||||
OsqueryAction?: ReturnType<typeof getLazyOsqueryAction>;
|
||||
}
|
||||
|
||||
export interface AppPluginStartDependencies {
|
||||
navigation: NavigationPublicPluginStart;
|
||||
|
|
|
@ -47,12 +47,17 @@ export const buildResultsQuery = ({
|
|||
size: 10000,
|
||||
},
|
||||
},
|
||||
unique_agents: {
|
||||
cardinality: {
|
||||
field: 'elastic_agent.id',
|
||||
},
|
||||
},
|
||||
},
|
||||
query: { bool: { filter } },
|
||||
from: activePage * querySize,
|
||||
size: querySize,
|
||||
track_total_hits: true,
|
||||
fields: agentId ? ['osquery.*'] : ['agent.*', 'osquery.*'],
|
||||
fields: ['elastic_agent.*', 'agent.*', 'osquery.*'],
|
||||
sort:
|
||||
sort?.map((sortConfig) => ({
|
||||
[sortConfig.field]: {
|
||||
|
|
|
@ -17301,7 +17301,6 @@
|
|||
"xpack.osquery.fleetIntegration.scheduleQueryGroupsButtonText": "クエリグループをスケジュール",
|
||||
"xpack.osquery.liveQueriesHistory.newLiveQueryButtonLabel": "新しいライブクエリ",
|
||||
"xpack.osquery.liveQueriesHistory.pageTitle": "ライブクエリ履歴",
|
||||
"xpack.osquery.liveQueryActionResults.summary.agentsQueriedLabelText": "エージェントがクエリされました",
|
||||
"xpack.osquery.liveQueryActionResults.summary.failedLabelText": "失敗",
|
||||
"xpack.osquery.liveQueryActionResults.summary.pendingLabelText": "未応答",
|
||||
"xpack.osquery.liveQueryActionResults.summary.successfulLabelText": "成功",
|
||||
|
@ -17316,8 +17315,6 @@
|
|||
"xpack.osquery.liveQueryActions.table.createdAtColumnTitle": "作成日時:",
|
||||
"xpack.osquery.liveQueryActions.table.queryColumnTitle": "クエリ",
|
||||
"xpack.osquery.liveQueryActions.table.viewDetailsColumnTitle": "詳細を表示",
|
||||
"xpack.osquery.liveQueryDetails.kpis.agentsFailedCountLabelText": "エージェントが失敗しました",
|
||||
"xpack.osquery.liveQueryDetails.kpis.agentsQueriedLabelText": "エージェントがクエリされました",
|
||||
"xpack.osquery.liveQueryDetails.pageTitle": "ライブクエリ詳細",
|
||||
"xpack.osquery.liveQueryDetails.viewLiveQueriesHistoryTitle": "ライブクエリ履歴を表示",
|
||||
"xpack.osquery.liveQueryForm.form.submitButtonLabel": "送信",
|
||||
|
|
|
@ -17539,7 +17539,6 @@
|
|||
"xpack.osquery.fleetIntegration.scheduleQueryGroupsButtonText": "计划查询组",
|
||||
"xpack.osquery.liveQueriesHistory.newLiveQueryButtonLabel": "新建实时查询",
|
||||
"xpack.osquery.liveQueriesHistory.pageTitle": "实时查询历史记录",
|
||||
"xpack.osquery.liveQueryActionResults.summary.agentsQueriedLabelText": "查询的代理",
|
||||
"xpack.osquery.liveQueryActionResults.summary.failedLabelText": "失败",
|
||||
"xpack.osquery.liveQueryActionResults.summary.pendingLabelText": "尚未响应",
|
||||
"xpack.osquery.liveQueryActionResults.summary.successfulLabelText": "成功",
|
||||
|
@ -17554,8 +17553,6 @@
|
|||
"xpack.osquery.liveQueryActions.table.createdAtColumnTitle": "创建于",
|
||||
"xpack.osquery.liveQueryActions.table.queryColumnTitle": "查询",
|
||||
"xpack.osquery.liveQueryActions.table.viewDetailsColumnTitle": "查看详情",
|
||||
"xpack.osquery.liveQueryDetails.kpis.agentsFailedCountLabelText": "失败的代理",
|
||||
"xpack.osquery.liveQueryDetails.kpis.agentsQueriedLabelText": "查询的代理",
|
||||
"xpack.osquery.liveQueryDetails.pageTitle": "实时查询详情",
|
||||
"xpack.osquery.liveQueryDetails.viewLiveQueriesHistoryTitle": "查看实时查询历史记录",
|
||||
"xpack.osquery.liveQueryForm.form.submitButtonLabel": "提交",
|
||||
|
|
|
@ -23088,10 +23088,10 @@ react-popper@^2.2.4:
|
|||
react-fast-compare "^3.0.1"
|
||||
warning "^4.0.2"
|
||||
|
||||
react-query@^3.13.10:
|
||||
version "3.13.10"
|
||||
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.13.10.tgz#b6a05e22a5debb6e2df79ada588179771cbd7df8"
|
||||
integrity sha512-wFvKhEDnOVL5bFL+9KPgNsiOOei1Ad+l6l1awCBuoX7xMG+SXXKDOF2uuZFsJe0w6gdthdWN+00021yepTR31g==
|
||||
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==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
broadcast-channel "^3.4.1"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue