[Enterprise Search] [Document Level Security] Access Control sync history (#159461)

## Summary

- Adds Access Control sync to the index overview page.
- Adds table switcher and changes table columns.

Content related syncs
![Screenshot 2023-06-12 at 14 49
10](2d4d5c44-2648-4d1d-ba86-aff38aae17fb)

Access control syncs
![Screenshot 2023-06-12 at 14 49
14](0ab11462-cecb-4099-955d-4f2f6d02197a)

When access control not enabled
![Screenshot 2023-06-12 at 14 54
11](e93430a4-d02a-46dc-b212-1ec1158fb7b8)


### 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)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [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/))
- [ ] 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))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Efe Gürkan YALAMAN 2023-06-12 17:45:02 +02:00 committed by GitHub
parent e8e5cec83d
commit ebcfebed05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 287 additions and 122 deletions

View file

@ -15,13 +15,19 @@ export interface FetchSyncJobsArgs {
connectorId: string;
from?: number;
size?: number;
type?: 'content' | 'access_control';
}
export type FetchSyncJobsResponse = Paginate<ConnectorSyncJob>;
export const fetchSyncJobs = async ({ connectorId, from = 0, size = 10 }: FetchSyncJobsArgs) => {
export const fetchSyncJobs = async ({
connectorId,
from = 0,
size = 10,
type,
}: FetchSyncJobsArgs) => {
const route = `/internal/enterprise_search/connectors/${connectorId}/sync_jobs`;
const query = { from, size };
const query = { from, size, type };
return await HttpLogic.values.http.get<Paginate<ConnectorSyncJob>>(route, { query });
};

View file

@ -5,129 +5,67 @@
* 2.0.
*/
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import { useActions, useValues } from 'kea';
import { useValues } from 'kea';
import { EuiBadge, EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import { EuiButtonGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SyncStatus } from '../../../../../../common/types/connectors';
import { FormattedDateTime } from '../../../../shared/formatted_date_time';
import { pageToPagination } from '../../../../shared/pagination/page_to_pagination';
import { durationToText } from '../../../utils/duration_to_text';
import { syncStatusToColor, syncStatusToText } from '../../../utils/sync_status_to_text';
import { KibanaLogic } from '../../../../shared/kibana';
import { IndexViewLogic } from '../index_view_logic';
import { SyncJobFlyout } from './sync_job_flyout';
import { SyncJobsViewLogic, SyncJobView } from './sync_jobs_view_logic';
import { SyncJobsHistoryTable } from './sync_jobs_history_table';
export const SyncJobs: React.FC = () => {
const { connectorId } = useValues(IndexViewLogic);
const { syncJobs, syncJobsLoading, syncJobsPagination } = useValues(SyncJobsViewLogic);
const { fetchSyncJobs } = useActions(SyncJobsViewLogic);
const [syncJobFlyout, setSyncJobFlyout] = useState<SyncJobView | undefined>(undefined);
useEffect(() => {
if (connectorId) {
fetchSyncJobs({
connectorId,
from: syncJobsPagination.from ?? 0,
size: syncJobsPagination.size ?? 10,
});
}
}, [connectorId]);
const columns: Array<EuiBasicTableColumn<SyncJobView>> = [
{
field: 'lastSync',
name: i18n.translate('xpack.enterpriseSearch.content.syncJobs.lastSync.columnTitle', {
defaultMessage: 'Last sync',
}),
render: (lastSync: string) => <FormattedDateTime date={new Date(lastSync)} />,
sortable: true,
truncateText: true,
},
{
field: 'duration',
name: i18n.translate('xpack.enterpriseSearch.content.syncJobs.syncDuration.columnTitle', {
defaultMessage: 'Sync duration',
}),
render: (duration: moment.Duration) => durationToText(duration),
sortable: true,
truncateText: true,
},
{
field: 'indexed_document_count',
name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.addedDocs.columnTitle', {
defaultMessage: 'Docs added',
}),
sortable: true,
truncateText: true,
},
{
field: 'deleted_document_count',
name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.deletedDocs.columnTitle', {
defaultMessage: 'Docs deleted',
}),
sortable: true,
truncateText: true,
},
{
field: 'status',
name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.syncStatus.columnTitle', {
defaultMessage: 'Status',
}),
render: (syncStatus: SyncStatus) => (
<EuiBadge color={syncStatusToColor(syncStatus)}>{syncStatusToText(syncStatus)}</EuiBadge>
),
truncateText: true,
},
{
actions: [
{
description: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.actions.viewJob.title',
{
defaultMessage: 'View this sync job',
}
),
icon: 'eye',
isPrimary: false,
name: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.actions.viewJob.caption',
{
defaultMessage: 'View this sync job',
}
),
onClick: (job) => setSyncJobFlyout(job),
type: 'icon',
},
],
},
];
const { hasDocumentLevelSecurityFeature } = useValues(IndexViewLogic);
const { productFeatures } = useValues(KibanaLogic);
const [selectedSyncJobCategory, setSelectedSyncJobCategory] = useState<string>('content');
const shouldShowAccessSyncs =
productFeatures.hasDocumentLevelSecurityEnabled && hasDocumentLevelSecurityFeature;
return (
<>
<SyncJobFlyout onClose={() => setSyncJobFlyout(undefined)} syncJob={syncJobFlyout} />
<EuiBasicTable
data-test-subj="entSearchContent-index-syncJobs-table"
items={syncJobs}
columns={columns}
hasActions
onChange={({ page: { index, size } }: { page: { index: number; size: number } }) => {
if (connectorId) {
fetchSyncJobs({ connectorId, from: index * size, size });
}
}}
pagination={pageToPagination(syncJobsPagination)}
tableLayout="fixed"
loading={syncJobsLoading}
/>
{shouldShowAccessSyncs && (
<EuiButtonGroup
legend={i18n.translate(
'xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.legend',
{ defaultMessage: 'Select sync job type to display.' }
)}
name={i18n.translate(
'xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.name',
{ defaultMessage: 'Sync job type' }
)}
idSelected={selectedSyncJobCategory}
onChange={(optionId) => {
setSelectedSyncJobCategory(optionId);
}}
options={[
{
id: 'content',
label: i18n.translate(
'xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.content.label',
{ defaultMessage: 'Content syncs' }
),
},
{
id: 'access_control',
label: i18n.translate(
'xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.accessControl.label',
{ defaultMessage: 'Access control syncs' }
),
},
]}
/>
)}
{selectedSyncJobCategory === 'content' ? (
<SyncJobsHistoryTable type="content" />
) : (
<SyncJobsHistoryTable type="access_control" />
)}
</>
);
};

