Skip to content

Commit

Permalink
axed janky infinite scrolling, fixed agent fetching, added search on id
Browse files Browse the repository at this point in the history
  • Loading branch information
lykkin committed Apr 16, 2021
1 parent 5f14876 commit 8cf3186
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 115 deletions.
5 changes: 3 additions & 2 deletions x-pack/plugins/osquery/public/agents/agent_grouper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ export class AgentGrouper {
opts.push({
label,
options: (data as Group[]).map(({ name, id, size: groupSize }) => ({
label: name,
label: name !== id ? `${name} (${id})` : name,
key: id,
color: getColor(groupType),
value: { groupType, id, size: groupSize },
})),
Expand All @@ -95,7 +96,7 @@ export class AgentGrouper {
opts.push({
label,
options: (data as Agent[]).map((agent: Agent) => ({
label: agent.local_metadata.host.hostname,
label: `${agent.local_metadata.host.hostname} (${agent.local_metadata.elastic.agent.id})`,
key: agent.local_metadata.elastic.agent.id,
color,
value: {
Expand Down
140 changes: 38 additions & 102 deletions x-pack/plugins/osquery/public/agents/agents_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,46 +13,43 @@ import { useAllAgents } from './use_all_agents';
import { useAgentGroups } from './use_agent_groups';
import { useOsqueryPolicies } from './use_osquery_policies';
import { AgentGrouper } from './agent_grouper';
import { getNumAgentsInGrouping, generateAgentCheck, getNumOverlapped } from './helpers';
import {
getNumAgentsInGrouping,
generateAgentCheck,
getNumOverlapped,
generateAgentSelection,
} from './helpers';

import { SELECT_AGENT_LABEL, generateSelectedAgentsMessage } from './translations';

import {
AGENT_GROUP_KEY,
SelectedGroups,
AgentOptionValue,
GroupOptionValue,
GroupOption,
AgentSelection,
} from './types';

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

interface AgentsTableProps {
agentSelection: AgentsSelection;
onChange: (payload: AgentsSelection) => void;
agentSelection: AgentSelection;
onChange: (payload: AgentSelection) => void;
}

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 [page, setPage] = useState<number>(1);
const [debouncedSearchValue, setDebouncedSearchValue] = useState<string>('');
useDebounce(
() => {
// reset the page, update the real search value, set the typing flag
setPage(1);
// update the real search value, set the typing flag
setDebouncedSearchValue(searchValue);
setModifyingSearch(false);
},
100,
DEBOUNCE_DELAY,
[searchValue]
);

Expand All @@ -64,12 +61,10 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ onChange }) => {
const grouper = useMemo(() => new AgentGrouper(), []);
const { agentsLoading, agents } = useAllAgents(osqueryPolicyData, debouncedSearchValue, {
perPage,
page,
});

// option related
const [options, setOptions] = useState<GroupOption[]>([]);
const [lastLabel, setLastLabel] = useState<string>('');
const [selectedOptions, setSelectedOptions] = useState<GroupOption[]>([]);
const [numAgentsSelected, setNumAgentsSelected] = useState<number>(0);

Expand All @@ -78,73 +73,23 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ onChange }) => {
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, page > 1);
grouper.updateGroup(AGENT_GROUP_KEY.Agent, agents);
const newOptions = grouper.generateOptions();
setOptions(newOptions);
if (newOptions.length) {
const lastGroup = newOptions[newOptions.length - 1].options;
if (lastGroup?.length) {
setLastLabel(lastGroup[lastGroup.length - 1].label);
}
}
}, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents, page, grouper]);
}, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents, grouper]);

const onSelection = useCallback(
(selection: GroupOption[]) => {
// TODO?: optimize this by making the selection computation 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: 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}`);
}
}
const {
newAgentSelection,
selectedAgents,
selectedGroups,
}: {
newAgentSelection: AgentSelection;
selectedAgents: AgentOptionValue[];
selectedGroups: SelectedGroups;
} = generateAgentSelection(selection);
if (newAgentSelection.allAgentsSelected) {
setNumAgentsSelected(totalNumAgents);
} else {
Expand All @@ -164,36 +109,26 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ onChange }) => {
[groups, onChange, totalNumAgents]
);

const renderOption = useCallback(
(option, searchVal, contentClassName) => {
const { label, value, key } = option;
if (label === lastLabel) {
setPage((p) => p + 1);
}
return value?.groupType === AGENT_GROUP_KEY.Agent ? (
<EuiHealth color={value?.online ? 'success' : 'danger'}>
<span className={contentClassName}>
<EuiHighlight search={searchVal}>{label}</EuiHighlight>
&nbsp;
<span>({key})</span>
</span>
</EuiHealth>
) : (
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}>
<span>[{value?.size}]</span>
&nbsp;
<EuiHighlight search={searchVal}>{label}</EuiHighlight>
&nbsp;
{value?.id && label !== value?.id && <span>({value?.id})</span>}
</span>
);
},
[lastLabel]
);
</EuiHealth>
) : (
<span className={contentClassName}>
<span>[{value?.size ?? 0}]</span>
&nbsp;
<EuiHighlight search={searchVal}>{label}</EuiHighlight>
</span>
);
}, []);

const onSearchChange = useCallback((v: string) => {
// set the typing flag and update the search value
setModifyingSearch(true);
setModifyingSearch(v !== '');
setSearchValue(v);
}, []);

Expand All @@ -205,6 +140,7 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ onChange }) => {
placeholder={SELECT_AGENT_LABEL}
isLoading={modifyingSearch || groupsLoading || agentsLoading}
options={options}
isClearable={true}
fullWidth={true}
onSearchChange={onSearchChange}
selectedOptions={selectedOptions}
Expand Down
63 changes: 62 additions & 1 deletion x-pack/plugins/osquery/public/agents/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import {
Group,
AgentOptionValue,
AggregationDataPoint,
AgentSelection,
GroupOptionValue,
GroupOption,
} from './types';

export type InspectResponse = Inspect & { response: string[] };
Expand All @@ -43,7 +46,8 @@ 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, id: 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) {
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/osquery/public/agents/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down
8 changes: 8 additions & 0 deletions x-pack/plugins/osquery/public/agents/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export interface SelectedGroups {

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;
Expand Down
13 changes: 6 additions & 7 deletions x-pack/plugins/osquery/public/agents/use_all_agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,29 @@ interface UseAllAgents {
}

interface RequestOptions {
perPage: number;
page: number;
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, page: 1 }
opts: RequestOptions = { perPage: 9000 }
) => {
const { perPage, page } = opts;
const { perPage } = opts;
const { http } = useKibana().services;
const { isLoading: agentsLoading, data: agentData } = useQuery(
['agents', osqueryPolicies, searchValue, page, perPage],
['agents', osqueryPolicies, searchValue, perPage],
async () => {
let kuery = `(${osqueryPolicies.map((p) => `policy_id:${p}`).join(' or ')})`;
if (searchValue) {
kuery += ` and local_metadata.host.hostname:*${searchValue}*`;
kuery += ` and (local_metadata.host.hostname:/${searchValue}/ or local_metadata.elatic.agent.id:/${searchValue}/)`;
}
return await http.get('/api/fleet/agents', {
query: {
kuery,
perPage,
page,
},
});
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down

0 comments on commit 8cf3186

Please sign in to comment.