Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4c7178d
UI scaffolded without additional options, hook created, submit working
Supplementing May 29, 2025
a67e2aa
form more scaffolded
Supplementing May 29, 2025
a7d3600
cleaned up UI, added more validation, hooked up missing fields to be …
Supplementing May 30, 2025
d35c7b3
added test cases
Supplementing May 30, 2025
9ac109a
added settings to request
Supplementing May 30, 2025
9523b04
updated typings
Supplementing May 30, 2025
77b3559
Merge branch 'main' into feature-single-agent-migration-ui
elasticmachine May 30, 2025
47991a7
Merge branch 'main' into feature-single-agent-migration-ui
elasticmachine Jun 2, 2025
0819d05
fix for wrong translation label
Supplementing Jun 2, 2025
70128e8
updated form to have rows for record fields, made type interface matc…
Supplementing Jun 2, 2025
629a180
Merge branch 'feature-single-agent-migration-ui' of https://github.co…
Supplementing Jun 2, 2025
cf8cd3b
Merge branch 'main' into feature-single-agent-migration-ui
Supplementing Jun 2, 2025
0081a35
removed console log
Supplementing Jun 2, 2025
ff3762b
Merge branch 'feature-single-agent-migration-ui' of https://github.co…
Supplementing Jun 2, 2025
06b40f3
moved headers form to component, changed url constructor, updated tests
Supplementing Jun 3, 2025
602a68a
fixed check for fleet-agent to check policy to handle case where agen…
Supplementing Jun 4, 2025
06c2480
Merge branch 'main' into feature-single-agent-migration-ui
elasticmachine Jun 4, 2025
6eea715
fixed test case mock
Supplementing Jun 4, 2025
3db88b2
Merge branch 'feature-single-agent-migration-ui' of https://github.co…
Supplementing Jun 4, 2025
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ export const agentRouteService = {
getAgentFileDeletePath: (fileId: string) =>
AGENT_API_ROUTES.DELETE_UPLOAD_FILE_PATTERN.replace('{fileId}', fileId),
getAgentsByActionsPath: () => AGENT_API_ROUTES.LIST_PATTERN,
postMigrateSingleAgent: (agentId: string) =>
AGENT_API_ROUTES.MIGRATE_PATTERN.replace('{agentId}', agentId),
};

export const outputRoutesService = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,31 @@ export interface DeleteAgentRequest {
agentId: string;
};
}

export interface MigrateSingleAgentRequest {
body: {
id: string;
enrollment_token: string;
uri: string;
settings?: {
ca_sha256?: string;
certificate_authorities?: string;
elastic_agent_cert?: string;
elastic_agent_cert_key?: string;
elastic_agent_cert_key_passphrase?: string;
headers?: Record<string, string>;
insecure?: boolean;
proxy_disabled?: boolean;
proxy_headers?: Record<string, string>;
proxy_url?: string;
staging?: boolean;
tags?: string;
replace_token?: boolean;
};
};
}
export interface MigrateSingleAgentResponse {
actionId: string;
}
export interface UpdateAgentRequest {
params: {
agentId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export { AgentTableHeader } from './table_header';
export { SearchAndFilterBar } from './search_and_filter_bar';
export { TableRowActions } from './table_row_actions';
export { TagsAddRemove } from './tags_add_remove';
export { AgentMigrateFlyout } from './migrate_agent_flyout';
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* 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, { useCallback } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFieldText,
EuiButtonEmpty,
EuiFormRow,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';

interface HeadersInputProps {
headers: Record<string, string>;
onUpdate: (headers: Record<string, string>) => void;
}

export const HeadersInput: React.FC<HeadersInputProps> = ({ headers, onUpdate }) => {
const addEmptyHeader = () => {
onUpdate({
...headers,
'': '',
});
};

const replaceKeyOrValue = useCallback(
(index: number, keyOrValue: string, updateType: 'key' | 'value') => {
const entries = Object.entries(headers);

if (updateType === 'key') {
const updatedEntries = entries.map((entry, i) =>
i === index ? [keyOrValue, entry[1]] : entry
);
onUpdate(Object.fromEntries(updatedEntries));
} else {
const updatedEntries = entries.map((entry, i) =>
i === index ? [entry[0], keyOrValue] : entry
);
onUpdate(Object.fromEntries(updatedEntries));
}
},
[headers, onUpdate]
);

return (
<>
{headers &&
Object.entries(headers).map(([key, value], index) => {
return (
<>
<EuiFlexGroup>
<EuiFlexItem grow={5}>
<EuiFieldText
placeholder={i18n.translate(
'xpack.fleet.agentList.migrateAgentFlyout.headersKeyPlaceholder',
{
defaultMessage: 'Key',
}
)}
onChange={(e) => {
replaceKeyOrValue(index, e.target.value, 'key');
}}
value={key}
fullWidth
/>
</EuiFlexItem>
<EuiFlexItem grow={5}>
<EuiFieldText
value={value}
placeholder={i18n.translate(
'xpack.fleet.agentList.migrateAgentFlyout.headersValuePlaceholder',
{
defaultMessage: 'Value',
}
)}
onChange={(e) => {
replaceKeyOrValue(index, e.target.value, 'value');
}}
fullWidth
/>
</EuiFlexItem>
<EuiFlexItem grow={0}>
<EuiButtonEmpty
iconType="cross"
onClick={() => {
// Get all entries from headers
const entries = Object.entries(headers);
// Filter out the entry at the specified index
const updatedEntries = entries.filter((_, i) => i !== index);
// Convert back to object and update the form state
onUpdate(Object.fromEntries(updatedEntries));
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
</>
);
})}

<EuiFormRow>
<EuiButtonEmpty
iconType="plusInCircle"
onClick={() => {
addEmptyHeader();
}}
>
<FormattedMessage
id="xpack.fleet.agentList.migrateAgentFlyout.addHeaderLabel"
defaultMessage="Add Row"
/>
</EuiButtonEmpty>
</EuiFormRow>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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 { fireEvent } from '@testing-library/dom';

import { createFleetTestRendererMock } from '../../../../../../../mock';

import { AgentMigrateFlyout } from '.';

describe('MigrateAgentFlyout', () => {
const renderer = createFleetTestRendererMock();
let component: ReturnType<typeof renderer.render>;

beforeEach(() => {
component = renderer.render(
<AgentMigrateFlyout
onClose={jest.fn()}
onSave={jest.fn()}
agents={[
{
active: true,
status: 'online',
local_metadata: { elastic: { agent: { version: '8.8.0' } } },
id: '1',
packages: [],
type: 'PERMANENT',
enrolled_at: new Date().toISOString(),
},
]}
/>
);
});
it('should render', () => {
expect(component).toBeDefined();
});

it('submit button should be disabled when form is invalid', () => {
// set the value of the url
const urlInput = component.getByTestId('migrateAgentFlyoutClusterUrlInput');
fireEvent.change(urlInput, { target: { value: 'somebadurl.com' } });

const submitButton = component.getByTestId('migrateAgentFlyoutSubmitButton');

expect(submitButton).toBeDisabled();
});

it('submit button should be enabled when form is valid', () => {
// set the value of the url
const urlInput = component.getByTestId('migrateAgentFlyoutClusterUrlInput');
fireEvent.change(urlInput, { target: { value: 'https://www.example.com' } });
// also set the value of enrollment token
const tokenInput = component.getByTestId('migrateAgentFlyoutEnrollmentTokenInput');
fireEvent.change(tokenInput, { target: { value: 'someToken' } });
const submitButton = component.getByTestId('migrateAgentFlyoutSubmitButton');

expect(submitButton).not.toBeDisabled();
});
});
Loading
Loading