Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,20 @@ import { Inspect, Maybe, PageInfoPaginated } from '../../common';
import { RequestOptionsPaginated } from '../..';
import { Agent } from '../../../shared_imports';

export interface AggregationDataPoint {
key: string;
}

export interface AgentAggregation {
[key: string]: {
buckets: AggregationDataPoint[];
};
}

export interface AgentsStrategyResponse extends IEsSearchResponse {
edges: Agent[];
totalCount: number;
aggregations?: AgentAggregation;
pageInfo: PageInfoPaginated;
inspect?: Maybe<Inspect>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type FactoryQueryTypes = OsqueryQueries;

export interface RequestBasicOptions extends IEsSearchRequest {
filterQuery: ESQuery | string | undefined;
aggregations?: { [key: string]: string };
docValueFields?: DocValueFields[];
factoryQueryType?: FactoryQueryTypes;
}
Expand Down
215 changes: 184 additions & 31 deletions x-pack/plugins/osquery/public/agents/agents_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,56 @@
*/

import { find } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import React, { Fragment, useCallback, useEffect, useMemo, useState, useRef } from 'react';
import {
EuiBasicTable,
EuiBasicTableColumn,
EuiBasicTableProps,
EuiTableSelectionType,
EuiModal,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalBody,
EuiModalFooter,
EuiOverlayMask,
EuiSelectable,
EuiSelectableOption,
EuiButton,
EuiButtonEmpty,
EuiHealth,
} from '@elastic/eui';

import { useAllAgents } from './use_all_agents';
import { useAgentGroups, ALL_AGENTS_GROUP_KEY } from './use_agent_groups';
import { Direction } from '../../common/search_strategy';
import { Agent } from '../../common/shared_imports';

export interface AgentsSelection {
agents: string[];
allAgentsSelected: boolean;
platformsSelected: string[];
policiesSelected: string[];
}

interface AgentsTableProps {
selectedAgents: string[];
onChange: (payload: string[]) => void;
agentSelection: AgentsSelection;
onChange: (payload: AgentsSelection) => void;
}

const AgentsTableComponent: React.FC<AgentsTableProps> = ({ selectedAgents, onChange }) => {
type GroupOption = EuiSelectableOption<{ type: string }>;

const AgentsTableComponent: React.FC<AgentsTableProps> = ({ agentSelection, onChange }) => {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(5);
const [sortField, setSortField] = useState<keyof Agent>('upgraded_at');
const [sortDirection, setSortDirection] = useState<Direction>(Direction.asc);
const [selectedItems, setSelectedItems] = useState([]);
const tableRef = useRef<EuiBasicTable<Agent>>(null);

const [isModalVisible, setIsModalVisible] = useState(false);
const closeModal = useCallback(() => setIsModalVisible(false), [setIsModalVisible]);
const showModal = useCallback(() => setIsModalVisible(true), [setIsModalVisible]);

const onTableChange: EuiBasicTableProps<Agent>['onChange'] = useCallback(
({ page = {}, sort = {} }) => {
const { index: newPageIndex, size: newPageSize } = page;
Expand All @@ -46,31 +70,87 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ selectedAgents, onCh
[]
);

const onSelectionChange: EuiTableSelectionType<{}>['onSelectionChange'] = useCallback(
(newSelectedItems) => {
setSelectedItems(newSelectedItems);

if (onChange) {
// @ts-expect-error update types
onChange(newSelectedItems.map((item) => item._id));
}
},
[onChange]
);

const renderStatus = (online: string) => {
const color = online ? 'success' : 'danger';
const label = online ? 'Online' : 'Offline';
return <EuiHealth color={color}>{label}</EuiHealth>;
};

const { loading: groupsLoading, groups } = useAgentGroups();
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
const [allAgentsSelected, setAllAgentsSelected] = useState<boolean>(false);
const [groupOptions, setGroupOptions] = useState<GroupOption[]>([]);
useEffect(() => {
const opts: GroupOption[] = [{ label: ALL_AGENTS_GROUP_KEY, type: 'all' }];
if (!allAgentsSelected) {
const selectedSet = new Set<string | undefined>(selectedGroups);
const generateOption = (type: string) => (name: string) => {
const option: GroupOption = { label: name, type };
if (selectedSet.has(name)) {
option.checked = 'on';
}
return option;
};
const platformOptions = groups.platforms.map(generateOption('platform'));
opts.push(...platformOptions);
const policyOptions = groups.policies.map(generateOption('policy'));
opts.push(...policyOptions);
} else {
opts[0].checked = 'on';
}
setGroupOptions(opts);
}, [groups.policies, groups.platforms, selectedGroups, allAgentsSelected]);

const onGroupChange = useCallback(
(newOptions: GroupOption[]) => {
const selectedPlatforms: string[] = [];
const selectedPolicies: string[] = [];
newOptions.forEach((opt) => {
if (opt.checked === 'on') {
switch (opt.type) {
case 'platform':
selectedPlatforms.push(opt.label);
break;
case 'policy':
selectedPolicies.push(opt.label);
break;
default:
break;
}
}
});
const selected = newOptions.filter((el) => el.checked === 'on').map((el) => el.label);
setSelectedGroups(selected);
const allSelected = selected.some((el) => el === ALL_AGENTS_GROUP_KEY);
setAllAgentsSelected(allSelected);
onChange({
...agentSelection,
allAgentsSelected: allSelected,
platformsSelected: selectedPlatforms,
policiesSelected: selectedPolicies,
});
},
[onChange, agentSelection]
);

const { data = {} } = useAllAgents({
activePage: pageIndex,
limit: pageSize,
direction: sortDirection,
sortField,
});

const onSelectionChange: EuiTableSelectionType<{}>['onSelectionChange'] = useCallback(
(newSelectedItems) => {
setSelectedItems(newSelectedItems);
onChange({
...agentSelection,
agents: newSelectedItems.map((item: { _id: string }) => item._id),
});
},
[onChange, agentSelection]
);

const columns: Array<EuiBasicTableColumn<{}>> = useMemo(
() => [
{
Expand All @@ -79,6 +159,12 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ selectedAgents, onCh
sortable: true,
truncateText: true,
},
{
field: 'local_metadata.os.family',
name: 'platform',
sortable: true,
truncateText: true,
},
{
field: 'local_metadata.host.name',
name: 'hostname',
Expand All @@ -94,6 +180,12 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ selectedAgents, onCh
],
[]
);
const searchProps = useMemo(
() => ({
'data-test-subj': 'selectableSearchHere',
}),
[]
);

const pagination = useMemo(
() => ({
Expand Down Expand Up @@ -128,6 +220,7 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ selectedAgents, onCh
);

useEffect(() => {
const selectedAgents = agentSelection?.agents;
if (
selectedAgents?.length &&
// @ts-expect-error update types
Expand All @@ -140,23 +233,83 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ selectedAgents, onCh
);
}
// @ts-expect-error update types
}, [selectedAgents, data.agents, selectedItems.length]);
}, [agentSelection, data.agents, selectedItems.length]);

let modal;

if (isModalVisible) {
modal = (
<EuiOverlayMask>
<EuiModal onClose={closeModal} initialFocus="[name=popswitch]">
<EuiModalHeader>
<EuiModalHeaderTitle>Select Agents</EuiModalHeaderTitle>
</EuiModalHeader>

<EuiModalBody>
{groupsLoading ? null : (
<EuiSelectable
aria-label="Searchable example"
searchable
searchProps={searchProps}
options={groupOptions}
onChange={onGroupChange}
>
{(list, search) => (
<Fragment>
{search}
{list}
</Fragment>
)}
</EuiSelectable>
)}
{allAgentsSelected || selectedGroups?.length ? null : (
<EuiBasicTable<Agent>
ref={tableRef}
// @ts-expect-error update types
// eslint-disable-next-line react-perf/jsx-no-new-array-as-prop
items={data.agents ?? []}
itemId="_id"
columns={columns}
pagination={pagination}
sorting={sorting}
isSelectable={true}
selection={selection}
onChange={onTableChange}
rowHeader="firstName"
/>
)}
</EuiModalBody>

<EuiModalFooter>
<EuiButtonEmpty onClick={closeModal}>Cancel</EuiButtonEmpty>

<EuiButton onClick={closeModal} fill>
Save
</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
);
}

let buttonText;
if (allAgentsSelected) {
buttonText = 'All Agents Selected';
} else if (selectedGroups.length) {
const numGroups = selectedGroups.length;
buttonText = `${numGroups} Agent Group${numGroups > 1 ? 's' : ''} Selected`;
} else if (agentSelection?.agents?.length) {
const numAgents = agentSelection.agents.length;
buttonText = `${numAgents} Agent${numAgents > 1 ? 's' : ''} Selected`;
} else {
buttonText = 'Select Agents';
}

return (
<EuiBasicTable<Agent>
ref={tableRef}
// @ts-expect-error update types
// eslint-disable-next-line react-perf/jsx-no-new-array-as-prop
items={data.agents ?? []}
itemId="_id"
columns={columns}
pagination={pagination}
sorting={sorting}
isSelectable={true}
selection={selection}
onChange={onTableChange}
rowHeader="firstName"
/>
<div>
<EuiButton onClick={showModal}>{buttonText}</EuiButton>
{modal}
</div>
);
};

Expand Down
8 changes: 8 additions & 0 deletions x-pack/plugins/osquery/public/agents/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ export const ERROR_ALL_AGENTS = i18n.translate('xpack.osquery.agents.errorSearch
export const FAIL_ALL_AGENTS = i18n.translate('xpack.osquery.agents.failSearchDescription', {
defaultMessage: `Failed to fetch agents`,
});

export const ERROR_AGENT_GROUPS = i18n.translate('xpack.osquery.agents.errorSearchDescription', {
defaultMessage: `An error has occurred while fetching agent groups.`,
});

export const FAIL_AGENT_GROUPS = i18n.translate('xpack.osquery.agents.errorSearchDescription', {
defaultMessage: `An error has occurred while fetching agent groups.`,
});
Loading