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] Allow to edit metadata #48682

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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() {
nchaulet marked this conversation as resolved.
Show resolved Hide resolved
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} />
nchaulet marked this conversation as resolved.
Show resolved Hide resolved
</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" />
nchaulet marked this conversation as resolved.
Show resolved Hide resolved
</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" />
nchaulet marked this conversation as resolved.
Show resolved Hide resolved
</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