mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Asset Management] Add live query history table (#94536)
This commit is contained in:
parent
283e2ca798
commit
64f30a224e
123 changed files with 4274 additions and 3089 deletions
|
@ -348,7 +348,7 @@
|
|||
"react-moment-proptypes": "^1.7.0",
|
||||
"react-monaco-editor": "^0.41.2",
|
||||
"react-popper-tooltip": "^2.10.1",
|
||||
"react-query": "^3.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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
"actions",
|
||||
"data",
|
||||
"dataEnhanced",
|
||||
"discover",
|
||||
"features",
|
||||
"fleet",
|
||||
"navigation",
|
||||
"triggersActionsUi"
|
||||
|
|
|
@ -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);
|
|
@ -1,196 +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 { find, map } from 'lodash/fp';
|
||||
import {
|
||||
EuiDataGrid,
|
||||
EuiDataGridProps,
|
||||
EuiDataGridColumn,
|
||||
EuiDataGridSorting,
|
||||
EuiHealth,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import React, { createContext, useState, useCallback, useContext, useMemo } from 'react';
|
||||
|
||||
import { useAllAgents } from './../agents/use_all_agents';
|
||||
import { useActionResults } from './use_action_results';
|
||||
import { useAllResults } from '../results/use_all_results';
|
||||
import { Direction, ResultEdges } from '../../common/search_strategy';
|
||||
import { useRouterNavigate } from '../common/lib/kibana';
|
||||
import { useOsqueryPolicies } from '../agents/use_osquery_policies';
|
||||
|
||||
const DataContext = createContext<ResultEdges>([]);
|
||||
|
||||
interface ActionResultsTableProps {
|
||||
actionId: string;
|
||||
}
|
||||
|
||||
const ActionResultsTableComponent: React.FC<ActionResultsTableProps> = ({ actionId }) => {
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 });
|
||||
const onChangeItemsPerPage = useCallback(
|
||||
(pageSize) =>
|
||||
setPagination((currentPagination) => ({
|
||||
...currentPagination,
|
||||
pageSize,
|
||||
pageIndex: 0,
|
||||
})),
|
||||
[setPagination]
|
||||
);
|
||||
const onChangePage = useCallback(
|
||||
(pageIndex) => setPagination((currentPagination) => ({ ...currentPagination, pageIndex })),
|
||||
[setPagination]
|
||||
);
|
||||
|
||||
const [columns] = useState<EuiDataGridColumn[]>([
|
||||
{
|
||||
id: 'status',
|
||||
displayAsText: 'status',
|
||||
defaultSortDirection: Direction.asc,
|
||||
},
|
||||
{
|
||||
id: 'rows_count',
|
||||
displayAsText: '# rows',
|
||||
defaultSortDirection: Direction.asc,
|
||||
},
|
||||
{
|
||||
id: 'agent_status',
|
||||
displayAsText: 'online',
|
||||
defaultSortDirection: Direction.asc,
|
||||
},
|
||||
{
|
||||
id: 'agent',
|
||||
displayAsText: 'agent',
|
||||
defaultSortDirection: Direction.asc,
|
||||
},
|
||||
{
|
||||
id: '@timestamp',
|
||||
displayAsText: '@timestamp',
|
||||
defaultSortDirection: Direction.asc,
|
||||
},
|
||||
]);
|
||||
|
||||
// ** Sorting config
|
||||
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
|
||||
|
||||
const { data: actionResultsData } = useActionResults({
|
||||
actionId,
|
||||
activePage: pagination.pageIndex,
|
||||
limit: pagination.pageSize,
|
||||
direction: Direction.asc,
|
||||
sortField: '@timestamp',
|
||||
});
|
||||
|
||||
const [visibleColumns, setVisibleColumns] = useState<string[]>(() => map('id', columns)); // initialize to the full set of columns
|
||||
|
||||
const columnVisibility = useMemo(() => ({ visibleColumns, setVisibleColumns }), [
|
||||
visibleColumns,
|
||||
setVisibleColumns,
|
||||
]);
|
||||
|
||||
const osqueryPolicyData = useOsqueryPolicies();
|
||||
const { agents } = useAllAgents(osqueryPolicyData);
|
||||
|
||||
const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo(
|
||||
() => ({ rowIndex, columnId }) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const data = useContext(DataContext);
|
||||
const value = data[rowIndex];
|
||||
|
||||
if (columnId === 'status') {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const linkProps = useRouterNavigate(
|
||||
`/live_query/${actionId}/results/${value.fields?.agent_id[0]}`
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiIcon type="checkInCircleFilled" />
|
||||
<EuiLink {...linkProps}>{'View results'}</EuiLink>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (columnId === 'rows_count') {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { data: allResultsData } = useAllResults({
|
||||
actionId,
|
||||
agentId: value.fields?.agent_id[0],
|
||||
activePage: pagination.pageIndex,
|
||||
limit: pagination.pageSize,
|
||||
direction: Direction.asc,
|
||||
sortField: '@timestamp',
|
||||
});
|
||||
// @ts-expect-error update types
|
||||
return allResultsData?.totalCount ?? '-';
|
||||
}
|
||||
|
||||
if (columnId === 'agent_status') {
|
||||
const agentIdValue = value.fields?.agent_id[0];
|
||||
const agent = find(['_id', agentIdValue], agents);
|
||||
const online = agent?.active;
|
||||
const color = online ? 'success' : 'danger';
|
||||
const label = online ? 'Online' : 'Offline';
|
||||
return <EuiHealth color={color}>{label}</EuiHealth>;
|
||||
}
|
||||
|
||||
if (columnId === 'agent') {
|
||||
const agentIdValue = value.fields?.agent_id[0];
|
||||
const agent = find(['_id', agentIdValue], agents);
|
||||
const agentName = agent?.local_metadata.host.name;
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const linkProps = useRouterNavigate(`/live_query/${actionId}/results/${agentIdValue}`);
|
||||
return (
|
||||
<EuiLink {...linkProps}>{`(${agent?.local_metadata.os.name}) ${agentName}`}</EuiLink>
|
||||
);
|
||||
}
|
||||
|
||||
if (columnId === '@timestamp') {
|
||||
// @ts-expect-error fields is optional
|
||||
return value.fields['@timestamp'];
|
||||
}
|
||||
|
||||
return '-';
|
||||
},
|
||||
[actionId, agents, pagination.pageIndex, pagination.pageSize]
|
||||
);
|
||||
|
||||
const tableSorting: EuiDataGridSorting = useMemo(
|
||||
() => ({ columns: sortingColumns, onSort: setSortingColumns }),
|
||||
[sortingColumns]
|
||||
);
|
||||
|
||||
const tablePagination = useMemo(
|
||||
() => ({
|
||||
...pagination,
|
||||
pageSizeOptions: [10, 50, 100],
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}),
|
||||
[onChangeItemsPerPage, onChangePage, pagination]
|
||||
);
|
||||
|
||||
return (
|
||||
// @ts-expect-error update types
|
||||
<DataContext.Provider value={actionResultsData?.results}>
|
||||
<EuiDataGrid
|
||||
aria-label="Osquery results"
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
// @ts-expect-error update types
|
||||
rowCount={actionResultsData?.totalCount}
|
||||
renderCellValue={renderCellValue}
|
||||
sorting={tableSorting}
|
||||
pagination={tablePagination}
|
||||
height="300px"
|
||||
/>
|
||||
</DataContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const ActionResultsTable = React.memo(ActionResultsTableComponent);
|
|
@ -16,15 +16,14 @@ export type InspectResponse = Inspect & { response: string[] };
|
|||
|
||||
export const generateTablePaginationOptions = (
|
||||
activePage: number,
|
||||
limit: number,
|
||||
isBucketSort?: boolean
|
||||
limit: number
|
||||
): PaginationInputPaginated => {
|
||||
const cursorStart = activePage * limit;
|
||||
return {
|
||||
activePage,
|
||||
cursorStart,
|
||||
fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5,
|
||||
querySize: isBucketSort ? limit : limit + cursorStart,
|
||||
querySize: limit,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { flatten, reverse, uniqBy } from 'lodash/fp';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { createFilter } from '../common/helpers';
|
||||
|
@ -20,6 +19,7 @@ import {
|
|||
Direction,
|
||||
} from '../../common/search_strategy';
|
||||
import { ESTermQuery } from '../../common/typed_json';
|
||||
import { queryClient } from '../query_client';
|
||||
|
||||
import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers';
|
||||
|
||||
|
@ -35,68 +35,91 @@ export interface ResultsArgs {
|
|||
interface UseActionResults {
|
||||
actionId: string;
|
||||
activePage: number;
|
||||
agentIds?: string[];
|
||||
direction: Direction;
|
||||
limit: number;
|
||||
sortField: string;
|
||||
filterQuery?: ESTermQuery | string;
|
||||
skip?: boolean;
|
||||
isLive?: boolean;
|
||||
}
|
||||
|
||||
export const useActionResults = ({
|
||||
actionId,
|
||||
activePage,
|
||||
agentIds,
|
||||
direction,
|
||||
limit,
|
||||
sortField,
|
||||
filterQuery,
|
||||
skip = false,
|
||||
isLive = false,
|
||||
}: UseActionResults) => {
|
||||
const { data } = useKibana().services;
|
||||
|
||||
const [resultsRequest, setHostRequest] = useState<ResultsRequestOptions | null>(null);
|
||||
|
||||
const response = useQuery(
|
||||
['actionResults', { actionId, activePage, direction, limit, sortField }],
|
||||
return useQuery(
|
||||
['actionResults', { actionId }],
|
||||
async () => {
|
||||
if (!resultsRequest) return Promise.resolve();
|
||||
|
||||
const responseData = await data.search
|
||||
.search<ResultsRequestOptions, ResultsStrategyResponse>(resultsRequest, {
|
||||
strategy: 'osquerySearchStrategy',
|
||||
})
|
||||
.search<ResultsRequestOptions, ResultsStrategyResponse>(
|
||||
{
|
||||
actionId,
|
||||
factoryQueryType: OsqueryQueries.actionResults,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
pagination: generateTablePaginationOptions(activePage, limit),
|
||||
sort: {
|
||||
direction,
|
||||
field: sortField,
|
||||
},
|
||||
},
|
||||
{
|
||||
strategy: 'osquerySearchStrategy',
|
||||
}
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
const totalResponded =
|
||||
// @ts-expect-error update types
|
||||
responseData.rawResponse?.aggregations?.aggs.responses_by_action_id?.doc_count;
|
||||
const aggsBuckets =
|
||||
// @ts-expect-error update types
|
||||
responseData.rawResponse?.aggregations?.aggs.responses_by_action_id?.responses.buckets;
|
||||
|
||||
const cachedData = queryClient.getQueryData(['actionResults', { actionId }]);
|
||||
|
||||
// @ts-expect-error update types
|
||||
const previousEdges = cachedData?.edges.length
|
||||
? // @ts-expect-error update types
|
||||
cachedData?.edges
|
||||
: agentIds?.map((agentId) => ({ fields: { agent_id: [agentId] } })) ?? [];
|
||||
|
||||
return {
|
||||
...responseData,
|
||||
results: responseData.edges,
|
||||
edges: reverse(uniqBy('fields.agent_id[0]', flatten([responseData.edges, previousEdges]))),
|
||||
aggregations: {
|
||||
totalResponded,
|
||||
// @ts-expect-error update types
|
||||
successful: aggsBuckets.find((bucket) => bucket.key === 'success')?.doc_count ?? 0,
|
||||
// @ts-expect-error update types
|
||||
failed: aggsBuckets.find((bucket) => bucket.key === 'error')?.doc_count ?? 0,
|
||||
},
|
||||
inspect: getInspectResponse(responseData, {} as InspectResponse),
|
||||
};
|
||||
},
|
||||
{
|
||||
refetchInterval: 1000,
|
||||
enabled: !skip && !!resultsRequest,
|
||||
initialData: {
|
||||
edges: [],
|
||||
aggregations: {
|
||||
totalResponded: 0,
|
||||
successful: 0,
|
||||
// @ts-expect-error update types
|
||||
pending: agentIds?.length ?? 0,
|
||||
failed: 0,
|
||||
},
|
||||
},
|
||||
refetchInterval: isLive ? 1000 : false,
|
||||
keepPreviousData: true,
|
||||
enabled: !skip && !!agentIds?.length,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setHostRequest((prevRequest) => {
|
||||
const myRequest = {
|
||||
...(prevRequest ?? {}),
|
||||
actionId,
|
||||
factoryQueryType: OsqueryQueries.actionResults,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
pagination: generateTablePaginationOptions(activePage, limit),
|
||||
sort: {
|
||||
direction,
|
||||
field: sortField,
|
||||
},
|
||||
};
|
||||
if (!deepEqual(prevRequest, myRequest)) {
|
||||
return myRequest;
|
||||
}
|
||||
return prevRequest;
|
||||
});
|
||||
}, [actionId, activePage, direction, filterQuery, limit, sortField]);
|
||||
|
||||
return response;
|
||||
};
|
||||
|
|
|
@ -5,128 +5,123 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty, isEqual, keys, map } from 'lodash/fp';
|
||||
import {
|
||||
EuiLink,
|
||||
EuiDataGrid,
|
||||
EuiDataGridProps,
|
||||
EuiDataGridColumn,
|
||||
EuiDataGridSorting,
|
||||
EuiLoadingContent,
|
||||
} from '@elastic/eui';
|
||||
import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBasicTable, EuiButtonIcon, EuiCodeBlock, formatDate } from '@elastic/eui';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
|
||||
import { useAllActions } from './use_all_actions';
|
||||
import { ActionEdges, Direction } from '../../common/search_strategy';
|
||||
import { Direction } from '../../common/search_strategy';
|
||||
import { useRouterNavigate } from '../common/lib/kibana';
|
||||
|
||||
const DataContext = createContext<ActionEdges>([]);
|
||||
interface ActionTableResultsButtonProps {
|
||||
actionId: string;
|
||||
}
|
||||
|
||||
const ActionTableResultsButton = React.memo<ActionTableResultsButtonProps>(({ actionId }) => {
|
||||
const navProps = useRouterNavigate(`live_queries/${actionId}`);
|
||||
|
||||
return <EuiButtonIcon iconType="visTable" {...navProps} />;
|
||||
});
|
||||
|
||||
ActionTableResultsButton.displayName = 'ActionTableResultsButton';
|
||||
|
||||
const ActionsTableComponent = () => {
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 });
|
||||
const onChangeItemsPerPage = useCallback(
|
||||
(pageSize) =>
|
||||
setPagination((currentPagination) => ({
|
||||
...currentPagination,
|
||||
pageSize,
|
||||
pageIndex: 0,
|
||||
})),
|
||||
[setPagination]
|
||||
);
|
||||
const onChangePage = useCallback(
|
||||
(pageIndex) => setPagination((currentPagination) => ({ ...currentPagination, pageIndex })),
|
||||
[setPagination]
|
||||
);
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
|
||||
const [columns, setColumns] = useState<EuiDataGridColumn[]>([]);
|
||||
|
||||
// ** Sorting config
|
||||
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
|
||||
|
||||
const { isLoading: actionsLoading, data: actionsData } = useAllActions({
|
||||
activePage: pagination.pageIndex,
|
||||
limit: pagination.pageSize,
|
||||
const { data: actionsData } = useAllActions({
|
||||
activePage: pageIndex,
|
||||
limit: pageSize,
|
||||
direction: Direction.desc,
|
||||
sortField: '@timestamp',
|
||||
});
|
||||
|
||||
// Column visibility
|
||||
const [visibleColumns, setVisibleColumns] = useState<string[]>([]); // initialize to the full set of columns
|
||||
const onTableChange = useCallback(({ page = {} }) => {
|
||||
const { index, size } = page;
|
||||
|
||||
const columnVisibility = useMemo(() => ({ visibleColumns, setVisibleColumns }), [
|
||||
visibleColumns,
|
||||
setVisibleColumns,
|
||||
]);
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
}, []);
|
||||
|
||||
const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo(
|
||||
() => ({ rowIndex, columnId }) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const data = useContext(DataContext);
|
||||
// @ts-expect-error fields is optional
|
||||
const value = data[rowIndex].fields[columnId];
|
||||
|
||||
if (columnId === 'action_id') {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const linkProps = useRouterNavigate(`/live_query/${value}`);
|
||||
return <EuiLink {...linkProps}>{value}</EuiLink>;
|
||||
}
|
||||
|
||||
return !isEmpty(value) ? value : '-';
|
||||
},
|
||||
const renderQueryColumn = useCallback(
|
||||
(_, item) => (
|
||||
<EuiCodeBlock language="sql" fontSize="s" paddingSize="none" transparentBackground>
|
||||
{item._source.data.query}
|
||||
</EuiCodeBlock>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const tableSorting: EuiDataGridSorting = useMemo(
|
||||
() => ({ columns: sortingColumns, onSort: setSortingColumns }),
|
||||
[setSortingColumns, sortingColumns]
|
||||
const renderAgentsColumn = useCallback((_, item) => <>{item.fields.agents?.length ?? 0}</>, []);
|
||||
|
||||
const renderTimestampColumn = useCallback(
|
||||
(_, item) => <>{formatDate(item.fields['@timestamp'][0])}</>,
|
||||
[]
|
||||
);
|
||||
|
||||
const tablePagination = useMemo(
|
||||
const renderActionsColumn = useCallback(
|
||||
(item) => <ActionTableResultsButton actionId={item.fields.action_id[0]} />,
|
||||
[]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'query',
|
||||
name: i18n.translate('xpack.osquery.liveQueryActions.table.queryColumnTitle', {
|
||||
defaultMessage: 'Query',
|
||||
}),
|
||||
truncateText: true,
|
||||
render: renderQueryColumn,
|
||||
},
|
||||
{
|
||||
field: 'agents',
|
||||
name: i18n.translate('xpack.osquery.liveQueryActions.table.agentsColumnTitle', {
|
||||
defaultMessage: 'Agents',
|
||||
}),
|
||||
width: '100px',
|
||||
render: renderAgentsColumn,
|
||||
},
|
||||
{
|
||||
field: 'created_at',
|
||||
name: i18n.translate('xpack.osquery.liveQueryActions.table.createdAtColumnTitle', {
|
||||
defaultMessage: 'Created at',
|
||||
}),
|
||||
width: '200px',
|
||||
render: renderTimestampColumn,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.osquery.liveQueryActions.table.viewDetailsColumnTitle', {
|
||||
defaultMessage: 'View details',
|
||||
}),
|
||||
actions: [
|
||||
{
|
||||
render: renderActionsColumn,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[renderActionsColumn, renderAgentsColumn, renderQueryColumn, renderTimestampColumn]
|
||||
);
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
...pagination,
|
||||
pageSizeOptions: [10, 50, 100],
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount: actionsData?.totalCount ?? 0,
|
||||
pageSizeOptions: [20, 50, 100],
|
||||
}),
|
||||
[onChangeItemsPerPage, onChangePage, pagination]
|
||||
[actionsData?.totalCount, pageIndex, pageSize]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-expect-error update types
|
||||
const newColumns = keys(actionsData?.actions[0]?.fields)
|
||||
.sort()
|
||||
.map((fieldName) => ({
|
||||
id: fieldName,
|
||||
displayAsText: fieldName.split('.')[1],
|
||||
defaultSortDirection: Direction.asc,
|
||||
}));
|
||||
|
||||
if (!isEqual(columns, newColumns)) {
|
||||
setColumns(newColumns);
|
||||
setVisibleColumns(map('id', newColumns));
|
||||
}
|
||||
// @ts-expect-error update types
|
||||
}, [columns, actionsData?.actions]);
|
||||
|
||||
if (actionsLoading) {
|
||||
return <EuiLoadingContent lines={10} />;
|
||||
}
|
||||
|
||||
return (
|
||||
// @ts-expect-error update types
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-array-as-prop
|
||||
<DataContext.Provider value={actionsData?.actions ?? []}>
|
||||
<EuiDataGrid
|
||||
aria-label="Osquery actions"
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
// @ts-expect-error update types
|
||||
rowCount={actionsData?.totalCount ?? 0}
|
||||
renderCellValue={renderCellValue}
|
||||
sorting={tableSorting}
|
||||
pagination={tablePagination}
|
||||
/>
|
||||
</DataContext.Provider>
|
||||
<EuiBasicTable
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-array-as-prop
|
||||
items={actionsData?.actions ?? []}
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
onChange={onTableChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -16,15 +16,14 @@ export type InspectResponse = Inspect & { response: string[] };
|
|||
|
||||
export const generateTablePaginationOptions = (
|
||||
activePage: number,
|
||||
limit: number,
|
||||
isBucketSort?: boolean
|
||||
limit: number
|
||||
): PaginationInputPaginated => {
|
||||
const cursorStart = activePage * limit;
|
||||
return {
|
||||
activePage,
|
||||
cursorStart,
|
||||
fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5,
|
||||
querySize: isBucketSort ? limit : limit + cursorStart,
|
||||
querySize: limit,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { createFilter } from '../common/helpers';
|
||||
|
@ -36,17 +34,20 @@ interface UseActionDetails {
|
|||
export const useActionDetails = ({ actionId, filterQuery, skip = false }: UseActionDetails) => {
|
||||
const { data } = useKibana().services;
|
||||
|
||||
const [actionDetailsRequest, setHostRequest] = useState<ActionDetailsRequestOptions | null>(null);
|
||||
|
||||
const response = useQuery(
|
||||
['action', { actionId }],
|
||||
return useQuery(
|
||||
['actionDetails', { actionId, filterQuery }],
|
||||
async () => {
|
||||
if (!actionDetailsRequest) return Promise.resolve();
|
||||
|
||||
const responseData = await data.search
|
||||
.search<ActionDetailsRequestOptions, ActionDetailsStrategyResponse>(actionDetailsRequest, {
|
||||
strategy: 'osquerySearchStrategy',
|
||||
})
|
||||
.search<ActionDetailsRequestOptions, ActionDetailsStrategyResponse>(
|
||||
{
|
||||
actionId,
|
||||
factoryQueryType: OsqueryQueries.actionDetails,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
},
|
||||
{
|
||||
strategy: 'osquerySearchStrategy',
|
||||
}
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
return {
|
||||
|
@ -55,24 +56,7 @@ export const useActionDetails = ({ actionId, filterQuery, skip = false }: UseAct
|
|||
};
|
||||
},
|
||||
{
|
||||
enabled: !skip && !!actionDetailsRequest,
|
||||
enabled: !skip,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setHostRequest((prevRequest) => {
|
||||
const myRequest = {
|
||||
...(prevRequest ?? {}),
|
||||
actionId,
|
||||
factoryQueryType: OsqueryQueries.actionDetails,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
};
|
||||
if (!deepEqual(prevRequest, myRequest)) {
|
||||
return myRequest;
|
||||
}
|
||||
return prevRequest;
|
||||
});
|
||||
}, [actionId, filterQuery]);
|
||||
|
||||
return response;
|
||||
};
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { createFilter } from '../common/helpers';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
|
@ -51,17 +49,24 @@ export const useAllActions = ({
|
|||
}: UseAllActions) => {
|
||||
const { data } = useKibana().services;
|
||||
|
||||
const [actionsRequest, setHostRequest] = useState<ActionsRequestOptions | null>(null);
|
||||
|
||||
const response = useQuery(
|
||||
return useQuery(
|
||||
['actions', { activePage, direction, limit, sortField }],
|
||||
async () => {
|
||||
if (!actionsRequest) return Promise.resolve();
|
||||
|
||||
const responseData = await data.search
|
||||
.search<ActionsRequestOptions, ActionsStrategyResponse>(actionsRequest, {
|
||||
strategy: 'osquerySearchStrategy',
|
||||
})
|
||||
.search<ActionsRequestOptions, ActionsStrategyResponse>(
|
||||
{
|
||||
factoryQueryType: OsqueryQueries.actions,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
pagination: generateTablePaginationOptions(activePage, limit),
|
||||
sort: {
|
||||
direction,
|
||||
field: sortField,
|
||||
},
|
||||
},
|
||||
{
|
||||
strategy: 'osquerySearchStrategy',
|
||||
}
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
return {
|
||||
|
@ -71,28 +76,8 @@ export const useAllActions = ({
|
|||
};
|
||||
},
|
||||
{
|
||||
enabled: !skip && !!actionsRequest,
|
||||
keepPreviousData: true,
|
||||
enabled: !skip,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setHostRequest((prevRequest) => {
|
||||
const myRequest = {
|
||||
...(prevRequest ?? {}),
|
||||
factoryQueryType: OsqueryQueries.actions,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
pagination: generateTablePaginationOptions(activePage, limit),
|
||||
sort: {
|
||||
direction,
|
||||
field: sortField,
|
||||
},
|
||||
};
|
||||
if (!deepEqual(prevRequest, myRequest)) {
|
||||
return myRequest;
|
||||
}
|
||||
return prevRequest;
|
||||
});
|
||||
}, [activePage, direction, filterQuery, limit, sortField]);
|
||||
|
||||
return response;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { EuiLink } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { pagePathGetters } from '../../../fleet/public';
|
||||
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
|
||||
import { useAgentPolicy } from './use_agent_policy';
|
||||
|
||||
interface AgentsPolicyLinkProps {
|
||||
policyId: string;
|
||||
}
|
||||
|
||||
const AgentsPolicyLinkComponent: React.FC<AgentsPolicyLinkProps> = ({ policyId }) => {
|
||||
const {
|
||||
application: { getUrlForApp, navigateToApp },
|
||||
} = useKibana().services;
|
||||
|
||||
const { data } = useAgentPolicy({ policyId });
|
||||
|
||||
const href = useMemo(
|
||||
() =>
|
||||
getUrlForApp('fleet', {
|
||||
path: `#` + pagePathGetters.policy_details({ policyId }),
|
||||
}),
|
||||
[getUrlForApp, policyId]
|
||||
);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event) => {
|
||||
if (!isModifiedEvent(event) && isLeftClickEvent(event)) {
|
||||
event.preventDefault();
|
||||
|
||||
return navigateToApp('fleet', {
|
||||
path: `#` + pagePathGetters.policy_details({ policyId }),
|
||||
});
|
||||
}
|
||||
},
|
||||
[navigateToApp, policyId]
|
||||
);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @elastic/eui/href-or-on-click
|
||||
<EuiLink href={href} onClick={handleClick}>
|
||||
{data?.name ?? policyId}
|
||||
</EuiLink>
|
||||
);
|
||||
};
|
||||
|
||||
export const AgentsPolicyLink = React.memo(AgentsPolicyLinkComponent);
|
9
x-pack/plugins/osquery/public/agent_policies/index.tsx
Normal file
9
x-pack/plugins/osquery/public/agent_policies/index.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 * from './use_agent_policies';
|
||||
export * from './use_agent_policy';
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import {
|
||||
agentPolicyRouteService,
|
||||
GetAgentPoliciesResponse,
|
||||
GetAgentPoliciesResponseItem,
|
||||
} from '../../../fleet/common';
|
||||
|
||||
export const useAgentPolicies = () => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
return useQuery<GetAgentPoliciesResponse, unknown, GetAgentPoliciesResponseItem[]>(
|
||||
['agentPolicies'],
|
||||
() =>
|
||||
http.get(agentPolicyRouteService.getListPath(), {
|
||||
query: {
|
||||
perPage: 100,
|
||||
},
|
||||
}),
|
||||
{
|
||||
initialData: { items: [], total: 0, page: 1, perPage: 100 },
|
||||
placeholderData: [],
|
||||
keepPreviousData: true,
|
||||
select: (response) => response.items,
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { agentPolicyRouteService } from '../../../fleet/common';
|
||||
|
||||
interface UseAgentPolicy {
|
||||
policyId: string;
|
||||
skip?: boolean;
|
||||
}
|
||||
|
||||
export const useAgentPolicy = ({ policyId, skip }: UseAgentPolicy) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
return useQuery(
|
||||
['agentPolicy', { policyId }],
|
||||
() => http.get(agentPolicyRouteService.getInfoPath(policyId)),
|
||||
{
|
||||
enabled: !skip,
|
||||
keepPreviousData: true,
|
||||
select: (response) => response.item,
|
||||
}
|
||||
);
|
||||
};
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { EuiComboBox, EuiHealth, EuiHighlight } from '@elastic/eui';
|
||||
import { find } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { EuiComboBox, EuiHealth, EuiHighlight, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { useDebounce } from 'react-use';
|
||||
import { useAllAgents } from './use_all_agents';
|
||||
|
@ -38,7 +39,7 @@ interface AgentsTableProps {
|
|||
const perPage = 10;
|
||||
const DEBOUNCE_DELAY = 100; // ms
|
||||
|
||||
const AgentsTableComponent: React.FC<AgentsTableProps> = ({ onChange }) => {
|
||||
const AgentsTableComponent: React.FC<AgentsTableProps> = ({ agentSelection, onChange }) => {
|
||||
// search related
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [modifyingSearch, setModifyingSearch] = useState<boolean>(false);
|
||||
|
@ -67,13 +68,34 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ onChange }) => {
|
|||
const [options, setOptions] = useState<GroupOption[]>([]);
|
||||
const [selectedOptions, setSelectedOptions] = useState<GroupOption[]>([]);
|
||||
const [numAgentsSelected, setNumAgentsSelected] = useState<number>(0);
|
||||
const defaultValueInitialized = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (agentSelection && !defaultValueInitialized.current && options.length) {
|
||||
if (agentSelection.policiesSelected) {
|
||||
const policyOptions = find(['label', 'Policy'], options);
|
||||
|
||||
if (policyOptions) {
|
||||
const defaultOptions = policyOptions.options?.filter((option) =>
|
||||
agentSelection.policiesSelected.includes(option.label)
|
||||
);
|
||||
|
||||
if (defaultOptions?.length) {
|
||||
setSelectedOptions(defaultOptions);
|
||||
}
|
||||
defaultValueInitialized.current = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [agentSelection, options]);
|
||||
|
||||
useEffect(() => {
|
||||
// update the groups when groups or agents have changed
|
||||
grouper.setTotalAgents(totalNumAgents);
|
||||
grouper.updateGroup(AGENT_GROUP_KEY.Platform, groups.platforms);
|
||||
grouper.updateGroup(AGENT_GROUP_KEY.Policy, groups.policies);
|
||||
grouper.updateGroup(AGENT_GROUP_KEY.Agent, agents);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
grouper.updateGroup(AGENT_GROUP_KEY.Agent, agents!);
|
||||
const newOptions = grouper.generateOptions();
|
||||
setOptions(newOptions);
|
||||
}, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents, grouper]);
|
||||
|
@ -134,8 +156,6 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ onChange }) => {
|
|||
|
||||
return (
|
||||
<div>
|
||||
{numAgentsSelected > 0 ? <span>{generateSelectedAgentsMessage(numAgentsSelected)}</span> : ''}
|
||||
|
||||
<EuiComboBox
|
||||
placeholder={SELECT_AGENT_LABEL}
|
||||
isLoading={modifyingSearch || groupsLoading || agentsLoading}
|
||||
|
@ -147,6 +167,8 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ onChange }) => {
|
|||
onChange={onSelection}
|
||||
renderOption={renderOption}
|
||||
/>
|
||||
<EuiSpacer size="xs" />
|
||||
{numAgentsSelected > 0 ? <span>{generateSelectedAgentsMessage(numAgentsSelected)}</span> : ''}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -159,15 +159,14 @@ export const generateAgentSelection = (selection: GroupOption[]) => {
|
|||
|
||||
export const generateTablePaginationOptions = (
|
||||
activePage: number,
|
||||
limit: number,
|
||||
isBucketSort?: boolean
|
||||
limit: number
|
||||
): PaginationInputPaginated => {
|
||||
const cursorStart = activePage * limit;
|
||||
return {
|
||||
activePage,
|
||||
cursorStart,
|
||||
fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5,
|
||||
querySize: isBucketSort ? limit : limit + cursorStart,
|
||||
querySize: limit,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -5,13 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mapKeys } from 'lodash';
|
||||
import { useQueries, UseQueryResult } from 'react-query';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import {
|
||||
AgentPolicy,
|
||||
agentPolicyRouteService,
|
||||
GetOneAgentPolicyResponse,
|
||||
} from '../../../fleet/common';
|
||||
import { agentPolicyRouteService, GetOneAgentPolicyResponse } from '../../../fleet/common';
|
||||
|
||||
export const useAgentPolicies = (policyIds: string[] = []) => {
|
||||
const { http } = useKibana().services;
|
||||
|
@ -26,13 +23,7 @@ export const useAgentPolicies = (policyIds: string[] = []) => {
|
|||
|
||||
const agentPoliciesLoading = agentResponse.some((p) => p.isLoading);
|
||||
const agentPolicies = agentResponse.map((p) => p.data?.item);
|
||||
const agentPolicyById = agentPolicies.reduce((acc, p) => {
|
||||
if (!p) {
|
||||
return acc;
|
||||
}
|
||||
acc[p.id] = p;
|
||||
return acc;
|
||||
}, {} as { [key: string]: AgentPolicy });
|
||||
const agentPolicyById = mapKeys(agentPolicies, 'id');
|
||||
|
||||
return { agentPoliciesLoading, agentPolicies, agentPolicyById };
|
||||
};
|
||||
|
|
39
x-pack/plugins/osquery/public/agents/use_agent_status.ts
Normal file
39
x-pack/plugins/osquery/public/agents/use_agent_status.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { GetAgentStatusResponse, agentRouteService } from '../../../fleet/common';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
|
||||
interface UseAgentStatus {
|
||||
policyId?: string;
|
||||
skip?: boolean;
|
||||
}
|
||||
|
||||
export const useAgentStatus = ({ policyId, skip }: UseAgentStatus) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
return useQuery<GetAgentStatusResponse, unknown, GetAgentStatusResponse['results']>(
|
||||
['agentStatus', policyId],
|
||||
() =>
|
||||
http.get(
|
||||
agentRouteService.getStatusPath(),
|
||||
policyId
|
||||
? {
|
||||
query: {
|
||||
policyId,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
),
|
||||
{
|
||||
enabled: !skip,
|
||||
select: (response) => response.results,
|
||||
}
|
||||
);
|
||||
};
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { GetAgentsResponse, agentRouteService } from '../../../fleet/common';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
|
||||
interface UseAllAgents {
|
||||
|
@ -27,14 +28,14 @@ export const useAllAgents = (
|
|||
) => {
|
||||
const { perPage } = opts;
|
||||
const { http } = useKibana().services;
|
||||
const { isLoading: agentsLoading, data: agentData } = useQuery(
|
||||
const { isLoading: agentsLoading, data: agentData } = useQuery<GetAgentsResponse>(
|
||||
['agents', osqueryPolicies, searchValue, perPage],
|
||||
async () => {
|
||||
() => {
|
||||
let kuery = `(${osqueryPolicies.map((p) => `policy_id:${p}`).join(' or ')})`;
|
||||
if (searchValue) {
|
||||
kuery += ` and (local_metadata.host.hostname:/${searchValue}/ or local_metadata.elastic.agent.id:/${searchValue}/)`;
|
||||
}
|
||||
return await http.get('/api/fleet/agents', {
|
||||
return http.get(agentRouteService.getListPath(), {
|
||||
query: {
|
||||
kuery,
|
||||
perPage,
|
||||
|
|
|
@ -7,20 +7,20 @@
|
|||
|
||||
import { useQuery } from 'react-query';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common';
|
||||
import { packagePolicyRouteService, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common';
|
||||
import { OSQUERY_INTEGRATION_NAME } from '../../common';
|
||||
|
||||
export const useOsqueryPolicies = () => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const { isLoading: osqueryPoliciesLoading, data: osqueryPolicies } = useQuery(
|
||||
['osqueryPolicies'],
|
||||
async () => {
|
||||
return await http.get('/api/fleet/package_policies', {
|
||||
() =>
|
||||
http.get(packagePolicyRouteService.getListPath(), {
|
||||
query: {
|
||||
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:osquery_manager`,
|
||||
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${OSQUERY_INTEGRATION_NAME}`,
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
{ select: (data) => data.items.map((p: { policy_id: string }) => p.policy_id) }
|
||||
);
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import ReactDOM from 'react-dom';
|
|||
import { Router } from 'react-router-dom';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { QueryClientProvider } from 'react-query';
|
||||
import { ReactQueryDevtools } from 'react-query/devtools';
|
||||
|
||||
import { useUiSetting$ } from '../../../../src/plugins/kibana_react/public';
|
||||
|
@ -23,8 +23,7 @@ import { AppPluginStartDependencies } from './types';
|
|||
import { OsqueryApp } from './components/app';
|
||||
import { DEFAULT_DARK_MODE, PLUGIN_NAME } from '../common';
|
||||
import { KibanaContextProvider } from './common/lib/kibana';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
import { queryClient } from './query_client';
|
||||
|
||||
const OsqueryAppContext = () => {
|
||||
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);
|
||||
|
|
9
x-pack/plugins/osquery/public/common/hooks/index.tsx
Normal file
9
x-pack/plugins/osquery/public/common/hooks/index.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 * from './use_discover_link';
|
||||
export * from './use_osquery_integration';
|
136
x-pack/plugins/osquery/public/common/hooks/use_breadcrumbs.tsx
Normal file
136
x-pack/plugins/osquery/public/common/hooks/use_breadcrumbs.tsx
Normal file
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ChromeBreadcrumb } from 'src/core/public';
|
||||
|
||||
import { BASE_PATH } from '../../../common/constants';
|
||||
import type { Page, DynamicPagePathValues } from '../page_paths';
|
||||
import { pagePathGetters } from '../page_paths';
|
||||
|
||||
import { useKibana } from '../lib/kibana';
|
||||
|
||||
const BASE_BREADCRUMB: ChromeBreadcrumb = {
|
||||
href: pagePathGetters.overview(),
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.appTitle', {
|
||||
defaultMessage: 'Osquery',
|
||||
}),
|
||||
};
|
||||
|
||||
const breadcrumbGetters: {
|
||||
[key in Page]?: (values: DynamicPagePathValues) => ChromeBreadcrumb[];
|
||||
} = {
|
||||
base: () => [BASE_BREADCRUMB],
|
||||
overview: () => [
|
||||
BASE_BREADCRUMB,
|
||||
{
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.overviewPageTitle', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
},
|
||||
],
|
||||
live_queries: () => [
|
||||
BASE_BREADCRUMB,
|
||||
{
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.liveQueriesPageTitle', {
|
||||
defaultMessage: 'Live queries',
|
||||
}),
|
||||
},
|
||||
],
|
||||
live_query_new: () => [
|
||||
BASE_BREADCRUMB,
|
||||
{
|
||||
href: pagePathGetters.live_queries(),
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.liveQueriesPageTitle', {
|
||||
defaultMessage: 'Live queries',
|
||||
}),
|
||||
},
|
||||
{
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.newLiveQueryPageTitle', {
|
||||
defaultMessage: 'New',
|
||||
}),
|
||||
},
|
||||
],
|
||||
live_query_details: ({ liveQueryId }) => [
|
||||
BASE_BREADCRUMB,
|
||||
{
|
||||
href: pagePathGetters.live_queries(),
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.liveQueriesPageTitle', {
|
||||
defaultMessage: 'Live queries',
|
||||
}),
|
||||
},
|
||||
{
|
||||
text: liveQueryId,
|
||||
},
|
||||
],
|
||||
scheduled_query_groups: () => [
|
||||
BASE_BREADCRUMB,
|
||||
{
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.scheduledQueryGroupsPageTitle', {
|
||||
defaultMessage: 'Scheduled query groups',
|
||||
}),
|
||||
},
|
||||
],
|
||||
scheduled_query_group_add: () => [
|
||||
BASE_BREADCRUMB,
|
||||
{
|
||||
href: pagePathGetters.scheduled_query_groups(),
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.scheduledQueryGroupsPageTitle', {
|
||||
defaultMessage: 'Scheduled query groups',
|
||||
}),
|
||||
},
|
||||
{
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.addScheduledQueryGroupsPageTitle', {
|
||||
defaultMessage: 'Add',
|
||||
}),
|
||||
},
|
||||
],
|
||||
scheduled_query_group_details: ({ scheduledQueryGroupName }) => [
|
||||
BASE_BREADCRUMB,
|
||||
{
|
||||
href: pagePathGetters.scheduled_query_groups(),
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.scheduledQueryGroupsPageTitle', {
|
||||
defaultMessage: 'Scheduled query groups',
|
||||
}),
|
||||
},
|
||||
{
|
||||
text: scheduledQueryGroupName,
|
||||
},
|
||||
],
|
||||
scheduled_query_group_edit: ({ scheduledQueryGroupName, scheduledQueryGroupId }) => [
|
||||
BASE_BREADCRUMB,
|
||||
{
|
||||
href: pagePathGetters.scheduled_query_groups(),
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.scheduledQueryGroupsPageTitle', {
|
||||
defaultMessage: 'Scheduled query groups',
|
||||
}),
|
||||
},
|
||||
{
|
||||
href: pagePathGetters.scheduled_query_group_details({ scheduledQueryGroupId }),
|
||||
text: scheduledQueryGroupName,
|
||||
},
|
||||
{
|
||||
text: i18n.translate('xpack.osquery.breadcrumbs.editScheduledQueryGroupsPageTitle', {
|
||||
defaultMessage: 'Edit',
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function useBreadcrumbs(page: Page, values: DynamicPagePathValues = {}) {
|
||||
const { chrome, http } = useKibana().services;
|
||||
const breadcrumbs: ChromeBreadcrumb[] =
|
||||
breadcrumbGetters[page]?.(values).map((breadcrumb) => ({
|
||||
...breadcrumb,
|
||||
href: breadcrumb.href ? http.basePath.prepend(`${BASE_PATH}${breadcrumb.href}`) : undefined,
|
||||
})) || [];
|
||||
const docTitle: string[] = [...breadcrumbs]
|
||||
.reverse()
|
||||
.map((breadcrumb) => breadcrumb.text as string);
|
||||
chrome.docTitle.change(docTitle);
|
||||
chrome.setBreadcrumbs(breadcrumbs);
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { FilterStateStore } from '../../../../../../src/plugins/data/common';
|
||||
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../lib/kibana';
|
||||
|
||||
interface UseDiscoverLink {
|
||||
filters: Array<{ key: string; value: string | number }>;
|
||||
}
|
||||
|
||||
export const useDiscoverLink = ({ filters }: UseDiscoverLink) => {
|
||||
const {
|
||||
application: { navigateToUrl },
|
||||
} = useKibana().services;
|
||||
const urlGenerator = useKibana().services.discover?.urlGenerator;
|
||||
const [discoverUrl, setDiscoverUrl] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const getDiscoverUrl = async () => {
|
||||
if (!urlGenerator?.createUrl) return;
|
||||
|
||||
const newUrl = await urlGenerator.createUrl({
|
||||
indexPatternId: 'logs-*',
|
||||
filters: filters.map((filter) => ({
|
||||
meta: {
|
||||
index: 'logs-*',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: filter.key,
|
||||
params: { query: filter.value },
|
||||
},
|
||||
query: { match_phrase: { action_id: filter.value } },
|
||||
$state: { store: FilterStateStore.APP_STATE },
|
||||
})),
|
||||
});
|
||||
setDiscoverUrl(newUrl);
|
||||
};
|
||||
getDiscoverUrl();
|
||||
}, [filters, urlGenerator]);
|
||||
|
||||
const onClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (!isModifiedEvent(event) && isLeftClickEvent(event) && discoverUrl) {
|
||||
event.preventDefault();
|
||||
|
||||
return navigateToUrl(discoverUrl);
|
||||
}
|
||||
},
|
||||
[discoverUrl, navigateToUrl]
|
||||
);
|
||||
|
||||
return {
|
||||
href: discoverUrl,
|
||||
onClick,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { find } from 'lodash/fp';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { GetPackagesResponse, epmRouteService } from '../../../../fleet/common';
|
||||
import { OSQUERY_INTEGRATION_NAME } from '../../../common';
|
||||
import { useKibana } from '../lib/kibana';
|
||||
|
||||
export const useOsqueryIntegration = () => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
return useQuery(
|
||||
'integrations',
|
||||
() =>
|
||||
http.get(epmRouteService.getListPath(), {
|
||||
query: {
|
||||
experimental: true,
|
||||
},
|
||||
}),
|
||||
{
|
||||
select: ({ response }: GetPackagesResponse) =>
|
||||
find(['name', OSQUERY_INTEGRATION_NAME], response),
|
||||
}
|
||||
);
|
||||
};
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
KibanaContextProvider,
|
||||
|
@ -24,6 +25,11 @@ export interface WithKibanaProps {
|
|||
|
||||
const useTypedKibana = () => useKibana<StartServices>();
|
||||
|
||||
const isModifiedEvent = (event: React.MouseEvent) =>
|
||||
!!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
|
||||
|
||||
const isLeftClickEvent = (event: React.MouseEvent) => event.button === 0;
|
||||
|
||||
const useRouterNavigate = (
|
||||
to: Parameters<typeof reactRouterNavigate>[1],
|
||||
onClickCallback?: Parameters<typeof reactRouterNavigate>[2]
|
||||
|
@ -35,6 +41,8 @@ const useRouterNavigate = (
|
|||
export {
|
||||
KibanaContextProvider,
|
||||
useRouterNavigate,
|
||||
isLeftClickEvent,
|
||||
isModifiedEvent,
|
||||
useTypedKibana as useKibana,
|
||||
useUiSetting,
|
||||
useUiSetting$,
|
||||
|
|
59
x-pack/plugins/osquery/public/common/page_paths.ts
Normal file
59
x-pack/plugins/osquery/public/common/page_paths.ts
Normal 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.
|
||||
*/
|
||||
|
||||
export type StaticPage =
|
||||
| 'base'
|
||||
| 'overview'
|
||||
| 'live_queries'
|
||||
| 'live_query_new'
|
||||
| 'scheduled_query_groups'
|
||||
| 'scheduled_query_group_add';
|
||||
|
||||
export type DynamicPage =
|
||||
| 'live_query_details'
|
||||
| 'scheduled_query_group_details'
|
||||
| 'scheduled_query_group_edit';
|
||||
|
||||
export type Page = StaticPage | DynamicPage;
|
||||
|
||||
export interface DynamicPagePathValues {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export const BASE_PATH = '/app/fleet';
|
||||
|
||||
// If routing paths are changed here, please also check to see if
|
||||
// `pagePathGetters()`, below, needs any modifications
|
||||
export const PAGE_ROUTING_PATHS = {
|
||||
overview: '/',
|
||||
live_queries: '/live_queries',
|
||||
live_query_new: '/live_queries/new',
|
||||
live_query_details: '/live_queries/:liveQueryId',
|
||||
scheduled_query_groups: '/scheduled_query_groups',
|
||||
scheduled_query_group_add: '/scheduled_query_groups/add',
|
||||
scheduled_query_group_details: '/scheduled_query_groups/:scheduledQueryGroupId',
|
||||
scheduled_query_group_edit: '/scheduled_query_groups/:scheduledQueryGroupId/edit',
|
||||
};
|
||||
|
||||
export const pagePathGetters: {
|
||||
[key in StaticPage]: () => string;
|
||||
} &
|
||||
{
|
||||
[key in DynamicPage]: (values: DynamicPagePathValues) => string;
|
||||
} = {
|
||||
base: () => '/',
|
||||
overview: () => '/',
|
||||
live_queries: () => '/live_queries',
|
||||
live_query_new: () => '/live_queries/new',
|
||||
live_query_details: ({ liveQueryId }) => `/live_queries/${liveQueryId}`,
|
||||
scheduled_query_groups: () => '/scheduled_query_groups',
|
||||
scheduled_query_group_add: () => '/scheduled_query_groups/add',
|
||||
scheduled_query_group_details: ({ scheduledQueryGroupId }) =>
|
||||
`/scheduled_query_groups/${scheduledQueryGroupId}`,
|
||||
scheduled_query_group_edit: ({ scheduledQueryGroupId }) =>
|
||||
`/scheduled_query_groups/${scheduledQueryGroupId}/edit`,
|
||||
};
|
|
@ -7,14 +7,15 @@
|
|||
|
||||
import React, { useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTabs, EuiTab } from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiTabs, EuiTab } from '@elastic/eui';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { Container, Nav, Wrapper } from './layouts';
|
||||
import { OsqueryAppRoutes } from '../routes';
|
||||
import { useRouterNavigate } from '../common/lib/kibana';
|
||||
import { ManageIntegrationLink } from './manage_integration_link';
|
||||
|
||||
export const OsqueryAppComponent = () => {
|
||||
const OsqueryAppComponent = () => {
|
||||
const location = useLocation();
|
||||
const section = useMemo(() => location.pathname.split('/')[1] ?? 'overview', [location.pathname]);
|
||||
|
||||
|
@ -25,20 +26,49 @@ export const OsqueryAppComponent = () => {
|
|||
<EuiFlexGroup gutterSize="l" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiTabs display="condensed">
|
||||
<EuiTab isSelected={section === 'overview'} {...useRouterNavigate('overview')}>
|
||||
{/* <EuiTab isSelected={section === 'overview'} {...useRouterNavigate('overview')}>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.appNavigation.overviewLinkText"
|
||||
defaultMessage="Overview"
|
||||
/>
|
||||
</EuiTab>
|
||||
<EuiTab isSelected={section === 'live_query'} {...useRouterNavigate('live_query')}>
|
||||
</EuiTab> */}
|
||||
<EuiTab
|
||||
isSelected={section === 'live_queries'}
|
||||
{...useRouterNavigate('live_queries')}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.appNavigation.liveQueryLinkText"
|
||||
defaultMessage="Live Query"
|
||||
id="xpack.osquery.appNavigation.liveQueriesLinkText"
|
||||
defaultMessage="Live queries"
|
||||
/>
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
isSelected={section === 'scheduled_query_groups'}
|
||||
{...useRouterNavigate('scheduled_query_groups')}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.appNavigation.scheduledQueryGroupsLinkText"
|
||||
defaultMessage="Scheduled query groups"
|
||||
/>
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s" direction="row">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty
|
||||
iconType="popout"
|
||||
href="https://ela.st/osquery-feedback"
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.appNavigation.sendFeedbackButton"
|
||||
defaultMessage="Send feedback"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<ManageIntegrationLink />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Nav>
|
||||
<OsqueryAppRoutes />
|
||||
|
|
37
x-pack/plugins/osquery/public/components/beta_badge.tsx
Normal file
37
x-pack/plugins/osquery/public/components/beta_badge.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { EuiBetaBadge, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const BetaBadgeRowWrapper = styled(EuiText)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
padding-left: ${({ theme }) => theme.eui.paddingSizes.s};
|
||||
`;
|
||||
|
||||
const betaBadgeLabel = i18n.translate('xpack.osquery.common.tabBetaBadgeLabel', {
|
||||
defaultMessage: 'Beta',
|
||||
});
|
||||
|
||||
const betaBadgeTooltipContent = i18n.translate('xpack.osquery.common.tabBetaBadgeTooltipContent', {
|
||||
defaultMessage:
|
||||
'This feature is under active development. Extra functionality is coming, and some functionality may change.',
|
||||
});
|
||||
|
||||
const BetaBadgeComponent = () => (
|
||||
<Wrapper>
|
||||
<EuiBetaBadge label={betaBadgeLabel} tooltipContent={betaBadgeTooltipContent} />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
export const BetaBadge = React.memo(BetaBadgeComponent);
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { pagePathGetters } from '../../../fleet/public';
|
||||
|
||||
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
|
||||
import { useOsqueryIntegration } from '../common/hooks';
|
||||
|
||||
const ManageIntegrationLinkComponent = () => {
|
||||
const {
|
||||
application: {
|
||||
getUrlForApp,
|
||||
navigateToApp,
|
||||
capabilities: {
|
||||
osquery: { save: hasSaveUICapabilities },
|
||||
},
|
||||
},
|
||||
} = useKibana().services;
|
||||
const { data: osqueryIntegration } = useOsqueryIntegration();
|
||||
|
||||
const integrationHref = useMemo(() => {
|
||||
if (osqueryIntegration) {
|
||||
return getUrlForApp('fleet', {
|
||||
path:
|
||||
'#' +
|
||||
pagePathGetters.integration_details_policies({
|
||||
pkgkey: `${osqueryIntegration.name}-${osqueryIntegration.version}`,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}, [getUrlForApp, osqueryIntegration]);
|
||||
|
||||
const integrationClick = useCallback(
|
||||
(event) => {
|
||||
if (!isModifiedEvent(event) && isLeftClickEvent(event)) {
|
||||
event.preventDefault();
|
||||
if (osqueryIntegration) {
|
||||
return navigateToApp('fleet', {
|
||||
path:
|
||||
'#' +
|
||||
pagePathGetters.integration_details_policies({
|
||||
pkgkey: `${osqueryIntegration.name}-${osqueryIntegration.version}`,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[navigateToApp, osqueryIntegration]
|
||||
);
|
||||
|
||||
return hasSaveUICapabilities && integrationHref ? (
|
||||
<EuiFlexItem>
|
||||
{
|
||||
// eslint-disable-next-line @elastic/eui/href-or-on-click
|
||||
<EuiButtonEmpty iconType="gear" href={integrationHref} onClick={integrationClick}>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.appNavigation.manageIntegrationButton"
|
||||
defaultMessage="Manage integration"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
</EuiFlexItem>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const ManageIntegrationLink = React.memo(ManageIntegrationLinkComponent);
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiCodeEditor } from '@elastic/eui';
|
||||
import 'brace/theme/tomorrow';
|
||||
|
||||
|
@ -22,30 +22,27 @@ const EDITOR_PROPS = {
|
|||
|
||||
interface OsqueryEditorProps {
|
||||
defaultValue: string;
|
||||
disabled?: boolean;
|
||||
onChange: (newValue: string) => void;
|
||||
}
|
||||
|
||||
const OsqueryEditorComponent: React.FC<OsqueryEditorProps> = ({ defaultValue, onChange }) => {
|
||||
const handleChange = useCallback(
|
||||
(newValue) => {
|
||||
onChange(newValue);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiCodeEditor
|
||||
value={defaultValue}
|
||||
mode="osquery"
|
||||
theme="tomorrow"
|
||||
onChange={handleChange}
|
||||
name="osquery_editor"
|
||||
setOptions={EDITOR_SET_OPTIONS}
|
||||
editorProps={EDITOR_PROPS}
|
||||
height="100px"
|
||||
width="100%"
|
||||
/>
|
||||
);
|
||||
};
|
||||
const OsqueryEditorComponent: React.FC<OsqueryEditorProps> = ({
|
||||
defaultValue,
|
||||
// disabled,
|
||||
onChange,
|
||||
}) => (
|
||||
<EuiCodeEditor
|
||||
value={defaultValue}
|
||||
mode="osquery"
|
||||
// isReadOnly={disabled}
|
||||
theme="tomorrow"
|
||||
onChange={onChange}
|
||||
name="osquery_editor"
|
||||
setOptions={EDITOR_SET_OPTIONS}
|
||||
editorProps={EDITOR_PROPS}
|
||||
height="200px"
|
||||
width="100%"
|
||||
/>
|
||||
);
|
||||
|
||||
export const OsqueryEditor = React.memo(OsqueryEditorComponent);
|
||||
|
|
|
@ -1,68 +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.
|
||||
*/
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-function-as-prop */
|
||||
|
||||
import { produce } from 'immer';
|
||||
import { EuiFlyout, EuiTitle, EuiFlyoutBody, EuiFlyoutHeader, EuiPortal } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { AddPackQueryForm } from '../../packs/common/add_pack_query';
|
||||
|
||||
// @ts-expect-error update types
|
||||
export const AddNewQueryFlyout = ({ data, handleChange, onClose }) => {
|
||||
// @ts-expect-error update types
|
||||
const handleSubmit = (payload) => {
|
||||
// @ts-expect-error update types
|
||||
const updatedPolicy = produce(data, (draft) => {
|
||||
draft.inputs[0].streams.push({
|
||||
data_stream: {
|
||||
type: 'logs',
|
||||
dataset: 'osquery_elastic_managed.osquery',
|
||||
},
|
||||
vars: {
|
||||
query: {
|
||||
type: 'text',
|
||||
value: payload.query.attributes.query,
|
||||
},
|
||||
interval: {
|
||||
type: 'text',
|
||||
value: `${payload.interval}`,
|
||||
},
|
||||
id: {
|
||||
type: 'text',
|
||||
value: payload.query.id,
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
onClose();
|
||||
handleChange({
|
||||
isValid: true,
|
||||
updatedPolicy,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiFlyout ownFocus onClose={onClose} aria-labelledby="flyoutTitle">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h2 id="flyoutTitle">Attach next query</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<AddPackQueryForm handleSubmit={handleSubmit} />
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
};
|
|
@ -1,36 +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 React, { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import qs from 'query-string';
|
||||
|
||||
import { Queries } from '../../queries';
|
||||
import { Packs } from '../../packs';
|
||||
import { LiveQuery } from '../../live_query';
|
||||
|
||||
const CustomTabTabsComponent = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const selectedTab = useMemo(() => qs.parse(location.search)?.tab, [location.search]);
|
||||
|
||||
if (selectedTab === 'packs') {
|
||||
return <Packs />;
|
||||
}
|
||||
|
||||
if (selectedTab === 'saved_queries') {
|
||||
return <Queries />;
|
||||
}
|
||||
|
||||
if (selectedTab === 'live_query') {
|
||||
return <LiveQuery />;
|
||||
}
|
||||
|
||||
return <Packs />;
|
||||
};
|
||||
|
||||
export const CustomTabTabs = React.memo(CustomTabTabsComponent);
|
|
@ -1,240 +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.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import produce from 'immer';
|
||||
import { find } from 'lodash/fp';
|
||||
import { EuiSpacer, EuiText, EuiHorizontalRule, EuiSuperSelect } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import {
|
||||
// UseField,
|
||||
useForm,
|
||||
useFormData,
|
||||
UseArray,
|
||||
getUseField,
|
||||
Field,
|
||||
ToggleField,
|
||||
Form,
|
||||
} from '../../shared_imports';
|
||||
|
||||
// import { OsqueryStreamField } from '../../scheduled_query/common/osquery_stream_field';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { ScheduledQueryQueriesTable } from './scheduled_queries_table';
|
||||
import { schema } from './schema';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
const EDIT_SCHEDULED_QUERY_FORM_ID = 'editScheduledQueryForm';
|
||||
|
||||
interface EditScheduledQueryFormProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: Array<Record<string, any>>;
|
||||
handleSubmit: () => Promise<void>;
|
||||
}
|
||||
|
||||
const EditScheduledQueryFormComponent: React.FC<EditScheduledQueryFormProps> = ({
|
||||
data,
|
||||
handleSubmit,
|
||||
}) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const {
|
||||
data: { saved_objects: packs } = {
|
||||
saved_objects: [],
|
||||
},
|
||||
} = useQuery('packs', () => http.get('/internal/osquery/pack'));
|
||||
|
||||
const { form } = useForm({
|
||||
id: EDIT_SCHEDULED_QUERY_FORM_ID,
|
||||
onSubmit: handleSubmit,
|
||||
schema,
|
||||
defaultValue: data,
|
||||
options: {
|
||||
stripEmptyFields: false,
|
||||
},
|
||||
// @ts-expect-error update types
|
||||
deserializer: (payload) => {
|
||||
const deserialized = produce(payload, (draft) => {
|
||||
// @ts-expect-error update types
|
||||
draft.streams = draft.inputs[0].streams.map(({ data_stream, enabled, vars }) => ({
|
||||
data: {
|
||||
data_stream,
|
||||
enabled,
|
||||
vars,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
return deserialized;
|
||||
},
|
||||
// @ts-expect-error update types
|
||||
serializer: (payload) => {
|
||||
const serialized = produce(payload, (draft) => {
|
||||
// @ts-expect-error update types
|
||||
if (draft.inputs) {
|
||||
// @ts-expect-error update types
|
||||
draft.inputs[0].config = {
|
||||
pack: {
|
||||
type: 'id',
|
||||
value: 'e33f5f30-705e-11eb-9e99-9f6b4d0d9506',
|
||||
},
|
||||
};
|
||||
// @ts-expect-error update types
|
||||
draft.inputs[0].type = 'osquery';
|
||||
// @ts-expect-error update types
|
||||
draft.inputs[0].streams = draft.inputs[0].streams?.map((stream) => stream.data) ?? [];
|
||||
}
|
||||
});
|
||||
|
||||
return serialized;
|
||||
},
|
||||
});
|
||||
|
||||
const { setFieldValue } = form;
|
||||
|
||||
const handlePackChange = useCallback(
|
||||
(value) => {
|
||||
const newPack = find(['id', value], packs);
|
||||
|
||||
setFieldValue(
|
||||
'streams',
|
||||
// @ts-expect-error update types
|
||||
newPack.queries.map((packQuery, index) => ({
|
||||
id: index,
|
||||
isNew: true,
|
||||
path: `streams[${index}]`,
|
||||
data: {
|
||||
data_stream: {
|
||||
type: 'logs',
|
||||
dataset: 'osquery_elastic_managed.osquery',
|
||||
},
|
||||
id: 'osquery-osquery_elastic_managed.osquery-7065c2dc-f835-4d13-9486-6eec515f39bd',
|
||||
vars: {
|
||||
query: {
|
||||
type: 'text',
|
||||
value: packQuery.query,
|
||||
},
|
||||
interval: {
|
||||
type: 'text',
|
||||
value: `${packQuery.interval}`,
|
||||
},
|
||||
id: {
|
||||
type: 'text',
|
||||
value: packQuery.id,
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
}))
|
||||
);
|
||||
},
|
||||
[packs, setFieldValue]
|
||||
);
|
||||
|
||||
const [formData] = useFormData({ form, watch: ['streams'] });
|
||||
|
||||
const scheduledQueries = useMemo(() => {
|
||||
if (formData.inputs) {
|
||||
// @ts-expect-error update types
|
||||
return formData.streams.reduce((acc, stream) => {
|
||||
if (!stream.data) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [...acc, stream.data];
|
||||
}, []);
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [formData]);
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<EuiSuperSelect
|
||||
// @ts-expect-error update types
|
||||
options={packs.map((pack) => ({
|
||||
value: pack.id,
|
||||
inputDisplay: (
|
||||
<>
|
||||
<EuiText>{pack.name}</EuiText>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p className="euiTextColor--subdued">{pack.description}</p>
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
valueOfSelected={packs[0]?.id}
|
||||
onChange={handlePackChange}
|
||||
/>
|
||||
<ScheduledQueryQueriesTable data={scheduledQueries} />
|
||||
<CommonUseField path="enabled" component={ToggleField} />
|
||||
<EuiHorizontalRule />
|
||||
<EuiSpacer />
|
||||
<UseArray path="streams" readDefaultValueOnForm={true}>
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
({ items, form: streamsForm, addItem, removeItem }) => {
|
||||
return (
|
||||
<>
|
||||
{/* {items.map((item) => {
|
||||
return (
|
||||
<UseField
|
||||
key={item.path}
|
||||
path={`${item.path}.data`}
|
||||
component={OsqueryStreamField}
|
||||
// eslint-disable-next-line react/jsx-no-bind, react-perf/jsx-no-new-function-as-prop
|
||||
removeItem={() => removeItem(item.id)}
|
||||
// readDefaultValueOnForm={true}
|
||||
defaultValue={
|
||||
item.isNew
|
||||
? // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
{
|
||||
data_stream: {
|
||||
type: 'logs',
|
||||
dataset: 'osquery_elastic_managed.osquery',
|
||||
},
|
||||
vars: {
|
||||
query: {
|
||||
type: 'text',
|
||||
value: 'select * from uptime',
|
||||
},
|
||||
interval: {
|
||||
type: 'text',
|
||||
value: '120',
|
||||
},
|
||||
id: {
|
||||
type: 'text',
|
||||
value: uuid.v4(),
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
}
|
||||
: get(item.path, streamsForm.getFormData())
|
||||
}
|
||||
/>
|
||||
);
|
||||
})} */}
|
||||
{/* <EuiButtonEmpty onClick={addItem} iconType="plusInCircleFilled">
|
||||
{'Add query'}
|
||||
</EuiButtonEmpty> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
</UseArray>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditScheduledQueryForm = React.memo(
|
||||
EditScheduledQueryFormComponent,
|
||||
(prevProps, nextProps) => deepEqual(prevProps.data, nextProps.data)
|
||||
);
|
|
@ -1,69 +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 React from 'react';
|
||||
|
||||
import { useForm, Form, getUseField, Field, FIELD_TYPES } from '../../shared_imports';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
const FORM_ID = 'inputStreamForm';
|
||||
|
||||
const schema = {
|
||||
data_stream: {
|
||||
dataset: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
type: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
},
|
||||
enabled: {
|
||||
type: FIELD_TYPES.TOGGLE,
|
||||
label: 'Active',
|
||||
},
|
||||
id: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
vars: {
|
||||
id: {
|
||||
type: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
value: { type: FIELD_TYPES.TEXT },
|
||||
},
|
||||
interval: {
|
||||
type: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
value: { type: FIELD_TYPES.TEXT },
|
||||
},
|
||||
query: {
|
||||
type: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
value: { type: FIELD_TYPES.TEXT },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error update types
|
||||
const InputStreamFormComponent = ({ data }) => {
|
||||
const { form } = useForm({
|
||||
id: FORM_ID,
|
||||
schema,
|
||||
defaultValue: data,
|
||||
});
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<CommonUseField path="vars.query.value" />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const InputStreamForm = React.memo(InputStreamFormComponent);
|
|
@ -1,64 +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.
|
||||
*/
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-object-as-prop */
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-array-as-prop */
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import produce from 'immer';
|
||||
import { EuiRadioGroup } from '@elastic/eui';
|
||||
|
||||
// @ts-expect-error update types
|
||||
export const ScheduledQueryInputType = ({ data, handleChange }) => {
|
||||
const radios = [
|
||||
{
|
||||
id: 'pack',
|
||||
label: 'Pack',
|
||||
},
|
||||
{
|
||||
id: 'saved_queries',
|
||||
label: 'Saved queries',
|
||||
},
|
||||
];
|
||||
|
||||
const onChange = useCallback(
|
||||
(optionId: string) => {
|
||||
// @ts-expect-error update types
|
||||
const updatedPolicy = produce(data, (draft) => {
|
||||
if (!draft.inputs[0].config) {
|
||||
draft.inputs[0].config = {
|
||||
input_source: {
|
||||
type: 'text',
|
||||
value: optionId,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
draft.inputs[0].config.input_source.value = optionId;
|
||||
}
|
||||
});
|
||||
|
||||
handleChange({
|
||||
isValid: true,
|
||||
updatedPolicy,
|
||||
});
|
||||
},
|
||||
[data, handleChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiRadioGroup
|
||||
options={radios}
|
||||
idSelected={data.inputs[0].config?.input_source?.value ?? 'saved_queries'}
|
||||
onChange={onChange}
|
||||
name="radio group"
|
||||
legend={{
|
||||
children: <span>{'Choose input type'}</span>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,92 +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 { snakeCase } from 'lodash/fp';
|
||||
import { EuiIcon, EuiSideNav } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import qs from 'query-string';
|
||||
|
||||
export const Navigation = () => {
|
||||
const { push } = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
const selectedItemName = useMemo(() => qs.parse(location.search)?.tab, [location.search]);
|
||||
|
||||
const handleTabClick = useCallback(
|
||||
(tab) => {
|
||||
push({
|
||||
search: qs.stringify({ tab }),
|
||||
});
|
||||
},
|
||||
[push]
|
||||
);
|
||||
|
||||
const createItem = useCallback(
|
||||
(name, data = {}) => ({
|
||||
...data,
|
||||
id: snakeCase(name),
|
||||
name,
|
||||
isSelected: selectedItemName === name,
|
||||
onClick: () => handleTabClick(snakeCase(name)),
|
||||
}),
|
||||
[handleTabClick, selectedItemName]
|
||||
);
|
||||
|
||||
const sideNav = useMemo(
|
||||
() => [
|
||||
createItem('Packs', {
|
||||
forceOpen: true,
|
||||
items: [
|
||||
createItem('List', {
|
||||
icon: <EuiIcon type="list" />,
|
||||
}),
|
||||
createItem('New pack', {
|
||||
icon: <EuiIcon type="listAdd" />,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
createItem('Saved Queries', {
|
||||
forceOpen: true,
|
||||
items: [
|
||||
createItem('List', {
|
||||
icon: <EuiIcon type="list" />,
|
||||
}),
|
||||
createItem('New query', {
|
||||
icon: <EuiIcon type="listAdd" />,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
// createItem('Scheduled Queries', {
|
||||
// forceOpen: true,
|
||||
// items: [
|
||||
// createItem('List', {
|
||||
// icon: <EuiIcon type="list" />,
|
||||
// }),
|
||||
// createItem('Schedule new query', {
|
||||
// icon: <EuiIcon type="listAdd" />,
|
||||
// }),
|
||||
// ],
|
||||
// }),
|
||||
createItem('Live Query', {
|
||||
forceOpen: true,
|
||||
items: [
|
||||
createItem('Run', {
|
||||
icon: <EuiIcon type="play" />,
|
||||
}),
|
||||
createItem('History', {
|
||||
icon: <EuiIcon type="tableDensityNormal" />,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
[createItem]
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
return <EuiSideNav items={sideNav} style={{ width: 200 }} />;
|
||||
};
|
|
@ -1,87 +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.
|
||||
*/
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-function-as-prop */
|
||||
|
||||
import { find } from 'lodash/fp';
|
||||
import { produce } from 'immer';
|
||||
import { EuiText, EuiSuperSelect } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
// @ts-expect-error update types
|
||||
export const ScheduledQueryPackSelector = ({ data, handleChange }) => {
|
||||
const { http } = useKibana().services;
|
||||
const {
|
||||
data: { saved_objects: packs } = {
|
||||
saved_objects: [],
|
||||
},
|
||||
} = useQuery('packs', () => http.get('/internal/osquery/pack'));
|
||||
|
||||
// @ts-expect-error update types
|
||||
const handlePackChange = (value) => {
|
||||
const newPack = find(['id', value], packs);
|
||||
|
||||
// @ts-expect-error update types
|
||||
const updatedPolicy = produce(data, (draft) => {
|
||||
draft.inputs[0].config.pack = {
|
||||
type: 'text',
|
||||
value: newPack.id,
|
||||
};
|
||||
// @ts-expect-error update types
|
||||
draft.inputs[0].streams = newPack.queries.map((packQuery) => ({
|
||||
data_stream: {
|
||||
type: 'logs',
|
||||
dataset: 'osquery_elastic_managed.osquery',
|
||||
},
|
||||
vars: {
|
||||
query: {
|
||||
type: 'text',
|
||||
value: packQuery.query,
|
||||
},
|
||||
interval: {
|
||||
type: 'text',
|
||||
value: `${packQuery.interval}`,
|
||||
},
|
||||
id: {
|
||||
type: 'text',
|
||||
value: packQuery.id,
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
}));
|
||||
});
|
||||
|
||||
handleChange({
|
||||
isValid: true,
|
||||
updatedPolicy,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiSuperSelect
|
||||
// @ts-expect-error update types
|
||||
options={packs.map((pack) => ({
|
||||
value: pack.id,
|
||||
inputDisplay: (
|
||||
<>
|
||||
<EuiText>{pack.name}</EuiText>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p className="euiTextColor--subdued">{pack.description}</p>
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
valueOfSelected={data.inputs[0].config}
|
||||
onChange={handlePackChange}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,142 +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.
|
||||
*/
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-function-as-prop */
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-object-as-prop */
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-array-as-prop */
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButtonIcon,
|
||||
EuiHealth,
|
||||
EuiDescriptionList,
|
||||
RIGHT_ALIGNMENT,
|
||||
} from '@elastic/eui';
|
||||
|
||||
// @ts-expect-error update types
|
||||
export const ScheduledQueryQueriesTable = ({ data }) => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const [sortField, setSortField] = useState('firstName');
|
||||
const [sortDirection, setSortDirection] = useState('asc');
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState({});
|
||||
|
||||
const onTableChange = ({ page = {}, sort = {} }) => {
|
||||
// @ts-expect-error update types
|
||||
const { index, size } = page;
|
||||
// @ts-expect-error update types
|
||||
const { field, direction } = sort;
|
||||
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
setSortField(field);
|
||||
setSortDirection(direction);
|
||||
};
|
||||
|
||||
// @ts-expect-error update types
|
||||
const toggleDetails = (item) => {
|
||||
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
|
||||
// @ts-expect-error update types
|
||||
if (itemIdToExpandedRowMapValues[item.id]) {
|
||||
// @ts-expect-error update types
|
||||
delete itemIdToExpandedRowMapValues[item.id];
|
||||
} else {
|
||||
const { online } = item;
|
||||
const color = online ? 'success' : 'danger';
|
||||
const label = online ? 'Online' : 'Offline';
|
||||
const listItems = [
|
||||
{
|
||||
title: 'Online',
|
||||
description: <EuiHealth color={color}>{label}</EuiHealth>,
|
||||
},
|
||||
];
|
||||
// @ts-expect-error update types
|
||||
itemIdToExpandedRowMapValues[item.id] = <EuiDescriptionList listItems={listItems} />;
|
||||
}
|
||||
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'vars.id.value',
|
||||
name: 'ID',
|
||||
},
|
||||
{
|
||||
field: 'vars.interval.value',
|
||||
name: 'Interval',
|
||||
},
|
||||
{
|
||||
field: 'enabled',
|
||||
name: 'Active',
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
actions: [
|
||||
{
|
||||
name: 'Clone',
|
||||
description: 'Clone this person',
|
||||
type: 'icon',
|
||||
icon: 'copy',
|
||||
onClick: () => '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
// @ts-expect-error update types
|
||||
render: (item) => (
|
||||
<EuiButtonIcon
|
||||
onClick={() => toggleDetails(item)}
|
||||
// @ts-expect-error update types
|
||||
aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'}
|
||||
// @ts-expect-error update types
|
||||
iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const pagination = {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount: data.inputs[0].streams.length,
|
||||
pageSizeOptions: [3, 5, 8],
|
||||
};
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
items={data.inputs[0].streams}
|
||||
itemId="id"
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
isExpandable={true}
|
||||
hasActions={true}
|
||||
// @ts-expect-error update types
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
// @ts-expect-error update types
|
||||
sorting={sorting}
|
||||
isSelectable={true}
|
||||
onChange={onTableChange}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,41 +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 { FIELD_TYPES } from '../../shared_imports';
|
||||
|
||||
export const schema = {
|
||||
name: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: 'Name',
|
||||
},
|
||||
description: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: 'Description',
|
||||
},
|
||||
namespace: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
enabled: {
|
||||
type: FIELD_TYPES.TOGGLE,
|
||||
},
|
||||
policy_id: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
streams: {
|
||||
type: FIELD_TYPES.MULTI_SELECT,
|
||||
vars: {
|
||||
query: {
|
||||
type: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
value: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './lazy_osquery_managed_empty_create_policy_extension';
|
||||
export * from './lazy_osquery_managed_empty_edit_policy_extension';
|
||||
export * from './lazy_osquery_managed_policy_create_extension';
|
||||
export * from './lazy_osquery_managed_policy_create_import_extension';
|
||||
export * from './lazy_osquery_managed_policy_edit_extension';
|
||||
export * from './lazy_osquery_managed_custom_extension';
|
||||
export * from './lazy_osquery_managed_custom_button_extension';
|
||||
|
|
|
@ -8,9 +8,13 @@
|
|||
import { lazy } from 'react';
|
||||
import { PackageCustomExtensionComponent } from '../../../fleet/public';
|
||||
|
||||
export const LazyOsqueryManagedCustomExtension = lazy<PackageCustomExtensionComponent>(async () => {
|
||||
const { OsqueryManagedCustomExtension } = await import('./osquery_managed_custom_extension');
|
||||
return {
|
||||
default: OsqueryManagedCustomExtension,
|
||||
};
|
||||
});
|
||||
export const LazyOsqueryManagedCustomButtonExtension = lazy<PackageCustomExtensionComponent>(
|
||||
async () => {
|
||||
const { OsqueryManagedCustomButtonExtension } = await import(
|
||||
'./osquery_managed_custom_button_extension'
|
||||
);
|
||||
return {
|
||||
default: OsqueryManagedCustomButtonExtension,
|
||||
};
|
||||
}
|
||||
);
|
|
@ -1,20 +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 { lazy } from 'react';
|
||||
import { PackagePolicyCreateExtensionComponent } from '../../../fleet/public';
|
||||
|
||||
export const LazyOsqueryManagedEmptyCreatePolicyExtension = lazy<PackagePolicyCreateExtensionComponent>(
|
||||
async () => {
|
||||
const { OsqueryManagedEmptyCreatePolicyExtension } = await import(
|
||||
'./osquery_managed_empty_create_policy_extension'
|
||||
);
|
||||
return {
|
||||
default: OsqueryManagedEmptyCreatePolicyExtension,
|
||||
};
|
||||
}
|
||||
);
|
|
@ -1,20 +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 { lazy } from 'react';
|
||||
import { PackagePolicyEditExtensionComponent } from '../../../fleet/public';
|
||||
|
||||
export const LazyOsqueryManagedEmptyEditPolicyExtension = lazy<PackagePolicyEditExtensionComponent>(
|
||||
async () => {
|
||||
const { OsqueryManagedEmptyEditPolicyExtension } = await import(
|
||||
'./osquery_managed_empty_edit_policy_extension'
|
||||
);
|
||||
return {
|
||||
default: OsqueryManagedEmptyEditPolicyExtension,
|
||||
};
|
||||
}
|
||||
);
|
|
@ -8,13 +8,13 @@
|
|||
import { lazy } from 'react';
|
||||
import { PackagePolicyCreateExtensionComponent } from '../../../fleet/public';
|
||||
|
||||
export const LazyOsqueryManagedPolicyCreateExtension = lazy<PackagePolicyCreateExtensionComponent>(
|
||||
export const LazyOsqueryManagedPolicyCreateImportExtension = lazy<PackagePolicyCreateExtensionComponent>(
|
||||
async () => {
|
||||
const { OsqueryManagedPolicyCreateExtension } = await import(
|
||||
'./osquery_managed_policy_create_extension'
|
||||
const { OsqueryManagedPolicyCreateImportExtension } = await import(
|
||||
'./osquery_managed_policy_create_import_extension'
|
||||
);
|
||||
return {
|
||||
default: OsqueryManagedPolicyCreateExtension,
|
||||
default: OsqueryManagedPolicyCreateImportExtension,
|
||||
};
|
||||
}
|
||||
);
|
|
@ -10,11 +10,11 @@ import { PackagePolicyEditExtensionComponent } from '../../../fleet/public';
|
|||
|
||||
export const LazyOsqueryManagedPolicyEditExtension = lazy<PackagePolicyEditExtensionComponent>(
|
||||
async () => {
|
||||
const { OsqueryManagedPolicyCreateExtension } = await import(
|
||||
'./osquery_managed_policy_create_extension'
|
||||
const { OsqueryManagedPolicyCreateImportExtension } = await import(
|
||||
'./osquery_managed_policy_create_import_extension'
|
||||
);
|
||||
return {
|
||||
default: OsqueryManagedPolicyCreateExtension,
|
||||
default: OsqueryManagedPolicyCreateImportExtension,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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, EuiCard, EuiIcon } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
|
||||
|
||||
interface NavigationButtonsProps {
|
||||
isDisabled?: boolean;
|
||||
integrationPolicyId?: string;
|
||||
agentPolicyId?: string;
|
||||
}
|
||||
|
||||
const NavigationButtonsComponent: React.FC<NavigationButtonsProps> = ({
|
||||
isDisabled,
|
||||
integrationPolicyId,
|
||||
agentPolicyId,
|
||||
}) => {
|
||||
const {
|
||||
application: { getUrlForApp, navigateToApp },
|
||||
} = useKibana().services;
|
||||
|
||||
const liveQueryHref = useMemo(
|
||||
() =>
|
||||
getUrlForApp('osquery', {
|
||||
path: agentPolicyId
|
||||
? `/live_queries/new?agentPolicyId=${agentPolicyId}`
|
||||
: ' `/live_queries/new',
|
||||
}),
|
||||
[agentPolicyId, getUrlForApp]
|
||||
);
|
||||
|
||||
const liveQueryClick = useCallback(
|
||||
(event) => {
|
||||
if (!isModifiedEvent(event) && isLeftClickEvent(event)) {
|
||||
event.preventDefault();
|
||||
navigateToApp('osquery', {
|
||||
path: agentPolicyId
|
||||
? `/live_queries/new?agentPolicyId=${agentPolicyId}`
|
||||
: ' `/live_queries/new',
|
||||
});
|
||||
}
|
||||
},
|
||||
[agentPolicyId, navigateToApp]
|
||||
);
|
||||
|
||||
const scheduleQueryGroupsHref = getUrlForApp('osquery', {
|
||||
path: integrationPolicyId
|
||||
? `/scheduled_query_groups/${integrationPolicyId}/edit`
|
||||
: `/scheduled_query_groups`,
|
||||
});
|
||||
|
||||
const scheduleQueryGroupsClick = useCallback(
|
||||
(event) => {
|
||||
if (!isModifiedEvent(event) && isLeftClickEvent(event)) {
|
||||
event.preventDefault();
|
||||
navigateToApp('osquery', {
|
||||
path: integrationPolicyId
|
||||
? `/scheduled_query_groups/${integrationPolicyId}/edit`
|
||||
: `/scheduled_query_groups`,
|
||||
});
|
||||
}
|
||||
},
|
||||
[navigateToApp, integrationPolicyId]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="l">
|
||||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
icon={<EuiIcon size="xl" type="console" />}
|
||||
title={i18n.translate('xpack.osquery.fleetIntegration.runLiveQueriesButtonText', {
|
||||
defaultMessage: 'Run live queries',
|
||||
})}
|
||||
href={liveQueryHref}
|
||||
onClick={liveQueryClick}
|
||||
description={''}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
icon={<EuiIcon size="xl" type="clock" />}
|
||||
title={i18n.translate('xpack.osquery.fleetIntegration.scheduleQueryGroupsButtonText', {
|
||||
defaultMessage: 'Schedule query groups',
|
||||
})}
|
||||
description={''}
|
||||
isDisabled={isDisabled}
|
||||
href={scheduleQueryGroupsHref}
|
||||
onClick={scheduleQueryGroupsClick}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
NavigationButtonsComponent.displayName = 'NavigationButtonsComponent';
|
||||
|
||||
export const NavigationButtons = React.memo(NavigationButtonsComponent);
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { PackageCustomExtensionComponentProps } from '../../../fleet/public';
|
||||
import { NavigationButtons } from './navigation_buttons';
|
||||
|
||||
/**
|
||||
* Exports Osquery-specific package policy instructions
|
||||
* for use in the Fleet app custom tab
|
||||
*/
|
||||
export const OsqueryManagedCustomButtonExtension = React.memo<PackageCustomExtensionComponentProps>(
|
||||
() => <NavigationButtons />
|
||||
);
|
||||
OsqueryManagedCustomButtonExtension.displayName = 'OsqueryManagedCustomButtonExtension';
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
|
||||
import { PackageCustomExtensionComponentProps } from '../../../fleet/public';
|
||||
import { CustomTabTabs } from './components/custom_tab_tabs';
|
||||
import { Navigation } from './components/navigation';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
/**
|
||||
* Exports Osquery-specific package policy instructions
|
||||
* for use in the Fleet app custom tab
|
||||
*/
|
||||
export const OsqueryManagedCustomExtension = React.memo<PackageCustomExtensionComponentProps>(
|
||||
() => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<Navigation />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CustomTabTabs />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
);
|
||||
OsqueryManagedCustomExtension.displayName = 'OsqueryManagedCustomExtension';
|
|
@ -1,43 +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 React, { useEffect } from 'react';
|
||||
import { produce } from 'immer';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { PackagePolicyCreateExtensionComponentProps } from '../../../fleet/public';
|
||||
|
||||
/**
|
||||
* Exports Osquery-specific package policy instructions
|
||||
* for use in the Fleet app create / edit package policy
|
||||
*/
|
||||
const OsqueryManagedEmptyCreatePolicyExtensionComponent: React.FC<PackagePolicyCreateExtensionComponentProps> = ({
|
||||
onChange,
|
||||
newPolicy,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
const updatedPolicy = produce(newPolicy, (draft) => {
|
||||
draft.inputs.forEach((input) => (input.streams = []));
|
||||
});
|
||||
|
||||
onChange({
|
||||
isValid: true,
|
||||
updatedPolicy,
|
||||
});
|
||||
});
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
OsqueryManagedEmptyCreatePolicyExtensionComponent.displayName =
|
||||
'OsqueryManagedEmptyCreatePolicyExtension';
|
||||
|
||||
export const OsqueryManagedEmptyCreatePolicyExtension = React.memo(
|
||||
OsqueryManagedEmptyCreatePolicyExtensionComponent,
|
||||
// we don't want to update the component if onChange has changed
|
||||
(prevProps, nextProps) => deepEqual(prevProps.newPolicy, nextProps.newPolicy)
|
||||
);
|
|
@ -1,23 +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 React from 'react';
|
||||
|
||||
import { PackagePolicyEditExtensionComponentProps } from '../../../fleet/public';
|
||||
|
||||
/**
|
||||
* Exports Osquery-specific package policy instructions
|
||||
* for use in the Fleet app edit package policy
|
||||
*/
|
||||
const OsqueryManagedEmptyEditPolicyExtensionComponent = () => <></>;
|
||||
|
||||
OsqueryManagedEmptyEditPolicyExtensionComponent.displayName =
|
||||
'OsqueryManagedEmptyEditPolicyExtension';
|
||||
|
||||
export const OsqueryManagedEmptyEditPolicyExtension = React.memo<PackagePolicyEditExtensionComponentProps>(
|
||||
OsqueryManagedEmptyEditPolicyExtensionComponent
|
||||
);
|
|
@ -1,53 +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 { EuiButton } from '@elastic/eui';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
|
||||
import { PackagePolicyCreateExtensionComponentProps } from '../../../fleet/public';
|
||||
import { ScheduledQueryInputType } from './components/input_type';
|
||||
import { ScheduledQueryPackSelector } from './components/pack_selector';
|
||||
import { ScheduledQueryQueriesTable } from './components/scheduled_queries_table';
|
||||
import { AddNewQueryFlyout } from './components/add_new_query_flyout';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
/**
|
||||
* Exports Osquery-specific package policy instructions
|
||||
* for use in the Fleet app create / edit package policy
|
||||
*/
|
||||
export const OsqueryManagedPolicyCreateExtension = React.memo<PackagePolicyCreateExtensionComponentProps>(
|
||||
({ onChange, newPolicy }) => {
|
||||
const [showAddQueryFlyout, setShowAddQueryFlyout] = useState(false);
|
||||
|
||||
const handleShowFlyout = useCallback(() => setShowAddQueryFlyout(true), []);
|
||||
const handleHideFlyout = useCallback(() => setShowAddQueryFlyout(false), []);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ScheduledQueryInputType data={newPolicy} handleChange={onChange} />
|
||||
{newPolicy.inputs[0].config?.input_source?.value === 'pack' && (
|
||||
<ScheduledQueryPackSelector data={newPolicy} handleChange={onChange} />
|
||||
)}
|
||||
{newPolicy.inputs[0].streams.length && (
|
||||
// @ts-expect-error update types
|
||||
<ScheduledQueryQueriesTable data={newPolicy} handleChange={onChange} />
|
||||
)}
|
||||
{newPolicy.inputs[0].config?.input_source?.value !== 'pack' && (
|
||||
<EuiButton fill onClick={handleShowFlyout}>
|
||||
{'Attach next query'}
|
||||
</EuiButton>
|
||||
)}
|
||||
{showAddQueryFlyout && (
|
||||
<AddNewQueryFlyout data={newPolicy} handleChange={onChange} onClose={handleHideFlyout} />
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
OsqueryManagedPolicyCreateExtension.displayName = 'OsqueryManagedPolicyCreateExtension';
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
* 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 { filter } from 'lodash/fp';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiCallOut, EuiLink } from '@elastic/eui';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
agentRouteService,
|
||||
agentPolicyRouteService,
|
||||
PackagePolicy,
|
||||
AgentPolicy,
|
||||
} from '../../../fleet/common';
|
||||
import {
|
||||
pagePathGetters,
|
||||
CreatePackagePolicyRouteState,
|
||||
PackagePolicyCreateExtensionComponentProps,
|
||||
PackagePolicyEditExtensionComponentProps,
|
||||
} from '../../../fleet/public';
|
||||
import { ScheduledQueryGroupQueriesTable } from '../scheduled_query_groups/scheduled_query_group_queries_table';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { NavigationButtons } from './navigation_buttons';
|
||||
|
||||
/**
|
||||
* Exports Osquery-specific package policy instructions
|
||||
* for use in the Fleet app create / edit package policy
|
||||
*/
|
||||
|
||||
export const OsqueryManagedPolicyCreateImportExtension = React.memo<
|
||||
PackagePolicyCreateExtensionComponentProps & {
|
||||
policy?: PackagePolicyEditExtensionComponentProps['policy'];
|
||||
}
|
||||
>(({ onChange, policy, newPolicy }) => {
|
||||
const [policyAgentsCount, setPolicyAgentsCount] = useState<number | null>(null);
|
||||
const [agentPolicy, setAgentPolicy] = useState<AgentPolicy | null>(null);
|
||||
const [editMode] = useState(!!policy);
|
||||
const {
|
||||
application: { getUrlForApp },
|
||||
http,
|
||||
} = useKibana().services;
|
||||
const { replace } = useHistory();
|
||||
|
||||
const agentsLinkHref = useMemo(() => {
|
||||
if (!policy?.policy_id) return '#';
|
||||
|
||||
return getUrlForApp('fleet', {
|
||||
path:
|
||||
`#` +
|
||||
pagePathGetters.policy_details({ policyId: policy?.policy_id }) +
|
||||
'?openEnrollmentFlyout=true',
|
||||
});
|
||||
}, [getUrlForApp, policy?.policy_id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editMode && policyAgentsCount === null) {
|
||||
const fetchAgentsCount = async () => {
|
||||
try {
|
||||
const response = await http.fetch(agentRouteService.getStatusPath(), {
|
||||
query: {
|
||||
policyId: policy?.policy_id,
|
||||
},
|
||||
});
|
||||
if (response.results) {
|
||||
setPolicyAgentsCount(response.results.total);
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const fetchAgentPolicyDetails = async () => {
|
||||
if (policy?.policy_id) {
|
||||
try {
|
||||
const response = await http.fetch(
|
||||
agentPolicyRouteService.getInfoPath(policy?.policy_id)
|
||||
);
|
||||
if (response.item) {
|
||||
setAgentPolicy(response.item);
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
||||
|
||||
fetchAgentsCount();
|
||||
fetchAgentPolicyDetails();
|
||||
}
|
||||
}, [editMode, http, policy?.policy_id, policyAgentsCount]);
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
by default Fleet set up streams with an empty scheduled query,
|
||||
this code removes that, so the user can schedule queries
|
||||
in the next step
|
||||
*/
|
||||
if (!editMode) {
|
||||
const updatedPolicy = produce(newPolicy, (draft) => {
|
||||
draft.inputs[0].streams = [];
|
||||
return draft;
|
||||
});
|
||||
onChange({
|
||||
isValid: true,
|
||||
updatedPolicy,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editMode) {
|
||||
replace({
|
||||
state: {
|
||||
onSaveNavigateTo: (newPackagePolicy) => [
|
||||
'fleet',
|
||||
{
|
||||
path:
|
||||
'#' +
|
||||
pagePathGetters.integration_policy_edit({
|
||||
packagePolicyId: newPackagePolicy.id,
|
||||
}),
|
||||
},
|
||||
],
|
||||
} as CreatePackagePolicyRouteState,
|
||||
});
|
||||
}
|
||||
}, [editMode, replace]);
|
||||
|
||||
const scheduledQueryGroupTableData = useMemo(() => {
|
||||
const policyWithoutEmptyQueries = produce(newPolicy, (draft) => {
|
||||
draft.inputs[0].streams = filter(['compiled_stream.id', null], draft.inputs[0].streams);
|
||||
return draft;
|
||||
});
|
||||
|
||||
return policyWithoutEmptyQueries;
|
||||
}, [newPolicy]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!editMode ? (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.osquery.fleetIntegration.saveIntegrationCalloutTitle',
|
||||
{ defaultMessage: 'Save the integration to access the options below' }
|
||||
)}
|
||||
iconType="save"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
) : null}
|
||||
{policyAgentsCount === 0 ? (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiCallOut title="No agents in the policy" color="warning" iconType="help">
|
||||
<p>
|
||||
{`Fleet has detected that you have not assigned yet any agent to the `}
|
||||
{
|
||||
<EuiLink href={agentsLinkHref}>
|
||||
{agentPolicy?.name ?? policy?.policy_id}
|
||||
</EuiLink>
|
||||
}
|
||||
{`. `}
|
||||
<br />
|
||||
<strong>{`Only agents within the policy with active Osquery Manager integration support the functionality presented below.`}</strong>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<NavigationButtons
|
||||
isDisabled={!editMode}
|
||||
integrationPolicyId={policy?.id}
|
||||
agentPolicyId={policy?.policy_id}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
|
||||
{editMode && scheduledQueryGroupTableData.inputs[0].streams.length ? (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<ScheduledQueryGroupQueriesTable data={scheduledQueryGroupTableData as PackagePolicy} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
OsqueryManagedPolicyCreateImportExtension.displayName = 'OsqueryManagedPolicyCreateImportExtension';
|
|
@ -19,10 +19,7 @@ const QueryAgentResultsComponent = () => {
|
|||
return (
|
||||
<>
|
||||
<EuiCodeBlock language="sql" fontSize="m" paddingSize="m">
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
data?.actionDetails._source?.data?.query
|
||||
}
|
||||
{data?.actionDetails._source?.data?.query}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer />
|
||||
<ResultsTable actionId={actionId} agentId={agentId} />
|
174
x-pack/plugins/osquery/public/live_queries/form/index.tsx
Normal file
174
x-pack/plugins/osquery/public/live_queries/form/index.tsx
Normal file
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiSteps, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import { UseField, Form, FormData, useForm, useFormData } from '../../shared_imports';
|
||||
import { AgentsTableField } from './agents_table_field';
|
||||
import { LiveQueryQueryField } from './live_query_query_field';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { ResultTabs } from '../../queries/edit/tabs';
|
||||
|
||||
const FORM_ID = 'liveQueryForm';
|
||||
|
||||
interface LiveQueryFormProps {
|
||||
defaultValue?: Partial<FormData> | undefined;
|
||||
onSubmit?: (payload: Record<string, string>) => Promise<void>;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
||||
defaultValue,
|
||||
// onSubmit,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
mutateAsync,
|
||||
isError,
|
||||
isSuccess,
|
||||
// error
|
||||
} = useMutation(
|
||||
(payload: Record<string, unknown>) =>
|
||||
http.post('/internal/osquery/action', {
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
{
|
||||
onSuccess,
|
||||
}
|
||||
);
|
||||
|
||||
const { form } = useForm({
|
||||
id: FORM_ID,
|
||||
// schema: formSchema,
|
||||
onSubmit: (payload) => {
|
||||
return mutateAsync(payload);
|
||||
},
|
||||
options: {
|
||||
stripEmptyFields: false,
|
||||
},
|
||||
defaultValue: defaultValue ?? {
|
||||
query: {
|
||||
id: null,
|
||||
query: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { submit } = form;
|
||||
|
||||
const actionId = useMemo(() => data?.actions[0].action_id, [data?.actions]);
|
||||
const agentIds = useMemo(() => data?.actions[0].agents, [data?.actions]);
|
||||
const [{ agentSelection, query }] = useFormData({ form, watch: ['agentSelection', 'query'] });
|
||||
|
||||
const agentSelected = useMemo(
|
||||
() =>
|
||||
agentSelection &&
|
||||
!!(
|
||||
agentSelection.allAgentsSelected ||
|
||||
agentSelection.agents?.length ||
|
||||
agentSelection.platformsSelected?.length ||
|
||||
agentSelection.policiesSelected?.length
|
||||
),
|
||||
[agentSelection]
|
||||
);
|
||||
|
||||
const queryValueProvided = useMemo(() => !!query?.query?.length, [query]);
|
||||
|
||||
const queryStatus = useMemo(() => {
|
||||
if (!agentSelected) return 'disabled';
|
||||
if (isError) return 'danger';
|
||||
if (isLoading) return 'loading';
|
||||
if (isSuccess) return 'complete';
|
||||
|
||||
return 'incomplete';
|
||||
}, [agentSelected, isError, isLoading, isSuccess]);
|
||||
|
||||
const resultsStatus = useMemo(() => (queryStatus === 'complete' ? 'incomplete' : 'disabled'), [
|
||||
queryStatus,
|
||||
]);
|
||||
|
||||
const queryComponentProps = useMemo(
|
||||
() => ({
|
||||
disabled: queryStatus === 'disabled',
|
||||
}),
|
||||
[queryStatus]
|
||||
);
|
||||
|
||||
const formSteps: EuiContainedStepProps[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: i18n.translate('xpack.osquery.liveQueryForm.steps.agentsStepHeading', {
|
||||
defaultMessage: 'Select agents',
|
||||
}),
|
||||
children: <UseField path="agentSelection" component={AgentsTableField} />,
|
||||
status: agentSelected ? 'complete' : 'incomplete',
|
||||
},
|
||||
{
|
||||
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}>
|
||||
<EuiButton disabled={!agentSelected || !queryValueProvided} onClick={submit}>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryForm.form.submitButtonLabel"
|
||||
defaultMessage="Submit"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
),
|
||||
status: queryStatus,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.osquery.liveQueryForm.steps.resultsStepHeading', {
|
||||
defaultMessage: 'Check results',
|
||||
}),
|
||||
children: actionId ? (
|
||||
<ResultTabs actionId={actionId} agentIds={agentIds} isLive={true} />
|
||||
) : null,
|
||||
status: resultsStatus,
|
||||
},
|
||||
],
|
||||
[
|
||||
actionId,
|
||||
agentIds,
|
||||
agentSelected,
|
||||
queryComponentProps,
|
||||
queryStatus,
|
||||
queryValueProvided,
|
||||
resultsStatus,
|
||||
submit,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<EuiSteps steps={formSteps} />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiveQueryForm = React.memo(LiveQueryFormComponent);
|
|
@ -15,13 +15,14 @@ import { FieldHook } from '../../shared_imports';
|
|||
import { OsqueryEditor } from '../../editor';
|
||||
|
||||
interface LiveQueryQueryFieldProps {
|
||||
disabled?: boolean;
|
||||
field: FieldHook<{
|
||||
id: string | null;
|
||||
query: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({ field }) => {
|
||||
const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({ disabled, field }) => {
|
||||
// const { http } = useKibana().services;
|
||||
// const { data } = useQuery('savedQueryList', () =>
|
||||
// http.get('/internal/osquery/saved_query', {
|
||||
|
@ -82,7 +83,7 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({ fiel
|
|||
onChange={handleSavedQueryChange}
|
||||
/>
|
||||
<EuiSpacer /> */}
|
||||
<OsqueryEditor defaultValue={value.query} onChange={handleEditorChange} />
|
||||
<OsqueryEditor defaultValue={value.query} disabled={disabled} onChange={handleEditorChange} />
|
||||
</>
|
||||
);
|
||||
};
|
22
x-pack/plugins/osquery/public/live_queries/index.tsx
Normal file
22
x-pack/plugins/osquery/public/live_queries/index.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { LiveQueryForm } from './form';
|
||||
import { FormData } from '../shared_imports';
|
||||
|
||||
interface LiveQueryProps {
|
||||
defaultValue?: Partial<FormData> | undefined;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const LiveQueryComponent: React.FC<LiveQueryProps> = ({ defaultValue, onSuccess }) => (
|
||||
<LiveQueryForm defaultValue={defaultValue} onSuccess={onSuccess} />
|
||||
);
|
||||
|
||||
export const LiveQuery = React.memo(LiveQueryComponent);
|
|
@ -1,52 +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 { EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { UseField, Form, useForm } from '../../shared_imports';
|
||||
import { AgentsTableField } from './agents_table_field';
|
||||
import { LiveQueryQueryField } from './live_query_query_field';
|
||||
|
||||
const FORM_ID = 'liveQueryForm';
|
||||
|
||||
interface LiveQueryFormProps {
|
||||
defaultValue?: unknown;
|
||||
onSubmit: (payload: Record<string, string>) => Promise<void>;
|
||||
}
|
||||
|
||||
const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ defaultValue, onSubmit }) => {
|
||||
const { form } = useForm({
|
||||
id: FORM_ID,
|
||||
// schema: formSchema,
|
||||
onSubmit,
|
||||
options: {
|
||||
stripEmptyFields: false,
|
||||
},
|
||||
defaultValue: {
|
||||
// @ts-expect-error update types
|
||||
query: defaultValue ?? {
|
||||
id: null,
|
||||
query: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { submit } = form;
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<UseField path="agentSelection" component={AgentsTableField} />
|
||||
<EuiSpacer />
|
||||
<UseField path="query" component={LiveQueryQueryField} />
|
||||
<EuiSpacer />
|
||||
<EuiButton onClick={submit}>{'Send query'}</EuiButton>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiveQueryForm = React.memo(LiveQueryFormComponent);
|
|
@ -1,47 +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 { EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { LiveQueryForm } from './form';
|
||||
import { ResultTabs } from '../queries/edit/tabs';
|
||||
|
||||
const LiveQueryComponent = () => {
|
||||
const location = useLocation();
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const createActionMutation = useMutation((payload: Record<string, unknown>) =>
|
||||
http.post('/internal/osquery/action', {
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
<LiveQueryForm
|
||||
defaultValue={location.state?.query}
|
||||
// @ts-expect-error update types
|
||||
onSubmit={createActionMutation.mutate}
|
||||
/>
|
||||
}
|
||||
|
||||
{createActionMutation.data && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<ResultTabs actionId={createActionMutation.data?.action.action_id} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiveQuery = React.memo(LiveQueryComponent);
|
|
@ -1,49 +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.
|
||||
*/
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-function-as-prop, react/jsx-no-bind */
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { EuiTextArea } from '@elastic/eui';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { ActionParamsProps } from '../../../triggers_actions_ui/public/types';
|
||||
|
||||
interface ExampleActionParams {
|
||||
message: string;
|
||||
}
|
||||
|
||||
const ExampleParamsFields: React.FunctionComponent<ActionParamsProps<ExampleActionParams>> = ({
|
||||
actionParams,
|
||||
editAction,
|
||||
index,
|
||||
errors,
|
||||
}) => {
|
||||
// console.error('actionParams', actionParams, index, errors);
|
||||
const { message } = actionParams;
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTextArea
|
||||
fullWidth
|
||||
isInvalid={errors.message.length > 0 && message !== undefined}
|
||||
name="message"
|
||||
value={message || ''}
|
||||
onChange={(e) => {
|
||||
editAction('message', e.target.value, index);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!message) {
|
||||
editAction('message', '', index);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
// Export as default in order to support lazy loading
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { ExampleParamsFields as default };
|
|
@ -1,73 +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 { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { ActionTypeModel, ValidationResult } from '../../../triggers_actions_ui/public/types';
|
||||
|
||||
interface ExampleActionParams {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function getActionType(): ActionTypeModel {
|
||||
return {
|
||||
id: '.osquery',
|
||||
iconClass: 'logoOsquery',
|
||||
selectMessage: i18n.translate(
|
||||
'xpack.osquery.components.builtinActionTypes.exampleAction.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Example Action is used to show how to create new action type UI.',
|
||||
}
|
||||
),
|
||||
actionTypeTitle: i18n.translate(
|
||||
'xpack.osquery.components.builtinActionTypes.exampleAction.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'Example Action',
|
||||
}
|
||||
),
|
||||
// @ts-expect-error update types
|
||||
validateConnector: (action): ValidationResult => {
|
||||
const validationResult = { errors: {} };
|
||||
const errors = {
|
||||
someConnectorField: new Array<string>(),
|
||||
};
|
||||
validationResult.errors = errors;
|
||||
if (!action.config.someConnectorField) {
|
||||
errors.someConnectorField.push(
|
||||
i18n.translate(
|
||||
'xpack.osquery.components.builtinActionTypes.error.requiredSomeConnectorFieldeText',
|
||||
{
|
||||
defaultMessage: 'SomeConnectorField is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: (actionParams: ExampleActionParams): ValidationResult => {
|
||||
const validationResult = { errors: {} };
|
||||
const errors = {
|
||||
message: new Array<string>(),
|
||||
};
|
||||
validationResult.errors = errors;
|
||||
if (!actionParams.message?.length) {
|
||||
errors.message.push(
|
||||
i18n.translate(
|
||||
'xpack.osquery.components.builtinActionTypes.error.requiredExampleMessageText',
|
||||
{
|
||||
defaultMessage: 'Message is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: lazy(() => import('./example_params_fields')),
|
||||
};
|
||||
}
|
|
@ -26,6 +26,7 @@ const EditPackPageComponent: React.FC<EditPackPageProps> = ({ onSuccess, packId
|
|||
queries: [],
|
||||
},
|
||||
} = useQuery(['pack', { id: packId }], ({ queryKey }) => {
|
||||
// @ts-expect-error update types
|
||||
return http.get(`/internal/osquery/pack/${queryKey[1].id}`);
|
||||
});
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
CoreStart,
|
||||
DEFAULT_APP_CATEGORIES,
|
||||
AppStatus,
|
||||
AppNavLinkStatus,
|
||||
AppUpdater,
|
||||
} from '../../../../src/core/public';
|
||||
import { Storage } from '../../../../src/plugins/kibana_utils/public';
|
||||
|
@ -24,28 +25,51 @@ import {
|
|||
StartPlugins,
|
||||
AppPluginStartDependencies,
|
||||
} from './types';
|
||||
import { PLUGIN_NAME } from '../common';
|
||||
import { OSQUERY_INTEGRATION_NAME, PLUGIN_NAME } from '../common';
|
||||
import { epmRouteService, GetPackagesResponse } from '../../fleet/common';
|
||||
import {
|
||||
LazyOsqueryManagedEmptyCreatePolicyExtension,
|
||||
LazyOsqueryManagedEmptyEditPolicyExtension,
|
||||
LazyOsqueryManagedPolicyCreateImportExtension,
|
||||
LazyOsqueryManagedPolicyEditExtension,
|
||||
LazyOsqueryManagedCustomButtonExtension,
|
||||
} from './fleet_integration';
|
||||
// import { getActionType } from './osquery_action_type';
|
||||
|
||||
export function toggleOsqueryPlugin(updater$: Subject<AppUpdater>, http: CoreStart['http']) {
|
||||
http.fetch('/api/fleet/epm/packages', { query: { experimental: true } }).then(({ response }) => {
|
||||
const installed = response.find(
|
||||
// @ts-expect-error update types
|
||||
(integration) =>
|
||||
integration?.name === 'osquery_elastic_managed' && integration?.status === 'installed'
|
||||
);
|
||||
updater$.next(() => ({
|
||||
status: installed ? AppStatus.accessible : AppStatus.inaccessible,
|
||||
}));
|
||||
});
|
||||
export function toggleOsqueryPlugin(
|
||||
updater$: Subject<AppUpdater>,
|
||||
http: CoreStart['http'],
|
||||
registerExtension?: StartPlugins['fleet']['registerExtension']
|
||||
) {
|
||||
http
|
||||
.fetch<GetPackagesResponse>(epmRouteService.getListPath(), { query: { experimental: true } })
|
||||
.then(({ response }) => {
|
||||
const installed = response.find(
|
||||
(integration) =>
|
||||
integration?.name === OSQUERY_INTEGRATION_NAME && integration?.status === 'installed'
|
||||
);
|
||||
|
||||
if (installed && registerExtension) {
|
||||
registerExtension({
|
||||
package: OSQUERY_INTEGRATION_NAME,
|
||||
view: 'package-detail-custom',
|
||||
component: LazyOsqueryManagedCustomButtonExtension,
|
||||
});
|
||||
}
|
||||
|
||||
updater$.next(() => ({
|
||||
navLinkStatus: installed ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden,
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
updater$.next(() => ({
|
||||
status: AppStatus.inaccessible,
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginStart> {
|
||||
private readonly appUpdater$ = new BehaviorSubject<AppUpdater>(() => ({}));
|
||||
private readonly appUpdater$ = new BehaviorSubject<AppUpdater>(() => ({
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
}));
|
||||
private kibanaVersion: string;
|
||||
private storage = new Storage(localStorage);
|
||||
|
||||
|
@ -53,11 +77,14 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
this.kibanaVersion = this.initializerContext.env.packageInfo.version;
|
||||
}
|
||||
|
||||
public setup(
|
||||
core: CoreSetup
|
||||
// plugins: SetupPlugins
|
||||
): OsqueryPluginSetup {
|
||||
const config = this.initializerContext.config.get<{ enabled: boolean }>();
|
||||
public setup(core: CoreSetup): OsqueryPluginSetup {
|
||||
const config = this.initializerContext.config.get<{
|
||||
enabled: boolean;
|
||||
actionEnabled: boolean;
|
||||
scheduledQueries: boolean;
|
||||
savedQueries: boolean;
|
||||
packs: boolean;
|
||||
}>();
|
||||
|
||||
if (!config.enabled) {
|
||||
return {};
|
||||
|
@ -71,6 +98,7 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
title: PLUGIN_NAME,
|
||||
order: 9030,
|
||||
updater$: this.appUpdater$,
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
category: DEFAULT_APP_CATEGORIES.management,
|
||||
async mount(params: AppMountParameters) {
|
||||
// Get start services as specified in kibana.json
|
||||
|
@ -88,41 +116,37 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
},
|
||||
});
|
||||
|
||||
// plugins.triggersActionsUi.actionTypeRegistry.register(getActionType());
|
||||
|
||||
// Return methods that should be available to other plugins
|
||||
return {};
|
||||
}
|
||||
|
||||
public start(core: CoreStart, plugins: StartPlugins): OsqueryPluginStart {
|
||||
const config = this.initializerContext.config.get<{ enabled: boolean }>();
|
||||
|
||||
if (!config.enabled) {
|
||||
return {};
|
||||
}
|
||||
const config = this.initializerContext.config.get<{
|
||||
enabled: boolean;
|
||||
actionEnabled: boolean;
|
||||
scheduledQueries: boolean;
|
||||
savedQueries: boolean;
|
||||
packs: boolean;
|
||||
}>();
|
||||
|
||||
if (plugins.fleet) {
|
||||
const { registerExtension } = plugins.fleet;
|
||||
|
||||
toggleOsqueryPlugin(this.appUpdater$, core.http);
|
||||
if (config.enabled) {
|
||||
toggleOsqueryPlugin(this.appUpdater$, core.http, registerExtension);
|
||||
}
|
||||
|
||||
registerExtension({
|
||||
package: 'osquery_elastic_managed',
|
||||
package: OSQUERY_INTEGRATION_NAME,
|
||||
view: 'package-policy-create',
|
||||
component: LazyOsqueryManagedEmptyCreatePolicyExtension,
|
||||
component: LazyOsqueryManagedPolicyCreateImportExtension,
|
||||
});
|
||||
|
||||
registerExtension({
|
||||
package: 'osquery_elastic_managed',
|
||||
package: OSQUERY_INTEGRATION_NAME,
|
||||
view: 'package-policy-edit',
|
||||
component: LazyOsqueryManagedEmptyEditPolicyExtension,
|
||||
component: LazyOsqueryManagedPolicyEditExtension,
|
||||
});
|
||||
|
||||
// registerExtension({
|
||||
// package: 'osquery_elastic_managed',
|
||||
// view: 'package-detail-custom',
|
||||
// component: LazyOsqueryManagedCustomExtension,
|
||||
// });
|
||||
} else {
|
||||
this.appUpdater$.next(() => ({
|
||||
status: AppStatus.inaccessible,
|
||||
|
|
|
@ -9,13 +9,15 @@ import { EuiTabbedContent, EuiSpacer } from '@elastic/eui';
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
import { ResultsTable } from '../../results/results_table';
|
||||
import { ActionResultsTable } from '../../action_results/action_results_table';
|
||||
import { ActionResultsSummary } from '../../action_results/action_results_summary';
|
||||
|
||||
interface ResultTabsProps {
|
||||
actionId: string;
|
||||
agentIds?: string[];
|
||||
isLive?: boolean;
|
||||
}
|
||||
|
||||
const ResultTabsComponent: React.FC<ResultTabsProps> = ({ actionId }) => {
|
||||
const ResultTabsComponent: React.FC<ResultTabsProps> = ({ actionId, agentIds, isLive }) => {
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
@ -24,7 +26,7 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({ actionId }) => {
|
|||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<ActionResultsTable actionId={actionId} />
|
||||
<ActionResultsSummary actionId={actionId} agentIds={agentIds} isLive={isLive} />
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
@ -34,12 +36,12 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({ actionId }) => {
|
|||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<ResultsTable actionId={actionId} />
|
||||
<ResultsTable actionId={actionId} isLive={isLive} />
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
[actionId]
|
||||
[actionId, agentIds, isLive]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import { EuiFormRow, EuiLink, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { OsqueryEditor } from '../../editor';
|
||||
|
@ -14,10 +17,34 @@ interface CodeEditorFieldProps {
|
|||
field: FieldHook<string>;
|
||||
}
|
||||
|
||||
const CodeEditorFieldComponent: React.FC<CodeEditorFieldProps> = ({ field }) => {
|
||||
const { value, setValue } = field;
|
||||
const OsquerySchemaLink = React.memo(() => (
|
||||
<EuiText size="xs">
|
||||
<EuiLink href="https://osquery.io/schema/4.7.0" target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.codeEditorField.osquerySchemaLinkLabel"
|
||||
defaultMessage="Osquery schema"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
));
|
||||
|
||||
return <OsqueryEditor defaultValue={value} onChange={setValue} />;
|
||||
OsquerySchemaLink.displayName = 'OsquerySchemaLink';
|
||||
|
||||
const CodeEditorFieldComponent: React.FC<CodeEditorFieldProps> = ({ field }) => {
|
||||
const { value, label, labelAppend, helpText, setValue } = field;
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={label}
|
||||
labelAppend={!isEmpty(labelAppend) ? labelAppend : <OsquerySchemaLink />}
|
||||
helpText={helpText}
|
||||
// isInvalid={typeof error === 'string'}
|
||||
// error={error}
|
||||
fullWidth
|
||||
>
|
||||
<OsqueryEditor defaultValue={value} onChange={setValue} />
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const CodeEditorField = React.memo(CodeEditorFieldComponent);
|
||||
|
|
10
x-pack/plugins/osquery/public/query_client.ts
Normal file
10
x-pack/plugins/osquery/public/query_client.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { QueryClient } from 'react-query';
|
||||
|
||||
export const queryClient = new QueryClient();
|
|
@ -16,15 +16,14 @@ export type InspectResponse = Inspect & { response: string[] };
|
|||
|
||||
export const generateTablePaginationOptions = (
|
||||
activePage: number,
|
||||
limit: number,
|
||||
isBucketSort?: boolean
|
||||
limit: number
|
||||
): PaginationInputPaginated => {
|
||||
const cursorStart = activePage * limit;
|
||||
return {
|
||||
activePage,
|
||||
cursorStart,
|
||||
fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5,
|
||||
querySize: isBucketSort ? limit : limit + cursorStart,
|
||||
querySize: limit,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -6,22 +6,40 @@
|
|||
*/
|
||||
|
||||
import { isEmpty, isEqual, keys, map } from 'lodash/fp';
|
||||
import { EuiDataGrid, EuiDataGridProps, EuiDataGridColumn, EuiLink } from '@elastic/eui';
|
||||
import {
|
||||
EuiDataGrid,
|
||||
EuiDataGridSorting,
|
||||
EuiDataGridProps,
|
||||
EuiDataGridColumn,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react';
|
||||
|
||||
import { EuiDataGridSorting } from '@elastic/eui';
|
||||
import { pagePathGetters } from '../../../fleet/public';
|
||||
import { useAllResults } from './use_all_results';
|
||||
import { Direction, ResultEdges } from '../../common/search_strategy';
|
||||
import { useRouterNavigate } from '../common/lib/kibana';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
|
||||
const DataContext = createContext<ResultEdges>([]);
|
||||
|
||||
interface ResultsTableComponentProps {
|
||||
actionId: string;
|
||||
agentId?: string;
|
||||
isLive?: boolean;
|
||||
}
|
||||
|
||||
const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({ actionId, agentId }) => {
|
||||
const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({ actionId, isLive }) => {
|
||||
const { getUrlForApp } = useKibana().services.application;
|
||||
|
||||
const getFleetAppUrl = useCallback(
|
||||
(agentId) =>
|
||||
getUrlForApp('fleet', {
|
||||
path: `#` + pagePathGetters.fleet_agent_details({ agentId }),
|
||||
}),
|
||||
[getUrlForApp]
|
||||
);
|
||||
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 });
|
||||
const onChangeItemsPerPage = useCallback(
|
||||
(pageSize) =>
|
||||
|
@ -39,22 +57,15 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({ actionId,
|
|||
|
||||
const [columns, setColumns] = useState<EuiDataGridColumn[]>([]);
|
||||
|
||||
// ** Sorting config
|
||||
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
|
||||
const onSort = useCallback(
|
||||
(newSortingColumns) => {
|
||||
setSortingColumns(newSortingColumns);
|
||||
},
|
||||
[setSortingColumns]
|
||||
);
|
||||
|
||||
const { data: allResultsData = [] } = useAllResults({
|
||||
const { data: allResultsData } = useAllResults({
|
||||
actionId,
|
||||
agentId,
|
||||
activePage: pagination.pageIndex,
|
||||
limit: pagination.pageSize,
|
||||
direction: Direction.asc,
|
||||
sortField: '@timestamp',
|
||||
isLive,
|
||||
});
|
||||
|
||||
const [visibleColumns, setVisibleColumns] = useState<string[]>([]);
|
||||
|
@ -68,24 +79,22 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({ actionId,
|
|||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const data = useContext(DataContext);
|
||||
|
||||
// @ts-expect-error fields is optional
|
||||
const value = data[rowIndex].fields[columnId];
|
||||
// @ts-expect-error update types
|
||||
const value = data[rowIndex % pagination.pageSize]?.fields[columnId];
|
||||
|
||||
if (columnId === 'agent.name') {
|
||||
// @ts-expect-error fields is optional
|
||||
const agentIdValue = data[rowIndex].fields['agent.id'];
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const linkProps = useRouterNavigate(`/live_query/${actionId}/results/${agentIdValue}`);
|
||||
return <EuiLink {...linkProps}>{value}</EuiLink>;
|
||||
// @ts-expect-error update types
|
||||
const agentIdValue = data[rowIndex % pagination.pageSize]?.fields['agent.id'];
|
||||
|
||||
return <EuiLink href={getFleetAppUrl(agentIdValue)}>{value}</EuiLink>;
|
||||
}
|
||||
|
||||
return !isEmpty(value) ? value : '-';
|
||||
},
|
||||
[actionId]
|
||||
[getFleetAppUrl, pagination.pageSize]
|
||||
);
|
||||
|
||||
const tableSorting = useMemo(() => ({ columns: sortingColumns, onSort }), [
|
||||
onSort,
|
||||
const tableSorting = useMemo(() => ({ columns: sortingColumns, onSort: setSortingColumns }), [
|
||||
sortingColumns,
|
||||
]);
|
||||
|
||||
|
@ -100,34 +109,32 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({ actionId,
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-expect-error update types
|
||||
if (!allResultsData?.results) {
|
||||
if (!allResultsData?.edges) {
|
||||
return;
|
||||
}
|
||||
// @ts-expect-error update types
|
||||
const newColumns = keys(allResultsData?.results[0]?.fields)
|
||||
|
||||
const newColumns = keys(allResultsData?.edges[0]?.fields)
|
||||
.sort()
|
||||
.reduce((acc, fieldName) => {
|
||||
if (fieldName === 'agent.name') {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
id: fieldName,
|
||||
displayAsText: 'agent',
|
||||
defaultSortDirection: Direction.asc,
|
||||
},
|
||||
];
|
||||
acc.push({
|
||||
id: fieldName,
|
||||
displayAsText: i18n.translate('xpack.osquery.liveQueryResults.table.agentColumnTitle', {
|
||||
defaultMessage: 'agent',
|
||||
}),
|
||||
defaultSortDirection: Direction.asc,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (fieldName.startsWith('osquery.')) {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
id: fieldName,
|
||||
displayAsText: fieldName.split('.')[1],
|
||||
defaultSortDirection: Direction.asc,
|
||||
},
|
||||
];
|
||||
acc.push({
|
||||
id: fieldName,
|
||||
displayAsText: fieldName.split('.')[1],
|
||||
defaultSortDirection: Direction.asc,
|
||||
});
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc;
|
||||
|
@ -137,22 +144,20 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({ actionId,
|
|||
setColumns(newColumns);
|
||||
setVisibleColumns(map('id', newColumns));
|
||||
}
|
||||
// @ts-expect-error update types
|
||||
}, [columns, allResultsData?.results]);
|
||||
}, [columns, allResultsData?.edges]);
|
||||
|
||||
return (
|
||||
// @ts-expect-error update types
|
||||
<DataContext.Provider value={allResultsData?.results}>
|
||||
<DataContext.Provider value={allResultsData?.edges}>
|
||||
<EuiDataGrid
|
||||
aria-label="Osquery results"
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
// @ts-expect-error update types
|
||||
rowCount={allResultsData?.totalCount ?? 0}
|
||||
renderCellValue={renderCellValue}
|
||||
sorting={tableSorting}
|
||||
pagination={tablePagination}
|
||||
height="300px"
|
||||
height="500px"
|
||||
/>
|
||||
</DataContext.Provider>
|
||||
);
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { createFilter } from '../common/helpers';
|
||||
|
@ -35,71 +33,55 @@ export interface ResultsArgs {
|
|||
interface UseAllResults {
|
||||
actionId: string;
|
||||
activePage: number;
|
||||
agentId?: string;
|
||||
direction: Direction;
|
||||
limit: number;
|
||||
sortField: string;
|
||||
filterQuery?: ESTermQuery | string;
|
||||
skip?: boolean;
|
||||
isLive?: boolean;
|
||||
}
|
||||
|
||||
export const useAllResults = ({
|
||||
actionId,
|
||||
activePage,
|
||||
agentId,
|
||||
direction,
|
||||
limit,
|
||||
sortField,
|
||||
filterQuery,
|
||||
skip = false,
|
||||
isLive = false,
|
||||
}: UseAllResults) => {
|
||||
const { data } = useKibana().services;
|
||||
|
||||
const [resultsRequest, setHostRequest] = useState<ResultsRequestOptions | null>(null);
|
||||
|
||||
const response = useQuery(
|
||||
return useQuery(
|
||||
['allActionResults', { actionId, activePage, direction, limit, sortField }],
|
||||
async () => {
|
||||
if (!resultsRequest) return Promise.resolve();
|
||||
|
||||
const responseData = await data.search
|
||||
.search<ResultsRequestOptions, ResultsStrategyResponse>(resultsRequest, {
|
||||
strategy: 'osquerySearchStrategy',
|
||||
})
|
||||
.search<ResultsRequestOptions, ResultsStrategyResponse>(
|
||||
{
|
||||
actionId,
|
||||
factoryQueryType: OsqueryQueries.results,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
pagination: generateTablePaginationOptions(activePage, limit),
|
||||
sort: {
|
||||
direction,
|
||||
field: sortField,
|
||||
},
|
||||
},
|
||||
{
|
||||
strategy: 'osquerySearchStrategy',
|
||||
}
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
return {
|
||||
...responseData,
|
||||
results: responseData.edges,
|
||||
inspect: getInspectResponse(responseData, {} as InspectResponse),
|
||||
};
|
||||
},
|
||||
{
|
||||
refetchInterval: 1000,
|
||||
enabled: !skip && !!resultsRequest,
|
||||
refetchInterval: isLive ? 1000 : false,
|
||||
enabled: !skip,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setHostRequest((prevRequest) => {
|
||||
const myRequest = {
|
||||
...(prevRequest ?? {}),
|
||||
actionId,
|
||||
agentId,
|
||||
factoryQueryType: OsqueryQueries.results,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
pagination: generateTablePaginationOptions(activePage, limit),
|
||||
sort: {
|
||||
direction,
|
||||
field: sortField,
|
||||
},
|
||||
};
|
||||
if (!deepEqual(prevRequest, myRequest)) {
|
||||
return myRequest;
|
||||
}
|
||||
return prevRequest;
|
||||
});
|
||||
}, [actionId, activePage, agentId, direction, filterQuery, limit, sortField]);
|
||||
|
||||
return response;
|
||||
};
|
||||
|
|
|
@ -8,24 +8,24 @@
|
|||
import React from 'react';
|
||||
import { Switch, Redirect, Route } from 'react-router-dom';
|
||||
|
||||
import { LiveQueries } from './live_query';
|
||||
import { useBreadcrumbs } from '../common/hooks/use_breadcrumbs';
|
||||
import { LiveQueries } from './live_queries';
|
||||
import { ScheduledQueryGroups } from './scheduled_query_groups';
|
||||
|
||||
const OsqueryAppRoutesComponent = () => (
|
||||
<Switch>
|
||||
{/* <Route path="/packs">
|
||||
<Packs />
|
||||
</Route>
|
||||
<Route path={`/scheduled_queries`}>
|
||||
<ScheduledQueries />
|
||||
</Route>
|
||||
<Route path={`/queries`}>
|
||||
<Queries />
|
||||
</Route> */}
|
||||
<Route path="/live_query">
|
||||
<LiveQueries />
|
||||
</Route>
|
||||
<Redirect to="/live_query" />
|
||||
</Switch>
|
||||
);
|
||||
const OsqueryAppRoutesComponent = () => {
|
||||
useBreadcrumbs('base');
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`/scheduled_query_groups`}>
|
||||
<ScheduledQueryGroups />
|
||||
</Route>
|
||||
<Route path="/live_queries">
|
||||
<LiveQueries />
|
||||
</Route>
|
||||
<Redirect to="/live_queries" />
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
export const OsqueryAppRoutes = React.memo(OsqueryAppRoutesComponent);
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiButton, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useDiscoverLink } from '../../../common/hooks';
|
||||
interface LiveQueryDetailsActionsMenuProps {
|
||||
actionId: string;
|
||||
}
|
||||
|
||||
const LiveQueryDetailsActionsMenuComponent: React.FC<LiveQueryDetailsActionsMenuProps> = ({
|
||||
actionId,
|
||||
}) => {
|
||||
const discoverLinkProps = useDiscoverLink({ filters: [{ key: 'action_id', value: actionId }] });
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
|
||||
const onButtonClick = useCallback(() => {
|
||||
setPopover((currentIsPopoverOpen) => !currentIsPopoverOpen);
|
||||
}, []);
|
||||
|
||||
const closePopover = useCallback(() => {
|
||||
setPopover(false);
|
||||
}, []);
|
||||
|
||||
const items = useMemo(
|
||||
() => [
|
||||
<EuiContextMenuItem key="copy" icon="copy" {...discoverLinkProps}>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryResults.viewResultsInDiscoverLabel"
|
||||
defaultMessage="View results in Discover"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
],
|
||||
[discoverLinkProps]
|
||||
);
|
||||
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<EuiButton iconType="arrowDown" iconSide="right" onClick={onButtonClick}>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryResults.actionsMenuButtonLabel"
|
||||
defaultMessage="Actions"
|
||||
/>
|
||||
</EuiButton>
|
||||
),
|
||||
[onButtonClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="liveQueryDetailsActionsMenu"
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={items} />
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiveQueryDetailsActionsMenu = React.memo(LiveQueryDetailsActionsMenuComponent);
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiCodeBlock,
|
||||
|
@ -28,6 +28,8 @@ import { useActionResults } from '../../../action_results/use_action_results';
|
|||
import { useActionDetails } from '../../../actions/use_action_details';
|
||||
import { ResultTabs } from '../../../queries/edit/tabs';
|
||||
import { LiveQueryDetailsActionsMenu } from './actions_menu';
|
||||
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
|
||||
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 0;
|
||||
|
@ -37,7 +39,8 @@ const Divider = styled.div`
|
|||
|
||||
const LiveQueryDetailsPageComponent = () => {
|
||||
const { actionId } = useParams<{ actionId: string }>();
|
||||
const liveQueryListProps = useRouterNavigate('live_query');
|
||||
useBreadcrumbs('live_query_details', { liveQueryId: actionId });
|
||||
const liveQueryListProps = useRouterNavigate('live_queries');
|
||||
|
||||
const { data } = useActionDetails({ actionId });
|
||||
const { data: actionResultsData } = useActionResults({
|
||||
|
@ -54,30 +57,21 @@ const LiveQueryDetailsPageComponent = () => {
|
|||
<EuiFlexItem>
|
||||
<EuiButtonEmpty iconType="arrowLeft" {...liveQueryListProps} flush="left" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryDetails.viewLiveQueriesListTitle"
|
||||
defaultMessage="View all live queries"
|
||||
id="xpack.osquery.liveQueryDetails.viewLiveQueriesHistoryTitle"
|
||||
defaultMessage="View live queries history"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<BetaBadgeRowWrapper>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryDetails.pageTitle"
|
||||
defaultMessage="Live query results"
|
||||
defaultMessage="Live query details"
|
||||
/>
|
||||
</h1>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryDetails.pageSubtitle"
|
||||
defaultMessage="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<BetaBadge />
|
||||
</BetaBadgeRowWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
|
@ -103,10 +97,7 @@ const LiveQueryDetailsPageComponent = () => {
|
|||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription className="eui-textNoWrap">
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
data?.actionDetails?.fields?.agents?.length ?? '0'
|
||||
}
|
||||
{data?.actionDetails?.fields?.agents?.length ?? '0'}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
|
@ -123,17 +114,13 @@ const LiveQueryDetailsPageComponent = () => {
|
|||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription className="eui-textNoWrap">
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
actionResultsData?.rawResponse?.aggregations?.responses?.buckets.find(
|
||||
// @ts-expect-error update types
|
||||
(bucket) => bucket.key === 'error'
|
||||
)?.doc_count ?? '0'
|
||||
}
|
||||
<EuiTextColor color={actionResultsData?.aggregations.failed ? 'danger' : 'default'}>
|
||||
{actionResultsData?.aggregations.failed}
|
||||
</EuiTextColor>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} key="agents_count_divider">
|
||||
<EuiFlexItem grow={false} key="agents_failed_count_divider">
|
||||
<Divider />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} key="actions_menu">
|
||||
|
@ -141,25 +128,16 @@ const LiveQueryDetailsPageComponent = () => {
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[
|
||||
actionId,
|
||||
// @ts-expect-error update types
|
||||
actionResultsData?.rawResponse?.aggregations?.responses?.buckets,
|
||||
// @ts-expect-error update types
|
||||
data?.actionDetails?.fields?.agents?.length,
|
||||
]
|
||||
[actionId, actionResultsData?.aggregations.failed, data?.actionDetails?.fields?.agents?.length]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn} rightColumn={RightColumn} rightColumnGrow={false}>
|
||||
<EuiCodeBlock language="sql" fontSize="m" paddingSize="m">
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
data?.actionDetails._source?.data?.query
|
||||
}
|
||||
{data?.actionDetails._source?.data?.query}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer />
|
||||
<ResultTabs actionId={actionId} />
|
||||
<ResultTabs actionId={actionId} agentIds={data?.actionDetails?.fields?.agents} />
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
|
@ -11,9 +11,10 @@ import { Switch, Route, useRouteMatch } from 'react-router-dom';
|
|||
import { LiveQueriesPage } from './list';
|
||||
import { NewLiveQueryPage } from './new';
|
||||
import { LiveQueryDetailsPage } from './details';
|
||||
import { LiveQueryAgentDetailsPage } from './agent_details';
|
||||
import { useBreadcrumbs } from '../../common/hooks/use_breadcrumbs';
|
||||
|
||||
const LiveQueriesComponent = () => {
|
||||
useBreadcrumbs('live_queries');
|
||||
const match = useRouteMatch();
|
||||
|
||||
return (
|
||||
|
@ -21,9 +22,6 @@ const LiveQueriesComponent = () => {
|
|||
<Route path={`${match.url}/new`}>
|
||||
<NewLiveQueryPage />
|
||||
</Route>
|
||||
<Route path={`${match.url}/:actionId/results/:agentId`}>
|
||||
<LiveQueryAgentDetailsPage />
|
||||
</Route>
|
||||
<Route path={`${match.url}/:actionId`}>
|
||||
<LiveQueryDetailsPage />
|
||||
</Route>
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useKibana, useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { ActionsTable } from '../../../actions/actions_table';
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
|
||||
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
|
||||
|
||||
const LiveQueriesPageComponent = () => {
|
||||
const hasSaveUICapabilities = useKibana().services.application.capabilities.osquery.save;
|
||||
useBreadcrumbs('live_queries');
|
||||
const newQueryLinkProps = useRouterNavigate('live_queries/new');
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<BetaBadgeRowWrapper>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueriesHistory.pageTitle"
|
||||
defaultMessage="Live queries history"
|
||||
/>
|
||||
</h1>
|
||||
<BetaBadge />
|
||||
</BetaBadgeRowWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const RightColumn = useMemo(
|
||||
() => (
|
||||
<EuiButton fill {...newQueryLinkProps} iconType="plusInCircle">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueriesHistory.newLiveQueryButtonLabel"
|
||||
defaultMessage="New live query"
|
||||
/>
|
||||
</EuiButton>
|
||||
),
|
||||
[newQueryLinkProps]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout
|
||||
leftColumn={LeftColumn}
|
||||
rightColumn={hasSaveUICapabilities ? RightColumn : undefined}
|
||||
rightColumnGrow={false}
|
||||
>
|
||||
<ActionsTable />
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiveQueriesPage = React.memo(LiveQueriesPageComponent);
|
|
@ -5,16 +5,39 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButtonEmpty, EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import qs from 'query-string';
|
||||
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { LiveQuery } from '../../../live_query';
|
||||
import { LiveQuery } from '../../../live_queries';
|
||||
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
|
||||
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
|
||||
|
||||
const NewLiveQueryPageComponent = () => {
|
||||
const liveQueryListProps = useRouterNavigate('live_query');
|
||||
useBreadcrumbs('live_query_new');
|
||||
const location = useLocation();
|
||||
const liveQueryListProps = useRouterNavigate('live_queries');
|
||||
|
||||
const formDefaultValue = useMemo(() => {
|
||||
const queryParams = qs.parse(location.search);
|
||||
|
||||
if (queryParams?.agentPolicyId) {
|
||||
return {
|
||||
agentSelection: {
|
||||
allAgentsSelected: false,
|
||||
agents: [],
|
||||
platformsSelected: [],
|
||||
policiesSelected: [queryParams?.agentPolicyId],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [location.search]);
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
|
@ -22,30 +45,21 @@ const NewLiveQueryPageComponent = () => {
|
|||
<EuiFlexItem>
|
||||
<EuiButtonEmpty iconType="arrowLeft" {...liveQueryListProps} flush="left" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.newLiveQuery.viewLiveQueriesListTitle"
|
||||
defaultMessage="View all live queries"
|
||||
id="xpack.osquery.newLiveQuery.viewLiveQueriesHistoryTitle"
|
||||
defaultMessage="View live queries history"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<BetaBadgeRowWrapper>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.newLiveQuery.pageTitle"
|
||||
defaultMessage="New Live query"
|
||||
defaultMessage="New live query"
|
||||
/>
|
||||
<BetaBadge />
|
||||
</h1>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.newLiveQuery.pageSubtitle"
|
||||
defaultMessage="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</BetaBadgeRowWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
|
@ -54,7 +68,7 @@ const NewLiveQueryPageComponent = () => {
|
|||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn}>
|
||||
<LiveQuery />
|
||||
<LiveQuery defaultValue={formDefaultValue} />
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
|
@ -1,82 +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 {
|
||||
EuiButtonEmpty,
|
||||
EuiText,
|
||||
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 { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useActionDetails } from '../../../actions/use_action_details';
|
||||
import { ResultsTable } from '../../../results/results_table';
|
||||
|
||||
const LiveQueryAgentDetailsPageComponent = () => {
|
||||
const { actionId, agentId } = useParams<{ actionId: string; agentId: string }>();
|
||||
const { data } = useActionDetails({ actionId });
|
||||
const liveQueryListProps = useRouterNavigate(`live_query/${actionId}`);
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty iconType="arrowLeft" {...liveQueryListProps} flush="left" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryAgentDetails.viewLiveQueryResultsTitle"
|
||||
defaultMessage="View all live query results"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryAgentDetails.pageTitle"
|
||||
defaultMessage="Live query {agentId} agent results"
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
values={{ agentId }}
|
||||
/>
|
||||
</h1>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryAgentDetails.pageSubtitle"
|
||||
defaultMessage="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[agentId, liveQueryListProps]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn}>
|
||||
<EuiCodeBlock language="sql" fontSize="m" paddingSize="m">
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
data?.actionDetails._source?.data?.query
|
||||
}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer />
|
||||
<ResultsTable actionId={actionId} agentId={agentId} />
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiveQueryAgentDetailsPage = React.memo(LiveQueryAgentDetailsPageComponent);
|
|
@ -1,63 +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 { EuiButton, EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { ActionsTable } from '../../../actions/actions_table';
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
|
||||
const LiveQueriesPageComponent = () => {
|
||||
const newQueryLinkProps = useRouterNavigate('live_query/new');
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryList.pageTitle"
|
||||
defaultMessage="Live queries"
|
||||
/>
|
||||
</h1>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryList.pageSubtitle"
|
||||
defaultMessage="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const RightColumn = useMemo(
|
||||
() => (
|
||||
<EuiButton fill {...newQueryLinkProps}>
|
||||
{'New live query'}
|
||||
</EuiButton>
|
||||
),
|
||||
[newQueryLinkProps]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn} rightColumn={RightColumn} rightColumnGrow={false}>
|
||||
<ActionsTable />
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiveQueriesPage = React.memo(LiveQueriesPageComponent);
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { ScheduledQueryGroupForm } from '../../../scheduled_query_groups/form';
|
||||
import { useOsqueryIntegration } from '../../../common/hooks';
|
||||
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
|
||||
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
|
||||
|
||||
const AddScheduledQueryGroupPageComponent = () => {
|
||||
useBreadcrumbs('scheduled_query_group_add');
|
||||
const scheduledQueryListProps = useRouterNavigate('scheduled_query_groups');
|
||||
const { data: osqueryIntegration } = useOsqueryIntegration();
|
||||
|
||||
const packageInfo = useMemo(() => {
|
||||
if (!osqueryIntegration) return;
|
||||
|
||||
return {
|
||||
name: osqueryIntegration.name,
|
||||
title: osqueryIntegration.title,
|
||||
version: osqueryIntegration.version,
|
||||
};
|
||||
}, [osqueryIntegration]);
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty iconType="arrowLeft" {...scheduledQueryListProps} flush="left" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.addScheduledQueryGroup.viewScheduledQueryGroupsListTitle"
|
||||
defaultMessage="View all scheduled query groups"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<BetaBadgeRowWrapper>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.addScheduledQueryGroup.pageTitle"
|
||||
defaultMessage="Add scheduled query group"
|
||||
/>
|
||||
</h1>
|
||||
<BetaBadge />
|
||||
</BetaBadgeRowWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[scheduledQueryListProps]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn}>
|
||||
{packageInfo && <ScheduledQueryGroupForm packageInfo={packageInfo} />}
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddScheduledQueryGroupPage = React.memo(AddScheduledQueryGroupPageComponent);
|
|
@ -5,10 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiButton, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { useDiscoverLink } from '../../../common/hooks';
|
||||
|
||||
interface LiveQueryDetailsActionsMenuProps {
|
||||
actionId: string;
|
||||
|
@ -17,13 +18,9 @@ interface LiveQueryDetailsActionsMenuProps {
|
|||
const LiveQueryDetailsActionsMenuComponent: React.FC<LiveQueryDetailsActionsMenuProps> = ({
|
||||
actionId,
|
||||
}) => {
|
||||
const services = useKibana().services;
|
||||
const discoverLinkProps = useDiscoverLink({ filters: [{ key: 'action_id', value: actionId }] });
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
|
||||
const discoverLinkHref = services?.application?.getUrlForApp('discover', {
|
||||
path: `#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(columns:!(),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logs-*',key:action_id,negate:!f,params:(query:'${actionId}'),type:phrase),query:(match_phrase:(action_id:'${actionId}')))),index:'logs-*',interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))`,
|
||||
});
|
||||
|
||||
const onButtonClick = useCallback(() => {
|
||||
setPopover((currentIsPopoverOpen) => !currentIsPopoverOpen);
|
||||
}, []);
|
||||
|
@ -34,17 +31,26 @@ const LiveQueryDetailsActionsMenuComponent: React.FC<LiveQueryDetailsActionsMenu
|
|||
|
||||
const items = useMemo(
|
||||
() => [
|
||||
<EuiContextMenuItem key="copy" icon="copy" href={discoverLinkHref}>
|
||||
Check results in Discover
|
||||
<EuiContextMenuItem key="copy" icon="copy" {...discoverLinkProps}>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryResults.viewResultsInDiscoverLabel"
|
||||
defaultMessage="View results in Discover"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
],
|
||||
[discoverLinkHref]
|
||||
[discoverLinkProps]
|
||||
);
|
||||
|
||||
const button = (
|
||||
<EuiButton iconType="arrowDown" iconSide="right" onClick={onButtonClick}>
|
||||
Actions
|
||||
</EuiButton>
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<EuiButton iconType="arrowDown" iconSide="right" onClick={onButtonClick}>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryResults.actionsMenuButtonLabel"
|
||||
defaultMessage="Actions"
|
||||
/>
|
||||
</EuiButton>
|
||||
),
|
||||
[onButtonClick]
|
||||
);
|
||||
|
||||
return (
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
} 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 { useKibana, useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useScheduledQueryGroup } from '../../../scheduled_query_groups/use_scheduled_query_group';
|
||||
import { ScheduledQueryGroupQueriesTable } from '../../../scheduled_query_groups/scheduled_query_group_queries_table';
|
||||
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
|
||||
import { AgentsPolicyLink } from '../../../agent_policies/agents_policy_link';
|
||||
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 0;
|
||||
height: 100%;
|
||||
border-left: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
`;
|
||||
|
||||
const ScheduledQueryGroupDetailsPageComponent = () => {
|
||||
const hasSaveUICapabilities = useKibana().services.application.capabilities.osquery.save;
|
||||
const { scheduledQueryGroupId } = useParams<{ scheduledQueryGroupId: string }>();
|
||||
const scheduledQueryGroupsListProps = useRouterNavigate('scheduled_query_groups');
|
||||
const editQueryLinkProps = useRouterNavigate(
|
||||
`scheduled_query_groups/${scheduledQueryGroupId}/edit`
|
||||
);
|
||||
|
||||
const { data } = useScheduledQueryGroup({ scheduledQueryGroupId });
|
||||
|
||||
useBreadcrumbs('scheduled_query_group_details', { scheduledQueryGroupName: data?.name ?? '' });
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty
|
||||
iconType="arrowLeft"
|
||||
{...scheduledQueryGroupsListProps}
|
||||
flush="left"
|
||||
size="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryDetails.viewAllScheduledQueriesListTitle"
|
||||
defaultMessage="View all scheduled query groups"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<BetaBadgeRowWrapper>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryDetails.pageTitle"
|
||||
defaultMessage="{queryName} details"
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
values={{
|
||||
queryName: data?.name,
|
||||
}}
|
||||
/>
|
||||
</h1>
|
||||
<BetaBadge />
|
||||
</BetaBadgeRowWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[data?.name, scheduledQueryGroupsListProps]
|
||||
);
|
||||
|
||||
const RightColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup justifyContent="flexEnd" direction="row">
|
||||
<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.scheduleQueryGroup.kpis.policyLabelText"
|
||||
defaultMessage="Policy"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription className="eui-textNoWrap">
|
||||
{data?.policy_id ? <AgentsPolicyLink policyId={data?.policy_id} /> : null}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
{hasSaveUICapabilities ? (
|
||||
<>
|
||||
<EuiFlexItem grow={false} key="agents_failed_count_divider">
|
||||
<Divider />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} key="edit_button">
|
||||
<EuiButton fill {...editQueryLinkProps} iconType="pencil">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryDetailsPage.editQueryButtonLabel"
|
||||
defaultMessage="Edit"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
) : undefined}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[data?.policy_id, editQueryLinkProps, hasSaveUICapabilities]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn} rightColumn={RightColumn} rightColumnGrow={false}>
|
||||
{data && <ScheduledQueryGroupQueriesTable data={data} />}
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScheduledQueryGroupDetailsPage = React.memo(ScheduledQueryGroupDetailsPageComponent);
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiLoadingContent } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { ScheduledQueryGroupForm } from '../../../scheduled_query_groups/form';
|
||||
import { useScheduledQueryGroup } from '../../../scheduled_query_groups/use_scheduled_query_group';
|
||||
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
|
||||
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
|
||||
|
||||
const EditScheduledQueryGroupPageComponent = () => {
|
||||
const { scheduledQueryGroupId } = useParams<{ scheduledQueryGroupId: string }>();
|
||||
const queryDetailsLinkProps = useRouterNavigate(
|
||||
`scheduled_query_groups/${scheduledQueryGroupId}`
|
||||
);
|
||||
|
||||
const { data } = useScheduledQueryGroup({ scheduledQueryGroupId });
|
||||
|
||||
useBreadcrumbs('scheduled_query_group_edit', { scheduledQueryGroupName: data?.name ?? '' });
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty iconType="arrowLeft" {...queryDetailsLinkProps} flush="left" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.editScheduledQuery.viewScheduledQueriesListTitle"
|
||||
defaultMessage="View {queryName} details"
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
values={{ queryName: data?.name }}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<BetaBadgeRowWrapper>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.editScheduledQuery.pageTitle"
|
||||
defaultMessage="Edit {queryName}"
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
values={{
|
||||
queryName: data?.name,
|
||||
}}
|
||||
/>
|
||||
</h1>
|
||||
<BetaBadge />
|
||||
</BetaBadgeRowWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[data?.name, queryDetailsLinkProps]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn}>
|
||||
{!data ? (
|
||||
<EuiLoadingContent lines={10} />
|
||||
) : (
|
||||
<ScheduledQueryGroupForm editMode={true} defaultValue={data} />
|
||||
)}
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditScheduledQueryGroupPage = React.memo(EditScheduledQueryGroupPageComponent);
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { Switch, Route, useRouteMatch } from 'react-router-dom';
|
||||
|
||||
import { ScheduledQueryGroupsPage } from './list';
|
||||
import { AddScheduledQueryGroupPage } from './add';
|
||||
import { EditScheduledQueryGroupPage } from './edit';
|
||||
import { ScheduledQueryGroupDetailsPage } from './details';
|
||||
import { useBreadcrumbs } from '../../common/hooks/use_breadcrumbs';
|
||||
|
||||
const ScheduledQueryGroupsComponent = () => {
|
||||
useBreadcrumbs('scheduled_query_groups');
|
||||
const match = useRouteMatch();
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${match.url}/add`}>
|
||||
<AddScheduledQueryGroupPage />
|
||||
</Route>
|
||||
<Route path={`${match.url}/:scheduledQueryGroupId/edit`}>
|
||||
<EditScheduledQueryGroupPage />
|
||||
</Route>
|
||||
<Route path={`${match.url}/:scheduledQueryGroupId`}>
|
||||
<ScheduledQueryGroupDetailsPage />
|
||||
</Route>
|
||||
<Route path={`${match.url}`}>
|
||||
<ScheduledQueryGroupsPage />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScheduledQueryGroups = React.memo(ScheduledQueryGroupsComponent);
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useKibana, useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { ScheduledQueryGroupsTable } from '../../../scheduled_query_groups/scheduled_query_groups_table';
|
||||
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
|
||||
|
||||
const ScheduledQueryGroupsPageComponent = () => {
|
||||
const hasSaveUICapabilities = useKibana().services.application.capabilities.osquery.save;
|
||||
const newQueryLinkProps = useRouterNavigate('scheduled_query_groups/add');
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<BetaBadgeRowWrapper>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryList.pageTitle"
|
||||
defaultMessage="Scheduled query groups"
|
||||
/>
|
||||
</h1>
|
||||
<BetaBadge />
|
||||
</BetaBadgeRowWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const RightColumn = useMemo(
|
||||
() => (
|
||||
<EuiButton fill {...newQueryLinkProps} iconType="plusInCircle">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryList.addScheduledQueryButtonLabel"
|
||||
defaultMessage="Add scheduled query group"
|
||||
/>
|
||||
</EuiButton>
|
||||
),
|
||||
[newQueryLinkProps]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout
|
||||
leftColumn={LeftColumn}
|
||||
rightColumn={hasSaveUICapabilities ? RightColumn : undefined}
|
||||
rightColumnGrow={false}
|
||||
>
|
||||
<ScheduledQueryGroupsTable />
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScheduledQueryGroupsPage = React.memo(ScheduledQueryGroupsPageComponent);
|
|
@ -1,169 +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 { find } from 'lodash/fp';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiFieldText,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
// @ts-expect-error update types
|
||||
const OsqueryStreamFieldComponent = ({ field, removeItem }) => {
|
||||
const { http } = useKibana().services;
|
||||
const { data: { saved_objects: savedQueries } = {} } = useQuery(['savedQueryList'], () =>
|
||||
http.get('/internal/osquery/saved_query', {
|
||||
query: { pageIndex: 0, pageSize: 100, sortField: 'updated_at', sortDirection: 'desc' },
|
||||
})
|
||||
);
|
||||
|
||||
const { setValue } = field;
|
||||
|
||||
const savedQueriesOptions = useMemo(
|
||||
() =>
|
||||
// @ts-expect-error update types
|
||||
(savedQueries ?? []).map((savedQuery) => ({
|
||||
text: savedQuery.attributes.name,
|
||||
value: savedQuery.id,
|
||||
})),
|
||||
[savedQueries]
|
||||
);
|
||||
|
||||
const handleSavedQueryChange = useCallback(
|
||||
(event) => {
|
||||
event.persist();
|
||||
const savedQueryId = event.target.value;
|
||||
const savedQuery = find(['id', savedQueryId], savedQueries);
|
||||
|
||||
if (savedQuery) {
|
||||
// @ts-expect-error update types
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
vars: {
|
||||
...prev.vars,
|
||||
id: {
|
||||
...prev.vars.id,
|
||||
value: savedQuery.id,
|
||||
},
|
||||
query: {
|
||||
...prev.vars.query,
|
||||
value: savedQuery.attributes.query,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
},
|
||||
[savedQueries, setValue]
|
||||
);
|
||||
|
||||
const handleEnabledChange = useCallback(() => {
|
||||
// @ts-expect-error update types
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
enabled: !prev.enabled,
|
||||
}));
|
||||
}, [setValue]);
|
||||
|
||||
const handleQueryChange = useCallback(
|
||||
(event) => {
|
||||
event.persist();
|
||||
// @ts-expect-error update types
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
vars: {
|
||||
...prev.vars,
|
||||
query: {
|
||||
...prev.vars.query,
|
||||
value: event.target.value,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const handleIntervalChange = useCallback(
|
||||
(event) => {
|
||||
event.persist();
|
||||
// @ts-expect-error update types
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
vars: {
|
||||
...prev.vars,
|
||||
interval: {
|
||||
...prev.vars.interval,
|
||||
value: event.target.value,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const handleIdChange = useCallback(
|
||||
(event) => {
|
||||
event.persist();
|
||||
// @ts-expect-error update types
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
vars: {
|
||||
...prev.vars,
|
||||
id: {
|
||||
...prev.vars.id,
|
||||
value: event.target.value,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiFormRow>
|
||||
<EuiSwitch label="Enabled" checked={field.value.enabled} onChange={handleEnabledChange} />
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow>
|
||||
<EuiButtonIcon aria-label="remove" onClick={removeItem} color="danger" iconType="trash" />
|
||||
</EuiFormRow>
|
||||
<EuiFormRow>
|
||||
<EuiSelect
|
||||
value={field.value.vars.id.value}
|
||||
hasNoInitialSelection
|
||||
options={savedQueriesOptions}
|
||||
onChange={handleSavedQueryChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow>
|
||||
<EuiFieldText value={field.value.vars.query.value} onChange={handleQueryChange} />
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow>
|
||||
<EuiFieldText value={field.value.vars.interval.value} onChange={handleIntervalChange} />
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow>
|
||||
<EuiFieldText value={field.value.vars.id.value} onChange={handleIdChange} />
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
<EuiHorizontalRule />
|
||||
</EuiForm>
|
||||
);
|
||||
};
|
||||
|
||||
export const OsqueryStreamField = React.memo(OsqueryStreamFieldComponent);
|
|
@ -1,153 +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 produce from 'immer';
|
||||
import { get, omit } from 'lodash/fp';
|
||||
import { EuiButton, EuiButtonEmpty, EuiSpacer, EuiHorizontalRule } from '@elastic/eui';
|
||||
import uuid from 'uuid';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
UseField,
|
||||
useForm,
|
||||
UseArray,
|
||||
getUseField,
|
||||
Field,
|
||||
ToggleField,
|
||||
Form,
|
||||
} from '../../shared_imports';
|
||||
|
||||
import { OsqueryStreamField } from '../common/osquery_stream_field';
|
||||
import { schema } from './schema';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
const EDIT_SCHEDULED_QUERY_FORM_ID = 'editScheduledQueryForm';
|
||||
|
||||
interface EditScheduledQueryFormProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
agentPolicies: Array<Record<string, any>>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: Array<Record<string, any>>;
|
||||
handleSubmit: () => Promise<void>;
|
||||
}
|
||||
|
||||
const EditScheduledQueryFormComponent: React.FC<EditScheduledQueryFormProps> = ({
|
||||
agentPolicies,
|
||||
data,
|
||||
handleSubmit,
|
||||
}) => {
|
||||
const agentPoliciesOptions = useMemo(
|
||||
() =>
|
||||
agentPolicies.map((policy) => ({
|
||||
value: policy.id,
|
||||
text: policy.name,
|
||||
})),
|
||||
[agentPolicies]
|
||||
);
|
||||
|
||||
const { form } = useForm({
|
||||
schema,
|
||||
id: EDIT_SCHEDULED_QUERY_FORM_ID,
|
||||
onSubmit: handleSubmit,
|
||||
defaultValue: data,
|
||||
// @ts-expect-error update types
|
||||
deserializer: (payload) => {
|
||||
const deserialized = produce(payload, (draft) => {
|
||||
// @ts-expect-error update types
|
||||
draft.inputs[0].streams.forEach((stream) => {
|
||||
delete stream.compiled_stream;
|
||||
});
|
||||
});
|
||||
|
||||
return deserialized;
|
||||
},
|
||||
// @ts-expect-error update types
|
||||
serializer: (payload) =>
|
||||
omit(['id', 'revision', 'created_at', 'created_by', 'updated_at', 'updated_by', 'version'], {
|
||||
...data,
|
||||
...payload,
|
||||
// @ts-expect-error update types
|
||||
inputs: [{ type: 'osquery', ...((payload.inputs && payload.inputs[0]) ?? {}) }],
|
||||
}),
|
||||
});
|
||||
|
||||
const { submit } = form;
|
||||
|
||||
const policyIdComponentProps = useMemo(
|
||||
() => ({
|
||||
euiFieldProps: {
|
||||
disabled: true,
|
||||
options: agentPoliciesOptions,
|
||||
},
|
||||
}),
|
||||
[agentPoliciesOptions]
|
||||
);
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<CommonUseField path="policy_id" componentProps={policyIdComponentProps} />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="name" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="description" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="inputs[0].enabled" component={ToggleField} />
|
||||
<EuiHorizontalRule />
|
||||
<EuiSpacer />
|
||||
<UseArray path="inputs[0].streams">
|
||||
{({ items, addItem, removeItem }) => (
|
||||
<>
|
||||
{items.map((item) => (
|
||||
<UseField
|
||||
key={item.path}
|
||||
path={item.path}
|
||||
component={OsqueryStreamField}
|
||||
// eslint-disable-next-line react/jsx-no-bind, react-perf/jsx-no-new-function-as-prop
|
||||
removeItem={() => removeItem(item.id)}
|
||||
defaultValue={
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
get(item.path, form.getFormData()) ?? {
|
||||
data_stream: {
|
||||
type: 'logs',
|
||||
dataset: 'osquery_elastic_managed.osquery',
|
||||
},
|
||||
vars: {
|
||||
query: {
|
||||
type: 'text',
|
||||
value: 'select * from uptime',
|
||||
},
|
||||
interval: {
|
||||
type: 'text',
|
||||
value: '120',
|
||||
},
|
||||
id: {
|
||||
type: 'text',
|
||||
value: uuid.v4(),
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<EuiButtonEmpty onClick={addItem} iconType="plusInCircleFilled">
|
||||
{'Add query'}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
)}
|
||||
</UseArray>
|
||||
<EuiHorizontalRule />
|
||||
<EuiSpacer />
|
||||
<EuiButton fill onClick={submit}>
|
||||
Save
|
||||
</EuiButton>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditScheduledQueryForm = React.memo(EditScheduledQueryFormComponent);
|
|
@ -1,48 +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 React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { EditScheduledQueryForm } from './form';
|
||||
|
||||
const EditScheduledQueryPageComponent = () => {
|
||||
const { http } = useKibana().services;
|
||||
const { scheduledQueryId } = useParams<{ scheduledQueryId: string }>();
|
||||
|
||||
const { data } = useQuery(['scheduledQuery', { scheduledQueryId }], () =>
|
||||
http.get(`/internal/osquery/scheduled_query/${scheduledQueryId}`)
|
||||
);
|
||||
|
||||
const { data: agentPolicies } = useQuery(
|
||||
['agentPolicy'],
|
||||
() => http.get(`/api/fleet/agent_policies`),
|
||||
{ initialData: { items: [] } }
|
||||
);
|
||||
|
||||
const updateScheduledQueryMutation = useMutation((payload) =>
|
||||
http.put(`/api/fleet/package_policies/${scheduledQueryId}`, { body: JSON.stringify(payload) })
|
||||
);
|
||||
|
||||
if (data) {
|
||||
return (
|
||||
<EditScheduledQueryForm
|
||||
data={data}
|
||||
// @ts-expect-error update types
|
||||
agentPolicies={agentPolicies?.items}
|
||||
// @ts-expect-error update types
|
||||
handleSubmit={updateScheduledQueryMutation.mutate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <div>Loading</div>;
|
||||
};
|
||||
|
||||
export const EditScheduledQueryPage = React.memo(EditScheduledQueryPageComponent);
|
|
@ -1,26 +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 { FIELD_TYPES } from '../../shared_imports';
|
||||
|
||||
export const schema = {
|
||||
policy_id: {
|
||||
type: FIELD_TYPES.SELECT,
|
||||
label: 'Policy',
|
||||
},
|
||||
name: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: 'Name',
|
||||
},
|
||||
description: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: 'Description',
|
||||
},
|
||||
streams: {
|
||||
type: FIELD_TYPES.MULTI_SELECT,
|
||||
},
|
||||
};
|
|
@ -1,38 +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 React from 'react';
|
||||
import { Switch, Route, useRouteMatch } from 'react-router-dom';
|
||||
|
||||
import { ScheduledQueriesPage } from './queries';
|
||||
import { NewScheduledQueryPage } from './new';
|
||||
import { EditScheduledQueryPage } from './edit';
|
||||
// import { QueryAgentResults } from './agent_results';
|
||||
// import { SavedQueriesPage } from './saved_query';
|
||||
|
||||
const ScheduledQueriesComponent = () => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${match.url}/new`}>
|
||||
<NewScheduledQueryPage />
|
||||
</Route>
|
||||
{/* <Route path={`${match.url}/:savedQueryId/results/:agentId`}>
|
||||
<QueryAgentResults />
|
||||
</Route> */}
|
||||
<Route path={`${match.url}/:scheduledQueryId`}>
|
||||
<EditScheduledQueryPage />
|
||||
</Route>
|
||||
<Route path={`${match.url}/`}>
|
||||
<ScheduledQueriesPage />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScheduledQueries = React.memo(ScheduledQueriesComponent);
|
|
@ -1,105 +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 { EuiButton, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
|
||||
import deepmerge from 'deepmerge';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { useForm, UseArray, UseField, getUseField, Field, Form } from '../../shared_imports';
|
||||
|
||||
import { OsqueryStreamField } from '../common/osquery_stream_field';
|
||||
import { defaultValue, schema } from './schema';
|
||||
import { combineMerge } from './utils';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
const NEW_SCHEDULED_QUERY_FORM_ID = 'newScheduledQueryForm';
|
||||
|
||||
interface NewScheduledQueryFormProps {
|
||||
handleSubmit: () => Promise<void>;
|
||||
}
|
||||
|
||||
const NewScheduledQueryFormComponent: React.FC<NewScheduledQueryFormProps> = ({ handleSubmit }) => {
|
||||
const { form } = useForm({
|
||||
schema,
|
||||
id: NEW_SCHEDULED_QUERY_FORM_ID,
|
||||
options: {
|
||||
stripEmptyFields: false,
|
||||
},
|
||||
onSubmit: handleSubmit,
|
||||
// @ts-expect-error update types
|
||||
defaultValue,
|
||||
serializer: (payload) =>
|
||||
deepmerge(defaultValue, payload, {
|
||||
arrayMerge: combineMerge,
|
||||
}),
|
||||
});
|
||||
const { submit } = form;
|
||||
|
||||
const StreamsContent = useCallback(
|
||||
({ items, addItem, removeItem }) => (
|
||||
<>
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
items.map((item) => (
|
||||
<UseField
|
||||
key={item.path}
|
||||
path={item.path}
|
||||
component={OsqueryStreamField}
|
||||
// eslint-disable-next-line react/jsx-no-bind, react-perf/jsx-no-new-function-as-prop
|
||||
removeItem={() => removeItem(item.id)}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
defaultValue={{
|
||||
data_stream: {
|
||||
type: 'logs',
|
||||
dataset: 'osquery_elastic_managed.osquery',
|
||||
},
|
||||
vars: {
|
||||
query: {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
interval: {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
id: {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<EuiButtonEmpty onClick={addItem} iconType="plusInCircleFilled">
|
||||
{'Add query'}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<CommonUseField path="name" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="description" />
|
||||
<EuiSpacer />
|
||||
<UseArray path="inputs[0].streams" initialNumberOfItems={1} readDefaultValueOnForm={false}>
|
||||
{StreamsContent}
|
||||
</UseArray>
|
||||
<EuiSpacer />
|
||||
<EuiButton fill onClick={submit}>
|
||||
{'Save'}
|
||||
</EuiButton>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewScheduledQueryForm = React.memo(NewScheduledQueryFormComponent);
|
|
@ -1,32 +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 React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { NewScheduledQueryForm } from './form';
|
||||
|
||||
const NewScheduledQueryPageComponent = () => {
|
||||
const { http } = useKibana().services;
|
||||
const history = useHistory();
|
||||
|
||||
const createScheduledQueryMutation = useMutation(
|
||||
(payload) => http.post(`/api/fleet/package_policies`, { body: JSON.stringify(payload) }),
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
history.push(`/scheduled_queries/${data.item.id}`);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// @ts-expect-error update types
|
||||
return <NewScheduledQueryForm handleSubmit={createScheduledQueryMutation.mutate} />;
|
||||
};
|
||||
|
||||
export const NewScheduledQueryPage = React.memo(NewScheduledQueryPageComponent);
|
|
@ -1,67 +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 { FIELD_TYPES } from '../../shared_imports';
|
||||
|
||||
export const defaultValue = {
|
||||
name: '',
|
||||
description: '',
|
||||
namespace: 'default',
|
||||
enabled: true,
|
||||
policy_id: '1e2bb670-686c-11eb-84b4-81282a213fcf',
|
||||
output_id: '',
|
||||
package: {
|
||||
name: 'osquery_elastic_managed',
|
||||
title: 'OSquery Elastic Managed',
|
||||
version: '0.1.2',
|
||||
},
|
||||
inputs: [
|
||||
{
|
||||
type: 'osquery',
|
||||
enabled: true,
|
||||
streams: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const schema = {
|
||||
name: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: 'Name',
|
||||
},
|
||||
description: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: 'Description',
|
||||
},
|
||||
namespace: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
enabled: {
|
||||
type: FIELD_TYPES.TOGGLE,
|
||||
},
|
||||
policy_id: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
inputs: {
|
||||
enabled: {
|
||||
type: FIELD_TYPES.TOGGLE,
|
||||
},
|
||||
streams: {
|
||||
type: FIELD_TYPES.MULTI_SELECT,
|
||||
vars: {
|
||||
query: {
|
||||
type: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
value: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,25 +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 deepmerge from 'deepmerge';
|
||||
|
||||
// @ts-expect-error update types
|
||||
export const combineMerge = (target, source, options) => {
|
||||
const destination = target.slice();
|
||||
|
||||
// @ts-expect-error update types
|
||||
source.forEach((item, index) => {
|
||||
if (typeof destination[index] === 'undefined') {
|
||||
destination[index] = options.cloneUnlessOtherwiseSpecified(item, options);
|
||||
} else if (options.isMergeableObject(item)) {
|
||||
destination[index] = deepmerge(target[index], item, options);
|
||||
} else if (target.indexOf(item) === -1) {
|
||||
destination.push(item);
|
||||
}
|
||||
});
|
||||
return destination;
|
||||
};
|
|
@ -1,185 +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 {
|
||||
EuiBasicTable,
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiCodeBlock,
|
||||
RIGHT_ALIGNMENT,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { Direction } from '../../../common/search_strategy';
|
||||
import { useKibana, useRouterNavigate } from '../../common/lib/kibana';
|
||||
|
||||
const ScheduledQueriesPageComponent = () => {
|
||||
const { push } = useHistory();
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const [sortField, setSortField] = useState('updated_at');
|
||||
const [sortDirection, setSortDirection] = useState(Direction.desc);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<Record<string, any>>({});
|
||||
const { http } = useKibana().services;
|
||||
const newQueryLinkProps = useRouterNavigate('scheduled_queries/new');
|
||||
|
||||
const { data = {} } = useQuery(
|
||||
['scheduledQueryList', { pageIndex, pageSize, sortField, sortDirection }],
|
||||
() =>
|
||||
http.get('/internal/osquery/scheduled_query', {
|
||||
query: {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
sortField,
|
||||
sortDirection,
|
||||
},
|
||||
}),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
// Refetch the data every 5 seconds
|
||||
refetchInterval: 5000,
|
||||
}
|
||||
);
|
||||
const { total = 0, items: savedQueries } = data;
|
||||
|
||||
const toggleDetails = useCallback(
|
||||
(item) => () => {
|
||||
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
|
||||
if (itemIdToExpandedRowMapValues[item.id]) {
|
||||
delete itemIdToExpandedRowMapValues[item.id];
|
||||
} else {
|
||||
// @ts-expect-error update types
|
||||
itemIdToExpandedRowMapValues[item.id] = item.inputs[0].streams.map((stream) => (
|
||||
<EuiCodeBlock key={stream} language="sql" fontSize="m" paddingSize="m">
|
||||
{`${stream.vars.query.value} every ${stream.vars.interval.value}s`}
|
||||
</EuiCodeBlock>
|
||||
));
|
||||
}
|
||||
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
|
||||
},
|
||||
[itemIdToExpandedRowMap]
|
||||
);
|
||||
|
||||
const renderExtendedItemToggle = useCallback(
|
||||
(item) => (
|
||||
<EuiButtonIcon
|
||||
onClick={toggleDetails(item)}
|
||||
aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'}
|
||||
iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
),
|
||||
[itemIdToExpandedRowMap, toggleDetails]
|
||||
);
|
||||
|
||||
const handleEditClick = useCallback((item) => push(`/scheduled_queries/${item.id}`), [push]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'name',
|
||||
name: 'Query name',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'enabled',
|
||||
name: 'Active',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'updated_at',
|
||||
name: 'Last updated at',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
actions: [
|
||||
{
|
||||
name: 'Edit',
|
||||
description: 'Edit or run this query',
|
||||
type: 'icon',
|
||||
icon: 'documentEdit',
|
||||
onClick: handleEditClick,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
render: renderExtendedItemToggle,
|
||||
},
|
||||
],
|
||||
[handleEditClick, renderExtendedItemToggle]
|
||||
);
|
||||
|
||||
const onTableChange = useCallback(({ page = {}, sort = {} }) => {
|
||||
setPageIndex(page.index);
|
||||
setPageSize(page.size);
|
||||
setSortField(sort.field);
|
||||
setSortDirection(sort.direction);
|
||||
}, []);
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount: total,
|
||||
pageSizeOptions: [3, 5, 8],
|
||||
}),
|
||||
[total, pageIndex, pageSize]
|
||||
);
|
||||
|
||||
const sorting = useMemo(
|
||||
() => ({
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
}),
|
||||
[sortDirection, sortField]
|
||||
);
|
||||
|
||||
const selection = useMemo(
|
||||
() => ({
|
||||
selectable: () => true,
|
||||
initialSelected: [],
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiButton fill {...newQueryLinkProps}>
|
||||
{'New query'}
|
||||
</EuiButton>
|
||||
|
||||
{savedQueries && (
|
||||
<EuiBasicTable
|
||||
items={savedQueries}
|
||||
itemId="id"
|
||||
// @ts-expect-error update types
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
isSelectable={true}
|
||||
selection={selection}
|
||||
onChange={onTableChange}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
rowHeader="id"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScheduledQueriesPage = React.memo(ScheduledQueriesPageComponent);
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 { produce } from 'immer';
|
||||
import { EuiSwitch, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import styled from 'styled-components';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
PackagePolicy,
|
||||
UpdatePackagePolicy,
|
||||
packagePolicyRouteService,
|
||||
} from '../../../fleet/common';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { useAgentStatus } from '../agents/use_agent_status';
|
||||
import { useAgentPolicy } from '../agent_policies/use_agent_policy';
|
||||
import { ConfirmDeployAgentPolicyModal } from './form/confirmation_modal';
|
||||
|
||||
const StyledEuiLoadingSpinner = styled(EuiLoadingSpinner)`
|
||||
margin-right: ${({ theme }) => theme.eui.paddingSizes.s};
|
||||
`;
|
||||
|
||||
interface ActiveStateSwitchProps {
|
||||
item: PackagePolicy;
|
||||
}
|
||||
|
||||
const ActiveStateSwitchComponent: React.FC<ActiveStateSwitchProps> = ({ item }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
application: {
|
||||
capabilities: {
|
||||
osquery: { save: hasSaveUICapabilities },
|
||||
},
|
||||
},
|
||||
} = useKibana().services;
|
||||
const [confirmationModal, setConfirmationModal] = useState(false);
|
||||
|
||||
const hideConfirmationModal = useCallback(() => setConfirmationModal(false), []);
|
||||
|
||||
const { data: agentStatus } = useAgentStatus({ policyId: item.policy_id });
|
||||
const { data: agentPolicy } = useAgentPolicy({ policyId: item.policy_id });
|
||||
|
||||
const { isLoading, mutate } = useMutation(
|
||||
({ id, ...payload }: UpdatePackagePolicy & { id: string }) =>
|
||||
http.put(packagePolicyRouteService.getUpdatePath(id), {
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
queryClient.invalidateQueries('scheduledQueries');
|
||||
toasts.addSuccess(
|
||||
response.item.enabled
|
||||
? i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.table.activatedSuccessToastMessageText',
|
||||
{
|
||||
defaultMessage: 'Successfully activated {scheduledQueryGroupName}',
|
||||
values: {
|
||||
scheduledQueryGroupName: response.item.name,
|
||||
},
|
||||
}
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.table.deactivatedSuccessToastMessageText',
|
||||
{
|
||||
defaultMessage: 'Successfully deactivated {scheduledQueryGroupName}',
|
||||
values: {
|
||||
scheduledQueryGroupName: response.item.name,
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const handleToggleActive = useCallback(() => {
|
||||
const updatedPolicy = produce<
|
||||
UpdatePackagePolicy & { id: string },
|
||||
Omit<PackagePolicy, 'revision' | 'updated_at' | 'updated_by' | 'created_at' | 'created_by'> &
|
||||
Partial<{
|
||||
revision: number;
|
||||
updated_at: string;
|
||||
updated_by: string;
|
||||
created_at: string;
|
||||
created_by: string;
|
||||
}>
|
||||
>(item, (draft) => {
|
||||
delete draft.revision;
|
||||
delete draft.updated_at;
|
||||
delete draft.updated_by;
|
||||
delete draft.created_at;
|
||||
delete draft.created_by;
|
||||
|
||||
draft.enabled = !item.enabled;
|
||||
draft.inputs[0].streams.forEach((stream) => {
|
||||
delete stream.compiled_stream;
|
||||
});
|
||||
|
||||
return draft;
|
||||
});
|
||||
|
||||
mutate(updatedPolicy);
|
||||
hideConfirmationModal();
|
||||
}, [hideConfirmationModal, item, mutate]);
|
||||
|
||||
const handleToggleActiveClick = useCallback(() => {
|
||||
if (agentStatus?.total) {
|
||||
return setConfirmationModal(true);
|
||||
}
|
||||
|
||||
handleToggleActive();
|
||||
}, [agentStatus?.total, handleToggleActive]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <StyledEuiLoadingSpinner />}
|
||||
<EuiSwitch
|
||||
checked={item.enabled}
|
||||
disabled={!hasSaveUICapabilities || isLoading}
|
||||
showLabel={false}
|
||||
label=""
|
||||
onChange={handleToggleActiveClick}
|
||||
/>
|
||||
{confirmationModal && agentStatus?.total && (
|
||||
<ConfirmDeployAgentPolicyModal
|
||||
onConfirm={handleToggleActive}
|
||||
onCancel={hideConfirmationModal}
|
||||
agentCount={agentStatus?.total}
|
||||
agentPolicy={agentPolicy}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ActiveStateSwitch = React.memo(ActiveStateSwitchComponent);
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlyout,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutFooter,
|
||||
EuiPortal,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { CodeEditorField } from '../../queries/form/code_editor_field';
|
||||
import { Form, useForm, FormData, getUseField, Field, FIELD_TYPES } from '../../shared_imports';
|
||||
|
||||
const FORM_ID = 'addQueryFlyoutForm';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
interface AddQueryFlyoutProps {
|
||||
onSave: (payload: FormData) => Promise<void>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const AddQueryFlyoutComponent: React.FC<AddQueryFlyoutProps> = ({ onSave, onClose }) => {
|
||||
const { form } = useForm({
|
||||
id: FORM_ID,
|
||||
// @ts-expect-error update types
|
||||
onSubmit: (payload, isValid) => {
|
||||
if (isValid) {
|
||||
onSave(payload);
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
schema: {
|
||||
id: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.idFieldLabel', {
|
||||
defaultMessage: 'ID',
|
||||
}),
|
||||
},
|
||||
query: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.queryFieldLabel', {
|
||||
defaultMessage: 'Query',
|
||||
}),
|
||||
},
|
||||
interval: {
|
||||
type: FIELD_TYPES.NUMBER,
|
||||
label: i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.intervalFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Interval (s)',
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { submit } = form;
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiFlyout size="s" ownFocus onClose={onClose} aria-labelledby="flyoutTitle">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h2 id="flyoutTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduleQueryGroup.queryFlyoutForm.addFormTitle"
|
||||
defaultMessage="Attach next query"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<Form form={form}>
|
||||
<CommonUseField path="id" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="query" component={CodeEditorField} />
|
||||
<EuiSpacer />
|
||||
{
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
<CommonUseField path="interval" euiFieldProps={{ append: 's' }} />
|
||||
}
|
||||
</Form>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryGroup.queryFlyoutForm.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton onClick={submit} fill>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryGroup.queryFlyoutForm.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddQueryFlyout = React.memo(AddQueryFlyoutComponent);
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiCallOut, EuiConfirmModal, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { AgentPolicy } from '../../../../fleet/common';
|
||||
|
||||
interface ConfirmDeployAgentPolicyModalProps {
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
agentCount: number;
|
||||
agentPolicy: AgentPolicy;
|
||||
}
|
||||
|
||||
const ConfirmDeployAgentPolicyModalComponent: React.FC<ConfirmDeployAgentPolicyModalProps> = ({
|
||||
onConfirm,
|
||||
onCancel,
|
||||
agentCount,
|
||||
agentPolicy,
|
||||
}) => (
|
||||
<EuiConfirmModal
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.agentPolicy.confirmModalTitle"
|
||||
defaultMessage="Save and deploy changes"
|
||||
/>
|
||||
}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
cancelButtonText={
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.agentPolicy.confirmModalCancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
}
|
||||
confirmButtonText={
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.agentPolicy.confirmModalConfirmButtonLabel"
|
||||
defaultMessage="Save and deploy changes"
|
||||
/>
|
||||
}
|
||||
buttonColor="primary"
|
||||
>
|
||||
<EuiCallOut
|
||||
iconType="iInCircle"
|
||||
title={i18n.translate('xpack.osquery.agentPolicy.confirmModalCalloutTitle', {
|
||||
defaultMessage:
|
||||
'This action will update {agentCount, plural, one {# agent} other {# agents}}',
|
||||
values: {
|
||||
agentCount,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div className="eui-textBreakWord">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.agentPolicy.confirmModalCalloutDescription"
|
||||
defaultMessage="Fleet has detected that the selected agent policy, {policyName}, is already in use by
|
||||
some of your agents. As a result of this action, Fleet will deploy updates to all agents
|
||||
that use this policy."
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
values={{
|
||||
policyName: <b>{agentPolicy.name}</b>,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="l" />
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.agentPolicy.confirmModalDescription"
|
||||
defaultMessage="This action can not be undone. Are you sure you wish to continue?"
|
||||
/>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
|
||||
export const ConfirmDeployAgentPolicyModal = React.memo(ConfirmDeployAgentPolicyModalComponent);
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlyout,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutFooter,
|
||||
EuiPortal,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { PackagePolicyInputStream } from '../../../../fleet/common';
|
||||
import { CodeEditorField } from '../../queries/form/code_editor_field';
|
||||
import { Form, useForm, getUseField, Field, FIELD_TYPES } from '../../shared_imports';
|
||||
|
||||
const FORM_ID = 'editQueryFlyoutForm';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
interface EditQueryFlyoutProps {
|
||||
defaultValue: PackagePolicyInputStream;
|
||||
onSave: (payload: FormData) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const EditQueryFlyout: React.FC<EditQueryFlyoutProps> = ({
|
||||
defaultValue,
|
||||
onSave,
|
||||
onClose,
|
||||
}) => {
|
||||
const { form } = useForm({
|
||||
id: FORM_ID,
|
||||
// @ts-expect-error update types
|
||||
onSubmit: (payload, isValid) => {
|
||||
if (isValid) {
|
||||
// @ts-expect-error update types
|
||||
onSave(payload);
|
||||
onClose();
|
||||
}
|
||||
return;
|
||||
},
|
||||
defaultValue,
|
||||
deserializer: (payload) => ({
|
||||
id: payload.vars.id.value,
|
||||
query: payload.vars.query.value,
|
||||
interval: payload.vars.interval.value,
|
||||
}),
|
||||
schema: {
|
||||
id: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.idFieldLabel', {
|
||||
defaultMessage: 'ID',
|
||||
}),
|
||||
},
|
||||
query: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.queryFieldLabel', {
|
||||
defaultMessage: 'Query',
|
||||
}),
|
||||
},
|
||||
interval: {
|
||||
type: FIELD_TYPES.NUMBER,
|
||||
label: i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.intervalFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Interval (s)',
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { submit } = form;
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiFlyout size="s" ownFocus onClose={onClose} aria-labelledby="flyoutTitle">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h2 id="flyoutTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduleQueryGroup.queryFlyoutForm.editFormTitle"
|
||||
defaultMessage="Edit query"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<Form form={form}>
|
||||
<CommonUseField path="id" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="query" component={CodeEditorField} />
|
||||
<EuiSpacer />
|
||||
{
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
<CommonUseField path="interval" euiFieldProps={{ append: 's' }} />
|
||||
}
|
||||
</Form>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryGroup.queryFlyoutForm.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton onClick={submit} fill>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryGroup.queryFlyoutForm.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue