mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Fleet][Agent Tamper Protection] Uninstall token table (#161760)
## Summary This PR adds an `Uninstall Tokens` tab to Fleet, where the user can: - see uninstall tokens for all policies - filter by policy ID (partial match) - view the hidden token by pressing the 👁️ button - open the Uninstall Command flyout to view the uninstall command for the selected policy <img src="0a98e26e
-09df-40e4-81de-02afebc5f830)" width="500px" /> <img src="c41d0841
-b496-440b-bfb4-cd28907e3196)" width="500px" /> <img src="c5c613b9
-9166-4bcc-8d7a-4a7b34f0698e)" width="500px" /> ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
b343bf9611
commit
d5996d7487
18 changed files with 872 additions and 177 deletions
|
@ -26,10 +26,13 @@ import {
|
|||
SETTINGS_FLEET_SERVER_HOST_HEADING,
|
||||
FLEET_SERVER_SETUP,
|
||||
LANDING_PAGE_ADD_FLEET_SERVER_BUTTON,
|
||||
UNINSTALL_TOKENS_TAB,
|
||||
UNINSTALL_TOKENS,
|
||||
} from '../../screens/fleet';
|
||||
import { AGENT_POLICY_NAME_LINK } from '../../screens/integrations';
|
||||
import { cleanupAgentPolicies, unenrollAgent } from '../../tasks/cleanup';
|
||||
import { setFleetServerHost } from '../../tasks/fleet_server';
|
||||
|
||||
describe('Home page', () => {
|
||||
before(() => {
|
||||
setFleetServerHost('https://fleetserver:8220');
|
||||
|
@ -143,6 +146,38 @@ describe('Home page', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Uninstall Tokens', () => {
|
||||
before(() => {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/fleet/agent_policies',
|
||||
body: { name: 'Agent policy for A11y test', namespace: 'default', id: 'agent-policy-a11y' },
|
||||
headers: { 'kbn-xsrf': 'cypress' },
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
navigateTo(FLEET);
|
||||
cy.getBySel(UNINSTALL_TOKENS_TAB).click();
|
||||
});
|
||||
after(() => {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/fleet/agent_policies/delete',
|
||||
body: { agentPolicyId: 'agent-policy-a11y' },
|
||||
headers: { 'kbn-xsrf': 'kibana' },
|
||||
});
|
||||
});
|
||||
it('Uninstall Tokens Table', () => {
|
||||
cy.getBySel(UNINSTALL_TOKENS.POLICY_ID_TABLE_FIELD).first().should('be.visible');
|
||||
checkA11y({ skipFailures: false });
|
||||
});
|
||||
it('Uninstall Command Flyout', () => {
|
||||
cy.getBySel(UNINSTALL_TOKENS.VIEW_UNINSTALL_COMMAND_BUTTON).first().click();
|
||||
cy.getBySel(UNINSTALL_TOKENS.UNINSTALL_COMMAND_FLYOUT).should('be.visible');
|
||||
checkA11y({ skipFailures: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data Streams', () => {
|
||||
before(() => {
|
||||
navigateTo(FLEET);
|
||||
|
|
|
@ -5,13 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FLEET, navigateTo } from '../tasks/navigation';
|
||||
import { cleanupAgentPolicies } from '../tasks/cleanup';
|
||||
import { ENROLLMENT_TOKENS_TAB, ENROLLMENT_TOKENS } from '../screens/fleet';
|
||||
import { ENROLLMENT_TOKENS } from '../screens/fleet';
|
||||
|
||||
describe('Enrollment token page', () => {
|
||||
before(() => {
|
||||
navigateTo(FLEET);
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/fleet/agent_policies',
|
||||
|
@ -24,9 +22,6 @@ describe('Enrollment token page', () => {
|
|||
},
|
||||
headers: { 'kbn-xsrf': 'cypress' },
|
||||
});
|
||||
cy.getBySel(ENROLLMENT_TOKENS_TAB, {
|
||||
timeout: 15000,
|
||||
}).click();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
|
@ -34,6 +29,7 @@ describe('Enrollment token page', () => {
|
|||
});
|
||||
|
||||
it('Create new Token', () => {
|
||||
cy.visit('app/fleet/enrollment-tokens');
|
||||
cy.getBySel(ENROLLMENT_TOKENS.CREATE_TOKEN_BUTTON).click();
|
||||
cy.getBySel(ENROLLMENT_TOKENS.CREATE_TOKEN_MODAL_NAME_FIELD).clear().type('New Token');
|
||||
cy.getBySel(ENROLLMENT_TOKENS.CREATE_TOKEN_MODAL_SELECT_FIELD).contains('Agent policy 1');
|
||||
|
|
90
x-pack/plugins/fleet/cypress/e2e/uninstall_token.cy.ts
Normal file
90
x-pack/plugins/fleet/cypress/e2e/uninstall_token.cy.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 type { UninstallToken } from '../../common/types/models/uninstall_token';
|
||||
|
||||
import { cleanupAgentPolicies } from '../tasks/cleanup';
|
||||
import { UNINSTALL_TOKENS } from '../screens/fleet';
|
||||
import type { GetUninstallTokenResponse } from '../../common/types/rest_spec/uninstall_token';
|
||||
|
||||
describe('Uninstall token page', () => {
|
||||
before(() => {
|
||||
cleanupAgentPolicies();
|
||||
generatePolicies();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('app/fleet/uninstall-tokens');
|
||||
cy.intercept('GET', 'api/fleet/uninstall_tokens/*').as('getTokenRequest');
|
||||
|
||||
cy.getBySel(UNINSTALL_TOKENS.POLICY_ID_TABLE_FIELD)
|
||||
.first()
|
||||
.then(($policyIdField) => $policyIdField[0].textContent)
|
||||
.as('policyIdInFirstLine');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cleanupAgentPolicies();
|
||||
});
|
||||
|
||||
it('should show token by clicking on the eye button', () => {
|
||||
// tokens are hidden by default
|
||||
cy.getBySel(UNINSTALL_TOKENS.TOKEN_FIELD).each(($tokenField) => {
|
||||
expect($tokenField).to.contain.text('••••••••••••••••••••••••••••••••');
|
||||
});
|
||||
|
||||
// token is reveiled when clicking on eye button
|
||||
cy.getBySel(UNINSTALL_TOKENS.SHOW_HIDE_TOKEN_BUTTON).first().click();
|
||||
|
||||
// we should show the correct token for the correct policy ID
|
||||
waitForFetchingUninstallToken().then((fetchedToken) => {
|
||||
cy.get('@policyIdInFirstLine').should('equal', fetchedToken.policy_id);
|
||||
|
||||
cy.getBySel(UNINSTALL_TOKENS.TOKEN_FIELD)
|
||||
.first()
|
||||
.should('not.contain.text', '••••••••••••••••••••••••••••••••')
|
||||
.should('contain.text', fetchedToken.token);
|
||||
});
|
||||
});
|
||||
|
||||
it("should show flyout by clicking on 'View uninstall command' button", () => {
|
||||
cy.getBySel(UNINSTALL_TOKENS.VIEW_UNINSTALL_COMMAND_BUTTON).first().click();
|
||||
|
||||
waitForFetchingUninstallToken().then((fetchedToken) => {
|
||||
cy.get('@policyIdInFirstLine').should('equal', fetchedToken.policy_id);
|
||||
|
||||
cy.getBySel(UNINSTALL_TOKENS.UNINSTALL_COMMAND_FLYOUT).should('exist');
|
||||
|
||||
cy.contains(`sudo elastic-agent uninstall --uninstall-token ${fetchedToken.token}`);
|
||||
cy.contains(`Valid for the following agent policy: ${fetchedToken.policy_id}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter for policy ID by partial match', () => {
|
||||
cy.getBySel(UNINSTALL_TOKENS.POLICY_ID_TABLE_FIELD).should('have.length.at.least', 3);
|
||||
|
||||
cy.getBySel(UNINSTALL_TOKENS.POLICY_ID_SEARCH_FIELD).type('licy-300');
|
||||
|
||||
cy.getBySel(UNINSTALL_TOKENS.POLICY_ID_TABLE_FIELD).should('have.length', 1);
|
||||
});
|
||||
|
||||
const generatePolicies = () => {
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/fleet/agent_policies',
|
||||
body: { name: `Agent policy ${i}00`, namespace: 'default', id: `agent-policy-${i}00` },
|
||||
headers: { 'kbn-xsrf': 'cypress' },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const waitForFetchingUninstallToken = (): Cypress.Chainable<UninstallToken> =>
|
||||
cy
|
||||
.wait('@getTokenRequest')
|
||||
.then((interception) => (interception.response?.body as GetUninstallTokenResponse).item);
|
||||
});
|
|
@ -12,6 +12,7 @@ export const LANDING_PAGE_ADD_FLEET_SERVER_BUTTON = 'fleetServerLanding.addFleet
|
|||
export const AGENTS_TAB = 'fleet-agents-tab';
|
||||
export const AGENT_POLICIES_TAB = 'fleet-agent-policies-tab';
|
||||
export const ENROLLMENT_TOKENS_TAB = 'fleet-enrollment-tokens-tab';
|
||||
export const UNINSTALL_TOKENS_TAB = 'fleet-uninstall-tokens-tab';
|
||||
export const DATA_STREAMS_TAB = 'fleet-datastreams-tab';
|
||||
export const SETTINGS_TAB = 'fleet-settings-tab';
|
||||
|
||||
|
@ -51,6 +52,16 @@ export const ENROLLMENT_TOKENS = {
|
|||
LIST_TABLE: 'enrollmentTokenListTable',
|
||||
TABLE_REVOKE_BTN: 'enrollmentTokenTable.revokeBtn',
|
||||
};
|
||||
|
||||
export const UNINSTALL_TOKENS = {
|
||||
POLICY_ID_SEARCH_FIELD: 'uninstallTokensPolicyIdSearchInput',
|
||||
POLICY_ID_TABLE_FIELD: 'uninstallTokensPolicyIdField',
|
||||
VIEW_UNINSTALL_COMMAND_BUTTON: 'uninstallTokensViewCommandButton',
|
||||
UNINSTALL_COMMAND_FLYOUT: 'uninstall-command-flyout',
|
||||
TOKEN_FIELD: 'apiKeyField',
|
||||
SHOW_HIDE_TOKEN_BUTTON: 'showHideTokenButton',
|
||||
};
|
||||
|
||||
export const SETTINGS_FLEET_SERVER_HOST_HEADING = 'fleetServerHostHeader';
|
||||
export const SETTINGS_SAVE_BTN = 'saveApplySettingsBtn';
|
||||
|
||||
|
|
|
@ -60,8 +60,10 @@ import { AgentsApp } from './sections/agents';
|
|||
import { MissingESRequirementsPage } from './sections/agents/agent_requirements_page';
|
||||
import { CreatePackagePolicyPage } from './sections/agent_policy/create_package_policy_page';
|
||||
import { EnrollmentTokenListPage } from './sections/agents/enrollment_token_list_page';
|
||||
import { UninstallTokenListPage } from './sections/agents/uninstall_token_list_page';
|
||||
import { SettingsApp } from './sections/settings';
|
||||
import { DebugPage } from './sections/debug';
|
||||
import { ExperimentalFeaturesService } from './services';
|
||||
|
||||
const FEEDBACK_URL = 'https://ela.st/fleet-feedback';
|
||||
|
||||
|
@ -320,6 +322,7 @@ export const AppRoutes = memo(
|
|||
({ setHeaderActionMenu }: { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'] }) => {
|
||||
const flyoutContext = useFlyoutContext();
|
||||
const fleetStatus = useFleetStatus();
|
||||
const { agentTamperProtectionEnabled } = ExperimentalFeaturesService.get();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -335,6 +338,11 @@ export const AppRoutes = memo(
|
|||
<Route path={FLEET_ROUTING_PATHS.enrollment_tokens}>
|
||||
<EnrollmentTokenListPage />
|
||||
</Route>
|
||||
{agentTamperProtectionEnabled && (
|
||||
<Route path={FLEET_ROUTING_PATHS.uninstall_tokens}>
|
||||
<UninstallTokenListPage />
|
||||
</Route>
|
||||
)}
|
||||
<Route path={FLEET_ROUTING_PATHS.data_streams}>
|
||||
<DataStreamApp />
|
||||
</Route>
|
||||
|
|
|
@ -139,6 +139,14 @@ const breadcrumbGetters: {
|
|||
}),
|
||||
},
|
||||
],
|
||||
uninstall_tokens: () => [
|
||||
BASE_BREADCRUMB,
|
||||
{
|
||||
text: i18n.translate('xpack.fleet.breadcrumbs.uninstallTokensPageTitle', {
|
||||
defaultMessage: 'Uninstall tokens',
|
||||
}),
|
||||
},
|
||||
],
|
||||
data_streams: () => [
|
||||
BASE_BREADCRUMB,
|
||||
{
|
||||
|
|
|
@ -12,6 +12,8 @@ import type { Section } from '../../sections';
|
|||
import { useLink, useConfig } from '../../hooks';
|
||||
import { WithHeaderLayout } from '../../../../layouts';
|
||||
|
||||
import { ExperimentalFeaturesService } from '../../services';
|
||||
|
||||
import { DefaultPageTitle } from './default_page_title';
|
||||
|
||||
interface Props {
|
||||
|
@ -27,70 +29,79 @@ export const DefaultLayout: React.FunctionComponent<Props> = ({
|
|||
}) => {
|
||||
const { getHref } = useLink();
|
||||
const { agents } = useConfig();
|
||||
const { agentTamperProtectionEnabled } = ExperimentalFeaturesService.get();
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: (
|
||||
<FormattedMessage id="xpack.fleet.appNavigation.agentsLinkText" defaultMessage="Agents" />
|
||||
),
|
||||
isSelected: section === 'agents',
|
||||
href: getHref('agent_list'),
|
||||
disabled: !agents?.enabled,
|
||||
'data-test-subj': 'fleet-agents-tab',
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.appNavigation.policiesLinkText"
|
||||
defaultMessage="Agent policies"
|
||||
/>
|
||||
),
|
||||
isSelected: section === 'agent_policies',
|
||||
href: getHref('policies_list'),
|
||||
'data-test-subj': 'fleet-agent-policies-tab',
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.appNavigation.enrollmentTokensText"
|
||||
defaultMessage="Enrollment tokens"
|
||||
/>
|
||||
),
|
||||
isSelected: section === 'enrollment_tokens',
|
||||
href: getHref('enrollment_tokens'),
|
||||
'data-test-subj': 'fleet-enrollment-tokens-tab',
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.appNavigation.uninstallTokensText"
|
||||
defaultMessage="Uninstall tokens"
|
||||
/>
|
||||
),
|
||||
isSelected: section === 'uninstall_tokens',
|
||||
href: getHref('uninstall_tokens'),
|
||||
'data-test-subj': 'fleet-uninstall-tokens-tab',
|
||||
isHidden: !agentTamperProtectionEnabled, // needed only for agentTamperProtectionEnabled feature flag
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.appNavigation.dataStreamsLinkText"
|
||||
defaultMessage="Data streams"
|
||||
/>
|
||||
),
|
||||
isSelected: section === 'data_streams',
|
||||
href: getHref('data_streams'),
|
||||
'data-test-subj': 'fleet-datastreams-tab',
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.appNavigation.settingsLinkText"
|
||||
defaultMessage="Settings"
|
||||
/>
|
||||
),
|
||||
isSelected: section === 'settings',
|
||||
href: getHref('settings'),
|
||||
'data-test-subj': 'fleet-settings-tab',
|
||||
},
|
||||
// the filtering below is needed only for agentTamperProtectionEnabled feature flag
|
||||
].filter(({ isHidden }) => !isHidden);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout
|
||||
leftColumn={<DefaultPageTitle />}
|
||||
rightColumn={rightColumn}
|
||||
tabs={[
|
||||
{
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.appNavigation.agentsLinkText"
|
||||
defaultMessage="Agents"
|
||||
/>
|
||||
),
|
||||
isSelected: section === 'agents',
|
||||
href: getHref('agent_list'),
|
||||
disabled: !agents?.enabled,
|
||||
'data-test-subj': 'fleet-agents-tab',
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.appNavigation.policiesLinkText"
|
||||
defaultMessage="Agent policies"
|
||||
/>
|
||||
),
|
||||
isSelected: section === 'agent_policies',
|
||||
href: getHref('policies_list'),
|
||||
'data-test-subj': 'fleet-agent-policies-tab',
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.appNavigation.enrollmentTokensText"
|
||||
defaultMessage="Enrollment tokens"
|
||||
/>
|
||||
),
|
||||
isSelected: section === 'enrollment_tokens',
|
||||
href: getHref('enrollment_tokens'),
|
||||
'data-test-subj': 'fleet-enrollment-tokens-tab',
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.appNavigation.dataStreamsLinkText"
|
||||
defaultMessage="Data streams"
|
||||
/>
|
||||
),
|
||||
isSelected: section === 'data_streams',
|
||||
href: getHref('data_streams'),
|
||||
'data-test-subj': 'fleet-datastreams-tab',
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.appNavigation.settingsLinkText"
|
||||
defaultMessage="Settings"
|
||||
/>
|
||||
),
|
||||
isSelected: section === 'settings',
|
||||
href: getHref('settings'),
|
||||
'data-test-subj': 'fleet-settings-tab',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<WithHeaderLayout leftColumn={<DefaultPageTitle />} rightColumn={rightColumn} tabs={tabs}>
|
||||
{children}
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
|
|
|
@ -20,7 +20,11 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage, FormattedDate } from '@kbn/i18n-react';
|
||||
import type { SendRequestResponse } from '@kbn/es-ui-shared-plugin/public/request/send_request';
|
||||
|
||||
import { ApiKeyField } from '../../../../../components/api_key_field';
|
||||
|
||||
import type { GetOneEnrollmentAPIKeyResponse } from '../../../../../../common/types';
|
||||
import {
|
||||
ENROLLMENT_API_KEYS_INDEX,
|
||||
SO_SEARCH_LIMIT,
|
||||
|
@ -42,73 +46,6 @@ import { DefaultLayout } from '../../../layouts';
|
|||
|
||||
import { ConfirmEnrollmentTokenDelete } from './components/confirm_delete_modal';
|
||||
|
||||
const ApiKeyField: React.FunctionComponent<{ apiKeyId: string }> = ({ apiKeyId }) => {
|
||||
const { notifications } = useStartServices();
|
||||
const [state, setState] = useState<'VISIBLE' | 'HIDDEN' | 'LOADING'>('HIDDEN');
|
||||
const [key, setKey] = useState<string | undefined>();
|
||||
|
||||
const toggleKey = async () => {
|
||||
if (state === 'VISIBLE') {
|
||||
setState('HIDDEN');
|
||||
} else if (state === 'HIDDEN') {
|
||||
try {
|
||||
setState('LOADING');
|
||||
const res = await sendGetOneEnrollmentAPIKey(apiKeyId);
|
||||
if (res.error) {
|
||||
throw res.error;
|
||||
}
|
||||
setKey(res.data?.item.api_key);
|
||||
setState('VISIBLE');
|
||||
} catch (err) {
|
||||
notifications.toasts.addError(err as Error, {
|
||||
title: 'Error',
|
||||
});
|
||||
setState('HIDDEN');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" size="xs">
|
||||
{state === 'VISIBLE'
|
||||
? key
|
||||
: '•••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••'}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
state === 'VISIBLE'
|
||||
? i18n.translate('xpack.fleet.enrollmentTokensList.hideTokenButtonLabel', {
|
||||
defaultMessage: 'Hide token',
|
||||
})
|
||||
: i18n.translate('xpack.fleet.enrollmentTokensList.showTokenButtonLabel', {
|
||||
defaultMessage: 'Show token',
|
||||
})
|
||||
}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label={
|
||||
state === 'VISIBLE'
|
||||
? i18n.translate('xpack.fleet.enrollmentTokensList.hideTokenButtonLabel', {
|
||||
defaultMessage: 'Hide token',
|
||||
})
|
||||
: i18n.translate('xpack.fleet.enrollmentTokensList.showTokenButtonLabel', {
|
||||
defaultMessage: 'Show token',
|
||||
})
|
||||
}
|
||||
color="text"
|
||||
onClick={toggleKey}
|
||||
iconType={state === 'VISIBLE' ? 'eyeClosed' : 'eye'}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const DeleteButton: React.FunctionComponent<{ apiKey: EnrollmentAPIKey; refresh: () => void }> = ({
|
||||
apiKey,
|
||||
refresh,
|
||||
|
@ -211,7 +148,16 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => {
|
|||
}),
|
||||
width: '215px',
|
||||
render: (apiKeyId: string) => {
|
||||
return <ApiKeyField apiKeyId={apiKeyId} />;
|
||||
return (
|
||||
<ApiKeyField
|
||||
apiKeyId={apiKeyId}
|
||||
sendGetAPIKey={sendGetOneEnrollmentAPIKey}
|
||||
tokenGetter={(response: SendRequestResponse<GetOneEnrollmentAPIKeyResponse>) =>
|
||||
response.data?.item.api_key
|
||||
}
|
||||
length={60}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* 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 type { UseRequestResponse } from '@kbn/es-ui-shared-plugin/public';
|
||||
|
||||
import { waitFor, fireEvent } from '@testing-library/react';
|
||||
|
||||
import type {
|
||||
GetUninstallTokensMetadataResponse,
|
||||
GetUninstallTokenResponse,
|
||||
} from '../../../../../../common/types/rest_spec/uninstall_token';
|
||||
import type { RequestError } from '../../../../../hooks';
|
||||
import { createFleetTestRendererMock } from '../../../../../mock';
|
||||
import {
|
||||
useGetUninstallToken,
|
||||
useGetUninstallTokens,
|
||||
sendGetUninstallToken,
|
||||
} from '../../../../../hooks/use_request/uninstall_tokens';
|
||||
import type {
|
||||
UninstallToken,
|
||||
UninstallTokenMetadata,
|
||||
} from '../../../../../../common/types/models/uninstall_token';
|
||||
|
||||
import { UninstallTokenListPage } from '.';
|
||||
|
||||
jest.mock('../../../../../hooks/use_request/uninstall_tokens', () => ({
|
||||
useGetUninstallToken: jest.fn(),
|
||||
useGetUninstallTokens: jest.fn(),
|
||||
sendGetUninstallToken: jest.fn(),
|
||||
}));
|
||||
|
||||
type MockResponseType<DataType> = Pick<
|
||||
UseRequestResponse<DataType, RequestError>,
|
||||
'data' | 'error' | 'isLoading'
|
||||
>;
|
||||
|
||||
describe('UninstallTokenList page', () => {
|
||||
const render = () => {
|
||||
const renderer = createFleetTestRendererMock();
|
||||
|
||||
return renderer.render(<UninstallTokenListPage />);
|
||||
};
|
||||
|
||||
const useGetUninstallTokenMock = useGetUninstallToken as jest.Mock;
|
||||
const useGetUninstallTokensMock = useGetUninstallTokens as jest.Mock;
|
||||
const sendGetUninstallTokenMock = sendGetUninstallToken as jest.Mock;
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when loading tokens', () => {
|
||||
it('should show loading message', () => {
|
||||
const getTokensResponseFixture: MockResponseType<GetUninstallTokensMetadataResponse> = {
|
||||
isLoading: true,
|
||||
error: null,
|
||||
};
|
||||
useGetUninstallTokensMock.mockReturnValue(getTokensResponseFixture);
|
||||
|
||||
const renderResult = render();
|
||||
|
||||
expect(renderResult.queryByTestId('uninstallTokenListTable')).toBeInTheDocument();
|
||||
expect(renderResult.queryByText('Loading uninstall tokens...')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are no uninstall tokens', () => {
|
||||
it('should show message when no tokens found', () => {
|
||||
const getTokensResponseFixture: MockResponseType<GetUninstallTokensMetadataResponse> = {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
data: { items: [], total: 0, page: 1, perPage: 20 },
|
||||
};
|
||||
useGetUninstallTokensMock.mockReturnValue(getTokensResponseFixture);
|
||||
|
||||
const renderResult = render();
|
||||
|
||||
expect(renderResult.queryByTestId('uninstallTokenListTable')).toBeInTheDocument();
|
||||
expect(renderResult.queryByText('No uninstall tokens found.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are tokens', () => {
|
||||
const uninstallTokenMetadataFixture1: UninstallTokenMetadata = {
|
||||
id: 'id-1',
|
||||
policy_id: 'policy-id-1',
|
||||
created_at: '2023-06-19T08:47:31.457Z',
|
||||
};
|
||||
|
||||
const uninstallTokenMetadataFixture2: UninstallTokenMetadata = {
|
||||
id: 'id-2',
|
||||
policy_id: 'policy-id-2',
|
||||
created_at: '2023-06-20T08:47:31.457Z',
|
||||
};
|
||||
|
||||
const uninstallTokenFixture: UninstallToken = {
|
||||
...uninstallTokenMetadataFixture1,
|
||||
token: '123456789',
|
||||
};
|
||||
|
||||
const getTokensResponseFixture: MockResponseType<GetUninstallTokensMetadataResponse> = {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
data: {
|
||||
items: [uninstallTokenMetadataFixture1, uninstallTokenMetadataFixture2],
|
||||
total: 2,
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
},
|
||||
};
|
||||
|
||||
const getTokenResponseFixture: MockResponseType<GetUninstallTokenResponse> = {
|
||||
error: null,
|
||||
isLoading: false,
|
||||
data: { item: uninstallTokenFixture },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
useGetUninstallTokensMock.mockReturnValue(getTokensResponseFixture);
|
||||
});
|
||||
|
||||
it('should render table with token', () => {
|
||||
const renderResult = render();
|
||||
|
||||
expect(renderResult.queryByTestId('uninstallTokenListTable')).toBeInTheDocument();
|
||||
expect(renderResult.queryByText('policy-id-1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide token by default', () => {
|
||||
const renderResult = render();
|
||||
|
||||
expect(renderResult.queryByText(uninstallTokenFixture.token)).not.toBeInTheDocument();
|
||||
expect(renderResult.queryAllByText('••••••••••••••••••••••••••••••••').length).toBe(2);
|
||||
});
|
||||
|
||||
it('should fetch and show token when clicking on the "Show" button', async () => {
|
||||
sendGetUninstallTokenMock.mockReturnValue(getTokenResponseFixture);
|
||||
|
||||
const renderResult = render();
|
||||
|
||||
renderResult.getAllByTestId('showHideTokenButton')[0].click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(renderResult.queryByText(uninstallTokenFixture.token)).toBeInTheDocument();
|
||||
});
|
||||
expect(sendGetUninstallTokenMock).toHaveBeenCalledWith(uninstallTokenMetadataFixture1.id);
|
||||
});
|
||||
|
||||
it('should show flyout for uninstall command when clicking on the "View uninstall command" button', async () => {
|
||||
useGetUninstallTokenMock.mockReturnValue(getTokenResponseFixture);
|
||||
const renderResult = render();
|
||||
|
||||
renderResult.getAllByTestId('uninstallTokensViewCommandButton')[0].click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(renderResult.queryByTestId('uninstall-command-flyout')).toBeInTheDocument();
|
||||
expect(
|
||||
renderResult.queryByText(`--uninstall-token ${uninstallTokenFixture.token}`, {
|
||||
exact: false,
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(useGetUninstallTokenMock).toHaveBeenCalledWith(uninstallTokenFixture.id);
|
||||
});
|
||||
|
||||
it('should filter by policyID', async () => {
|
||||
const renderResult = render();
|
||||
|
||||
fireEvent.change(renderResult.getByTestId('uninstallTokensPolicyIdSearchInput'), {
|
||||
target: { value: 'searched policy id' },
|
||||
});
|
||||
|
||||
expect(useGetUninstallTokensMock).toHaveBeenCalledWith({
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
policyId: 'searched policy id',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* 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 type { CriteriaWithPagination, EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { EuiFieldSearch } from '@elastic/eui';
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { EuiBasicTable, EuiText } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { FormattedDate, FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { SendRequestResponse } from '@kbn/es-ui-shared-plugin/public';
|
||||
|
||||
import { ApiKeyField } from '../../../../../components/api_key_field';
|
||||
import type { UninstallTokenMetadata } from '../../../../../../common/types/models/uninstall_token';
|
||||
import {
|
||||
sendGetUninstallToken,
|
||||
useGetUninstallTokens,
|
||||
} from '../../../../../hooks/use_request/uninstall_tokens';
|
||||
import { useBreadcrumbs, usePagination } from '../../../hooks';
|
||||
import { DefaultLayout } from '../../../layouts';
|
||||
import type { GetUninstallTokenResponse } from '../../../../../../common/types/rest_spec/uninstall_token';
|
||||
import { UninstallCommandFlyout } from '../../../components';
|
||||
|
||||
import {
|
||||
ACTIONS_TITLE,
|
||||
CREATED_AT_TITLE,
|
||||
VIEW_UNINSTALL_COMMAND_LABEL,
|
||||
POLICY_ID_TITLE,
|
||||
SEARCH_BY_POLICY_ID_PLACEHOLDER,
|
||||
TOKEN_TITLE,
|
||||
} from './translations';
|
||||
|
||||
const PolicyIdField = ({ policyId }: { policyId: string }) => (
|
||||
<EuiText
|
||||
size="s"
|
||||
className="eui-textTruncate"
|
||||
title={policyId}
|
||||
data-test-subj="uninstallTokensPolicyIdField"
|
||||
>
|
||||
{policyId}
|
||||
</EuiText>
|
||||
);
|
||||
|
||||
const ViewUninstallCommandButton = ({ onClick }: { onClick: () => void }) => (
|
||||
<EuiToolTip content={VIEW_UNINSTALL_COMMAND_LABEL}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="uninstallTokensViewCommandButton"
|
||||
aria-label={VIEW_UNINSTALL_COMMAND_LABEL}
|
||||
onClick={onClick}
|
||||
iconType="inspect"
|
||||
color="text"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
||||
const NoItemsMessage = ({ isLoading }: { isLoading: boolean }) =>
|
||||
isLoading ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.uninstallTokenList.loadingTokensMessage"
|
||||
defaultMessage="Loading uninstall tokens..."
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.uninstallTokenList.emptyMessage"
|
||||
defaultMessage="No uninstall tokens found."
|
||||
/>
|
||||
);
|
||||
|
||||
export const UninstallTokenListPage = () => {
|
||||
useBreadcrumbs('uninstall_tokens');
|
||||
|
||||
const [policyIdSearch, setPolicyIdSearch] = useState<string>('');
|
||||
const [tokenIdForFlyout, setTokenIdForFlyout] = useState<string | null>(null);
|
||||
|
||||
const { pagination, setPagination, pageSizeOptions } = usePagination();
|
||||
|
||||
const { isLoading, data } = useGetUninstallTokens({
|
||||
perPage: pagination.pageSize,
|
||||
page: pagination.currentPage,
|
||||
policyId: policyIdSearch,
|
||||
});
|
||||
|
||||
const tokens = data?.items ?? [];
|
||||
const total = data?.total ?? 0;
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<UninstallTokenMetadata>> = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'policy_id',
|
||||
name: POLICY_ID_TITLE,
|
||||
render: (policyId: string) => <PolicyIdField policyId={policyId} />,
|
||||
},
|
||||
{
|
||||
field: 'created_at',
|
||||
name: CREATED_AT_TITLE,
|
||||
width: '130px',
|
||||
render: (createdAt: string) =>
|
||||
createdAt ? (
|
||||
<FormattedDate year="numeric" month="short" day="2-digit" value={createdAt} />
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
field: 'id',
|
||||
name: TOKEN_TITLE,
|
||||
width: '300px',
|
||||
render: (uninstallTokenId: string) => (
|
||||
<ApiKeyField
|
||||
apiKeyId={uninstallTokenId}
|
||||
sendGetAPIKey={sendGetUninstallToken}
|
||||
tokenGetter={(response: SendRequestResponse<GetUninstallTokenResponse>) =>
|
||||
response.data?.item.token
|
||||
}
|
||||
length={32}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
name: ACTIONS_TITLE,
|
||||
align: 'center',
|
||||
width: '70px',
|
||||
render: (_: any, { id }: UninstallTokenMetadata) => (
|
||||
<ViewUninstallCommandButton onClick={() => setTokenIdForFlyout(id)} />
|
||||
),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const handleTablePagination = useCallback(
|
||||
({ page }: CriteriaWithPagination<UninstallTokenMetadata>) => {
|
||||
setPagination((prevPagination) => ({
|
||||
...prevPagination,
|
||||
currentPage: page.index + 1,
|
||||
pageSize: page.size,
|
||||
}));
|
||||
},
|
||||
[setPagination]
|
||||
);
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(searchString: string): void => {
|
||||
setPolicyIdSearch(searchString);
|
||||
setPagination((prevPagination) => ({ ...prevPagination, currentPage: 1 }));
|
||||
},
|
||||
[setPagination]
|
||||
);
|
||||
|
||||
return (
|
||||
<DefaultLayout section="uninstall_tokens">
|
||||
{tokenIdForFlyout && (
|
||||
<UninstallCommandFlyout
|
||||
onClose={() => setTokenIdForFlyout(null)}
|
||||
target="agent"
|
||||
uninstallTokenId={tokenIdForFlyout}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EuiText color="subdued" size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.uninstallTokenList.pageDescription"
|
||||
defaultMessage="Uninstall token allows you to get the uninstall command if you need to uninstall the Agent/Endpoint on the Host."
|
||||
/>
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFieldSearch
|
||||
onSearch={handleSearch}
|
||||
incremental
|
||||
fullWidth
|
||||
placeholder={SEARCH_BY_POLICY_ID_PLACEHOLDER}
|
||||
data-test-subj="uninstallTokensPolicyIdSearchInput"
|
||||
/>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiBasicTable<UninstallTokenMetadata>
|
||||
data-test-subj="uninstallTokenListTable"
|
||||
items={tokens}
|
||||
columns={columns}
|
||||
itemId="id"
|
||||
loading={isLoading}
|
||||
pagination={{
|
||||
pageIndex: pagination.currentPage - 1,
|
||||
pageSize: pagination.pageSize,
|
||||
totalItemCount: total,
|
||||
pageSizeOptions,
|
||||
}}
|
||||
onChange={handleTablePagination}
|
||||
noItemsMessage={<NoItemsMessage isLoading={isLoading} />}
|
||||
hasActions={true}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
</DefaultLayout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const POLICY_ID_TITLE = i18n.translate('xpack.fleet.uninstallTokenList.policyIdTitle', {
|
||||
defaultMessage: 'Policy ID',
|
||||
});
|
||||
|
||||
export const CREATED_AT_TITLE = i18n.translate('xpack.fleet.uninstallTokenList.createdAtTitle', {
|
||||
defaultMessage: 'Created at',
|
||||
});
|
||||
|
||||
export const TOKEN_TITLE = i18n.translate('xpack.fleet.uninstallTokenList.tokenTitle', {
|
||||
defaultMessage: 'Token',
|
||||
});
|
||||
export const ACTIONS_TITLE = i18n.translate('xpack.fleet.uninstallTokenList.actionsTitle', {
|
||||
defaultMessage: 'Actions',
|
||||
});
|
||||
|
||||
export const VIEW_UNINSTALL_COMMAND_LABEL = i18n.translate(
|
||||
'xpack.fleet.uninstallTokenList.viewUninstallCommandLabel',
|
||||
{ defaultMessage: 'View uninstall command' }
|
||||
);
|
||||
|
||||
export const SEARCH_BY_POLICY_ID_PLACEHOLDER = i18n.translate(
|
||||
'xpack.fleet.uninstallTokenList.searchByPolicyPlaceholder',
|
||||
{ defaultMessage: 'Search by policy ID' }
|
||||
);
|
|
@ -13,5 +13,6 @@ export type Section =
|
|||
| 'agents'
|
||||
| 'agent_policies'
|
||||
| 'enrollment_tokens'
|
||||
| 'uninstall_tokens'
|
||||
| 'data_streams'
|
||||
| 'settings';
|
||||
|
|
101
x-pack/plugins/fleet/public/components/api_key_field.tsx
Normal file
101
x-pack/plugins/fleet/public/components/api_key_field.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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, { useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonIcon,
|
||||
EuiToolTip,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import type { SendRequestResponse } from '@kbn/es-ui-shared-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { useStartServices } from '../hooks';
|
||||
|
||||
export const ApiKeyField: React.FunctionComponent<{
|
||||
apiKeyId: string;
|
||||
length: number;
|
||||
sendGetAPIKey: (id: string) => Promise<SendRequestResponse>;
|
||||
tokenGetter: (response: SendRequestResponse) => string | undefined;
|
||||
}> = ({ apiKeyId, length, sendGetAPIKey, tokenGetter }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { notifications } = useStartServices();
|
||||
const [state, setState] = useState<'VISIBLE' | 'HIDDEN' | 'LOADING'>('HIDDEN');
|
||||
const [key, setKey] = useState<string | undefined>();
|
||||
|
||||
const tokenMask = useMemo(() => '•'.repeat(length), [length]);
|
||||
|
||||
const toggleKey = async () => {
|
||||
if (state === 'VISIBLE') {
|
||||
setState('HIDDEN');
|
||||
} else if (state === 'HIDDEN') {
|
||||
try {
|
||||
setState('LOADING');
|
||||
const res = await sendGetAPIKey(apiKeyId);
|
||||
if (res.error) {
|
||||
throw res.error;
|
||||
}
|
||||
setKey(tokenGetter(res));
|
||||
setState('VISIBLE');
|
||||
} catch (err) {
|
||||
notifications.toasts.addError(err as Error, {
|
||||
title: 'Error',
|
||||
});
|
||||
setState('HIDDEN');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="xs"
|
||||
css={css`
|
||||
font-family: ${euiTheme.font.familyCode};
|
||||
`}
|
||||
data-test-subj="apiKeyField"
|
||||
>
|
||||
{state === 'VISIBLE' ? key : tokenMask}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
state === 'VISIBLE'
|
||||
? i18n.translate('xpack.fleet.enrollmentTokensList.hideTokenButtonLabel', {
|
||||
defaultMessage: 'Hide token',
|
||||
})
|
||||
: i18n.translate('xpack.fleet.enrollmentTokensList.showTokenButtonLabel', {
|
||||
defaultMessage: 'Show token',
|
||||
})
|
||||
}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label={
|
||||
state === 'VISIBLE'
|
||||
? i18n.translate('xpack.fleet.enrollmentTokensList.hideTokenButtonLabel', {
|
||||
defaultMessage: 'Hide token',
|
||||
})
|
||||
: i18n.translate('xpack.fleet.enrollmentTokensList.showTokenButtonLabel', {
|
||||
defaultMessage: 'Show token',
|
||||
})
|
||||
}
|
||||
color="text"
|
||||
onClick={toggleKey}
|
||||
iconType={state === 'VISIBLE' ? 'eyeClosed' : 'eye'}
|
||||
data-test-subj="showHideTokenButton"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -19,6 +19,7 @@ import type {
|
|||
GetUninstallTokenResponse,
|
||||
} from '../../../common/types/rest_spec/uninstall_token';
|
||||
|
||||
import type { TestRenderer } from '../../mock';
|
||||
import { createFleetTestRendererMock } from '../../mock';
|
||||
|
||||
import {
|
||||
|
@ -28,8 +29,8 @@ import {
|
|||
|
||||
import type { RequestError } from '../../hooks';
|
||||
|
||||
import type { UninstallCommandFlyoutProps } from './uninstall_command_flyout';
|
||||
import { UninstallCommandFlyout } from './uninstall_command_flyout';
|
||||
import type { UninstallCommandTarget } from './types';
|
||||
|
||||
jest.mock('../../hooks/use_request/uninstall_tokens', () => ({
|
||||
useGetUninstallToken: jest.fn(),
|
||||
|
@ -56,15 +57,21 @@ describe('UninstallCommandFlyout', () => {
|
|||
const useGetUninstallTokensMock = useGetUninstallTokens as jest.Mock;
|
||||
const useGetUninstallTokenMock = useGetUninstallToken as jest.Mock;
|
||||
|
||||
const render = (props: Partial<UninstallCommandFlyoutProps> = {}) => {
|
||||
const renderer = createFleetTestRendererMock();
|
||||
let renderer: TestRenderer;
|
||||
|
||||
return renderer.render(
|
||||
<UninstallCommandFlyout onClose={() => {}} target="agent" policyId="policy_id" {...props} />
|
||||
const render = () =>
|
||||
renderer.render(
|
||||
<UninstallCommandFlyout onClose={() => {}} policyId="policy_id" target="agent" />
|
||||
);
|
||||
|
||||
const renderForTarget = (target: UninstallCommandTarget) =>
|
||||
renderer.render(
|
||||
<UninstallCommandFlyout onClose={() => {}} policyId="policy_id" target={target} />
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
renderer = createFleetTestRendererMock();
|
||||
|
||||
const getTokensResponseFixture: MockResponseType<GetUninstallTokensMetadataResponse> = {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
@ -87,16 +94,20 @@ describe('UninstallCommandFlyout', () => {
|
|||
useGetUninstallTokenMock.mockReturnValue(getTokenResponseFixture);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('uninstall command targets', () => {
|
||||
it('renders flyout for Agent', () => {
|
||||
const renderResult = render({ target: 'agent' });
|
||||
const renderResult = renderForTarget('agent');
|
||||
|
||||
expect(renderResult.queryByText(/Uninstall Elastic Agent on your host/)).toBeInTheDocument();
|
||||
expect(renderResult.queryByText(/Uninstall Elastic Defend/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders flyout for Endpoint integration', () => {
|
||||
const renderResult = render({ target: 'endpoint' });
|
||||
const renderResult = renderForTarget('endpoint');
|
||||
|
||||
expect(renderResult.queryByText(/Uninstall Elastic Defend/)).toBeInTheDocument();
|
||||
expect(
|
||||
|
@ -232,4 +243,28 @@ describe('UninstallCommandFlyout', () => {
|
|||
expect(renderResult.queryByText(/Unknown error/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using either with `policyId` or `uninstallTokenId`', () => {
|
||||
it('should perform 2 fetches when using with `policyId`', () => {
|
||||
renderer.render(
|
||||
<UninstallCommandFlyout onClose={() => {}} policyId="policy_id" target="agent" />
|
||||
);
|
||||
|
||||
expect(useGetUninstallTokensMock).toHaveBeenCalled();
|
||||
expect(useGetUninstallTokenMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should perform only 1 fetch when providing `uninstallTokenId`', () => {
|
||||
renderer.render(
|
||||
<UninstallCommandFlyout
|
||||
onClose={() => {}}
|
||||
uninstallTokenId="theProvidedTokenId"
|
||||
target="agent"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(useGetUninstallTokensMock).not.toHaveBeenCalled();
|
||||
expect(useGetUninstallTokenMock).toHaveBeenCalledWith('theProvidedTokenId');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -106,13 +106,26 @@ const ErrorFetchingUninstallToken = ({ error }: { error: RequestError | null })
|
|||
const UninstallCommandsByTokenId = ({ uninstallTokenId }: { uninstallTokenId: string }) => {
|
||||
const { isLoading, error, data } = useGetUninstallToken(uninstallTokenId);
|
||||
const token = data?.item.token;
|
||||
const policyId = data?.item.policy_id;
|
||||
|
||||
return isLoading ? (
|
||||
<Loading size="l" />
|
||||
) : error || !token ? (
|
||||
<ErrorFetchingUninstallToken error={error} />
|
||||
) : (
|
||||
<UninstallCommandsPerPlatform token={token} />
|
||||
<>
|
||||
<UninstallCommandsPerPlatform token={token} />
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiText data-test-subj="uninstall-command-flyout-policy-id-hint">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentUninstallCommandFlyout.validForPolicyId"
|
||||
defaultMessage="Valid for the following agent policy:"
|
||||
/>{' '}
|
||||
<EuiCode>{policyId}</EuiCode>
|
||||
</EuiText>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -129,14 +142,29 @@ const UninstallCommandsByPolicyId = ({ policyId }: { policyId: string }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export interface UninstallCommandFlyoutProps {
|
||||
interface BaseProps {
|
||||
target: UninstallCommandTarget;
|
||||
policyId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface PropsWithPolicyId extends BaseProps {
|
||||
policyId: string;
|
||||
uninstallTokenId?: never;
|
||||
}
|
||||
interface PropsWithTokenId extends BaseProps {
|
||||
uninstallTokenId: string;
|
||||
policyId?: never;
|
||||
}
|
||||
|
||||
export type UninstallCommandFlyoutProps = PropsWithPolicyId | PropsWithTokenId;
|
||||
|
||||
/** Flyout to show uninstall commands.
|
||||
*
|
||||
* Provide EITHER `policyId` OR `tokenId` for showing the token.
|
||||
*/
|
||||
export const UninstallCommandFlyout: React.FunctionComponent<UninstallCommandFlyoutProps> = ({
|
||||
policyId,
|
||||
uninstallTokenId,
|
||||
onClose,
|
||||
target,
|
||||
}) => {
|
||||
|
@ -160,17 +188,11 @@ export const UninstallCommandFlyout: React.FunctionComponent<UninstallCommandFly
|
|||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<UninstallCommandsByPolicyId policyId={policyId} />
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiText data-test-subj="uninstall-command-flyout-policy-id-hint">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentUninstallCommandFlyout.validForPolicyId"
|
||||
defaultMessage="Valid for the following agent policy:"
|
||||
/>{' '}
|
||||
<EuiCode>{policyId}</EuiCode>
|
||||
</EuiText>
|
||||
{uninstallTokenId ? (
|
||||
<UninstallCommandsByTokenId uninstallTokenId={uninstallTokenId} />
|
||||
) : policyId ? (
|
||||
<UninstallCommandsByPolicyId policyId={policyId} />
|
||||
) : null}
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
);
|
||||
|
|
|
@ -14,6 +14,7 @@ export type StaticPage =
|
|||
| 'policies'
|
||||
| 'policies_list'
|
||||
| 'enrollment_tokens'
|
||||
| 'uninstall_tokens'
|
||||
| 'data_streams'
|
||||
| 'settings'
|
||||
| 'settings_create_outputs'
|
||||
|
@ -72,6 +73,7 @@ export const FLEET_ROUTING_PATHS = {
|
|||
edit_integration: '/policies/:policyId/edit-integration/:packagePolicyId',
|
||||
upgrade_package_policy: '/policies/:policyId/upgrade-package-policy/:packagePolicyId',
|
||||
enrollment_tokens: '/enrollment-tokens',
|
||||
uninstall_tokens: '/uninstall-tokens',
|
||||
data_streams: '/data-streams',
|
||||
settings: '/settings',
|
||||
settings_create_fleet_server_hosts: '/settings/create-fleet-server-hosts',
|
||||
|
@ -220,6 +222,7 @@ export const pagePathGetters: {
|
|||
agent_details_logs: ({ agentId }) => [FLEET_BASE_PATH, `/agents/${agentId}/logs`],
|
||||
agent_details_diagnostics: ({ agentId }) => [FLEET_BASE_PATH, `/agents/${agentId}/diagnostics`],
|
||||
enrollment_tokens: () => [FLEET_BASE_PATH, '/enrollment-tokens'],
|
||||
uninstall_tokens: () => [FLEET_BASE_PATH, FLEET_ROUTING_PATHS.uninstall_tokens],
|
||||
data_streams: () => [FLEET_BASE_PATH, '/data-streams'],
|
||||
settings: () => [FLEET_BASE_PATH, FLEET_ROUTING_PATHS.settings],
|
||||
settings_edit_fleet_server_hosts: ({ itemId }) => [
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { uninstallTokensRouteService } from '../../../common/services';
|
||||
|
||||
import type {
|
||||
|
@ -13,28 +15,30 @@ import type {
|
|||
GetUninstallTokenResponse,
|
||||
} from '../../../common/types/rest_spec/uninstall_token';
|
||||
|
||||
import { useRequest } from './use_request';
|
||||
import type { RequestError } from './use_request';
|
||||
import { sendRequest, sendRequestForRq } from './use_request';
|
||||
|
||||
export const useGetUninstallTokens = ({
|
||||
policyId,
|
||||
page,
|
||||
perPage,
|
||||
}: GetUninstallTokensMetadataRequest['query'] = {}) => {
|
||||
const query: GetUninstallTokensMetadataRequest['query'] = {
|
||||
policyId,
|
||||
page,
|
||||
perPage,
|
||||
};
|
||||
|
||||
return useRequest<GetUninstallTokensMetadataResponse>({
|
||||
method: 'get',
|
||||
path: uninstallTokensRouteService.getListPath(),
|
||||
query,
|
||||
});
|
||||
};
|
||||
export const useGetUninstallTokens = (query: GetUninstallTokensMetadataRequest['query'] = {}) =>
|
||||
useQuery<GetUninstallTokensMetadataResponse, RequestError>(['useGetUninstallTokens', query], () =>
|
||||
sendRequestForRq({
|
||||
method: 'get',
|
||||
path: uninstallTokensRouteService.getListPath(),
|
||||
query,
|
||||
})
|
||||
);
|
||||
|
||||
export const useGetUninstallToken = (uninstallTokenId: string) =>
|
||||
useRequest<GetUninstallTokenResponse>({
|
||||
useQuery<GetUninstallTokenResponse, RequestError>(
|
||||
['useGetUninstallToken', uninstallTokenId],
|
||||
() =>
|
||||
sendRequestForRq({
|
||||
method: 'get',
|
||||
path: uninstallTokensRouteService.getInfoPath(uninstallTokenId),
|
||||
})
|
||||
);
|
||||
|
||||
export const sendGetUninstallToken = (uninstallTokenId: string) =>
|
||||
sendRequest<GetUninstallTokenResponse>({
|
||||
method: 'get',
|
||||
path: uninstallTokensRouteService.getInfoPath(uninstallTokenId),
|
||||
});
|
||||
|
|
|
@ -39,6 +39,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
// define custom kibana server args here
|
||||
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
|
||||
|
||||
// add feature flags here
|
||||
`--xpack.fleet.enableExperimental=${JSON.stringify(['agentTamperProtectionEnabled'])}`,
|
||||
|
||||
`--logging.loggers=${JSON.stringify([
|
||||
...getKibanaCliLoggers(xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs')),
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue