Skip to content

Commit

Permalink
Merge branch 'main' into discover-fix-194043
Browse files Browse the repository at this point in the history
  • Loading branch information
elasticmachine authored Oct 2, 2024
2 parents 3dfa761 + d0d2032 commit b608dd7
Show file tree
Hide file tree
Showing 20 changed files with 367 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const ApiKeyContext = createContext<APIKeyContext>({
initialiseKey: () => {},
});

export const SearchApiKeyProvider: React.FC = ({ children }) => {
export const SearchApiKeyProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
const { http } = useKibana().services;
const [state, dispatch] = useReducer(reducer, initialState);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { delay, parseLogFile } from '../test_utils';
import '../jest_matchers';
import { expectDocumentsMigratedToHighestVersion } from '../kibana_migrator_test_kit.expect';

const PARALLEL_MIGRATORS = 4;
const PARALLEL_MIGRATORS = 3;
type Job<T> = () => Promise<T>;

const getLogFile = (node: number) => join(__dirname, `multiple_kb_nodes_${node}.log`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 { calculateRowOptions } from './calculate_row_options';

describe('calculateRowOptions', () => {
it('should return all options when cardinality is greater than all row options', () => {
const rowOptions = [5, 10, 20, 50, 100];
const cardinality = 150;
expect(calculateRowOptions(rowOptions, cardinality)).toEqual([5, 10, 20, 50, 100]);
});

it('should return options up to and including cardinality', () => {
const rowOptions = [5, 10, 20, 50, 100];
const cardinality = 30;
expect(calculateRowOptions(rowOptions, cardinality)).toEqual([5, 10, 20, 50]);
});

it('should return at least one option even if cardinality is less than all options', () => {
const rowOptions = [5, 10, 20, 50, 100];
const cardinality = 3;
expect(calculateRowOptions(rowOptions, cardinality)).toEqual([5]);
});

it('should handle cardinality of zero', () => {
const rowOptions = [5, 10, 20, 50, 100];
const cardinality = 0;
expect(calculateRowOptions(rowOptions, cardinality)).toEqual([5]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* 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 const calculateRowOptions = (rowOptions: number[], cardinality: number): number[] => {
return rowOptions.reduce((acc, v) => {
if (v <= cardinality) {
acc.push(v);
} else if (acc.length === 0 || acc[acc.length - 1] < cardinality) {
acc.push(v);
}
return acc;
}, [] as number[]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,10 @@ export const SwimlaneContainer: FC<SwimlaneProps> = ({
const isPaginationVisible =
(showSwimlane || isLoading) &&
swimlaneLimit !== undefined &&
swimlaneLimit > (perPage ?? 5) &&
onPaginationChange &&
fromPage &&
perPage;
swimlaneLimit > 5 &&
!!onPaginationChange &&
!!fromPage &&
!!perPage;

const rowsCount = swimlaneData?.laneLabels?.length ?? 0;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { calculateRowOptions } from './calculate_row_options';

interface SwimLanePaginationProps {
fromPage: number;
Expand Down Expand Up @@ -51,25 +52,26 @@ export const SwimLanePagination: FC<SwimLanePaginationProps> = ({

const pageCount = Math.ceil(cardinality / perPage);

const items = [5, 10, 20, 50, 100].map((v) => {
return (
<EuiContextMenuItem
key={`${v}_rows`}
icon={v === perPage ? 'check' : 'empty'}
onClick={() => {
closePopover();
setPerPage(v);
}}
data-test-subj={`${v} rows`}
>
<FormattedMessage
id="xpack.ml.explorer.swimLaneSelectRowsPerPage"
defaultMessage="{rowsCount} rows"
values={{ rowsCount: v }}
/>
</EuiContextMenuItem>
);
});
const rowOptions = [5, 10, 20, 50, 100];
const items = calculateRowOptions(rowOptions, cardinality);

const menuItems = items.map((v) => (
<EuiContextMenuItem
key={`${v}_rows`}
icon={v === perPage ? 'check' : 'empty'}
onClick={() => {
closePopover();
setPerPage(v);
}}
data-test-subj={`${v} rows`}
>
<FormattedMessage
id="xpack.ml.explorer.swimLaneSelectRowsPerPage"
defaultMessage="{rowsCount} rows"
values={{ rowsCount: v }}
/>
</EuiContextMenuItem>
));

return (
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
Expand Down Expand Up @@ -97,7 +99,7 @@ export const SwimLanePagination: FC<SwimLanePaginationProps> = ({
closePopover={closePopover}
panelPaddingSize="none"
>
<EuiContextMenuPanel items={items} data-test-subj="mlSwimLanePageSizePanel" />
<EuiContextMenuPanel items={menuItems} data-test-subj="mlSwimLanePageSizePanel" />
</EuiPopover>
</EuiFlexItem>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@

import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { render, fireEvent, screen } from '@testing-library/react';
import { render, fireEvent, screen, within } from '@testing-library/react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks';
import EmailParamsFields from './email_params';
import { getIsExperimentalFeatureEnabled } from '../../common/get_experimental_features';
import { getFormattedEmailOptions } from './email_params';

jest.mock('@kbn/kibana-react-plugin/public', () => ({
useKibana: jest.fn(),
Expand All @@ -28,6 +29,24 @@ const mockKibana = () => {
});
};

const emailTestCases = [
{
field: 'to',
fieldValue: 'new1@test.com, new2@test.com , new1@test.com, ',
expected: ['test@test.com', 'new1@test.com', 'new2@test.com'],
},
{
field: 'cc',
fieldValue: 'newcc1@test.com, newcc2@test.com , newcc1@test.com, ',
expected: ['cc@test.com', 'newcc1@test.com', 'newcc2@test.com'],
},
{
field: 'bcc',
fieldValue: 'newbcc1@test.com, newbcc2@test.com , newbcc1@test.com, ',
expected: ['bcc@test.com', 'newbcc1@test.com', 'newbcc2@test.com'],
},
];

describe('EmailParamsFields renders', () => {
beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -62,6 +81,40 @@ describe('EmailParamsFields renders', () => {
expect(await screen.findByTestId('messageTextArea')).toBeVisible();
});

emailTestCases.forEach(({ field, fieldValue, expected }) => {
test(`"${field}" field value updates correctly when comma-separated emails are pasted`, async () => {
const actionParams = {
cc: ['cc@test.com'],
bcc: ['bcc@test.com'],
to: ['test@test.com'],
subject: 'test',
message: 'test message',
};

const editAction = jest.fn();

render(
<IntlProvider locale="en">
<EmailParamsFields
actionParams={actionParams}
errors={{ to: [], cc: [], bcc: [], subject: [], message: [] }}
editAction={editAction}
defaultMessage={'Some default message'}
index={0}
/>
</IntlProvider>
);

const euiComboBox = screen.getByTestId(`${field}EmailAddressInput`);
const input = within(euiComboBox).getByTestId('comboBoxSearchInput');
fireEvent.change(input, { target: { value: fieldValue } });
expect(input).toHaveValue(fieldValue);

fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
expect(editAction).toHaveBeenCalledWith(field, expected, 0);
});
});

test('message param field is rendered with default value if not set', () => {
const actionParams = {
cc: [],
Expand Down Expand Up @@ -234,3 +287,48 @@ describe('EmailParamsFields renders', () => {
expect(editAction).not.toHaveBeenCalled();
});
});

describe('getFormattedEmailOptions', () => {
test('should return new options added to previous options', () => {
const searchValue = 'test@test.com, other@test.com';
const previousOptions = [{ label: 'existing@test.com' }];
const newOptions = getFormattedEmailOptions(searchValue, previousOptions);

expect(newOptions).toEqual([
{ label: 'existing@test.com' },
{ label: 'test@test.com' },
{ label: 'other@test.com' },
]);
});

test('should trim extra spaces in search value', () => {
const searchValue = ' test@test.com , other@test.com , ';
const previousOptions: Array<{ label: string }> = [];
const newOptions = getFormattedEmailOptions(searchValue, previousOptions);

expect(newOptions).toEqual([{ label: 'test@test.com' }, { label: 'other@test.com' }]);
});

test('should prevent duplicate email addresses', () => {
const searchValue = 'duplicate@test.com, duplicate@test.com';
const previousOptions = [{ label: 'existing@test.com' }, { label: 'duplicate@test.com' }];
const newOptions = getFormattedEmailOptions(searchValue, previousOptions);

expect(newOptions).toEqual([{ label: 'existing@test.com' }, { label: 'duplicate@test.com' }]);
});

test('should return previous options if search value is empty', () => {
const searchValue = '';
const previousOptions = [{ label: 'existing@test.com' }];
const newOptions = getFormattedEmailOptions(searchValue, previousOptions);
expect(newOptions).toEqual([{ label: 'existing@test.com' }]);
});

test('should handle single email without comma', () => {
const searchValue = 'single@test.com';
const previousOptions = [{ label: 'existing@test.com' }];
const newOptions = getFormattedEmailOptions(searchValue, previousOptions);

expect(newOptions).toEqual([{ label: 'existing@test.com' }, { label: 'single@test.com' }]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ import { EmailActionParams } from '../types';

const noop = () => {};

export const getFormattedEmailOptions = (
searchValue: string,
previousOptions: Array<{ label: string }>
): Array<{ label: string }> => {
if (!searchValue.trim()) return previousOptions;
const previousEmails: string[] = previousOptions.map((option) => option.label);
const allUniqueEmails: Set<string> = new Set(previousEmails);
searchValue.split(',').forEach((email) => {
const trimmedEmail = email.trim();
if (trimmedEmail) allUniqueEmails.add(trimmedEmail);
});
const formattedOptions = Array.from(allUniqueEmails).map((email) => ({ label: email }));
return formattedOptions;
};

export const EmailParamsFields = ({
actionParams,
editAction,
Expand Down Expand Up @@ -105,7 +120,7 @@ export const EmailParamsFields = ({
data-test-subj="toEmailAddressInput"
selectedOptions={toOptions}
onCreateOption={(searchValue: string) => {
const newOptions = [...toOptions, { label: searchValue }];
const newOptions = getFormattedEmailOptions(searchValue, toOptions);
editAction(
'to',
newOptions.map((newOption) => newOption.label),
Expand Down Expand Up @@ -148,7 +163,7 @@ export const EmailParamsFields = ({
data-test-subj="ccEmailAddressInput"
selectedOptions={ccOptions}
onCreateOption={(searchValue: string) => {
const newOptions = [...ccOptions, { label: searchValue }];
const newOptions = getFormattedEmailOptions(searchValue, ccOptions);
editAction(
'cc',
newOptions.map((newOption) => newOption.label),
Expand Down Expand Up @@ -192,7 +207,7 @@ export const EmailParamsFields = ({
data-test-subj="bccEmailAddressInput"
selectedOptions={bccOptions}
onCreateOption={(searchValue: string) => {
const newOptions = [...bccOptions, { label: searchValue }];
const newOptions = getFormattedEmailOptions(searchValue, bccOptions);
editAction(
'bcc',
newOptions.map((newOption) => newOption.label),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ describe('KibanaDiscoveryService', () => {
savedObjectsRepository.find.mockResolvedValueOnce(createFindResponse(mockActiveNodes));

it('returns the active kibana nodes', async () => {
const onNodesCounted = jest.fn();
const kibanaDiscoveryService = new KibanaDiscoveryService({
savedObjectsRepository,
logger,
Expand All @@ -254,6 +255,7 @@ describe('KibanaDiscoveryService', () => {
active_nodes_lookback: DEFAULT_ACTIVE_NODES_LOOK_BACK_DURATION,
interval: DEFAULT_DISCOVERY_INTERVAL_MS,
},
onNodesCounted,
});

const activeNodes = await kibanaDiscoveryService.getActiveKibanaNodes();
Expand All @@ -265,6 +267,7 @@ describe('KibanaDiscoveryService', () => {
type: BACKGROUND_TASK_NODE_SO_NAME,
});
expect(activeNodes).toEqual(mockActiveNodes);
expect(onNodesCounted).toHaveBeenCalledWith(mockActiveNodes.length);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface DiscoveryServiceParams {
currentNode: string;
savedObjectsRepository: ISavedObjectsRepository;
logger: Logger;
onNodesCounted?: (numOfNodes: number) => void;
}

interface DiscoveryServiceUpsertParams {
Expand All @@ -34,13 +35,15 @@ export class KibanaDiscoveryService {
private logger: Logger;
private stopped = false;
private timer: NodeJS.Timeout | undefined;
private onNodesCounted?: (numOfNodes: number) => void;

constructor({ config, currentNode, savedObjectsRepository, logger }: DiscoveryServiceParams) {
this.activeNodesLookBack = config.active_nodes_lookback;
this.discoveryInterval = config.interval;
this.savedObjectsRepository = savedObjectsRepository;
this.logger = logger;
this.currentNode = currentNode;
constructor(opts: DiscoveryServiceParams) {
this.activeNodesLookBack = opts.config.active_nodes_lookback;
this.discoveryInterval = opts.config.interval;
this.savedObjectsRepository = opts.savedObjectsRepository;
this.logger = opts.logger;
this.currentNode = opts.currentNode;
this.onNodesCounted = opts.onNodesCounted;
}

private async upsertCurrentNode({ id, lastSeen }: DiscoveryServiceUpsertParams) {
Expand Down Expand Up @@ -106,6 +109,10 @@ export class KibanaDiscoveryService {
filter: `${BACKGROUND_TASK_NODE_SO_NAME}.attributes.last_seen > now-${this.activeNodesLookBack}`,
});

if (this.onNodesCounted) {
this.onNodesCounted(activeNodes.length);
}

return activeNodes;
}

Expand Down
Loading

0 comments on commit b608dd7

Please sign in to comment.