diff --git a/x-pack/legacy/plugins/fleet/public/components/search_bar.tsx b/x-pack/legacy/plugins/fleet/public/components/search_bar.tsx index 282c4e8a77cbf5..87f282b9bd2186 100644 --- a/x-pack/legacy/plugins/fleet/public/components/search_bar.tsx +++ b/x-pack/legacy/plugins/fleet/public/components/search_bar.tsx @@ -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; @@ -28,14 +27,13 @@ interface Suggestion { } interface Props { - libs: FrontendLibs; value: string; fieldPrefix: string; onChange: (newValue: string) => void; } -export const SearchBar: SFC = ({ libs, value, fieldPrefix, onChange }) => { - const { suggestions } = useSuggestions(libs.elasticsearch, fieldPrefix, value); +export const SearchBar: SFC = ({ value, fieldPrefix, onChange }) => { + const { suggestions } = useSuggestions(fieldPrefix, value); const onAutocompleteClick = (suggestion: Suggestion) => { onChange( @@ -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([]); diff --git a/x-pack/legacy/plugins/fleet/public/hooks/use_libs.tsx b/x-pack/legacy/plugins/fleet/public/hooks/use_libs.tsx new file mode 100644 index 00000000000000..bba7e6e2eb4056 --- /dev/null +++ b/x-pack/legacy/plugins/fleet/public/hooks/use_libs.tsx @@ -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(null); + +export function useLibs() { + const libs = useContext(LibsContext); + if (libs === null) { + throw new Error('You need to provide LibsContext'); + } + return libs; +} diff --git a/x-pack/legacy/plugins/fleet/public/index.tsx b/x-pack/legacy/plugins/fleet/public/index.tsx index 30fb8492f9e40f..17d3b072bf29f8 100644 --- a/x-pack/legacy/plugins/fleet/public/index.tsx +++ b/x-pack/legacy/plugins/fleet/public/index.tsx @@ -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, - + + + , 'management' diff --git a/x-pack/legacy/plugins/fleet/public/lib/adapters/agent/rest_agent_adapter.ts b/x-pack/legacy/plugins/fleet/public/lib/adapters/agent/rest_agent_adapter.ts index cba06def225eb3..805696d075c16e 100644 --- a/x-pack/legacy/plugins/fleet/public/lib/adapters/agent/rest_agent_adapter.ts +++ b/x-pack/legacy/plugins/fleet/public/lib/adapters/agent/rest_agent_adapter.ts @@ -89,7 +89,7 @@ export class RestAgentAdapter extends AgentAdapter { } public async update(id: string, beatData: Partial): Promise { - await this.REST.put>(`/api/fleet/agent/${id}`, beatData); + await this.REST.put>(`/api/fleet/agents/${id}`, beatData); return true; } diff --git a/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/agent_events_table.tsx b/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/agent_events_table.tsx index 9b4b2e7638895a..a2abe67950af7d 100644 --- a/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/agent_events_table.tsx +++ b/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/agent_events_table.tsx @@ -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; @@ -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, @@ -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, @@ -177,7 +177,7 @@ export const AgentEventsTable: SFC<{ libs: FrontendLibs; agent: Agent }> = ({ li - + diff --git a/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/details_section.tsx b/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/details_section.tsx index 31f785bcafd0f7..dea3a8397cf161 100644 --- a/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/details_section.tsx +++ b/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/details_section.tsx @@ -4,8 +4,8 @@ * 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, @@ -13,35 +13,34 @@ import { 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 ( + + + {label} + {children} + + + ); +}; -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 { @@ -50,73 +49,80 @@ interface Props { onClickUnenroll: () => void; } export const AgentDetailSection: SFC = ({ 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: , + }, { 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 ? ( - - ) : ( - '-' + description: ( + + + ), }, - ].concat(metadataItems); + ]; return ( <> - -

- -

+ +

+ +

- + + + + +
+ + + {items.map((item, idx) => ( + + {item.description} + + ))} + + metadataFlyout.show()}>View metadata - - - - - - - - + {metadataFlyout.isVisible && } ); }; diff --git a/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/metadata_flyout.tsx b/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/metadata_flyout.tsx new file mode 100644 index 00000000000000..e25b923be00913 --- /dev/null +++ b/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/metadata_flyout.tsx @@ -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 = ({ 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 ( + flyout.hide()} size="s" aria-labelledby="flyoutTitle"> + + +

+ +

+
+
+ + +

+ +

+
+ + + + +

+ +

+
+ + + + + +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/metadata_form.tsx b/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/metadata_form.tsx new file mode 100644 index 00000000000000..95cd02fdf34667 --- /dev/null +++ b/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/metadata_form.tsx @@ -0,0 +1,174 @@ +/* + * 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, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButtonEmpty, + EuiPopover, + EuiFormRow, + EuiButton, + EuiFlexItem, + EuiFieldText, + EuiFlexGroup, + EuiForm, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AxiosError } from 'axios'; +import { Agent } from '../../../../common/types/domain_data'; +import { useLibs } from '../../../hooks/use_libs'; +import { useAgentRefresh } from '../hooks/use_agent'; + +function useInput() { + const [value, setValue] = useState(''); + + return { + value, + onChange: (e: React.ChangeEvent) => { + setValue(e.target.value); + }, + clear: () => { + setValue(''); + }, + }; +} + +function useAddMetadataForm(agent: Agent, done: () => void) { + const libs = useLibs(); + const refreshAgent = useAgentRefresh(); + const keyInput = useInput(); + const valueInput = useInput(); + const [state, setState] = useState<{ + isLoading: boolean; + error: null | string; + }>({ + isLoading: false, + error: null, + }); + + function clearInputs() { + keyInput.clear(); + valueInput.clear(); + } + + function setError(error: AxiosError) { + setState({ + isLoading: false, + error: + error.isAxiosError && error.response && error.response.data + ? error.response.data.message + : error.message, + }); + } + + async function success() { + await refreshAgent(); + setState({ + isLoading: false, + error: null, + }); + clearInputs(); + done(); + } + + return { + state, + onSubmit: async (e: React.FormEvent | React.MouseEvent) => { + e.preventDefault(); + setState({ + ...state, + isLoading: true, + }); + + try { + await libs.agents.update(agent.id, { + user_provided_metadata: { + ...agent.user_provided_metadata, + [keyInput.value]: valueInput.value, + }, + }); + await success(); + } catch (error) { + setError(error); + } + }, + inputs: { + keyInput, + valueInput, + }, + }; +} + +export const MetadataForm: SFC<{ agent: Agent }> = ({ agent }) => { + const [isOpen, setOpen] = useState(false); + + const form = useAddMetadataForm(agent, () => { + setOpen(false); + }); + const { keyInput, valueInput } = form.inputs; + + const button = ( + setOpen(true)} color={'text'}> + + + ); + return ( + <> + setOpen(false)} + initialFocus="[id=fleet-details-metadata-form]" + > +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + ); +}; diff --git a/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/metadata_section.tsx b/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/metadata_section.tsx deleted file mode 100644 index 69aa4499cb90a5..00000000000000 --- a/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/metadata_section.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiText, EuiSpacer, EuiDescriptionList } from '@elastic/eui'; -import React, { SFC } from 'react'; -import { Agent } from '../../../../common/types/domain_data'; - -export const AgentMetadataSection: SFC<{ agent: Agent }> = ({ agent }) => { - const mapMetadata = (obj: { [key: string]: string } | undefined) => { - return Object.keys(obj || {}).map(key => ({ - title: key, - description: obj ? obj[key] : '', - })); - }; - - const items = mapMetadata(agent.local_metadata).concat(mapMetadata(agent.user_provided_metadata)); - - return ( - <> - -

- -

-
- - - - - - - ); -}; diff --git a/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/policy_section.tsx b/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/policy_section.tsx deleted file mode 100644 index 95a6aabada6dc3..00000000000000 --- a/x-pack/legacy/plugins/fleet/public/pages/agent_details/components/policy_section.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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, EuiText, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; -import { Agent } from '../../../../common/types/domain_data'; - -export const PolicySection: SFC<{ agent: Agent }> = ({ agent }) => ( - <> - -

- -

-
- - - - - - - {agent.policy_id} - - - - - - - - -); diff --git a/x-pack/legacy/plugins/fleet/public/pages/agent_details/hooks/use_agent.tsx b/x-pack/legacy/plugins/fleet/public/pages/agent_details/hooks/use_agent.tsx index 456f174e160ff7..a0c3705ff4872a 100644 --- a/x-pack/legacy/plugins/fleet/public/pages/agent_details/hooks/use_agent.tsx +++ b/x-pack/legacy/plugins/fleet/public/pages/agent_details/hooks/use_agent.tsx @@ -3,11 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { useState, useEffect } from 'react'; -import { AgentsLib } from '../../../lib/agent'; +import React, { useState, useEffect } from 'react'; import { Agent } from '../../../../common/types/domain_data'; +import { useLibs } from '../../../hooks/use_libs'; -export function useGetAgent(agents: AgentsLib, id: string) { +export function useGetAgent(id: string) { + const { agents } = useLibs(); const [state, setState] = useState<{ isLoading: boolean; agent: Agent | null; @@ -18,11 +19,11 @@ export function useGetAgent(agents: AgentsLib, id: string) { error: null, }); - const fetchAgent = async () => { + const fetchAgent = async (refresh = false) => { setState({ - isLoading: true, - agent: null, + ...state, error: null, + isLoading: !refresh, }); try { const agent = await agents.get(id); @@ -45,6 +46,12 @@ export function useGetAgent(agents: AgentsLib, id: string) { return { ...state, - refreshAgent: fetchAgent, + refreshAgent: () => fetchAgent(true), }; } + +export const AgentRefreshContext = React.createContext({ refresh: () => {} }); + +export function useAgentRefresh() { + return React.useContext(AgentRefreshContext).refresh; +} diff --git a/x-pack/legacy/plugins/fleet/public/pages/agent_details/hooks/use_unenroll.tsx b/x-pack/legacy/plugins/fleet/public/pages/agent_details/hooks/use_unenroll.tsx index 6a19a9187ef1f6..ea708dd6743845 100644 --- a/x-pack/legacy/plugins/fleet/public/pages/agent_details/hooks/use_unenroll.tsx +++ b/x-pack/legacy/plugins/fleet/public/pages/agent_details/hooks/use_unenroll.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { useState } from 'react'; -import { AgentsLib } from '../../../lib/agent'; +import { useLibs } from '../../../hooks/use_libs'; -export function useUnenroll(agents: AgentsLib, refreshAgent: () => Promise, agentId: string) { +export function useUnenroll(refreshAgent: () => Promise, agentId: string) { + const { agents } = useLibs(); const [state, setState] = useState< | { confirm: false; diff --git a/x-pack/legacy/plugins/fleet/public/pages/agent_details/index.tsx b/x-pack/legacy/plugins/fleet/public/pages/agent_details/index.tsx index fb5e69519a43d7..981a7189bba3c1 100644 --- a/x-pack/legacy/plugins/fleet/public/pages/agent_details/index.tsx +++ b/x-pack/legacy/plugins/fleet/public/pages/agent_details/index.tsx @@ -14,17 +14,15 @@ import { EuiPageContent, EuiCallOut, EuiText, + EuiSpacer, } from '@elastic/eui'; import { RouteComponentProps } from 'react-router-dom'; import { AgentEventsTable } from './components/agent_events_table'; import { Loading } from '../../components/loading'; -import { PolicySection } from './components/policy_section'; import { AgentDetailSection } from './components/details_section'; -import { AgentMetadataSection } from './components/metadata_section'; -import { FrontendLibs } from '../../lib/types'; import { ModalConfirmUnenroll } from './components/modal_confirm_unenroll'; import { useUnenroll } from './hooks/use_unenroll'; -import { useGetAgent } from './hooks/use_agent'; +import { useGetAgent, AgentRefreshContext } from './hooks/use_agent'; export const Layout: SFC = ({ children }) => ( @@ -32,20 +30,17 @@ export const Layout: SFC = ({ children }) => ( ); -type Props = { - libs: FrontendLibs; -} & RouteComponentProps<{ +type Props = RouteComponentProps<{ agentId: string; }>; export const AgentDetailsPage: SFC = ({ - libs, match: { params: { agentId }, }, }) => { - const { agent, isLoading, error, refreshAgent } = useGetAgent(libs.agents, agentId); - const unenroll = useUnenroll(libs.agents, refreshAgent, agentId); + const { agent, isLoading, error, refreshAgent } = useGetAgent(agentId); + const unenroll = useUnenroll(refreshAgent, agentId); if (isLoading) { return ; @@ -81,34 +76,36 @@ export const AgentDetailsPage: SFC = ({ } return ( - - {unenroll.state.confirm && ( - - )} - - - - - - - - - - - - + + + {unenroll.state.confirm && ( + + )} + + + + + + + + + + + + - - - - + + - - - +
+ + ); }; diff --git a/x-pack/legacy/plugins/fleet/public/pages/agent_list/components/agent_enrollment.tsx b/x-pack/legacy/plugins/fleet/public/pages/agent_list/components/agent_enrollment.tsx index 38e1b0f4f9b787..18914f957a8871 100644 --- a/x-pack/legacy/plugins/fleet/public/pages/agent_list/components/agent_enrollment.tsx +++ b/x-pack/legacy/plugins/fleet/public/pages/agent_list/components/agent_enrollment.tsx @@ -20,19 +20,19 @@ import { EuiFilterGroup, EuiFilterButton, } from '@elastic/eui'; -import { FrontendLibs } from '../../../lib/types'; import { ShellEnrollmentInstructions, ContainerEnrollmentInstructions, ToolsEnrollmentInstructions, } from './enrollment_instructions'; +import { useLibs } from '../../../hooks/use_libs'; interface RouterProps { - libs: FrontendLibs; onClose: () => void; } -export const AgentEnrollmentFlyout: React.SFC = ({ libs, onClose }) => { +export const AgentEnrollmentFlyout: React.SFC = ({ onClose }) => { + const libs = useLibs(); const [quickInstallType, setQuickInstallType] = useState<'shell' | 'container' | 'tools'>( 'shell' ); diff --git a/x-pack/legacy/plugins/fleet/public/pages/agent_list/index.tsx b/x-pack/legacy/plugins/fleet/public/pages/agent_list/index.tsx index 40286959c1cadb..75a158748e5538 100644 --- a/x-pack/legacy/plugins/fleet/public/pages/agent_list/index.tsx +++ b/x-pack/legacy/plugins/fleet/public/pages/agent_list/index.tsx @@ -23,18 +23,15 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AGENT_POLLING_THRESHOLD_MS } from '../../../common/constants'; import { Agent } from '../../../common/types/domain_data'; -import { FrontendLibs } from '../../lib/types'; import { AgentHealth } from '../../components/agent_health'; import { ConnectedLink } from '../../components/navigation/connected_link'; import { usePagination } from '../../hooks/use_pagination'; import { SearchBar } from '../../components/search_bar'; import { AgentEnrollmentFlyout } from './components/agent_enrollment'; +import { useLibs } from '../../hooks/use_libs'; -interface RouterProps { - libs: FrontendLibs; -} - -export const AgentListPage: React.SFC = ({ libs }) => { +export const AgentListPage: React.SFC<{}> = () => { + const libs = useLibs(); // Agent data states const [isLoading, setIsLoading] = useState(true); const [agents, setAgents] = useState([]); @@ -170,7 +167,7 @@ export const AgentListPage: React.SFC = ({ libs }) => { {isEnrollmentFlyoutOpen ? ( - setIsEnrollmentFlyoutOpen(false)} /> + setIsEnrollmentFlyoutOpen(false)} /> ) : null} @@ -191,7 +188,7 @@ export const AgentListPage: React.SFC = ({ libs }) => { - + {libs.framework.capabilities.write && ( diff --git a/x-pack/legacy/plugins/fleet/public/routes.tsx b/x-pack/legacy/plugins/fleet/public/routes.tsx index 2daedc5e39e71c..2b20e1daaf799a 100644 --- a/x-pack/legacy/plugins/fleet/public/routes.tsx +++ b/x-pack/legacy/plugins/fleet/public/routes.tsx @@ -5,77 +5,70 @@ */ import { get } from 'lodash'; -import React, { Component } from 'react'; +import React, { useState, useEffect, SFC } from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; import { Loading } from './components/loading'; import { ChildRoutes } from './components/navigation/child_routes'; import { URLStateProps, WithURLState } from './hooks/with_url_state'; -import { FrontendLibs } from './lib/types'; import { routeMap } from './pages'; +import { useLibs } from './hooks/use_libs'; -interface RouterProps { - libs: FrontendLibs; -} +function useWaitUntilFrameworkReady() { + const libs = useLibs(); + const [isLoading, setIsLoading] = useState(true); -interface RouterState { - loading: boolean; -} + const waitUntilReady = async () => { + try { + await libs.framework.waitUntilFrameworkReady(); + } catch (e) { + // Silently swallow error + } + setIsLoading(false); + }; -export class AppRoutes extends Component { - constructor(props: RouterProps) { - super(props); - this.state = { - loading: true, - }; - } + useEffect(() => { + waitUntilReady(); + }, []); - public async componentWillMount() { - if (this.state.loading === true) { - try { - await this.props.libs.framework.waitUntilFrameworkReady(); - } catch (e) { - // Silently swallow error - } + return { isLoading }; +} - this.setState({ - loading: false, - }); - } - } +export const AppRoutes: SFC = () => { + const { isLoading } = useWaitUntilFrameworkReady(); + const libs = useLibs(); - public render() { - if (this.state.loading === true) { - return ; - } + if (isLoading === true) { + return ; + } - return ( - - {/* Redirects mapping */} - - {/* License check (UI displays when license exists but is expired) */} - {get(this.props.libs.framework.info, 'license.expired', true) && ( - - !props.location.pathname.includes('/error') ? ( - - ) : null - } - /> - )} + return ( + + {/* Redirects mapping */} + + {/* License check (UI displays when license exists but is expired) */} + {get(libs.framework.info, 'license.expired', true) && ( + + !props.location.pathname.includes('/error') ? ( + + ) : null + } + /> + )} - {!this.props.libs.framework.capabilities.read && ( - - !props.location.pathname.includes('/error') ? ( - - ) : null - } - /> - )} + {!libs.framework.capabilities.read && ( + + !props.location.pathname.includes('/error') ? ( + + ) : null + } + /> + )} - {/* Ensure security is eanabled for elastic and kibana */} - {/* TODO: Disabled for now as we don't have this info set up on backend yet */} - {/* {!get(this.props.libs.framework.info, 'security.enabled', true) && ( + {/* Ensure security is eanabled for elastic and kibana */} + {/* TODO: Disabled for now as we don't have this info set up on backend yet */} + {/* {!get(this.props.libs.framework.info, 'security.enabled', true) && ( !props.location.pathname.includes('/error') ? ( @@ -85,23 +78,14 @@ export class AppRoutes extends Component { /> )} */} - {/* This app does not make use of a homepage. The main page is agents list */} - } /> - + {/* This app does not make use of a homepage. The main page is agents list */} + } /> + - {/* Render routes from the FS */} - - {(URLProps: URLStateProps) => ( - - )} - - - ); - } -} + {/* Render routes from the FS */} + + {(URLProps: URLStateProps) => } + + + ); +}; diff --git a/x-pack/legacy/plugins/fleet/server/libs/agent.ts b/x-pack/legacy/plugins/fleet/server/libs/agent.ts index c8af35a4726c21..47ce7f27967c1a 100644 --- a/x-pack/legacy/plugins/fleet/server/libs/agent.ts +++ b/x-pack/legacy/plugins/fleet/server/libs/agent.ts @@ -102,6 +102,33 @@ export class AgentLib { } } + public async update( + user: FrameworkUser, + agentId: string, + data: { + user_provided_metadata?: any; + } + ) { + const agent = await this.getById(user, agentId); + if (!agent) { + throw Boom.notFound('Agent not found'); + } + + if (data.user_provided_metadata) { + const localMetadataKeys = Object.keys(agent.local_metadata || {}); + + const hasConflict = Object.keys(data.user_provided_metadata).find( + k => localMetadataKeys.indexOf(k) >= 0 + ); + + if (hasConflict) { + throw Boom.badRequest(`It's not allowed to update local metadata (${hasConflict}).`); + } + } + + this.agentsRepository.update(user, agentId, data); + } + public async unenroll( user: FrameworkUser, ids: string[] diff --git a/x-pack/legacy/plugins/fleet/server/repositories/agents/default.ts b/x-pack/legacy/plugins/fleet/server/repositories/agents/default.ts index 6744ee4a174894..6ec63eb47a3e72 100644 --- a/x-pack/legacy/plugins/fleet/server/repositories/agents/default.ts +++ b/x-pack/legacy/plugins/fleet/server/repositories/agents/default.ts @@ -109,7 +109,7 @@ export class AgentsRepository implements AgentsRepositoryType { if (newData.local_metadata) { updateData.local_metadata = JSON.stringify(newData.local_metadata); } - if (updateData.user_provided_metadata) { + if (newData.user_provided_metadata) { updateData.user_provided_metadata = JSON.stringify(newData.user_provided_metadata); } diff --git a/x-pack/legacy/plugins/fleet/server/routes/agents/put.ts b/x-pack/legacy/plugins/fleet/server/routes/agents/put.ts new file mode 100644 index 00000000000000..47a2bdd8afe079 --- /dev/null +++ b/x-pack/legacy/plugins/fleet/server/routes/agents/put.ts @@ -0,0 +1,40 @@ +/* + * 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 * as Joi from 'joi'; +import { FrameworkRequest } from '../../adapters/framework/adapter_types'; +import { ReturnTypeUpdate } from '../../../common/return_types'; +import { FleetServerLib } from '../../libs/types'; +import { Agent } from '../../../common/types/domain_data'; + +export const createPUTAgentsRoute = (libs: FleetServerLib) => ({ + method: 'PUT', + path: '/api/fleet/agents/{agentId}', + options: { + tags: ['access:fleet-write'], + validate: { + payload: Joi.object({ + user_provided_metadata: Joi.object().optional(), + }), + }, + }, + handler: async ( + request: FrameworkRequest<{ + params: { agentId: string }; + payload: { + user_provided_metadata: any; + }; + }> + ): Promise> => { + const { user, params, payload } = request; + const { agentId } = params; + + await libs.agents.update(user, agentId, payload); + const agent = (await libs.agents.getById(user, agentId)) as Agent; + + return { item: agent, success: true, action: 'updated' }; + }, +}); diff --git a/x-pack/legacy/plugins/fleet/server/routes/init_api.ts b/x-pack/legacy/plugins/fleet/server/routes/init_api.ts index aba8093af5573a..b67e99e0a6bf08 100644 --- a/x-pack/legacy/plugins/fleet/server/routes/init_api.ts +++ b/x-pack/legacy/plugins/fleet/server/routes/init_api.ts @@ -23,6 +23,7 @@ import { createGETAgentEventsRoute } from './agents/events'; import { createGETInstallScript } from './install'; import { createGETAgentsRoute } from './agents/get'; import { createPOSTAgentsUnenrollRoute } from './agents/unenroll'; +import { createPUTAgentsRoute } from './agents/put'; export function initRestApi(server: Server, libs: FleetServerLib) { const frameworkAdapter = new HapiFrameworkAdapter(server); @@ -38,6 +39,7 @@ export function initRestApi(server: Server, libs: FleetServerLib) { function createAgentsRoutes(adapter: HapiFrameworkAdapter, libs: FleetServerLib) { adapter.registerRoute(createListAgentsRoute(libs)); adapter.registerRoute(createGETAgentsRoute(libs)); + adapter.registerRoute(createPUTAgentsRoute(libs)); adapter.registerRoute(createDeleteAgentsRoute(libs)); adapter.registerRoute(createEnrollAgentsRoute(libs)); adapter.registerRoute(createPOSTAgentsUnenrollRoute(libs));