[Asset Management] Add live query history table (#94536)

This commit is contained in:
Patryk Kopyciński 2021-04-19 20:10:34 +02:00 committed by GitHub
parent 283e2ca798
commit 64f30a224e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
123 changed files with 4274 additions and 3089 deletions

View file

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

View file

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

View file

@ -20,6 +20,8 @@
"actions",
"data",
"dataEnhanced",
"discover",
"features",
"fleet",
"navigation",
"triggersActionsUi"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -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> : ''}
&nbsp;
<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>
);
};

View file

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

View file

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

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
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`,
};

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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 {
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