Skip to content

Commit

Permalink
[Fleet] [Debug UI] Implement "Agent Policy Debugger" UI (#131335)
Browse files Browse the repository at this point in the history
* Add initial agent policy debugger module

* Fix clear button in agent policy select

* Implement deletion of selected policy

* Fix layout of combo-box/button

* Add searchable ID to agent policy labels

* Add description text to debugger module

* Fixup loading/error logic
  • Loading branch information
kpollich authored May 3, 2022
1 parent 11b6825 commit 6ee7ff0
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState } from 'react';
import {
EuiButton,
EuiCallOut,
EuiCode,
EuiComboBox,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';

import { useQuery } from 'react-query';

import { FormattedMessage } from '@kbn/i18n-react';

import { sendGetAgentPolicies, useLink } from '../../../hooks';
import { SO_SEARCH_LIMIT } from '../../../constants';

import { policyHasFleetServer } from '../../../services';
import type { AgentPolicy } from '../../../types';
import { AgentPolicyDeleteProvider } from '../../agent_policy/components';

import { queryClient } from '..';

import { CodeBlock } from './code_block';

const fetchAgentPolicies = async () => {
const response = await sendGetAgentPolicies({
full: true,
perPage: SO_SEARCH_LIMIT,
sortOrder: 'asc',
});

if (response.error) {
throw new Error(response.error.message);
}

return response;
};

export const AgentPolicyDebugger: React.FunctionComponent = () => {
const { getHref } = useLink();
const [selectedPolicyId, setSelectedPolicyId] = useState<string>();

// TODO: Depending on the number of agent policies, this might need to be switched to
// `useInfinite` query with an infinite scrolling approach in the dropdown options.
const { data, status } = useQuery('debug-agent-policies', fetchAgentPolicies);

const agentPolicies = data?.data?.items ?? [];
const comboBoxOptions = agentPolicies.map((policy) => ({
label: `${policy.name} - ${policy.id}`,
value: policy.id,
}));

const selectedOptions = selectedPolicyId
? [comboBoxOptions.find((option) => option.value === selectedPolicyId)!]
: [];

const selectedAgentPolicy = agentPolicies.find((policy) => policy.id === selectedPolicyId);

const onDelete = () => {
setSelectedPolicyId(undefined);
queryClient.invalidateQueries('debug-agent-policies');
};

if (status === 'error') {
return (
<EuiCallOut title="Error" color="danger">
Error fetching Agent Policies
</EuiCallOut>
);
}

return (
<>
<EuiTitle size="l">
<h2>Agent Policy Debugger</h2>
</EuiTitle>

<EuiSpacer size="m" />

<EuiText grow={false}>
<p>
Search for an Agent Policy using its name or <EuiCode>id</EuiCode> value. Use the code
block below to diagnose any potential issues with the policy{"'"}s configuration.
</p>
</EuiText>

<EuiSpacer size="m" />

<EuiFlexGroup alignItems="center" justifyContent="flexStart">
<EuiFlexItem
grow={false}
css={`
min-width: 600px;
`}
>
<EuiComboBox
aria-label="Select an Agent Policy"
placeholder="Select an Agent Policy"
fullWidth
options={comboBoxOptions}
singleSelection={{ asPlainText: true }}
selectedOptions={selectedOptions}
isLoading={status === 'loading'}
onChange={(newSelectedOptions) => {
// Handle "clear" action
if (!newSelectedOptions.length) {
setSelectedPolicyId(undefined);
} else {
setSelectedPolicyId(newSelectedOptions[0].value);
}
}}
/>
</EuiFlexItem>
{selectedPolicyId && (
<AgentPolicyDeleteProvider
hasFleetServer={policyHasFleetServer(selectedAgentPolicy as AgentPolicy)}
>
{(deleteAgentPolicyPrompt) => {
return (
<EuiFlexItem grow={false}>
<div>
<EuiButton
color="danger"
onClick={() => deleteAgentPolicyPrompt(selectedPolicyId, onDelete)}
>
<FormattedMessage
id="xpack.fleet.policyForm.deletePolicyActionText"
defaultMessage="Delete policy"
/>
</EuiButton>
</div>
</EuiFlexItem>
);
}}
</AgentPolicyDeleteProvider>
)}
</EuiFlexGroup>

{selectedPolicyId && (
<>
<EuiSpacer size="m" />

<EuiLink target="_blank" href={getHref('policy_details', { policyId: selectedPolicyId })}>
View Agent Policy in Fleet UI
</EuiLink>

<EuiSpacer size="m" />

<CodeBlock value={JSON.stringify(selectedAgentPolicy, null, 2)} />
</>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';

import { CodeEditor } from '@kbn/kibana-react-plugin/public';

/**
* A read-only code block with various default settings suitable for displaying API responses, etc
*/
export const CodeBlock: React.FunctionComponent<{ value: string }> = ({ value }) => {
return (
<CodeEditor
isCopyable
languageId=""
height="600px"
width="100%"
options={{
minimap: {
enabled: false,
},
scrollBeyondLastLine: false,
readOnly: true,
tabSize: 2,
lineNumbers: 'off',
lineNumbersMinChars: 0,
glyphMargin: false,
lineDecorationsWidth: 0,
overviewRulerBorder: false,
}}
value={value}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export * from './agent_policy_debugger';
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,33 @@
*/

import React from 'react';
import { EuiPage, EuiPageBody, EuiPageHeader, EuiSpacer } from '@elastic/eui';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';

import { AgentPolicyDebugger } from './components';

// TODO: Evaluate moving this react-query initialization up to the main Fleet app
// setup if we end up pursuing wider adoption of react-query.
const queryClient = new QueryClient();
export const queryClient = new QueryClient();

export const DebugPage: React.FunctionComponent = () => {
return (
<QueryClientProvider client={queryClient}>
<main>Debug Page</main>
<EuiPage>
<EuiPageBody panelled>
<EuiPageHeader
pageTitle="Fleet Debugging Dashboard"
iconType="wrench"
description={`This page provides an interface for managing Fleet's data and diagnosing issues.
Be aware that these debugging tools can be destructive in nature, and you should proceed with caution.`}
/>

<EuiSpacer size="xl" />

<AgentPolicyDebugger />
</EuiPageBody>
</EuiPage>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
Expand Down

0 comments on commit 6ee7ff0

Please sign in to comment.