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

Initial pass at agents list #47177

Merged
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 9 additions & 1 deletion src/legacy/ui/public/management/sections_register.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,16 @@ management.register('kibana', {
icon: 'logoKibana',
});

management.register('ingest', {
display: i18n.translate('common.ui.management.dataIngestionDisplayName', {
defaultMessage: 'Data Ingestion',
}),
order: 30,
icon: 'logoAPM',
});

management.register('logstash', {
display: 'Logstash',
order: 30,
order: 40,
icon: 'logoLogstash',
});
27 changes: 27 additions & 0 deletions x-pack/legacy/plugins/fleet/public/components/agent_health.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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 React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiHealth } from '@elastic/eui';

interface Props {
agent: any;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this type should be Agent but the response doesn't return that type atm, so just using any for now

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should be Agent less access_token and enrollment_token... we prob need to adjust these types a bit / create a new type

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I re adjusted the type for the agent detail page, I can create a PR with it later tonight

}

export const AgentHealth: React.SFC<Props> = ({ agent }) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a simplified component for now as the information returned doesn't contain detailed status information, only active: (true|false)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is due to the events API being added secondary to this. Active has nothing to do with the status though. An unenrolled agent becomes active:false

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The status needs to be derived from the (events of an agent) + (last checkin with the timeout of the last checkin based on the agent type, with ephemeral agents having not been seen in X time having no effect to the status , temporary showing in just an nuteral offline state if not seen in polling time from policy * 3 with the default polling time being 30sec, permanent is in a degraded status if not seen in 2x polling time, and an error status of offline if not seen in 4x)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until we have events flowing in, just using the calculation of the last checkin time should be fine

