Skip to content

Commit

Permalink
[Asset Management] Add live query history table (#94536)
Browse files Browse the repository at this point in the history
  • Loading branch information
patrykkopycinski authored Apr 19, 2021
1 parent 283e2ca commit 64f30a2
Show file tree
Hide file tree
Showing 123 changed files with 4,274 additions and 3,089 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@
"react-moment-proptypes": "^1.7.0",
"react-monaco-editor": "^0.41.2",
"react-popper-tooltip": "^2.10.1",
"react-query": "^3.12.0",
"react-query": "^3.13.10",
"react-resize-detector": "^4.2.0",
"react-reverse-portal": "^1.0.4",
"react-router-redux": "^4.0.8",
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/osquery/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@

export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000;
export const DEFAULT_DARK_MODE = 'theme:darkMode';
export const OSQUERY_INTEGRATION_NAME = 'osquery_manager';
export const BASE_PATH = '/app/osquery';
2 changes: 2 additions & 0 deletions x-pack/plugins/osquery/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"actions",
"data",
"dataEnhanced",
"discover",
"features",
"fleet",
"navigation",
"triggersActionsUi"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
/*
* 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.
*/

/* eslint-disable @typescript-eslint/no-unused-vars */

import { i18n } from '@kbn/i18n';
import {
EuiLink,
EuiFlexGroup,
EuiFlexItem,
EuiCard,
EuiTextColor,
EuiSpacer,
EuiDescriptionList,
EuiInMemoryTable,
EuiCodeBlock,
} from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';

import { pagePathGetters } from '../../../fleet/public';
import { useActionResults } from './use_action_results';
import { useAllResults } from '../results/use_all_results';
import { Direction } from '../../common/search_strategy';
import { useKibana } from '../common/lib/kibana';

interface ActionResultsSummaryProps {
actionId: string;
agentIds?: string[];
isLive?: boolean;
}

const renderErrorMessage = (error: string) => (
<EuiCodeBlock language="shell" fontSize="s" paddingSize="none" transparentBackground>
{error}
</EuiCodeBlock>
);

const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
actionId,
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 {
// @ts-expect-error update types
data: { aggregations, edges },
} = useActionResults({
actionId,
activePage: pageIndex,
agentIds,
limit: pageSize,
direction: Direction.asc,
sortField: '@timestamp',
isLive,
});

const { data: logsResults } = useAllResults({
actionId,
activePage: pageIndex,
limit: pageSize,
direction: Direction.asc,
sortField: '@timestamp',
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: 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]
);

const renderAgentIdColumn = useCallback(
(agentId) => (
<EuiLink
className="eui-textTruncate"
href={getUrlForApp('fleet', {
path: `#` + pagePathGetters.fleet_agent_details({ agentId }),
})}
target="_blank"
>
{agentId}
</EuiLink>
),
[getUrlForApp]
);

const renderRowsColumn = useCallback(
(_, item) => {
if (!logsResults) return '-';
const agentId = item.fields.agent_id[0];

return (
// @ts-expect-error update types
logsResults?.rawResponse?.aggregations?.count_by_agent_id?.buckets?.find(
// @ts-expect-error update types
(bucket) => bucket.key === agentId
)?.doc_count ?? '-'
);
},
[logsResults]
);

const renderStatusColumn = useCallback((_, item) => {
if (!item.fields.completed_at) {
return i18n.translate('xpack.osquery.liveQueryActionResults.table.pendingStatusText', {
defaultMessage: 'pending',
});
}

if (item.fields['error.keyword']) {
return i18n.translate('xpack.osquery.liveQueryActionResults.table.errorStatusText', {
defaultMessage: 'error',
});
}

return i18n.translate('xpack.osquery.liveQueryActionResults.table.successStatusText', {
defaultMessage: 'success',
});
}, []);

const columns = useMemo(
() => [
{
field: 'status',
name: i18n.translate('xpack.osquery.liveQueryActionResults.table.statusColumnTitle', {
defaultMessage: 'Status',
}),
render: renderStatusColumn,
},
{
field: 'fields.agent_id[0]',
name: i18n.translate('xpack.osquery.liveQueryActionResults.table.agentIdColumnTitle', {
defaultMessage: 'Agent Id',
}),
truncateText: true,
render: renderAgentIdColumn,
},
{
field: 'fields.rows[0]',
name: i18n.translate(
'xpack.osquery.liveQueryActionResults.table.resultRowsNumberColumnTitle',
{
defaultMessage: 'Number of result rows',
}
),
render: renderRowsColumn,
},
{
field: 'fields.error[0]',
name: i18n.translate('xpack.osquery.liveQueryActionResults.table.errorColumnTitle', {
defaultMessage: 'Error',
}),
render: renderErrorMessage,
},
],
[renderAgentIdColumn, renderRowsColumn, renderStatusColumn]
);

const pagination = useMemo(
() => ({
initialPageSize: 20,
pageSizeOptions: [10, 20, 50, 100],
}),
[]
);

return (
<>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiCard title="" description="" textAlign="left">
<EuiDescriptionList
compressed
textStyle="reverse"
type="responsiveColumn"
listItems={listItems}
/>
</EuiCard>
</EuiFlexItem>
</EuiFlexGroup>

{edges.length ? (
<>
<EuiSpacer />
<EuiInMemoryTable items={edges} columns={columns} pagination={pagination} />
</>
) : null}
</>
);
};

export const ActionResultsSummary = React.memo(ActionResultsSummaryComponent);
Loading

0 comments on commit 64f30a2

Please sign in to comment.