[Osquery] Fix 7.14 UX issues (#104257)

This commit is contained in:
Patryk Kopyciński 2021-07-09 02:39:46 +03:00 committed by GitHub
parent 6d9d1db6ac
commit fe6eb09936
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 625 additions and 435 deletions

View file

@ -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",

View file

@ -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);

View file

@ -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';

View file

@ -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}
/>
);
};

View file

@ -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);

View file

@ -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}`);
}
}

View 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';

View file

@ -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(),

View file

@ -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],
});
}
},

View file

@ -30,7 +30,6 @@ export const useAgentPolicies = () => {
}),
{
initialData: { items: [], total: 0, page: 1, perPage: 100 },
placeholderData: [],
keepPreviousData: true,
select: (response) => response.items,
onSuccess: () => setErrorToast(),

View file

@ -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]);

View file

@ -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';

View file

@ -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);

View file

@ -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}

View file

@ -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

View file

@ -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);

View file

@ -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>
)}
</>
);
};

View file

@ -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 },
});
};

View file

@ -78,7 +78,7 @@ export const useAllResults = ({
};
},
{
refetchInterval: isLive ? 1000 : false,
refetchInterval: isLive ? 5000 : false,
enabled: !skip,
onSuccess: () => setErrorToast(),
onError: (error: Error) =>

View file

@ -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]')}

View file

@ -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);

View file

@ -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}
/>
</>
);
};

View file

@ -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,
}}

View file

@ -40,7 +40,7 @@ export const useSavedQueries = ({
{
keepPreviousData: true,
// Refetch the data every 10 seconds
refetchInterval: isLive ? 10000 : false,
refetchInterval: isLive ? 5000 : false,
}
);
};

View file

@ -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 ? (

View file

@ -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: [],

View 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 { getLazyOsqueryAction } from './lazy_osquery_action';

View file

@ -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>
);
};

View file

@ -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 };

View file

@ -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;

View file

@ -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]: {

View file

@ -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": "送信",

View file

@ -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": "提交",

View file

@ -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"