mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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  Access control syncs  When access control not enabled  ### 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:
parent
e8e5cec83d
commit
ebcfebed05
7 changed files with 287 additions and 122 deletions
|
@ -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 });
|
||||
};
|
||||
|
||||
|
|
|
@ -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" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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 '';
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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' } },
|
||||
}),
|
||||
|
|
|
@ -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 });
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue