Skip to content

Commit

Permalink
[Fleet] Allow to edit metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
nchaulet committed Oct 21, 2019
1 parent fc506b0 commit ad4ca9f
Show file tree
Hide file tree
Showing 20 changed files with 480 additions and 228 deletions.
11 changes: 5 additions & 6 deletions x-pack/legacy/plugins/fleet/public/components/search_bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ import {
// @ts-ignore
EuiSuggest,
} from '@elastic/eui';
import { FrontendLibs } from '../lib/types';
import { ElasticsearchLib } from '../lib/elasticsearch';
import { useDebounce } from '../hooks/use_debounce';
import { useLibs } from '../hooks/use_libs';

const DEBOUNCE_SEARCH_MS = 150;

Expand All @@ -28,14 +27,13 @@ interface Suggestion {
}

interface Props {
libs: FrontendLibs;
value: string;
fieldPrefix: string;
onChange: (newValue: string) => void;
}

export const SearchBar: SFC<Props> = ({ libs, value, fieldPrefix, onChange }) => {
const { suggestions } = useSuggestions(libs.elasticsearch, fieldPrefix, value);
export const SearchBar: SFC<Props> = ({ value, fieldPrefix, onChange }) => {
const { suggestions } = useSuggestions(fieldPrefix, value);

const onAutocompleteClick = (suggestion: Suggestion) => {
onChange(
Expand Down Expand Up @@ -73,7 +71,8 @@ function transformSuggestionType(type: string): { iconType: string; color: strin
}
}

function useSuggestions(elasticsearch: ElasticsearchLib, fieldPrefix: string, search: string) {
function useSuggestions(fieldPrefix: string, search: string) {
const { elasticsearch } = useLibs();
const debouncedSearch = useDebounce(search, DEBOUNCE_SEARCH_MS);
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);

Expand Down
18 changes: 18 additions & 0 deletions x-pack/legacy/plugins/fleet/public/hooks/use_libs.tsx
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 React, { useContext } from 'react';
import { FrontendLibs } from '../lib/types';

export const LibsContext = React.createContext<FrontendLibs | null>(null);

export function useLibs() {
const libs = useContext(LibsContext);
if (libs === null) {
throw new Error('You need to provide LibsContext');
}
return libs;
}
5 changes: 4 additions & 1 deletion x-pack/legacy/plugins/fleet/public/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ import { BASE_PATH } from '../common/constants';
import { compose } from './lib/compose/kibana';
import { FrontendLibs } from './lib/types';
import { AppRoutes } from './routes';
import { LibsContext } from './hooks/use_libs';

async function startApp(libs: FrontendLibs) {
libs.framework.renderUIAtPath(
BASE_PATH,
<I18nContext>
<HashRouter basename="/fleet">
<AppRoutes libs={libs} />
<LibsContext.Provider value={libs}>
<AppRoutes libs={libs} />
</LibsContext.Provider>
</HashRouter>
</I18nContext>,
'management'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export class RestAgentAdapter extends AgentAdapter {
}

public async update(id: string, beatData: Partial<Agent>): Promise<boolean> {
await this.REST.put<ReturnTypeUpdate<Agent>>(`/api/fleet/agent/${id}`, beatData);
await this.REST.put<ReturnTypeUpdate<Agent>>(`/api/fleet/agents/${id}`, beatData);
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedTime } from '@kbn/i18n/react';
import { AgentEvent, Agent } from '../../../../common/types/domain_data';
import { usePagination } from '../../../hooks/use_pagination';
import { FrontendLibs } from '../../../lib/types';
import { SearchBar } from '../../../components/search_bar';
import { useDebounce } from '../../../hooks/use_debounce';
import { useLibs } from '../../../hooks/use_libs';

const DEBOUNCE_SEARCH_MS = 300;

Expand All @@ -43,11 +43,11 @@ function useSearch() {
}

function useGetAgentEvents(
libs: FrontendLibs,
agent: Agent,
search: string,
pagination: { currentPage: number; pageSize: number }
) {
const libs = useLibs();
const [state, setState] = useState<{ list: AgentEvent[]; total: number; isLoading: boolean }>({
list: [],
total: 0,
Expand Down Expand Up @@ -96,11 +96,11 @@ function useGetAgentEvents(
return { ...state, refresh: fetchAgentEvents };
}

export const AgentEventsTable: SFC<{ libs: FrontendLibs; agent: Agent }> = ({ libs, agent }) => {
export const AgentEventsTable: SFC<{ agent: Agent }> = ({ agent }) => {
const { pageSizeOptions, pagination, setPagination } = usePagination();
const { search, setSearch } = useSearch();

const { list, total, isLoading, refresh } = useGetAgentEvents(libs, agent, search, pagination);
const { list, total, isLoading, refresh } = useGetAgentEvents(agent, search, pagination);
const paginationOptions = {
pageIndex: pagination.currentPage - 1,
pageSize: pagination.pageSize,
Expand Down Expand Up @@ -177,7 +177,7 @@ export const AgentEventsTable: SFC<{ libs: FrontendLibs; agent: Agent }> = ({ li
<EuiSpacer size="l" />
<EuiFlexGroup>
<EuiFlexItem>
<SearchBar value={search} libs={libs} onChange={setSearch} fieldPrefix={'agent_events'} />
<SearchBar value={search} onChange={setSearch} fieldPrefix={'agent_events'} />
</EuiFlexItem>
<EuiFlexItem grow={null}>
<EuiButton color="secondary" iconType="refresh" onClick={onClickRefresh}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,43 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { SFC } from 'react';
import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react';
import React, { SFC, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiTitle,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiDescriptionList,
EuiButton,
EuiDescriptionListTitle,
EuiDescriptionListDescription,
EuiButtonEmpty,
EuiLink,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Agent } from '../../../../common/types/domain_data';
import { AgentHealth } from '../../../components/agent_health';
import { AgentMetadataFlyout } from './metadata_flyout';

const MAX_METADATA = 5;
const PREFERRED_METADATA = ['ip', 'system', 'region', 'memory'];
const Item: SFC<{ label: string }> = ({ label, children }) => {
return (
<EuiFlexItem grow={false}>
<EuiDescriptionList compressed>
<EuiDescriptionListTitle>{label}</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{children}</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiFlexItem>
);
};

function getMetadataTitle(key: string): string {
switch (key) {
case 'ip':
return i18n.translate('xpack.fleet.agentMetadata.ipLabel', {
defaultMessage: 'IP Adress',
});
case 'system':
return i18n.translate('xpack.fleet.agentMetadata.systemLabel', {
defaultMessage: 'System',
});
case 'region':
return i18n.translate('xpack.fleet.agentMetadata.regionLabel', {
defaultMessage: 'Region',
});
case 'memory':
return i18n.translate('xpack.fleet.agentMetadata.memoryLabel', {
defaultMessage: 'Memory',
});
default:
return key;
}
function useFlyout() {
const [isVisible, setVisible] = useState(false);
return {
isVisible,
show: () => setVisible(true),
hide: () => setVisible(false),
};
}

interface Props {
Expand All @@ -50,73 +49,74 @@ interface Props {
onClickUnenroll: () => void;
}
export const AgentDetailSection: SFC<Props> = ({ agent, onClickUnenroll, unenrollment }) => {
const mapMetadata = (obj: { [key: string]: string } | undefined) => {
return Object.keys(obj || {}).map(key => ({
key,
value: obj ? obj[key] : '',
}));
};

const metadataItems = mapMetadata(agent.local_metadata)
.concat(mapMetadata(agent.user_provided_metadata))
.filter(item => PREFERRED_METADATA.indexOf(item.key) >= 0)
.map(item => ({
title: getMetadataTitle(item.key),
description: item.value,
}))
.slice(0, MAX_METADATA);

const metadataFlyout = useFlyout();
const items = [
{
title: i18n.translate('xpack.fleet.agentDetails.statusLabel', {
defaultMessage: 'Status',
}),
description: <AgentHealth agent={agent} />,
},
{
title: i18n.translate('xpack.fleet.agentDetails.idLabel', {
defaultMessage: 'Agent ID',
defaultMessage: 'ID',
}),
description: agent.id,
},
{
title: i18n.translate('xpack.fleet.agentDetails.typeLabel', {
defaultMessage: 'Agent Type',
defaultMessage: 'Type',
}),
description: agent.type,
},
{
title: i18n.translate('xpack.fleet.agentDetails.lastCheckinLabel', {
defaultMessage: 'Last checkin',
title: i18n.translate('xpack.fleet.agentDetails.policyLabel', {
defaultMessage: 'Policy',
}),
description: agent.last_checkin ? (
<FormattedRelative value={new Date(agent.last_checkin)} />
) : (
'-'
description: (
<EuiLink color="text">
<FormattedMessage
id="xpack.fleet.agentDetails.policyLink"
defaultMessage="{policy} (view)"
values={{ policy: agent.policy_id }}
/>
</EuiLink>
),
},
].concat(metadataItems);
];

return (
<>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h3>
<EuiTitle size="l">
<h1>
<FormattedMessage id="xpack.fleet.agentDetails.title" defaultMessage="Agent Detail" />
</h3>
</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AgentHealth agent={agent} />
<EuiButton
disabled={unenrollment.loading === true || agent.active === false}
isLoading={unenrollment.loading}
onClick={onClickUnenroll}
>
<FormattedMessage id="xpack.fleet.agentDetails.unenroll" defaultMessage="Unenroll" />
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size={'xl'} />
<EuiFlexGroup alignItems="flexStart" justifyContent="spaceBetween">
{items.map((item, idx) => (
<Item key={idx} label={item.title}>
{item.description}
</Item>
))}
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={() => metadataFlyout.show()}>View metadata</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
<EuiDescriptionList type="column" compressed listItems={items} />
<EuiSpacer size="m" />
<EuiFlexItem grow={false}>
<EuiButton
disabled={unenrollment.loading === true || agent.active === false}
isLoading={unenrollment.loading}
onClick={onClickUnenroll}
>
<FormattedMessage id="xpack.fleet.agentDetails.unenroll" defaultMessage="Unenroll" />
</EuiButton>
</EuiFlexItem>
{metadataFlyout.isVisible && <AgentMetadataFlyout flyout={metadataFlyout} agent={agent} />}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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, { SFC } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiTitle,
EuiSpacer,
EuiDescriptionList,
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiHorizontalRule,
} from '@elastic/eui';

import { Agent } from '../../../../common/types/domain_data';
import { MetadataForm } from './metadata_form';

interface Props {
agent: Agent;
flyout: { hide: () => void };
}
export const AgentMetadataFlyout: SFC<Props> = ({ agent, flyout }) => {
const mapMetadata = (obj: { [key: string]: string } | undefined) => {
return Object.keys(obj || {}).map(key => ({
title: key,
description: obj ? obj[key] : '',
}));
};

const localItems = mapMetadata(agent.local_metadata);
const userProvidedItems = mapMetadata(agent.user_provided_metadata);

return (
<EuiFlyout onClose={() => flyout.hide()} size="s" aria-labelledby="flyoutTitle">
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2 id="flyoutTitle">
<FormattedMessage
id="xpack.fleet.agentDetails.metadataSectionTitle"
defaultMessage="Metadata"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.fleet.agentDetails.localMetadataSectionSubtitle"
defaultMessage="Local metadata"
/>
</h3>
</EuiTitle>
<EuiHorizontalRule />
<EuiDescriptionList type="column" compressed listItems={localItems} />
<EuiSpacer size="xxl" />
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.fleet.agentDetails.userProvidedMetadataSectionSubtitle"
defaultMessage="User provided metadata"
/>
</h3>
</EuiTitle>
<EuiHorizontalRule />
<EuiDescriptionList type="column" compressed listItems={userProvidedItems} />
<EuiSpacer size="m" />

<MetadataForm agent={agent} />
</EuiFlyoutBody>
</EuiFlyout>
);
};
Loading

0 comments on commit ad4ca9f

Please sign in to comment.