View file

@ -0,0 +1,181 @@
/*
* 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, { useEffect, useState } from 'react';
import { useActions, useValues } from 'kea';
import { EuiBadge, EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SyncJobType, SyncStatus } from '../../../../../../common/types/connectors';
import { FormattedDateTime } from '../../../../shared/formatted_date_time';
import { pageToPagination } from '../../../../shared/pagination/page_to_pagination';
import { durationToText } from '../../../utils/duration_to_text';
import {
syncJobTypeToText,
syncStatusToColor,
syncStatusToText,
} from '../../../utils/sync_status_to_text';
import { IndexViewLogic } from '../index_view_logic';
import { SyncJobFlyout } from './sync_job_flyout';
import { SyncJobsViewLogic, SyncJobView } from './sync_jobs_view_logic';
interface SyncJobHistoryTableProps {
type: 'content' | 'access_control';
}
export const SyncJobsHistoryTable: React.FC<SyncJobHistoryTableProps> = ({ type }) => {
const { connectorId } = useValues(IndexViewLogic);
const { fetchSyncJobs } = useActions(SyncJobsViewLogic);
const { syncJobs, syncJobsLoading, syncJobsPagination } = useValues(SyncJobsViewLogic);
const [syncJobFlyout, setSyncJobFlyout] = useState<SyncJobView | undefined>(undefined);
const columns: Array<EuiBasicTableColumn<SyncJobView>> = [
{
field: 'lastSync',
name: i18n.translate('xpack.enterpriseSearch.content.syncJobs.lastSync.columnTitle', {
defaultMessage: 'Last sync',
}),
render: (lastSync: string) => <FormattedDateTime date={new Date(lastSync)} />,
sortable: true,
truncateText: true,
},
{
field: 'duration',
name: i18n.translate('xpack.enterpriseSearch.content.syncJobs.syncDuration.columnTitle', {
defaultMessage: 'Sync duration',
}),
render: (duration: moment.Duration) => durationToText(duration),
sortable: true,
truncateText: true,
},
...(type === 'content'
? [
{
field: 'indexed_document_count',
name: i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.addedDocs.columnTitle',
{
defaultMessage: 'Docs added',
}
),
sortable: true,
truncateText: true,
},
{
field: 'deleted_document_count',
name: i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.deletedDocs.columnTitle',
{
defaultMessage: 'Docs deleted',
}
),
sortable: true,
truncateText: true,
},
{
field: 'job_type',
name: i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.syncJobType.columnTitle',
{
defaultMessage: 'Content sync type',
}
),
render: (syncType: SyncJobType) => {
const syncJobTypeText = syncJobTypeToText(syncType);
if (syncJobTypeText.length === 0) return null;
return <EuiBadge color="hollow">{syncJobTypeText}</EuiBadge>;
},
sortable: true,
truncateText: true,
},
]
: []),
...(type === 'access_control'
? [
{
field: 'indexed_document_count',
name: i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.identitySync.columnTitle',
{
defaultMessage: 'Identities synced',
}
),
sortable: true,
truncateText: true,
},
]
: []),
{
field: 'status',
name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.syncStatus.columnTitle', {
defaultMessage: 'Status',
}),
render: (syncStatus: SyncStatus) => (
<EuiBadge color={syncStatusToColor(syncStatus)}>{syncStatusToText(syncStatus)}</EuiBadge>
),
truncateText: true,
},
{
actions: [
{
description: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.actions.viewJob.title',
{
defaultMessage: 'View this sync job',
}
),
icon: 'eye',
isPrimary: false,
name: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.actions.viewJob.caption',
{
defaultMessage: 'View this sync job',
}
),
onClick: (job) => setSyncJobFlyout(job),
type: 'icon',
},
],
},
];
useEffect(() => {
if (connectorId) {
fetchSyncJobs({
connectorId,
from: syncJobsPagination.from ?? 0,
size: syncJobsPagination.size ?? 10,
type,
});
}
}, [connectorId, type]);
return (
<>
<SyncJobFlyout onClose={() => setSyncJobFlyout(undefined)} syncJob={syncJobFlyout} />
<EuiBasicTable
data-test-subj={`entSearchContent-index-${type}-syncJobs-table`}
items={syncJobs}
columns={columns}
hasActions
onChange={({ page: { index, size } }: { page: { index: number; size: number } }) => {
if (connectorId) {
fetchSyncJobs({ connectorId, from: index * size, size, type });
}
}}
pagination={pageToPagination(syncJobsPagination)}
tableLayout="fixed"
loading={syncJobsLoading}
/>
</>
);
};

View file

@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
import { SyncStatus } from '../../../../common/types/connectors';
import { SyncJobType, SyncStatus } from '../../../../common/types/connectors';
export function syncStatusToText(status: SyncStatus): string {
switch (status) {
@ -56,3 +56,18 @@ export function syncStatusToColor(status: SyncStatus): string {
return 'warning';
}
}
export const syncJobTypeToText = (syncType: SyncJobType): string => {
switch (syncType) {
case SyncJobType.FULL:
return i18n.translate('xpack.enterpriseSearch.content.syncJobType.full', {
defaultMessage: 'Full content',
});
case SyncJobType.INCREMENTAL:
return i18n.translate('xpack.enterpriseSearch.content.syncJobType.incremental', {
defaultMessage: 'Incremental content',
});
default:
return '';
}
};

View file

@ -8,7 +8,7 @@
import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
import { CONNECTORS_JOBS_INDEX } from '../..';
import { ConnectorSyncJob } from '../../../common/types/connectors';
import { ConnectorSyncJob, SyncJobType } from '../../../common/types/connectors';
import { Paginate } from '../../../common/types/pagination';
import { isNotNullish } from '../../../common/utils/is_not_nullish';
@ -32,19 +32,42 @@ export const fetchSyncJobsByConnectorId = async (
client: IScopedClusterClient,
connectorId: string,
from: number,
size: number
size: number,
syncJobType: 'content' | 'access_control' | 'all' = 'all'
): Promise<Paginate<ConnectorSyncJob>> => {
try {
const query =
syncJobType === 'all'
? {
term: {
'connector.id': connectorId,
},
}
: {
bool: {
filter: [
{
term: {
'connector.id': connectorId,
},
},
{
terms: {
job_type:
syncJobType === 'content'
? [SyncJobType.FULL, SyncJobType.INCREMENTAL]
: [SyncJobType.ACCESS_CONTROL],
},
},
],
},
};
const result = await fetchWithPagination(
async () =>
await client.asCurrentUser.search<ConnectorSyncJob>({
from,
index: CONNECTORS_JOBS_INDEX,
query: {
term: {
'connector.id': connectorId,
},
},
query,
size,
sort: { created_at: { order: 'desc' } },
}),

View file

@ -208,6 +208,7 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
query: schema.object({
from: schema.number({ defaultValue: 0, min: 0 }),
size: schema.number({ defaultValue: 10, min: 0 }),
type: schema.maybe(schema.string()),
}),
},
},
@ -217,7 +218,8 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
client,
request.params.connectorId,
request.query.from,
request.query.size
request.query.size,
request.query.type as 'content' | 'access_control' | 'all'
);
return response.ok({ body: result });
})

View file

@ -127,8 +127,8 @@ export function createConnectorDocument({
pipeline,
scheduling: {
access_control: { enabled: false, interval: '0 0 0 * * ?' },
incremental: { enabled: false, interval: '0 0 0 * * ?' },
full: { enabled: false, interval: '0 0 0 * * ?' },
incremental: { enabled: false, interval: '0 0 0 * * ?' },
},
service_type: serviceType || null,
status: ConnectorStatus.CREATED,