Skip to content

Commit

Permalink
Merge branch 'main' into api/serverlessQA
Browse files Browse the repository at this point in the history
  • Loading branch information
MadameSheema authored Apr 15, 2024
2 parents 77fa125 + 59edae2 commit 08b8ebc
Show file tree
Hide file tree
Showing 71 changed files with 1,563 additions and 771 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { render, screen, fireEvent } from '@testing-library/react';
import React from 'react';
import { CancelSyncJobModal } from './sync_job_cancel_modal';
import '@testing-library/jest-dom/extend-expect';
import { I18nProvider } from '@kbn/i18n-react';

describe('CancelSyncJobModal', () => {
const mockSyncJobId = '123';
const mockOnConfirmCb = jest.fn();
const mockOnCancel = jest.fn();

beforeEach(() => {
render(
<I18nProvider>
<CancelSyncJobModal
syncJobId={mockSyncJobId}
onConfirmCb={mockOnConfirmCb}
onCancel={mockOnCancel}
isLoading={false}
errorMessages={[]}
/>
</I18nProvider>
);
});

test('renders the sync job ID', () => {
const syncJobIdElement = screen.getByTestId('confirmModalBodyText');
expect(syncJobIdElement).toHaveTextContent(`Sync job ID: ${mockSyncJobId}`);
});

test('calls onConfirmCb when confirm button is clicked', () => {
const confirmButton = screen.getByText('Confirm');
fireEvent.click(confirmButton);
expect(mockOnConfirmCb).toHaveBeenCalledWith(mockSyncJobId);
});

test('calls onCancel when cancel button is clicked', () => {
const cancelButton = screen.getByTestId('confirmModalCancelButton');
fireEvent.click(cancelButton);
expect(mockOnCancel).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { EuiConfirmModal, EuiText, EuiCode, EuiSpacer, EuiConfirmModalProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';

export type CancelSyncModalProps = Omit<EuiConfirmModalProps, 'onConfirm'> & {
onConfirmCb: (syncJobId: string) => void;
syncJobId: string;
errorMessages?: string[];
};

export const CancelSyncJobModal: React.FC<CancelSyncModalProps> = ({
syncJobId,
onCancel,
onConfirmCb,
isLoading,
}) => {
return (
<EuiConfirmModal
title={i18n.translate('searchConnectors.syncJobs.cancelSyncModal.title', {
defaultMessage: 'Cancel sync job',
})}
onCancel={onCancel}
onConfirm={() => onConfirmCb(syncJobId)}
cancelButtonText={i18n.translate('searchConnectors.syncJobs.cancelSyncModal.cancelButton', {
defaultMessage: 'Cancel',
})}
confirmButtonText={i18n.translate('searchConnectors.syncJobs.cancelSyncModal.confirmButton', {
defaultMessage: 'Confirm',
})}
buttonColor="danger"
confirmButtonDisabled={isLoading}
isLoading={isLoading}
>
<EuiText size="s">
<FormattedMessage
id="searchConnectors.syncJobs.cancelSyncModal.description"
defaultMessage="Are you sure you want to cancel this sync job?"
/>
<EuiSpacer size="m" />
<FormattedMessage
id="searchConnectors.syncJobs.cancelSyncModal.syncJobId"
defaultMessage="Sync job ID:"
/>
&nbsp;
<EuiCode>{syncJobId}</EuiCode>
</EuiText>
</EuiConfirmModal>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,29 @@ import {
EuiBadge,
EuiBasicTable,
EuiBasicTableColumn,
EuiButtonIcon,
Pagination,
} from '@elastic/eui';

import { i18n } from '@kbn/i18n';
import { ConnectorSyncJob, SyncJobType, SyncStatus } from '../..';
import { ConnectorSyncJob, isSyncCancellable, SyncJobType, SyncStatus } from '../..';

import { syncJobTypeToText, syncStatusToColor, syncStatusToText } from '../..';
import { durationToText, getSyncJobDuration } from '../../utils/duration_to_text';
import { FormattedDateTime } from '../../utils/formatted_date_time';
import { SyncJobFlyout } from './sync_job_flyout';
import { CancelSyncJobModal, CancelSyncModalProps } from './sync_job_cancel_modal';

interface SyncJobHistoryTableProps {
isLoading?: boolean;
onPaginate: (criteria: CriteriaWithPagination<ConnectorSyncJob>) => void;
pagination: Pagination;
syncJobs: ConnectorSyncJob[];
type: 'content' | 'access_control';
cancelConfirmModalProps?: Pick<CancelSyncModalProps, 'isLoading' | 'onConfirmCb'> & {
syncJobIdToCancel?: ConnectorSyncJob['id'];
setSyncJobIdToCancel: (syncJobId: ConnectorSyncJob['id'] | undefined) => void;
};
}

export const SyncJobsTable: React.FC<SyncJobHistoryTableProps> = ({
Expand All @@ -38,6 +44,12 @@ export const SyncJobsTable: React.FC<SyncJobHistoryTableProps> = ({
pagination,
syncJobs,
type,
cancelConfirmModalProps = {
onConfirmCb: () => {},
isLoading: false,
setSyncJobIdToCancel: () => {},
syncJobIdToCancel: undefined,
},
}) => {
const [selectedSyncJob, setSelectedSyncJob] = useState<ConnectorSyncJob | undefined>(undefined);
const columns: Array<EuiBasicTableColumn<ConnectorSyncJob>> = [
Expand Down Expand Up @@ -127,6 +139,33 @@ export const SyncJobsTable: React.FC<SyncJobHistoryTableProps> = ({
onClick: (job) => setSelectedSyncJob(job),
type: 'icon',
},
...(cancelConfirmModalProps
? [
{
render: (job: ConnectorSyncJob) => {
return isSyncCancellable(job.status) ? (
<EuiButtonIcon
iconType="cross"
color="danger"
onClick={() => cancelConfirmModalProps.setSyncJobIdToCancel(job.id)}
aria-label={i18n.translate(
'searchConnectors.index.syncJobs.actions.cancelSyncJob.caption',
{
defaultMessage: 'Cancel this sync job',
}
)}
>
{i18n.translate('searchConnectors.index.syncJobs.actions.deleteJob.caption', {
defaultMessage: 'Delete',
})}
</EuiButtonIcon>
) : (
<></>
);
},
},
]
: []),
],
},
];
Expand All @@ -136,6 +175,13 @@ export const SyncJobsTable: React.FC<SyncJobHistoryTableProps> = ({
{Boolean(selectedSyncJob) && (
<SyncJobFlyout onClose={() => setSelectedSyncJob(undefined)} syncJob={selectedSyncJob} />
)}
{Boolean(cancelConfirmModalProps) && cancelConfirmModalProps?.syncJobIdToCancel && (
<CancelSyncJobModal
{...cancelConfirmModalProps}
syncJobId={cancelConfirmModalProps.syncJobIdToCancel}
onCancel={() => cancelConfirmModalProps.setSyncJobIdToCancel(undefined)}
/>
)}
<EuiBasicTable
data-test-subj={`entSearchContent-index-${type}-syncJobs-table`}
items={syncJobs}
Expand Down
32 changes: 32 additions & 0 deletions packages/kbn-search-connectors/lib/cancel_sync.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { cancelSync } from './cancel_sync';

describe('cancelSync lib function', () => {
const mockClient = {
transport: {
request: jest.fn(),
},
};

it('should cancel a sync', async () => {
mockClient.transport.request.mockImplementation(() => ({
success: true,
}));

await expect(cancelSync(mockClient as unknown as ElasticsearchClient, '1234')).resolves.toEqual(
{ success: true }
);
expect(mockClient.transport.request).toHaveBeenCalledWith({
method: 'PUT',
path: '/_connector/_sync_job/1234/_cancel',
});
});
});
18 changes: 18 additions & 0 deletions packages/kbn-search-connectors/lib/cancel_sync.ts
Original file line number Diff line number Diff line change
@@ -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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { ConnectorAPICancelSyncResponse } from '../types';

export const cancelSync = async (client: ElasticsearchClient, syncJobId: string) => {
const result = await client.transport.request<ConnectorAPICancelSyncResponse>({
method: 'PUT',
path: `/_connector/_sync_job/${syncJobId}/_cancel`,
});
return result;
};
15 changes: 15 additions & 0 deletions packages/kbn-search-connectors/lib/collect_connector_stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,8 @@ function syncJobsStatsByState(syncJobs: ConnectorSyncJob[]): SyncJobStatsByState
let idle = 0;
let running = 0;
let duration = 0;
const errors = new Map<string, number>();
let topErrors: string[] = [];

for (const syncJob of syncJobs) {
completed += syncJob.status === SyncStatus.COMPLETED ? 1 : 0;
Expand All @@ -386,6 +388,18 @@ function syncJobsStatsByState(syncJobs: ConnectorSyncJob[]): SyncJobStatsByState
duration += Math.floor((completedAt.getTime() - startedAt.getTime()) / 1000);
}
}
if (syncJob.status === SyncStatus.ERROR && syncJob.error) {
errors.set(syncJob.error, (errors.get(syncJob.error) ?? 0) + 1);
}
}

if (errors.size <= 5) {
topErrors = [...errors.keys()];
} else {
topErrors = [...errors.entries()]
.sort((a, b) => b[1] - a[1])
.map((a) => a[0])
.slice(0, 5);
}

return {
Expand All @@ -399,5 +413,6 @@ function syncJobsStatsByState(syncJobs: ConnectorSyncJob[]): SyncJobStatsByState
idle,
running,
totalDurationSeconds: duration,
topErrors,
} as SyncJobStatsByState;
}
Loading

0 comments on commit 08b8ebc

Please sign in to comment.