Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fleet] Filter agents list table by policy name #49968

Merged
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
@@ -98,6 +98,7 @@ function useSuggestions(fieldPrefix: string, search: string) {

useEffect(() => {
fetchSuggestions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearch]);

return {
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Policy } from '../../../../scripts/mock_spec/types';

export class PolicyAdapter {
private memoryDB: Policy[];

constructor(db: Policy[]) {
this.memoryDB = db;
}

public async getAll(): Promise<Policy[]> {
return this.memoryDB;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { Policy } from '../../../../scripts/mock_spec/types';
import { RestAPIAdapter } from '../rest_api/adapter_types';
import { PolicyAdapter } from './memory_policy_adapter';

const POLICIES_SERVER_HOST = `${window.location.protocol}//${window.location.hostname}:4010`;

export class RestPolicyAdapter extends PolicyAdapter {
constructor(private readonly REST: RestAPIAdapter) {
super([]);
}

public async getAll() {
try {
return await this.REST.get<Policy[]>(`${POLICIES_SERVER_HOST}/policies`);
} catch (e) {
return [];
}
}
}
4 changes: 4 additions & 0 deletions x-pack/legacy/plugins/fleet/public/lib/compose/kibana.ts
Original file line number Diff line number Diff line change
@@ -14,10 +14,12 @@ import { management } from 'ui/management';
import { toastNotifications } from 'ui/notify';
import routes from 'ui/routes';
import { RestAgentAdapter } from '../adapters/agent/rest_agent_adapter';
import { RestPolicyAdapter } from '../adapters/policy/rest_policy_adapter';
import { RestElasticsearchAdapter } from '../adapters/elasticsearch/rest';
import { KibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter';
import { AxiosRestAPIAdapter } from '../adapters/rest_api/axios_rest_api_adapter';
import { AgentsLib } from '../agent';
import { PoliciesLib } from '../policy';
import { ElasticsearchLib } from '../elasticsearch';
import { FrontendLibs } from '../types';
import { PLUGIN } from '../../../common/constants/plugin';
@@ -32,6 +34,7 @@ export function compose(): FrontendLibs {
const esAdapter = new RestElasticsearchAdapter(api, INDEX_NAMES.FLEET);
const elasticsearchLib = new ElasticsearchLib(esAdapter);
const agents = new AgentsLib(new RestAgentAdapter(api));
const policies = new PoliciesLib(new RestPolicyAdapter(api));

const framework = new FrameworkLib(
new KibanaFrameworkAdapter(
@@ -50,6 +53,7 @@ export function compose(): FrontendLibs {
framework,
elasticsearch: elasticsearchLib,
agents,
policies,
};
return libs;
}
6 changes: 5 additions & 1 deletion x-pack/legacy/plugins/fleet/public/lib/compose/memory.ts
Original file line number Diff line number Diff line change
@@ -14,9 +14,11 @@ import { uiModules } from 'ui/modules';
// @ts-ignore: path dynamic for kibana
import routes from 'ui/routes';
// @ts-ignore: path dynamic for kibana
import { MemoryAgentAdapter } from '../adapters/agent/memory_agents_adapter';
import { MemoryAgentAdapter } from '../adapters/agent/memory_agent_adapter';
import { PolicyAdapter } from '../adapters/policy/memory_policy_adapter';
import { KibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter';
import { AgentsLib } from '../agent';
import { PoliciesLib } from '../policy';
import { FrameworkLib } from '../framework';
import { FrontendLibs } from '../types';
import { MemoryElasticsearchAdapter } from '../adapters/elasticsearch/memory';
@@ -38,6 +40,7 @@ export function compose(
const elasticsearchLib = new ElasticsearchLib(esAdapter);

const agents = new AgentsLib(new MemoryAgentAdapter([]));
const policies = new PoliciesLib(new PolicyAdapter([]));

const pluginUIModule = uiModules.get('app/fleet');

@@ -57,6 +60,7 @@ export function compose(
framework,
elasticsearch: elasticsearchLib,
agents,
policies,
};
return libs;
}
4 changes: 4 additions & 0 deletions x-pack/legacy/plugins/fleet/public/lib/compose/scripts.ts
Original file line number Diff line number Diff line change
@@ -5,10 +5,12 @@
*/

import { RestAgentAdapter } from '../adapters/agent/rest_agent_adapter';
import { RestPolicyAdapter } from '../adapters/policy/rest_policy_adapter';
import { MemoryElasticsearchAdapter } from '../adapters/elasticsearch/memory';
import { TestingFrameworkAdapter } from '../adapters/framework/testing_framework_adapter';
import { NodeAxiosAPIAdapter } from '../adapters/rest_api/node_axios_api_adapter';
import { AgentsLib } from '../agent';
import { PoliciesLib } from '../policy';
import { ElasticsearchLib } from '../elasticsearch';
import { FrameworkLib } from '../framework';
import { FrontendLibs } from '../types';
@@ -19,6 +21,7 @@ export function compose(basePath: string): FrontendLibs {
const elasticsearchLib = new ElasticsearchLib(esAdapter);

const agents = new AgentsLib(new RestAgentAdapter(api));
const policies = new PoliciesLib(new RestPolicyAdapter(api));

const framework = new FrameworkLib(
new TestingFrameworkAdapter(
@@ -50,6 +53,7 @@ export function compose(basePath: string): FrontendLibs {
framework,
elasticsearch: elasticsearchLib,
agents,
policies,
};
return libs;
}
17 changes: 17 additions & 0 deletions x-pack/legacy/plugins/fleet/public/lib/policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { Policy } from '../../scripts/mock_spec/types';
import { PolicyAdapter } from './adapters/policy/memory_policy_adapter';

export class PoliciesLib {
constructor(private readonly adapter: PolicyAdapter) {}

/** Get an array of all policies */
public getAll = async (): Promise<Policy[]> => {
return await this.adapter.getAll();
};
}
2 changes: 2 additions & 0 deletions x-pack/legacy/plugins/fleet/public/lib/types.ts
Original file line number Diff line number Diff line change
@@ -7,13 +7,15 @@ import { IModule, IScope } from 'angular';
import { AxiosRequestConfig } from 'axios';
import { FrameworkAdapter } from './adapters/framework/adapter_types';
import { AgentsLib } from './agent';
import { PoliciesLib } from './policy';
import { ElasticsearchLib } from './elasticsearch';
import { FrameworkLib } from './framework';

export interface FrontendLibs {
elasticsearch: ElasticsearchLib;
framework: FrameworkLib;
agents: AgentsLib;
policies: PoliciesLib;
}

export type FramworkAdapterConstructable = new (uiModule: IModule) => FrameworkAdapter;
Original file line number Diff line number Diff line change
@@ -91,6 +91,7 @@ function useGetAgentEvents(
};
useEffect(() => {
fetchAgentEvents();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [agent.id, debouncedSearch, pagination]);

return { ...state, refresh: fetchAgentEvents };
Original file line number Diff line number Diff line change
@@ -42,6 +42,7 @@ export function useGetAgent(id: string) {
};
useEffect(() => {
fetchAgent();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);

return {
Original file line number Diff line number Diff line change
@@ -86,10 +86,10 @@ export const AgentDetailsPage: SFC<Props> = ({
<EuiFlexItem grow={null}>
<EuiHorizontalRule />
</EuiFlexItem>
<EuiFlexItem grow={null}></EuiFlexItem>
<EuiFlexItem grow={null} />
</EuiFlexItem>
<EuiFlexItem grow={7}>
<EuiFlexItem grow={null}></EuiFlexItem>
<EuiFlexItem grow={null} />
</EuiFlexItem>
</EuiFlexGroup>
</Layout>
146 changes: 123 additions & 23 deletions x-pack/legacy/plugins/fleet/public/pages/agent_list/index.tsx
Original file line number Diff line number Diff line change
@@ -20,6 +20,10 @@ import {
EuiSearchBar,
EuiLink,
EuiSwitch,
EuiFilterGroup,
EuiPopover,
EuiFilterSelectItem,
EuiFilterButton,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -48,34 +52,73 @@ export const AgentListPage: React.SFC<{}> = () => {
const [selectedAgents, setSelectedAgents] = useState<Agent[]>([]);
const [areAllAgentsSelected, setAreAllAgentsSelected] = useState<boolean>(false);

// Policies state (for filtering)
const [policies, setPolicies] = useState<any[]>([]);
const [isPoliciesLoading, setIsPoliciesLoading] = useState<boolean>(false);
const [isPoliciesFilterOpen, setIsPoliciesFilterOpen] = useState<boolean>(false);
const [selectedPolicies, setSelectedPolicies] = useState<string[]>([]);

// Add a policy id to current search
const addPolicyFilter = (policyId: string) => {
setSelectedPolicies([...selectedPolicies, policyId]);
};

// Remove a policy id from current search
const removePolicyFilter = (policyId: string) => {
setSelectedPolicies(selectedPolicies.filter(policy => policy !== policyId));
};

// Agent enrollment flyout state
const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState<boolean>(false);

// Fetch agents method
const fetchAgents = async () => {
setIsLoading(true);
setLastPolledAgentsMs(new Date().getTime());

// Build kuery from current search and policy filter states
let kuery = search.trim();
if (selectedPolicies.length) {
if (kuery) {
kuery = `(${kuery}) and`;
}
kuery = `${kuery} agents.policy_id : (${selectedPolicies
.map(policy => `"${policy}"`)
.join(' or ')})`;
}

const { list, total } = await libs.agents.getAll(
pagination.currentPage,
pagination.pageSize,
search,
kuery,
showInactive
);

setAgents(list);
setTotalAgents(total);
setIsLoading(false);
};

// Fetch policies method
const fetchPolicies = async () => {
setIsPoliciesLoading(true);
setPolicies(await libs.policies.getAll());
setIsPoliciesLoading(false);
};

// Load initial list of agents
useEffect(() => {
fetchAgents();
fetchPolicies();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showInactive]);

// Update agents if pagination or query state changes
// Update agents if pagination, query, or policy filter state changes
useEffect(() => {
fetchAgents();
setAreAllAgentsSelected(false);
}, [pagination, search]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pagination, search, selectedPolicies]);

// Poll for agents on interval
useInterval(() => {
@@ -272,17 +315,60 @@ export const AgentListPage: React.SFC<{}> = () => {
</EuiFlexItem>
) : null}
<EuiFlexItem grow={4}>
<SearchBar
value={search}
onChange={newSearch => {
setPagination({
...pagination,
currentPage: 1,
});
setSearch(newSearch);
}}
fieldPrefix="agents"
/>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={3}>
<SearchBar
value={search}
onChange={newSearch => {
setPagination({
...pagination,
currentPage: 1,
});
setSearch(newSearch);
}}
fieldPrefix="agents"
/>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EuiFilterGroup>
<EuiPopover
ownFocus
button={
<EuiFilterButton
iconType="arrowDown"
onClick={() => setIsPoliciesFilterOpen(!isPoliciesFilterOpen)}
isSelected={isPoliciesFilterOpen}
hasActiveFilters={selectedPolicies.length > 0}
disabled={isPoliciesLoading}
>
Policies
</EuiFilterButton>
}
isOpen={isPoliciesFilterOpen}
closePopover={() => setIsPoliciesFilterOpen(false)}
panelPaddingSize="none"
>
<div className="euiFilterSelect__items">
{policies.map((policy, index) => (
<EuiFilterSelectItem
checked={selectedPolicies.includes(policy.id) ? 'on' : undefined}
key={index}
onClick={() => {
if (selectedPolicies.includes(policy.id)) {
removePolicyFilter(policy.id);
} else {
addPolicyFilter(policy.id);
}
}}
>
{policy.name}
</EuiFilterSelectItem>
))}
</div>
</EuiPopover>
</EuiFilterGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{libs.framework.capabilities.write && (
<EuiFlexItem grow={false}>
@@ -305,15 +391,29 @@ export const AgentListPage: React.SFC<{}> = () => {
className="fleet__agentList__table"
loading={isLoading}
noItemsMessage={
isLoading
? i18n.translate('xpack.fleet.agentList.loadingAgentsMessage', {
defaultMessage: 'Loading agents…',
})
: !search.trim() && totalAgents === 0
? emptyPrompt
: i18n.translate('xpack.fleet.agentList.noFilteredAgentsPrompt', {
defaultMessage: 'No agents found',
})
isLoading ? (
<FormattedMessage
id="xpack.fleet.agentList.loadingAgentsMessage"
defaultMessage="Loading agents…"
/>
) : !search.trim() && selectedPolicies.length === 0 && totalAgents === 0 ? (
emptyPrompt
) : (
<FormattedMessage
id="xpack.fleet.agentList.noFilteredAgentsPrompt"
defaultMessage="No agents found. {clearFiltersLink}"
values={{
clearFiltersLink: (
<EuiLink onClick={() => setSearch('')}>
<FormattedMessage
id="xpack.fleet.agentList.clearFiltersLinkText"
defaultMessage="Clear filters"
/>
</EuiLink>
),
}}
/>
)
}
items={totalAgents ? agents : []}
itemId="id"
1 change: 1 addition & 0 deletions x-pack/legacy/plugins/fleet/public/routes.tsx
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@ function useWaitUntilFrameworkReady() {

useEffect(() => {
waitUntilReady();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return { isLoading };
Original file line number Diff line number Diff line change
@@ -21,10 +21,12 @@
},
"properties": {
"id": {
"type": "string"
"type": "string",
"example": "policy_example"
},
"name": {
"type": "string"
"type": "string",
"example": "Example Policy"
},
"datasources": {
"type": "array",
45 changes: 41 additions & 4 deletions x-pack/legacy/plugins/fleet/scripts/mock_spec/openapi.json
Original file line number Diff line number Diff line change
@@ -29,18 +29,21 @@
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "./models/policy.v1.json"
},
{
"type": "array",
"items": {
"$ref": "./models/policy.v1.json"
}
},
{
"$ref": "./models/policy.v1.json"
}
]
}
}
},
"headers": {
"$ref": "#/components/headers"
}
}
},
@@ -61,6 +64,16 @@
"description": "Return polices linked to a datasource",
"operationId": "getPolicies"
},
"options": {
"responses": {
"200": {
"description": "OK",
"headers": {
"$ref": "#/components/headers"
}
}
}
},
"parameters": []
},
"/datasources": {
@@ -96,5 +109,29 @@
}
}
},
"components": {}
"components": {
"headers": {
"Access-Control-Allow-Origin": {
"description": "CORS",
"schema": {
"type": "string"
},
"example": "http://localhost:5601"
},
"Access-Control-Allow-Credentials": {
"description": "CORS",
"schema": {
"type": "boolean"
},
"example": "true"
},
"Access-Control-Allow-Headers": {
"description": "CORS",
"schema": {
"type": "string"
},
"example": "kbn-xsrf, kbn-version, credentials"
}
}
}
}
2 changes: 1 addition & 1 deletion x-pack/legacy/plugins/fleet/scripts/mock_spec/script.ts
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ run(
async ({ log }) => {
const specPath = resolve(__dirname, 'openapi.json');
const prismPath = resolve(__dirname, '../../node_modules/.bin/prism');
const prismProc = spawn(prismPath, ['mock', specPath]);
const prismProc = spawn(prismPath, ['mock', specPath, '--cors=false']);

prismProc.stdout.on('data', data => {
process.stdout.write(data);
129 changes: 129 additions & 0 deletions x-pack/legacy/plugins/fleet/scripts/mock_spec/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

/**
* Has config from zero or more datasources.
*/
export interface Policy {
datasources?: Datasource[];
description?: string;
id: string;
name?: string;
status: Status;
}

/**
* A package with a use case (eg prod_west). The use case ID must be unique. A datasource
* can have multiple streams.
*/
export interface Datasource {
id?: string;
/**
* Should be unique
*/
name: string;
package: Package;
read_alias?: string;
streams: Stream[];
}

/**
* Multiple dashboard templates and multiple input templates, eg access log, error log,
* metrics, consisting of index template, ingest pipeline, ML jobs.
*/
export interface Package {
assets: Asset[];
description?: string;
name: string;
title?: string;
version: string;
}

export interface Asset {
id: string;
type: AssetType;
}

/**
* Types of assets which can be installed/removed
*/
export enum AssetType {
DataFrameTransform = 'data-frame-transform',
IlmPolicy = 'ilm-policy',
IndexTemplate = 'index-template',
IngestPipeline = 'ingest-pipeline',
MlJob = 'ml-job',
RollupJob = 'rollup-job',
}

/**
* A combination of an input type, the required config, an output, and any processors
*/
export interface Stream {
config?: { [key: string]: any };
id: string;
input: Input;
output: Output;
processors?: string[];
}

/**
* Where the data comes from
*/
export interface Input {
/**
* Mix of configurable and required properties still TBD. Object for now might become string
*/
config: { [key: string]: any };
fields?: Array<{ [key: string]: any }>;
id?: string;
ilm_policy?: string;
index_template?: string;
/**
* Need a distinction for "main" ingest pipeline. Should be handled during install. Likely
* by package/manifest format
*/
ingest_pipelines?: string[];
type: InputType;
}

export enum InputType {
Etc = 'etc',
Log = 'log',
MetricDocker = 'metric/docker',
MetricSystem = 'metric/system',
}

/**
* Where to send the data
*/
export interface Output {
api_token?: string;
/**
* contains everything not otherwise specified (e.g. TLS, etc)
*/
config?: { [key: string]: any };
id: string;
/**
* unique alias with write index
*/
index_name?: string;
ingest_pipeline?: string;
name: string;
type: OutputType;
url?: string;
}

export enum OutputType {
Elasticsearch = 'elasticsearch',
Else = 'else',
Something = 'something',
}

export enum Status {
Active = 'active',
Inactive = 'inactive',
}