if (agent.active) {
return (
<EuiHealth color="success">
<FormattedMessage id="xpack.fleet.agentHealth.onlineStatusText" defaultMessage="Online" />
</EuiHealth>
);
}
return (
<EuiHealth color="subdued">
<FormattedMessage id="xpack.fleet.agentHealth.offlineStatusText" defaultMessage="Offline" />
</EuiHealth>
);
};
10 changes: 1 addition & 9 deletions x-pack/legacy/plugins/fleet/public/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,8 @@ async function startApp(libs: FrontendLibs) {
await libs.framework.waitUntilFrameworkReady();

if (libs.framework.licenseIsAtLeast('standard')) {
libs.framework.registerManagementSection({
id: 'data_collection',
name: i18n.translate('xpack.fleet.dataCollectionManagementSectionLabel', {
defaultMessage: 'Data Collection',
}),
iconName: 'logoAPM',
});

libs.framework.registerManagementUI({
sectionId: 'data_collection',
sectionId: 'ingest',
name: i18n.translate('xpack.fleet.fleetManagementLinkLabel', {
defaultMessage: 'Fleet',
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ export class RestAgentAdapter extends AgentAdapter {

public async getAll(ESQuery?: string): Promise<Agent[]> {
try {
return (await this.REST.get<ReturnTypeList<Agent>>('/api/fleet/agents/all', { ESQuery }))
.list;
return (await this.REST.get<ReturnTypeList<Agent>>('/api/fleet/agents', { ESQuery })).list;
} catch (e) {
return [];
}
Expand Down
222 changes: 222 additions & 0 deletions x-pack/legacy/plugins/fleet/public/pages/agent_list/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/*
* 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 React, { useState, useEffect } from 'react';
import {
EuiInMemoryTable,
EuiPageBody,
EuiPageContent,
EuiTitle,
EuiSpacer,
EuiText,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiEmptyPrompt,
EuiLink,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { FrontendLibs } from '../../lib/types';
import { AgentHealth } from '../../components/agent_health';

interface RouterProps {
libs: FrontendLibs;
}

export const AgentListPage: React.SFC<RouterProps> = ({ libs }) => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [agents, setAgents] = useState<any[]>([]);

const fetchAgents = async () => {
setIsLoading(true);
setAgents(await libs.agents.getAll());
setIsLoading(false);
};

// Load agents
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should prob use a timer, and poll every X seconds

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, I will change this when I do the refactoring work to support manual pagination. with EuiInMemoryTable, refreshing data resets the table state!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah true I forgot it did that

fetchAgents();
}, []);

// Some agents retrieved, set up table props
const columns = [
{
field: 'local_metadata.host',
name: i18n.translate('xpack.fleet.agentList.hostColumnTitle', {
defaultMessage: 'Host',
}),
truncateText: true,
sortable: true,
},
// {
// field: 'id',
// name: i18n.translate('xpack.fleet.agentList.metaColumnTitle', {
// defaultMessage: 'Meta',
// }),
// truncateText: true,
// sortable: true,
// render: () => <span>some-region</span>,
// },
{
field: 'policy_id',
name: i18n.translate('xpack.fleet.agentList.policyColumnTitle', {
defaultMessage: 'Policy',
}),
truncateText: true,
sortable: true,
},
// {
// field: 'event_rate',
// name: i18n.translate('xpack.fleet.agentList.eventsColumnTitle', {
// defaultMessage: 'Events (24h)',
// }),
// truncateText: true,
// sortable: true,
// render: () => <span>34</span>,
// },
{
field: 'active',
name: i18n.translate('xpack.fleet.agentList.statusColumnTitle', {
defaultMessage: 'Status',
}),
truncateText: true,
sortable: true,
render: (active: boolean, agent: any) => <AgentHealth agent={agent} />,
},
{
name: i18n.translate('xpack.fleet.agentList.actionsColumnTitle', {
defaultMessage: 'Actions',
}),
actions: [
{
render: () => {
return (
<EuiLink color="primary" onClick={() => {}}>
<FormattedMessage
id="xpack.fleet.agentList.viewActionLinkText"
defaultMessage="view"
/>
</EuiLink>
);
},
},
],
width: '100px',
},
];

const sorting = {
sort: {
field: 'last_checkin',
direction: 'asc',
},
};

const pagination = {
initialPageSize: 20,
pageSizeOptions: [10, 20, 50],
};

const search = {
box: {
incremental: true,
schema: true,
},
filters: [
{
type: 'field_value_selection',
field: 'policy_id',
name: i18n.translate('xpack.fleet.agentList.policyFilterLabel', {
defaultMessage: 'Policy',
}),
multiSelect: true,
options: [...new Set(agents.map(agent => agent.policy_id))].map(policy => ({
value: policy,
})),
},
],
toolsRight: (
<EuiFlexGroup gutterSize="m" justifyContent="spaceAround">
<EuiFlexItem>
<EuiButton color="secondary" iconType="refresh" onClick={fetchAgents}>
<FormattedMessage id="xpack.fleet.agentList.reloadButton" defaultMessage="Reload" />
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton fill iconType="plusInCircle">
<FormattedMessage
id="xpack.fleet.agentList.addButton"
defaultMessage="Install new agent"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
),
};

const emptyPrompt = (
<EuiEmptyPrompt
title={
<h2>
<FormattedMessage
id="xpack.fleet.agentList.noAgentsPrompt"
defaultMessage="No agents installed"
/>
</h2>
}
actions={
<EuiButton fill iconType="plusInCircle">
<FormattedMessage
id="xpack.fleet.agentList.addButton"
defaultMessage="Install new agent"
/>
</EuiButton>
}
/>
);

return (
<EuiPageBody>
<EuiPageContent>
<EuiTitle size="l">
<h1>
<FormattedMessage id="xpack.fleet.agentList.pageTitle" defaultMessage="Elastic Fleet" />
</h1>
</EuiTitle>
<EuiSpacer size="s" />
<EuiTitle size="s">
<EuiText color="subdued">
<FormattedMessage
id="xpack.fleet.agentList.pageDescription"
defaultMessage="Use agents to faciliate data collection for your Elastic stack."
/>
</EuiText>
</EuiTitle>
<EuiSpacer size="m" />
<EuiInMemoryTable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not really familiar with elastic components but it is ok to load all the agents?

Copy link
Contributor Author

@jen-huang jen-huang Oct 3, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this component has built in pagination but will likely run into performance problems once there are thousands of items. my intention is to refactor this into EuiBasicTable with manual pagination after this week since I see that we intend to support pagination in the API endpoints via page query param (correct me if I'm wrong!)

loading={isLoading}
message={
isLoading
? i18n.translate('xpack.fleet.agentList.loadingAgentsMessage', {
defaultMessage: 'Loading agents…',
})
: agents.length === 0
? emptyPrompt
: i18n.translate('xpack.fleet.agentList.noFilteredAgentsPrompt', {
defaultMessage: 'No agents found',
})
}
items={agents}
itemId="id"
columns={columns}
search={search}
sorting={sorting}
pagination={pagination}
/>
</EuiPageContent>
</EuiPageBody>
);
};
8 changes: 2 additions & 6 deletions x-pack/legacy/plugins/fleet/public/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,14 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EnforceSecurityPage } from './error/enforce_security';
import { InvalidLicensePage } from './error/invalid_license';
import { NoAccessPage } from './error/no_access';

const IndexPage = () => {
return <span>Elastic Fleet</span>;
};
import { AgentListPage } from './agent_list';

export const routeMap = [
{ path: '/error/enforce_security', component: EnforceSecurityPage },
{ path: '/error/invalid_license', component: InvalidLicensePage },
{ path: '/error/no_access', component: NoAccessPage },
{ path: '/', component: IndexPage },
{ path: '/agents', component: AgentListPage },
];
5 changes: 3 additions & 2 deletions x-pack/legacy/plugins/fleet/public/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { routeMap } from './pages';
interface RouterProps {
libs: FrontendLibs;
}

interface RouterState {
loading: boolean;
}
Expand Down Expand Up @@ -74,8 +75,8 @@ export class AppRoutes extends Component<RouterProps, RouterState> {
/>
)} */}

{/* This app does not make use of a homepage. The mainpage is overview/enrolled_agents */}
{/* <Route path="/" exact={true} render={() => <Redirect to="/overview/enrolled_agents" />} /> */}
{/* This app does not make use of a homepage. The main page is agents list */}
<Route path="/" exact={true} render={() => <Redirect to="/agents" />} />
</Switch>

{/* Render routes from the FS */}
Expand Down