Skip to content

Commit

Permalink
janky infinite loading agents, async searching
Browse files Browse the repository at this point in the history
  • Loading branch information
lykkin committed Apr 16, 2021
1 parent e1e6b56 commit 5f14876
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 114 deletions.
117 changes: 117 additions & 0 deletions x-pack/plugins/osquery/public/agents/agent_grouper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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,
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,
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;
}
}
182 changes: 85 additions & 97 deletions x-pack/plugins/osquery/public/agents/agents_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,25 @@
* 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 {
getNumAgentsInGrouping,
generateAgentCheck,
getNumOverlapped,
generateColorPicker,
} from './helpers';
import { AgentGrouper } from './agent_grouper';
import { getNumAgentsInGrouping, generateAgentCheck, getNumOverlapped } from './helpers';

import {
ALL_AGENTS_LABEL,
AGENT_PLATFORMS_LABEL,
AGENT_POLICY_LABEL,
SELECT_AGENT_LABEL,
AGENT_SELECTION_LABEL,
generateSelectedAgentsMessage,
} from './translations';
import { SELECT_AGENT_LABEL, generateSelectedAgentsMessage } from './translations';

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

export interface AgentsSelection {
agents: string[];
Expand All @@ -42,81 +37,61 @@ interface AgentsTableProps {
onChange: (payload: AgentsSelection) => void;
}

type GroupOption = EuiComboBoxOptionOption<AgentOptionValue | GroupOptionValue>;

const getColor = generateColorPicker();

const generateOptions = (groupType: AGENT_GROUP_KEY, label: string, collection: Group[]) => {
return {
label,
options: collection.map(({ name, id, size }) => ({
label: name,
color: getColor(groupType),
value: { groupType, id, size },
})),
};
}
const perPage = 10;

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);
setDebouncedSearchValue(searchValue);
setModifyingSearch(false);
},
100,
[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,
page,
});

// option related
const [options, setOptions] = useState<GroupOption[]>([]);
const [lastLabel, setLastLabel] = useState<string>('');
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(generateOptions(groupType, AGENT_PLATFORMS_LABEL, groups.platforms))
}

if (groups.policies.length > 0) {
const groupType = AGENT_GROUP_KEY.Policy;
opts.push(generateOptions(groupType, AGENT_POLICY_LABEL, groups.policies))
}

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,
key: agent.local_metadata.elastic.agent.id,
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,
},
})),
});
// 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, page > 1);
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);
}
}
setLoading(false);
setOptions(opts);
}, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents]);
}, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents, page, grouper]);

const onSelection = useCallback(
(selection: GroupOption[]) => {
// TODO?: optimize this by making it incremental
// TODO?: optimize this by making the selection computation incremental
const newAgentSelection: AgentsSelection = {
agents: [],
allAgentsSelected: false,
Expand Down Expand Up @@ -189,36 +164,49 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ onChange }) => {
[groups, onChange, totalNumAgents]
);

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

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

return (
<div>
<h2>{SELECT_AGENT_LABEL}</h2>
{numAgentsSelected > 0 ? <span>{generateSelectedAgentsMessage(numAgentsSelected)}</span> : ''}
&nbsp;
<EuiComboBox
placeholder="Select or create options"
isLoading={loading}
placeholder={SELECT_AGENT_LABEL}
isLoading={modifyingSearch || groupsLoading || agentsLoading}
options={options}
fullWidth={true}
onSearchChange={onSearchChange}
selectedOptions={selectedOptions}
onChange={onSelection}
renderOption={renderOption}
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/osquery/public/agents/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { TermsAggregate } from '@elastic/elasticsearch/api/types';
import { EuiComboBoxOptionOption } from '@elastic/eui';

interface BaseDataPoint {
key: string;
Expand All @@ -29,6 +30,8 @@ export interface SelectedGroups {
[groupType: string]: { [groupName: string]: number };
}

export type GroupOption = EuiComboBoxOptionOption<AgentOptionValue | GroupOptionValue>;

interface BaseGroupOption {
id?: string;
groupType: AGENT_GROUP_KEY;
Expand Down
Loading

0 comments on commit 5f14876

Please sign in to comment.