mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Asset Management] Agent picker follow up (#97357)
This commit is contained in:
parent
e321f57f64
commit
e0da8b2e96
10 changed files with 344 additions and 158 deletions
118
x-pack/plugins/osquery/public/agents/agent_grouper.ts
Normal file
118
x-pack/plugins/osquery/public/agents/agent_grouper.ts
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 { Agent } from '../../common/shared_imports';
|
||||
import { generateColorPicker } from './helpers';
|
||||
import {
|
||||
ALL_AGENTS_LABEL,
|
||||
AGENT_PLATFORMS_LABEL,
|
||||
AGENT_POLICY_LABEL,
|
||||
AGENT_SELECTION_LABEL,
|
||||
} from './translations';
|
||||
import { AGENT_GROUP_KEY, Group, GroupOption } from './types';
|
||||
|
||||
const getColor = generateColorPicker();
|
||||
|
||||
const generateGroup = <T = Group>(label: string, groupType: AGENT_GROUP_KEY) => {
|
||||
return {
|
||||
label,
|
||||
groupType,
|
||||
color: getColor(groupType),
|
||||
size: 0,
|
||||
data: [] as T[],
|
||||
};
|
||||
};
|
||||
|
||||
export class AgentGrouper {
|
||||
groupOrder = [
|
||||
AGENT_GROUP_KEY.All,
|
||||
AGENT_GROUP_KEY.Platform,
|
||||
AGENT_GROUP_KEY.Policy,
|
||||
AGENT_GROUP_KEY.Agent,
|
||||
];
|
||||
groups = {
|
||||
[AGENT_GROUP_KEY.All]: generateGroup(ALL_AGENTS_LABEL, AGENT_GROUP_KEY.All),
|
||||
[AGENT_GROUP_KEY.Platform]: generateGroup(AGENT_PLATFORMS_LABEL, AGENT_GROUP_KEY.Platform),
|
||||
[AGENT_GROUP_KEY.Policy]: generateGroup(AGENT_POLICY_LABEL, AGENT_GROUP_KEY.Policy),
|
||||
[AGENT_GROUP_KEY.Agent]: generateGroup<Agent>(AGENT_SELECTION_LABEL, AGENT_GROUP_KEY.Agent),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
updateGroup(key: AGENT_GROUP_KEY, data: any[], append = false) {
|
||||
if (!data?.length) {
|
||||
return;
|
||||
}
|
||||
const group = this.groups[key];
|
||||
if (append) {
|
||||
group.data.push(...data);
|
||||
} else {
|
||||
group.data = data;
|
||||
}
|
||||
group.size = data.length;
|
||||
}
|
||||
|
||||
setTotalAgents(total: number): void {
|
||||
this.groups[AGENT_GROUP_KEY.All].size = total;
|
||||
}
|
||||
|
||||
generateOptions(): GroupOption[] {
|
||||
const opts: GroupOption[] = [];
|
||||
for (const key of this.groupOrder) {
|
||||
const { label, size, groupType, data, color } = this.groups[key];
|
||||
if (size === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case AGENT_GROUP_KEY.All:
|
||||
opts.push({
|
||||
label,
|
||||
options: [
|
||||
{
|
||||
label,
|
||||
value: { groupType, size },
|
||||
color,
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
case AGENT_GROUP_KEY.Platform:
|
||||
case AGENT_GROUP_KEY.Policy:
|
||||
opts.push({
|
||||
label,
|
||||
options: (data as Group[]).map(({ name, id, size: groupSize }) => ({
|
||||
label: name !== id ? `${name} (${id})` : name,
|
||||
key: id,
|
||||
color: getColor(groupType),
|
||||
value: { groupType, id, size: groupSize },
|
||||
})),
|
||||
});
|
||||
break;
|
||||
case AGENT_GROUP_KEY.Agent:
|
||||
opts.push({
|
||||
label,
|
||||
options: (data as Agent[]).map((agent: Agent) => ({
|
||||
label: `${agent.local_metadata.host.hostname} (${agent.local_metadata.elastic.agent.id})`,
|
||||
key: agent.local_metadata.elastic.agent.id,
|
||||
color,
|
||||
value: {
|
||||
groupType,
|
||||
groups: {
|
||||
policy: agent.policy_id ?? '',
|
||||
platform: agent.local_metadata.os.platform,
|
||||
},
|
||||
id: agent.local_metadata.elastic.agent.id,
|
||||
online: agent.active,
|
||||
},
|
||||
})),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
}
|
|
@ -5,179 +5,98 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiHealth, EuiHighlight } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { EuiComboBox, EuiHealth, EuiHighlight } from '@elastic/eui';
|
||||
|
||||
import { useDebounce } from 'react-use';
|
||||
import { useAllAgents } from './use_all_agents';
|
||||
import { useAgentGroups } from './use_agent_groups';
|
||||
import { useOsqueryPolicies } from './use_osquery_policies';
|
||||
import { Agent } from '../../common/shared_imports';
|
||||
import { AgentGrouper } from './agent_grouper';
|
||||
import {
|
||||
getNumAgentsInGrouping,
|
||||
generateAgentCheck,
|
||||
getNumOverlapped,
|
||||
generateColorPicker,
|
||||
generateAgentSelection,
|
||||
} from './helpers';
|
||||
|
||||
import { SELECT_AGENT_LABEL, generateSelectedAgentsMessage } from './translations';
|
||||
|
||||
import {
|
||||
ALL_AGENTS_LABEL,
|
||||
AGENT_PLATFORMS_LABEL,
|
||||
AGENT_POLICY_LABEL,
|
||||
SELECT_AGENT_LABEL,
|
||||
AGENT_SELECTION_LABEL,
|
||||
generateSelectedAgentsMessage,
|
||||
} from './translations';
|
||||
|
||||
import { AGENT_GROUP_KEY, SelectedGroups, AgentOptionValue, GroupOptionValue } from './types';
|
||||
|
||||
export interface AgentsSelection {
|
||||
agents: string[];
|
||||
allAgentsSelected: boolean;
|
||||
platformsSelected: string[];
|
||||
policiesSelected: string[];
|
||||
}
|
||||
AGENT_GROUP_KEY,
|
||||
SelectedGroups,
|
||||
AgentOptionValue,
|
||||
GroupOption,
|
||||
AgentSelection,
|
||||
} from './types';
|
||||
|
||||
interface AgentsTableProps {
|
||||
agentSelection: AgentsSelection;
|
||||
onChange: (payload: AgentsSelection) => void;
|
||||
agentSelection: AgentSelection;
|
||||
onChange: (payload: AgentSelection) => void;
|
||||
}
|
||||
|
||||
type GroupOption = EuiComboBoxOptionOption<AgentOptionValue | GroupOptionValue>;
|
||||
|
||||
const getColor = generateColorPicker();
|
||||
const perPage = 10;
|
||||
const DEBOUNCE_DELAY = 100; // ms
|
||||
|
||||
const AgentsTableComponent: React.FC<AgentsTableProps> = ({ onChange }) => {
|
||||
// search related
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [modifyingSearch, setModifyingSearch] = useState<boolean>(false);
|
||||
const [debouncedSearchValue, setDebouncedSearchValue] = useState<string>('');
|
||||
useDebounce(
|
||||
() => {
|
||||
// update the real search value, set the typing flag
|
||||
setDebouncedSearchValue(searchValue);
|
||||
setModifyingSearch(false);
|
||||
},
|
||||
DEBOUNCE_DELAY,
|
||||
[searchValue]
|
||||
);
|
||||
|
||||
// grouping related
|
||||
const osqueryPolicyData = useOsqueryPolicies();
|
||||
const { loading: groupsLoading, totalCount: totalNumAgents, groups } = useAgentGroups(
|
||||
osqueryPolicyData
|
||||
);
|
||||
const { agents } = useAllAgents(osqueryPolicyData);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const grouper = useMemo(() => new AgentGrouper(), []);
|
||||
const { agentsLoading, agents } = useAllAgents(osqueryPolicyData, debouncedSearchValue, {
|
||||
perPage,
|
||||
});
|
||||
|
||||
// option related
|
||||
const [options, setOptions] = useState<GroupOption[]>([]);
|
||||
const [selectedOptions, setSelectedOptions] = useState<GroupOption[]>([]);
|
||||
const [numAgentsSelected, setNumAgentsSelected] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const allAgentsLabel = ALL_AGENTS_LABEL;
|
||||
const opts: GroupOption[] = [
|
||||
{
|
||||
label: allAgentsLabel,
|
||||
options: [
|
||||
{
|
||||
label: allAgentsLabel,
|
||||
value: { groupType: AGENT_GROUP_KEY.All, size: totalNumAgents },
|
||||
color: getColor(AGENT_GROUP_KEY.All),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
if (groups.platforms.length > 0) {
|
||||
const groupType = AGENT_GROUP_KEY.Platform;
|
||||
opts.push({
|
||||
label: AGENT_PLATFORMS_LABEL,
|
||||
options: groups.platforms.map(({ name, size }) => ({
|
||||
label: name,
|
||||
color: getColor(groupType),
|
||||
value: { groupType, size },
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (groups.policies.length > 0) {
|
||||
const groupType = AGENT_GROUP_KEY.Policy;
|
||||
opts.push({
|
||||
label: AGENT_POLICY_LABEL,
|
||||
options: groups.policies.map(({ name, size }) => ({
|
||||
label: name,
|
||||
color: getColor(groupType),
|
||||
value: { groupType, size },
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (agents && agents.length > 0) {
|
||||
const groupType = AGENT_GROUP_KEY.Agent;
|
||||
opts.push({
|
||||
label: AGENT_SELECTION_LABEL,
|
||||
options: (agents as Agent[]).map((agent: Agent) => ({
|
||||
label: agent.local_metadata.host.hostname,
|
||||
color: getColor(groupType),
|
||||
value: {
|
||||
groupType,
|
||||
groups: { policy: agent.policy_id ?? '', platform: agent.local_metadata.os.platform },
|
||||
id: agent.local_metadata.elastic.agent.id,
|
||||
online: agent.active,
|
||||
},
|
||||
})),
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
setOptions(opts);
|
||||
}, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents]);
|
||||
// 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);
|
||||
const newOptions = grouper.generateOptions();
|
||||
setOptions(newOptions);
|
||||
}, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents, grouper]);
|
||||
|
||||
const onSelection = useCallback(
|
||||
(selection: GroupOption[]) => {
|
||||
// TODO?: optimize this by making it incremental
|
||||
const newAgentSelection: AgentsSelection = {
|
||||
agents: [],
|
||||
allAgentsSelected: false,
|
||||
platformsSelected: [],
|
||||
policiesSelected: [],
|
||||
};
|
||||
// parse through the selections to be able to determine how many are actually selected
|
||||
const selectedAgents = [];
|
||||
const selectedGroups: SelectedGroups = {
|
||||
policy: {},
|
||||
platform: {},
|
||||
};
|
||||
|
||||
// TODO: clean this up, make it less awkward
|
||||
for (const opt of selection) {
|
||||
const groupType = opt.value?.groupType;
|
||||
let value;
|
||||
switch (groupType) {
|
||||
case AGENT_GROUP_KEY.All:
|
||||
newAgentSelection.allAgentsSelected = true;
|
||||
break;
|
||||
case AGENT_GROUP_KEY.Platform:
|
||||
value = opt.value as GroupOptionValue;
|
||||
if (!newAgentSelection.allAgentsSelected) {
|
||||
// we don't need to calculate diffs when all agents are selected
|
||||
selectedGroups.platform[opt.label] = value.size;
|
||||
}
|
||||
newAgentSelection.platformsSelected.push(opt.label);
|
||||
break;
|
||||
case AGENT_GROUP_KEY.Policy:
|
||||
value = opt.value as GroupOptionValue;
|
||||
if (!newAgentSelection.allAgentsSelected) {
|
||||
// we don't need to calculate diffs when all agents are selected
|
||||
selectedGroups.policy[opt.label] = value.size ?? 0;
|
||||
}
|
||||
newAgentSelection.policiesSelected.push(opt.label);
|
||||
break;
|
||||
case AGENT_GROUP_KEY.Agent:
|
||||
value = opt.value as AgentOptionValue;
|
||||
if (!newAgentSelection.allAgentsSelected) {
|
||||
// we don't need to count how many agents are selected if they are all selected
|
||||
selectedAgents.push(opt.value);
|
||||
}
|
||||
// TODO: fix this casting by updating the opt type to be a union
|
||||
newAgentSelection.agents.push(value.id as string);
|
||||
break;
|
||||
default:
|
||||
// this should never happen!
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`unknown group type ${groupType}`);
|
||||
}
|
||||
}
|
||||
// TODO?: optimize this by making the selection computation incremental
|
||||
const {
|
||||
newAgentSelection,
|
||||
selectedAgents,
|
||||
selectedGroups,
|
||||
}: {
|
||||
newAgentSelection: AgentSelection;
|
||||
selectedAgents: AgentOptionValue[];
|
||||
selectedGroups: SelectedGroups;
|
||||
} = generateAgentSelection(selection);
|
||||
if (newAgentSelection.allAgentsSelected) {
|
||||
setNumAgentsSelected(totalNumAgents);
|
||||
} else {
|
||||
const checkAgent = generateAgentCheck(selectedGroups);
|
||||
setNumAgentsSelected(
|
||||
// filter out all the agents counted by selected policies and platforms
|
||||
selectedAgents.filter((a) => checkAgent(a as AgentOptionValue)).length +
|
||||
selectedAgents.filter(checkAgent).length +
|
||||
// add the number of agents added via policy and platform groups
|
||||
getNumAgentsInGrouping(selectedGroups) -
|
||||
// subtract the number of agents double counted by policy/platform selections
|
||||
|
@ -190,32 +109,40 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ onChange }) => {
|
|||
[groups, onChange, totalNumAgents]
|
||||
);
|
||||
|
||||
const renderOption = useCallback((option, searchValue, contentClassName) => {
|
||||
const renderOption = useCallback((option, searchVal, contentClassName) => {
|
||||
const { label, value } = option;
|
||||
return value?.groupType === AGENT_GROUP_KEY.Agent ? (
|
||||
<EuiHealth color={value?.online ? 'success' : 'danger'}>
|
||||
<span className={contentClassName}>
|
||||
<EuiHighlight search={searchValue}>{label}</EuiHighlight>
|
||||
<EuiHighlight search={searchVal}>{label}</EuiHighlight>
|
||||
</span>
|
||||
</EuiHealth>
|
||||
) : (
|
||||
<span className={contentClassName}>
|
||||
<EuiHighlight search={searchValue}>{label}</EuiHighlight>
|
||||
<span>[{value?.size ?? 0}]</span>
|
||||
|
||||
<span>({value?.size})</span>
|
||||
<EuiHighlight search={searchVal}>{label}</EuiHighlight>
|
||||
</span>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const onSearchChange = useCallback((v: string) => {
|
||||
// set the typing flag and update the search value
|
||||
setModifyingSearch(v !== '');
|
||||
setSearchValue(v);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>{SELECT_AGENT_LABEL}</h2>
|
||||
{numAgentsSelected > 0 ? <span>{generateSelectedAgentsMessage(numAgentsSelected)}</span> : ''}
|
||||
|
||||
<EuiComboBox
|
||||
placeholder="Select or create options"
|
||||
isLoading={loading}
|
||||
placeholder={SELECT_AGENT_LABEL}
|
||||
isLoading={modifyingSearch || groupsLoading || agentsLoading}
|
||||
options={options}
|
||||
isClearable={true}
|
||||
fullWidth={true}
|
||||
onSearchChange={onSearchChange}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={onSelection}
|
||||
renderOption={renderOption}
|
||||
|
|
|
@ -33,6 +33,7 @@ describe('processAggregations', () => {
|
|||
const { platforms, policies, overlap } = processAggregations(input);
|
||||
expect(platforms).toEqual([
|
||||
{
|
||||
id: 'darwin',
|
||||
name: 'darwin',
|
||||
size: 200,
|
||||
},
|
||||
|
@ -59,10 +60,12 @@ describe('processAggregations', () => {
|
|||
expect(platforms).toEqual([]);
|
||||
expect(policies).toEqual([
|
||||
{
|
||||
id: '8cd01a60-8a74-11eb-86cb-c58693443a4f',
|
||||
name: '8cd01a60-8a74-11eb-86cb-c58693443a4f',
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
id: '8cd06880-8a74-11eb-86cb-c58693443a4f',
|
||||
name: '8cd06880-8a74-11eb-86cb-c58693443a4f',
|
||||
size: 100,
|
||||
},
|
||||
|
@ -107,16 +110,19 @@ describe('processAggregations', () => {
|
|||
const { platforms, policies, overlap } = processAggregations(input);
|
||||
expect(platforms).toEqual([
|
||||
{
|
||||
id: 'darwin',
|
||||
name: 'darwin',
|
||||
size: 200,
|
||||
},
|
||||
]);
|
||||
expect(policies).toEqual([
|
||||
{
|
||||
id: '8cd01a60-8a74-11eb-86cb-c58693443a4f',
|
||||
name: '8cd01a60-8a74-11eb-86cb-c58693443a4f',
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
id: '8cd06880-8a74-11eb-86cb-c58693443a4f',
|
||||
name: '8cd06880-8a74-11eb-86cb-c58693443a4f',
|
||||
size: 100,
|
||||
},
|
||||
|
|
|
@ -20,6 +20,9 @@ import {
|
|||
Group,
|
||||
AgentOptionValue,
|
||||
AggregationDataPoint,
|
||||
AgentSelection,
|
||||
GroupOptionValue,
|
||||
GroupOption,
|
||||
} from './types';
|
||||
|
||||
export type InspectResponse = Inspect & { response: string[] };
|
||||
|
@ -43,11 +46,12 @@ export const processAggregations = (aggs: Record<string, Aggregate>) => {
|
|||
const platformTerms = aggs.platforms as TermsAggregate<AggregationDataPoint>;
|
||||
const policyTerms = aggs.policies as TermsAggregate<AggregationDataPoint>;
|
||||
|
||||
const policies = policyTerms?.buckets.map((o) => ({ name: o.key, size: o.doc_count })) ?? [];
|
||||
const policies =
|
||||
policyTerms?.buckets.map((o) => ({ name: o.key, id: o.key, size: o.doc_count })) ?? [];
|
||||
|
||||
if (platformTerms?.buckets) {
|
||||
for (const { key, doc_count: size, policies: platformPolicies } of platformTerms.buckets) {
|
||||
platforms.push({ name: key, size });
|
||||
platforms.push({ name: key, id: key, size });
|
||||
if (platformPolicies?.buckets && policies.length > 0) {
|
||||
overlap[key] = platformPolicies.buckets.reduce((acc: { [key: string]: number }, pol) => {
|
||||
acc[pol.key] = pol.doc_count;
|
||||
|
@ -96,6 +100,63 @@ export const generateAgentCheck = (selectedGroups: SelectedGroups) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const generateAgentSelection = (selection: GroupOption[]) => {
|
||||
const newAgentSelection: AgentSelection = {
|
||||
agents: [],
|
||||
allAgentsSelected: false,
|
||||
platformsSelected: [],
|
||||
policiesSelected: [],
|
||||
};
|
||||
// parse through the selections to be able to determine how many are actually selected
|
||||
const selectedAgents: AgentOptionValue[] = [];
|
||||
const selectedGroups: SelectedGroups = {
|
||||
policy: {},
|
||||
platform: {},
|
||||
};
|
||||
|
||||
// TODO: clean this up, make it less awkward
|
||||
for (const opt of selection) {
|
||||
const groupType = opt.value?.groupType;
|
||||
let value;
|
||||
switch (groupType) {
|
||||
case AGENT_GROUP_KEY.All:
|
||||
newAgentSelection.allAgentsSelected = true;
|
||||
break;
|
||||
case AGENT_GROUP_KEY.Platform:
|
||||
value = opt.value as GroupOptionValue;
|
||||
if (!newAgentSelection.allAgentsSelected) {
|
||||
// we don't need to calculate diffs when all agents are selected
|
||||
selectedGroups.platform[opt.value?.id ?? opt.label] = value.size;
|
||||
}
|
||||
newAgentSelection.platformsSelected.push(opt.label);
|
||||
break;
|
||||
case AGENT_GROUP_KEY.Policy:
|
||||
value = opt.value as GroupOptionValue;
|
||||
if (!newAgentSelection.allAgentsSelected) {
|
||||
// we don't need to calculate diffs when all agents are selected
|
||||
selectedGroups.policy[opt.value?.id ?? opt.label] = value.size;
|
||||
}
|
||||
newAgentSelection.policiesSelected.push(opt.label);
|
||||
break;
|
||||
case AGENT_GROUP_KEY.Agent:
|
||||
value = opt.value as AgentOptionValue;
|
||||
if (!newAgentSelection.allAgentsSelected) {
|
||||
// we don't need to count how many agents are selected if they are all selected
|
||||
selectedAgents.push(value);
|
||||
}
|
||||
if (value?.id) {
|
||||
newAgentSelection.agents.push(value.id);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// this should never happen!
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`unknown group type ${groupType}`);
|
||||
}
|
||||
}
|
||||
return { newAgentSelection, selectedGroups, selectedAgents };
|
||||
};
|
||||
|
||||
export const generateTablePaginationOptions = (
|
||||
activePage: number,
|
||||
limit: number,
|
||||
|
|
|
@ -40,7 +40,7 @@ export const AGENT_SELECTION_LABEL = i18n.translate('xpack.osquery.agents.select
|
|||
});
|
||||
|
||||
export const SELECT_AGENT_LABEL = i18n.translate('xpack.osquery.agents.selectAgentLabel', {
|
||||
defaultMessage: `Select Agents`,
|
||||
defaultMessage: `Select agents or groups`,
|
||||
});
|
||||
|
||||
export const ERROR_ALL_AGENTS = i18n.translate('xpack.osquery.agents.errorSearchDescription', {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { TermsAggregate } from '@elastic/elasticsearch/api/types';
|
||||
import { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
interface BaseDataPoint {
|
||||
key: string;
|
||||
|
@ -17,6 +18,7 @@ export type AggregationDataPoint = BaseDataPoint & {
|
|||
};
|
||||
|
||||
export interface Group {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
}
|
||||
|
@ -28,14 +30,23 @@ export interface SelectedGroups {
|
|||
[groupType: string]: { [groupName: string]: number };
|
||||
}
|
||||
|
||||
export type GroupOption = EuiComboBoxOptionOption<AgentOptionValue | GroupOptionValue>;
|
||||
|
||||
export interface AgentSelection {
|
||||
agents: string[];
|
||||
allAgentsSelected: boolean;
|
||||
platformsSelected: string[];
|
||||
policiesSelected: string[];
|
||||
}
|
||||
|
||||
interface BaseGroupOption {
|
||||
id?: string;
|
||||
groupType: AGENT_GROUP_KEY;
|
||||
}
|
||||
|
||||
export type AgentOptionValue = BaseGroupOption & {
|
||||
groups: { [groupType: string]: string };
|
||||
online: boolean;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type GroupOptionValue = BaseGroupOption & {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { useAgentPolicies } from './use_agent_policies';
|
||||
|
||||
import {
|
||||
OsqueryQueries,
|
||||
|
@ -25,6 +26,7 @@ interface UseAgentGroups {
|
|||
export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseAgentGroups) => {
|
||||
const { data } = useKibana().services;
|
||||
|
||||
const { agentPoliciesLoading, agentPolicyById } = useAgentPolicies(osqueryPolicies);
|
||||
const [platforms, setPlatforms] = useState<Group[]>([]);
|
||||
const [policies, setPolicies] = useState<Group[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
@ -78,14 +80,22 @@ export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseA
|
|||
|
||||
setPlatforms(newPlatforms);
|
||||
setOverlap(newOverlap);
|
||||
setPolicies(newPolicies);
|
||||
setPolicies(
|
||||
newPolicies.map((p) => {
|
||||
const name = agentPolicyById[p.id]?.name ?? p.name;
|
||||
return {
|
||||
...p,
|
||||
name,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
setTotalCount(responseData.totalCount);
|
||||
},
|
||||
{
|
||||
enabled: !osqueryPoliciesLoading,
|
||||
enabled: !osqueryPoliciesLoading && !agentPoliciesLoading,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
38
x-pack/plugins/osquery/public/agents/use_agent_policies.ts
Normal file
38
x-pack/plugins/osquery/public/agents/use_agent_policies.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { useQueries, UseQueryResult } from 'react-query';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import {
|
||||
AgentPolicy,
|
||||
agentPolicyRouteService,
|
||||
GetOneAgentPolicyResponse,
|
||||
} from '../../../fleet/common';
|
||||
|
||||
export const useAgentPolicies = (policyIds: string[] = []) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const agentResponse = useQueries(
|
||||
policyIds.map((policyId) => ({
|
||||
queryKey: ['agentPolicy', policyId],
|
||||
queryFn: () => http.get(agentPolicyRouteService.getInfoPath(policyId)),
|
||||
enabled: policyIds.length > 0,
|
||||
}))
|
||||
) as Array<UseQueryResult<GetOneAgentPolicyResponse>>;
|
||||
|
||||
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 });
|
||||
|
||||
return { agentPoliciesLoading, agentPolicies, agentPolicyById };
|
||||
};
|
|
@ -14,16 +14,30 @@ interface UseAllAgents {
|
|||
osqueryPoliciesLoading: boolean;
|
||||
}
|
||||
|
||||
export const useAllAgents = ({ osqueryPolicies, osqueryPoliciesLoading }: UseAllAgents) => {
|
||||
// TODO: properly fetch these in an async manner
|
||||
interface RequestOptions {
|
||||
perPage?: number;
|
||||
page?: number;
|
||||
}
|
||||
|
||||
// TODO: break out the paginated vs all cases into separate hooks
|
||||
export const useAllAgents = (
|
||||
{ osqueryPolicies, osqueryPoliciesLoading }: UseAllAgents,
|
||||
searchValue = '',
|
||||
opts: RequestOptions = { perPage: 9000 }
|
||||
) => {
|
||||
const { perPage } = opts;
|
||||
const { http } = useKibana().services;
|
||||
const { isLoading: agentsLoading, data: agentData } = useQuery(
|
||||
['agents', osqueryPolicies],
|
||||
['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', {
|
||||
query: {
|
||||
kuery: osqueryPolicies.map((p) => `policy_id:${p}`).join(' or '),
|
||||
perPage: 9000,
|
||||
kuery,
|
||||
perPage,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
|
||||
import React, { useCallback } from 'react';
|
||||
import { FieldHook } from '../../shared_imports';
|
||||
import { AgentsTable, AgentsSelection } from '../../agents/agents_table';
|
||||
import { AgentsTable } from '../../agents/agents_table';
|
||||
import { AgentSelection } from '../../agents/types';
|
||||
|
||||
interface AgentsTableFieldProps {
|
||||
field: FieldHook<AgentsSelection>;
|
||||
field: FieldHook<AgentSelection>;
|
||||
}
|
||||
|
||||
const AgentsTableFieldComponent: React.FC<AgentsTableFieldProps> = ({ field }) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue