[Enterprise Search] Adds a job detail flyout for connectors (#144794)

## Summary

This adds a jobs flyout to the overview page of every connector index.
<img width="748" alt="Screenshot 2022-11-08 at 11 56 58"
src="https://user-images.githubusercontent.com/94373878/200548683-3255fab2-12e5-4dba-9fb6-38d009c93d69.png">
This commit is contained in:
Sander Philipse 2022-11-09 11:56:21 +01:00 committed by GitHub
parent 098b5db77b
commit a7fdac4564
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1747 additions and 56 deletions

View file

@ -98,6 +98,11 @@ export interface FilteringConfig {
draft: FilteringRules;
}
export enum TriggerMethod {
ON_DEMAND = 'on-demand',
SCHEDULED = 'scheduled',
}
export interface Connector {
api_key_id: string | null;
configuration: ConnectorConfiguration;
@ -126,14 +131,25 @@ export interface Connector {
export type ConnectorDocument = Omit<Connector, 'id'>;
export interface ConnectorSyncJob {
cancelation_requested_at: string | null;
canceled_at: string | null;
completed_at: string | null;
connector?: ConnectorDocument;
connector_id: string;
created_at: string;
deleted_document_count: number;
error: string | null;
filtering: FilteringRules | null;
id: string;
index_name: string;
indexed_document_count: number;
indexed_document_volume: number;
last_seen: string;
metadata: Record<string, unknown>;
pipeline: IngestPipelineParams | null;
started_at: string;
status: SyncStatus;
trigger_method: TriggerMethod;
worker_hostname: string;
}
export type ConnectorSyncJobDocument = Omit<ConnectorSyncJob, 'id'>;

View file

@ -0,0 +1,39 @@
/*
* 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 moment from 'moment';
import { ConnectorSyncJob, TriggerMethod, SyncStatus } from '../../../../common/types/connectors';
import { SyncJobView } from '../components/search_index/sync_jobs/sync_jobs_view_logic';
export const syncJob: ConnectorSyncJob = {
cancelation_requested_at: null,
canceled_at: null,
completed_at: '2022-09-05T15:59:39.816+00:00',
connector_id: 'we2284IBjobuR2-lAuXh',
created_at: '2022-09-05T14:59:39.816+00:00',
deleted_document_count: 20,
error: null,
filtering: null,
id: 'id',
index_name: 'indexName',
indexed_document_count: 50,
indexed_document_volume: 40,
last_seen: '2022-09-05T15:59:39.816+00:00',
metadata: {},
pipeline: null,
trigger_method: TriggerMethod.ON_DEMAND,
started_at: '2022-09-05T14:59:39.816+00:00',
status: SyncStatus.COMPLETED,
worker_hostname: 'hostname_fake',
};
export const syncJobView: SyncJobView = {
...syncJob,
duration: moment.duration(1, 'hour'),
lastSync: '2022-09-05T15:59:39.816+00:00',
};

View file

@ -22,7 +22,7 @@ import { CrawlRequestsPanel } from './crawler/crawl_requests_panel/crawl_request
import { CrawlerTotalStats } from './crawler_total_stats';
import { GenerateApiKeyPanel } from './generate_api_key_panel';
import { OverviewLogic } from './overview.logic';
import { SyncJobs } from './sync_jobs';
import { SyncJobs } from './sync_jobs/sync_jobs';
export const SearchIndexOverview: React.FC = () => {
const { indexData } = useValues(OverviewLogic);

View file

@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DocumentsPanel renders 1`] = `
<FlyoutPanel
title="Documents"
>
<EuiBasicTable
columns={
Array [
Object {
"field": "added",
"name": "Added",
},
Object {
"field": "removed",
"name": "Removed",
},
Object {
"field": "total",
"name": "Total",
},
Object {
"field": "volume",
"name": "Volume",
"render": [Function],
},
]
}
items={
Array [
Object {
"added": 10,
"removed": 0,
"total": 305,
"volume": 1120,
},
]
}
noItemsMessage={
<EuiI18n
default="No items found"
token="euiBasicTable.noItemsMessage"
/>
}
responsive={true}
tableLayout="fixed"
/>
</FlyoutPanel>
`;

View file

@ -0,0 +1,115 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EventsPanel renders 1`] = `
<FlyoutPanel
title="Events"
>
<EuiBasicTable
columns={
Array [
Object {
"field": "title",
"name": "State",
"width": "50%",
},
Object {
"field": "date",
"name": "Time",
"width": "50%",
},
]
}
items={
Array [
Object {
"date": "Mon Oct 24 2022 02:44:19 GMT+0000",
"title": "Canceled",
},
Object {
"date": "Mon Oct 24 2022 02:44:19 GMT+0000",
"title": "Cancelation requested",
},
Object {
"date": "Mon Oct 24 2022 02:44:19 GMT+0000",
"title": "Completed",
},
Object {
"date": "Mon Oct 24 2022 02:44:19 GMT+0000",
"title": "Last updated",
},
Object {
"date": "Mon Oct 24 2022 02:44:19 GMT+0000",
"title": "Sync started",
},
Object {
"date": "Mon Oct 24 2022 02:44:19 GMT+0000",
"title": "Sync requested manually",
},
]
}
noItemsMessage={
<EuiI18n
default="No items found"
token="euiBasicTable.noItemsMessage"
/>
}
responsive={true}
tableLayout="fixed"
/>
</FlyoutPanel>
`;
exports[`EventsPanel renders with some values missing 1`] = `
<FlyoutPanel
title="Events"
>
<EuiBasicTable
columns={
Array [
Object {
"field": "title",
"name": "State",
"width": "50%",
},
Object {
"field": "date",
"name": "Time",
"width": "50%",
},
]
}
items={
Array [
Object {
"date": "Mon Oct 24 2022 02:44:19 GMT+0000",
"title": "Canceled",
},
Object {
"date": "Mon Oct 24 2022 02:44:19 GMT+0000",
"title": "Completed",
},
Object {
"date": "Mon Oct 24 2022 02:44:19 GMT+0000",
"title": "Last updated",
},
Object {
"date": "Mon Oct 24 2022 02:44:19 GMT+0000",
"title": "Sync started",
},
Object {
"date": "Mon Oct 24 2022 02:44:19 GMT+0000",
"title": "Sync requested manually",
},
]
}
noItemsMessage={
<EuiI18n
default="No items found"
token="euiBasicTable.noItemsMessage"
/>
}
responsive={true}
tableLayout="fixed"
/>
</FlyoutPanel>
`;

View file

@ -0,0 +1,202 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilteringPanel renders 1`] = `
<Fragment>
<FlyoutPanel
title="Filtering"
>
<EuiBasicTable
columns={
Array [
Object {
"field": "policy",
"name": "Pipeline setting",
"render": [Function],
},
Object {
"field": "rule",
"name": "Rule",
"render": [Function],
},
Object {
"field": "value",
"name": "Value",
"render": [Function],
},
]
}
items={Array []}
noItemsMessage={
<EuiI18n
default="No items found"
token="euiBasicTable.noItemsMessage"
/>
}
responsive={true}
tableLayout="fixed"
/>
</FlyoutPanel>
</Fragment>
`;
exports[`FilteringPanel renders advanced snippet 1`] = `
<Fragment>
<FlyoutPanel
title="Filtering"
>
<EuiBasicTable
columns={
Array [
Object {
"field": "policy",
"name": "Pipeline setting",
"render": [Function],
},
Object {
"field": "rule",
"name": "Rule",
"render": [Function],
},
Object {
"field": "value",
"name": "Value",
"render": [Function],
},
]
}
items={
Array [
Object {
"order": 0,
"policy": "include",
"rule": "equals",
"value": "THIS VALUE",
},
Object {
"order": 1,
"policy": "exclude",
"rule": "contains",
"value": "THIS VALUE",
},
Object {
"order": 2,
"policy": "exclude",
"rule": "ends_with",
"value": "THIS VALUE",
},
Object {
"order": 4,
"policy": "exclude",
"rule": "<",
"value": "THIS VALUE",
},
Object {
"order": 5,
"policy": "include",
"rule": ">",
"value": "THIS VALUE",
},
]
}
noItemsMessage={
<EuiI18n
default="No items found"
token="euiBasicTable.noItemsMessage"
/>
}
responsive={true}
tableLayout="fixed"
/>
</FlyoutPanel>
<EuiSpacer />
<FlyoutPanel
title="Advanced filtering rules"
>
<EuiPanel
hasShadow={false}
>
<EuiCodeBlock
language="json"
transparentBackground={true}
>
{
"one": "two",
"three": "four"
}
</EuiCodeBlock>
</EuiPanel>
</FlyoutPanel>
</Fragment>
`;
exports[`FilteringPanel renders filtering rules list 1`] = `
<Fragment>
<FlyoutPanel
title="Filtering"
>
<EuiBasicTable
columns={
Array [
Object {
"field": "policy",
"name": "Pipeline setting",
"render": [Function],
},
Object {
"field": "rule",
"name": "Rule",
"render": [Function],
},
Object {
"field": "value",
"name": "Value",
"render": [Function],
},
]
}
items={
Array [
Object {
"order": 0,
"policy": "include",
"rule": "equals",
"value": "THIS VALUE",
},
Object {
"order": 1,
"policy": "exclude",
"rule": "contains",
"value": "THIS VALUE",
},
Object {
"order": 2,
"policy": "exclude",
"rule": "ends_with",
"value": "THIS VALUE",
},
Object {
"order": 4,
"policy": "exclude",
"rule": "<",
"value": "THIS VALUE",
},
Object {
"order": 5,
"policy": "include",
"rule": ">",
"value": "THIS VALUE",
},
]
}
noItemsMessage={
<EuiI18n
default="No items found"
token="euiBasicTable.noItemsMessage"
/>
}
responsive={true}
tableLayout="fixed"
/>
</FlyoutPanel>
</Fragment>
`;

View file

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FlyoutPanel renders 1`] = `
<EuiPanel
color="subdued"
hasShadow={false}
paddingSize="l"
>
<EuiTitle
size="xs"
>
<h4>
Title
</h4>
</EuiTitle>
<EuiSpacer />
</EuiPanel>
`;

View file

@ -0,0 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinePanel renders 1`] = `
<FlyoutPanel
title="Pipeline"
>
<EuiBasicTable
columns={
Array [
Object {
"field": "setting",
"name": "Pipeline setting",
},
Object {
"field": "value",
"name": "Value",
},
]
}
items={
Array [
Object {
"setting": "Pipeline name",
"value": "name",
},
Object {
"setting": "Extract binary content",
"value": true,
},
Object {
"setting": "Reduce whitespace",
"value": true,
},
Object {
"setting": "Machine learning inference",
"value": false,
},
]
}
noItemsMessage={
<EuiI18n
default="No items found"
token="euiBasicTable.noItemsMessage"
/>
}
responsive={true}
tableLayout="fixed"
/>
</FlyoutPanel>
`;

View file

@ -0,0 +1,152 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SyncCalloutsPanel renders 1`] = `
<Fragment>
<EuiFlexItem>
<EuiCallOut
color="success"
iconType="check"
title="Sync complete"
>
Completed at Mon Sep 05 2022 15:59:39 GMT+0000
</EuiCallOut>
</EuiFlexItem>
<EuiFlexItem>
<EuiCallOut
color="primary"
iconType="iInCircle"
title="Sync started manually"
>
Started at Mon Sep 05 2022 14:59:39 GMT+0000.
</EuiCallOut>
</EuiFlexItem>
</Fragment>
`;
exports[`SyncCalloutsPanel renders canceled job 1`] = `
<Fragment>
<EuiFlexItem>
<EuiCallOut
color="success"
iconType="check"
title="Sync complete"
>
Completed at Mon Sep 05 2022 15:59:39 GMT+0000
</EuiCallOut>
</EuiFlexItem>
<EuiFlexItem>
<EuiCallOut
color="danger"
iconType="cross"
title="Sync canceled"
>
Sync canceled at .
</EuiCallOut>
</EuiFlexItem>
<EuiFlexItem>
<EuiCallOut
color="primary"
iconType="iInCircle"
title="Sync started manually"
>
Started at Mon Sep 05 2022 14:59:39 GMT+0000.
</EuiCallOut>
</EuiFlexItem>
</Fragment>
`;
exports[`SyncCalloutsPanel renders different trigger method 1`] = `
<Fragment>
<EuiFlexItem>
<EuiCallOut
color="success"
iconType="check"
title="Sync complete"
>
Completed at Mon Sep 05 2022 15:59:39 GMT+0000
</EuiCallOut>
</EuiFlexItem>
<EuiFlexItem>
<EuiCallOut
color="warning"
iconType="clock"
title="In progress"
>
Sync has been running for 1h 0m 0s.
</EuiCallOut>
</EuiFlexItem>
<EuiFlexItem>
<EuiCallOut
color="primary"
iconType="iInCircle"
title="Sync started by schedule"
>
Started at Mon Sep 05 2022 14:59:39 GMT+0000.
</EuiCallOut>
</EuiFlexItem>
</Fragment>
`;
exports[`SyncCalloutsPanel renders error job 1`] = `
<Fragment>
<EuiFlexItem>
<EuiCallOut
color="success"
iconType="check"
title="Sync complete"
>
Completed at Mon Sep 05 2022 15:59:39 GMT+0000
</EuiCallOut>
</EuiFlexItem>
<EuiFlexItem>
<EuiCallOut
color="danger"
iconType="cross"
title="Sync failure"
>
Sync failure: .
</EuiCallOut>
</EuiFlexItem>
<EuiFlexItem>
<EuiCallOut
color="primary"
iconType="iInCircle"
title="Sync started manually"
>
Started at Mon Sep 05 2022 14:59:39 GMT+0000.
</EuiCallOut>
</EuiFlexItem>
</Fragment>
`;
exports[`SyncCalloutsPanel renders in progress job 1`] = `
<Fragment>
<EuiFlexItem>
<EuiCallOut
color="success"
iconType="check"
title="Sync complete"
>
Completed at Mon Sep 05 2022 15:59:39 GMT+0000
</EuiCallOut>
</EuiFlexItem>
<EuiFlexItem>
<EuiCallOut
color="warning"
iconType="clock"
title="In progress"
>
Sync has been running for 1h 0m 0s.
</EuiCallOut>
</EuiFlexItem>
<EuiFlexItem>
<EuiCallOut
color="primary"
iconType="iInCircle"
title="Sync started manually"
>
Started at Mon Sep 05 2022 14:59:39 GMT+0000.
</EuiCallOut>
</EuiFlexItem>
</Fragment>
`;

View file

@ -0,0 +1,27 @@
/*
* 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 { shallow } from 'enzyme';
import { SyncJobDocumentsPanel } from './documents_panel';
describe('DocumentsPanel', () => {
const documents = {
added: 10,
removed: 0,
total: 305,
volume: 1120,
};
it('renders', () => {
const wrapper = shallow(<SyncJobDocumentsPanel {...documents} />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,60 @@
/*
* 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 { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import { ByteSizeValue } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { FlyoutPanel } from './flyout_panel';
interface SyncJobDocumentsPanelProps {
added: number;
removed: number;
total: number;
volume: number;
}
export const SyncJobDocumentsPanel: React.FC<SyncJobDocumentsPanelProps> = (syncJobDocuments) => {
const columns: Array<EuiBasicTableColumn<SyncJobDocumentsPanelProps>> = [
{
field: 'added',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.documents.added', {
defaultMessage: 'Added',
}),
},
{
field: 'removed',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.documents.removed', {
defaultMessage: 'Removed',
}),
},
{
field: 'total',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.documents.total', {
defaultMessage: 'Total',
}),
},
{
field: 'volume',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.documents.volume', {
defaultMessage: 'Volume',
}),
render: (volume: number) => new ByteSizeValue(volume).toString(),
},
];
return (
<FlyoutPanel
title={i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.documents.title', {
defaultMessage: 'Documents',
})}
>
<EuiBasicTable columns={columns} items={[syncJobDocuments]} />
</FlyoutPanel>
);
};

View file

@ -0,0 +1,43 @@
/*
* 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 { shallow } from 'enzyme';
import { TriggerMethod } from '../../../../../../common/types/connectors';
import { SyncJobEventsPanel } from './events_panel';
describe('EventsPanel', () => {
const events = {
cancelationRequestedAt: '2022-10-24T02:44:19.660365+00:00',
canceledAt: '2022-10-24T02:44:19.660365+00:00',
completed: '2022-10-24T02:44:19.660365+00:00',
lastUpdated: '2022-10-24T02:44:19.660365+00:00',
syncRequestedAt: '2022-10-24T02:44:19.660365+00:00',
syncStarted: '2022-10-24T02:44:19.660365+00:00',
triggerMethod: TriggerMethod.ON_DEMAND,
};
it('renders', () => {
const wrapper = shallow(<SyncJobEventsPanel {...events} />);
expect(wrapper).toMatchSnapshot();
});
it('renders with some values missing', () => {
const wrapper = shallow(
<SyncJobEventsPanel
{...events}
cancelationRequestedAt=""
triggerMethod={TriggerMethod.ON_DEMAND}
/>
);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,121 @@
/*
* 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 moment from 'moment';
import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { TriggerMethod } from '../../../../../../common/types/connectors';
import { dateToString } from '../../../utils/date_to_string';
import { FlyoutPanel } from './flyout_panel';
export interface SyncJobsEventPanelProps {
cancelationRequestedAt?: string;
canceledAt?: string;
completed?: string;
lastUpdated?: string;
syncRequestedAt?: string;
syncStarted?: string;
triggerMethod: TriggerMethod;
}
interface SyncJobEvent {
date?: string;
title: string;
}
export const SyncJobEventsPanel: React.FC<SyncJobsEventPanelProps> = ({
cancelationRequestedAt,
canceledAt,
completed,
lastUpdated,
syncRequestedAt,
syncStarted,
triggerMethod,
}) => {
const events: SyncJobEvent[] = [
{
date: dateToString(syncRequestedAt),
title:
triggerMethod === TriggerMethod.ON_DEMAND
? i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.events.syncRequestedManually',
{ defaultMessage: 'Sync requested manually' }
)
: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.events.syncRequestedScheduled',
{ defaultMessage: 'Sync requested by schedule' }
),
},
{
date: dateToString(syncStarted),
title: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.events.syncStarted', {
defaultMessage: 'Sync started',
}),
},
{
date: dateToString(lastUpdated),
title: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.events.lastUpdated', {
defaultMessage: 'Last updated',
}),
},
{
date: dateToString(completed),
title: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.events.completed', {
defaultMessage: 'Completed',
}),
},
{
date: dateToString(cancelationRequestedAt),
title: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.events.cancelationRequested',
{
defaultMessage: 'Cancelation requested',
}
),
},
{
date: dateToString(canceledAt),
title: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.events.canceled', {
defaultMessage: 'Canceled',
}),
},
]
.filter(({ date }) => !!date)
.sort(({ date }, { date: dateB }) => (moment(date).isAfter(moment(dateB)) ? 1 : -1));
const columns: Array<EuiBasicTableColumn<SyncJobEvent>> = [
{
field: 'title',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.events.state', {
defaultMessage: 'State',
}),
width: '50%',
},
{
field: 'date',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.events.time', {
defaultMessage: 'Time',
}),
width: '50%',
},
];
return (
<FlyoutPanel
title={i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.events.title', {
defaultMessage: 'Events',
})}
>
<EuiBasicTable columns={columns} items={events} />
</FlyoutPanel>
);
};

View file

@ -0,0 +1,78 @@
/*
* 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 { shallow } from 'enzyme';
import {
FilteringPolicy,
FilteringRule,
FilteringRuleRule,
} from '../../../../../../common/types/connectors';
import { FilteringPanel } from './filtering_panel';
describe('FilteringPanel', () => {
const filteringRules = [
{
order: 1,
policy: FilteringPolicy.EXCLUDE,
rule: FilteringRuleRule.CONTAINS,
value: 'THIS VALUE',
},
{
order: 2,
policy: FilteringPolicy.EXCLUDE,
rule: FilteringRuleRule.ENDS_WITH,
value: 'THIS VALUE',
},
{
order: 0,
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.EQUALS,
value: 'THIS VALUE',
},
{
order: 5,
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.GT,
value: 'THIS VALUE',
},
{
order: 4,
policy: FilteringPolicy.EXCLUDE,
rule: FilteringRuleRule.LT,
value: 'THIS VALUE',
},
] as FilteringRule[];
it('renders', () => {
const wrapper = shallow(<FilteringPanel filteringRules={[]} />);
expect(wrapper).toMatchSnapshot();
});
it('renders filtering rules list', () => {
const wrapper = shallow(<FilteringPanel filteringRules={filteringRules} />);
expect(wrapper).toMatchSnapshot();
});
it('renders advanced snippet', () => {
const wrapper = shallow(
<FilteringPanel
advancedSnippet={{
created_at: 'whatever',
updated_at: 'sometime',
value: { one: 'two', three: 'four' },
}}
filteringRules={filteringRules}
/>
);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,99 @@
/*
* 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 {
EuiBasicTable,
EuiBasicTableColumn,
EuiCode,
EuiCodeBlock,
EuiPanel,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
FilteringPolicy,
FilteringRule,
FilteringRuleRule,
FilteringRules,
} from '../../../../../../common/types/connectors';
import { filteringRuleToText, filteringPolicyToText } from '../../../utils/filtering_rule_helpers';
import { FlyoutPanel } from './flyout_panel';
interface FilteringPanelProps {
advancedSnippet?: FilteringRules['advanced_snippet'];
filteringRules: FilteringRule[];
}
export const FilteringPanel: React.FC<FilteringPanelProps> = ({
advancedSnippet,
filteringRules,
}) => {
const columns: Array<EuiBasicTableColumn<FilteringRule>> = [
{
field: 'policy',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.filtering.policy', {
defaultMessage: 'Pipeline setting',
}),
render: (policy: FilteringPolicy) => filteringPolicyToText(policy),
},
{
field: 'rule',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.filtering.rule', {
defaultMessage: 'Rule',
}),
render: (rule: FilteringRuleRule) => filteringRuleToText(rule),
},
{
field: 'value',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.filtering.value', {
defaultMessage: 'Value',
}),
render: (value: string) => <EuiCode>{value}</EuiCode>,
},
];
return (
<>
<FlyoutPanel
title={i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.filteringTitle', {
defaultMessage: 'Filtering',
})}
>
<EuiBasicTable
columns={columns}
items={filteringRules.sort(({ order }, { order: secondOrder }) => order - secondOrder)}
/>
</FlyoutPanel>
{!!advancedSnippet?.value ? (
<>
<EuiSpacer />
<FlyoutPanel
title={i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.filteringAdvancedTitle',
{
defaultMessage: 'Advanced filtering rules',
}
)}
>
<EuiPanel hasShadow={false}>
<EuiCodeBlock transparentBackground language="json">
{JSON.stringify(advancedSnippet.value, undefined, 2)}
</EuiCodeBlock>
</EuiPanel>
</FlyoutPanel>
</>
) : (
<></>
)}
</>
);
};

View file

@ -0,0 +1,20 @@
/*
* 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 { shallow } from 'enzyme';
import { FlyoutPanel } from './flyout_panel';
describe('FlyoutPanel', () => {
it('renders', () => {
const wrapper = shallow(<FlyoutPanel title="Title" />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,26 @@
/*
* 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 { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
interface FlyoutPanelProps {
title: string;
}
export const FlyoutPanel: React.FC<FlyoutPanelProps> = ({ children, title }) => {
return (
<EuiPanel paddingSize="l" color="subdued" hasShadow={false}>
<EuiTitle size="xs">
<h4>{title}</h4>
</EuiTitle>
<EuiSpacer />
{children}
</EuiPanel>
);
};

View file

@ -0,0 +1,26 @@
/*
* 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 { shallow } from 'enzyme';
import { PipelinePanel } from './pipeline_panel';
describe('PipelinePanel', () => {
const pipeline = {
extract_binary_content: true,
name: 'name',
reduce_whitespace: true,
run_ml_inference: false,
};
it('renders', () => {
const wrapper = shallow(<PipelinePanel pipeline={pipeline} />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,81 @@
/*
* 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 { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { IngestPipelineParams } from '../../../../../../common/types/connectors';
import { FlyoutPanel } from './flyout_panel';
interface PipelinePanelProps {
pipeline: IngestPipelineParams;
}
export const PipelinePanel: React.FC<PipelinePanelProps> = ({ pipeline }) => {
const items: Array<{ setting: string; value: string | boolean }> = [
{
setting: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.pipeline.name', {
defaultMessage: 'Pipeline name',
}),
value: pipeline.name,
},
{
setting: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.pipeline.extractBinaryContent',
{
defaultMessage: 'Extract binary content',
}
),
value: pipeline.extract_binary_content,
},
{
setting: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.pipeline.reduceWhitespace',
{
defaultMessage: 'Reduce whitespace',
}
),
value: pipeline.reduce_whitespace,
},
{
setting: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.pipeline.runMlInference',
{
defaultMessage: 'Machine learning inference',
}
),
value: pipeline.run_ml_inference,
},
];
const columns: Array<EuiBasicTableColumn<{ setting: string; value: string | boolean }>> = [
{
field: 'setting',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.pipeline.setting', {
defaultMessage: 'Pipeline setting',
}),
},
{
field: 'value',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.documents.value', {
defaultMessage: 'Value',
}),
},
];
return (
<FlyoutPanel
title={i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.pipeline.title', {
defaultMessage: 'Pipeline',
})}
>
<EuiBasicTable columns={columns} items={items} />
</FlyoutPanel>
);
};

View file

@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { syncJobView } from '../../../__mocks__/sync_job.mock';
import React from 'react';
import { shallow } from 'enzyme';
import { SyncStatus, TriggerMethod } from '../../../../../../common/types/connectors';
import { SyncJobCallouts } from './sync_callouts';
describe('SyncCalloutsPanel', () => {
it('renders', () => {
const wrapper = shallow(<SyncJobCallouts syncJob={syncJobView} />);
expect(wrapper).toMatchSnapshot();
});
it('renders error job', () => {
const wrapper = shallow(
<SyncJobCallouts syncJob={{ ...syncJobView, status: SyncStatus.ERROR }} />
);
expect(wrapper).toMatchSnapshot();
});
it('renders canceled job', () => {
const wrapper = shallow(
<SyncJobCallouts syncJob={{ ...syncJobView, status: SyncStatus.CANCELED }} />
);
expect(wrapper).toMatchSnapshot();
});
it('renders in progress job', () => {
const wrapper = shallow(
<SyncJobCallouts syncJob={{ ...syncJobView, status: SyncStatus.IN_PROGRESS }} />
);
expect(wrapper).toMatchSnapshot();
});
it('renders different trigger method', () => {
const wrapper = shallow(
<SyncJobCallouts
syncJob={{
...syncJobView,
status: SyncStatus.IN_PROGRESS,
trigger_method: TriggerMethod.SCHEDULED,
}}
/>
);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,138 @@
/*
* 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 { EuiFlexItem, EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SyncStatus, TriggerMethod } from '../../../../../../common/types/connectors';
import { dateToString } from '../../../utils/date_to_string';
import { durationToText } from '../../../utils/duration_to_text';
import { SyncJobView } from './sync_jobs_view_logic';
interface SyncJobCalloutsProps {
syncJob: SyncJobView;
}
export const SyncJobCallouts: React.FC<SyncJobCalloutsProps> = ({ syncJob }) => {
return (
<>
{!!syncJob.completed_at && (
<EuiFlexItem>
<EuiCallOut
color="success"
iconType="check"
title={i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.completedTitle', {
defaultMessage: 'Sync complete',
})}
>
{i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.completedDescription', {
defaultMessage: 'Completed at {date}',
values: {
date: dateToString(syncJob.completed_at),
},
})}
</EuiCallOut>
</EuiFlexItem>
)}
{syncJob.status === SyncStatus.ERROR && (
<EuiFlexItem>
<EuiCallOut
color="danger"
iconType="cross"
title={i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.failureTitle', {
defaultMessage: 'Sync failure',
})}
>
{i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.failureDescription', {
defaultMessage: 'Sync failure: {error}.',
values: {
error: syncJob.error,
},
})}
</EuiCallOut>
</EuiFlexItem>
)}
{syncJob.status === SyncStatus.CANCELED && (
<EuiFlexItem>
<EuiCallOut
color="danger"
iconType="cross"
title={i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.canceledTitle', {
defaultMessage: 'Sync canceled',
})}
>
{i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.canceledDescription', {
defaultMessage: 'Sync canceled at {date}.',
values: {
date: dateToString(syncJob.canceled_at ?? ''),
},
})}
</EuiCallOut>
</EuiFlexItem>
)}
{syncJob.status === SyncStatus.IN_PROGRESS && (
<EuiFlexItem>
<EuiCallOut
color="warning"
iconType="clock"
title={i18n.translate(
'xpack.enterpriseSearch.content.syncJobs.flyout.inProgressTitle',
{
defaultMessage: 'In progress',
}
)}
>
{i18n.translate(
'xpack.enterpriseSearch.content.syncJobs.flyout.inProgressDescription',
{
defaultMessage: 'Sync has been running for {duration}.',
values: {
duration: durationToText(syncJob.duration),
},
}
)}
</EuiCallOut>
</EuiFlexItem>
)}
{!!syncJob.started_at && (
<EuiFlexItem>
<EuiCallOut
color="primary"
iconType="iInCircle"
title={
syncJob.trigger_method === TriggerMethod.ON_DEMAND
? i18n.translate(
'xpack.enterpriseSearch.content.syncJobs.flyout.syncStartedManually',
{
defaultMessage: 'Sync started manually',
}
)
: i18n.translate(
'xpack.enterpriseSearch.content.syncJobs.flyout.syncStartedScheduled',
{
defaultMessage: 'Sync started by schedule',
}
)
}
>
{i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.startedAtDescription', {
defaultMessage: 'Started at {date}.',
values: {
date: dateToString(syncJob.started_at),
},
})}
</EuiCallOut>
</EuiFlexItem>
)}
</>
);
};

View file

@ -0,0 +1,106 @@
/*
* 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 {
EuiBasicTable,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SyncJobDocumentsPanel } from './documents_panel';
import { SyncJobEventsPanel } from './events_panel';
import { FilteringPanel } from './filtering_panel';
import { FlyoutPanel } from './flyout_panel';
import { PipelinePanel } from './pipeline_panel';
import { SyncJobCallouts } from './sync_callouts';
import { SyncJobView } from './sync_jobs_view_logic';
interface SyncJobFlyoutProps {
onClose: () => void;
syncJob?: SyncJobView;
}
export const SyncJobFlyout: React.FC<SyncJobFlyoutProps> = ({ onClose, syncJob }) => {
const visible = !!syncJob;
return visible ? (
<EuiFlyout onClose={onClose}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>
{i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.title', {
defaultMessage: 'Event log',
})}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiFlexGroup direction="column">
<SyncJobCallouts syncJob={syncJob} />
<EuiFlexItem>
<FlyoutPanel
title={i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.sync', {
defaultMessage: 'Sync',
})}
>
<EuiBasicTable
columns={[
{
field: 'id',
name: i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.sync.id', {
defaultMessage: 'ID',
}),
},
]}
items={[{ id: syncJob.id }]}
/>
</FlyoutPanel>
</EuiFlexItem>
<EuiFlexItem>
<SyncJobDocumentsPanel
added={syncJob.indexed_document_count}
total={0}
removed={syncJob.deleted_document_count}
volume={syncJob.indexed_document_volume ?? 0}
/>
</EuiFlexItem>
<EuiFlexItem>
<SyncJobEventsPanel
canceledAt={syncJob.canceled_at ?? ''}
cancelationRequestedAt={syncJob.cancelation_requested_at ?? ''}
syncRequestedAt={syncJob.created_at}
syncStarted={syncJob.started_at}
completed={syncJob.completed_at ?? ''}
lastUpdated={syncJob.last_seen}
triggerMethod={syncJob.trigger_method}
/>
</EuiFlexItem>
<EuiFlexItem>
<FilteringPanel
advancedSnippet={syncJob.filtering?.advanced_snippet}
filteringRules={syncJob.filtering?.rules ?? []}
/>
</EuiFlexItem>
{syncJob.pipeline && (
<EuiFlexItem>
<PipelinePanel pipeline={syncJob.pipeline} />
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyoutBody>
</EuiFlyout>
) : (
<></>
);
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useActions, useValues } from 'kea';
@ -13,20 +13,23 @@ import { EuiBadge, EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SyncStatus } from '../../../../../common/types/connectors';
import { SyncStatus } from '../../../../../../common/types/connectors';
import { FormattedDateTime } from '../../../shared/formatted_date_time';
import { durationToText } from '../../utils/duration_to_text';
import { FormattedDateTime } from '../../../../shared/formatted_date_time';
import { durationToText } from '../../../utils/duration_to_text';
import { syncStatusToColor, syncStatusToText } from '../../utils/sync_status_to_text';
import { syncStatusToColor, syncStatusToText } from '../../../utils/sync_status_to_text';
import { IndexViewLogic } from './index_view_logic';
import { IndexViewLogic } from '../index_view_logic';
import { SyncJobFlyout } from './sync_job_flyout';
import { SyncJobsViewLogic, SyncJobView } from './sync_jobs_view_logic';
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) {
@ -47,7 +50,6 @@ export const SyncJobs: React.FC = () => {
render: (lastSync: string) => <FormattedDateTime date={new Date(lastSync)} />,
sortable: true,
truncateText: true,
width: '25%',
},
{
field: 'duration',
@ -57,7 +59,6 @@ export const SyncJobs: React.FC = () => {
render: (duration: moment.Duration) => durationToText(duration),
sortable: true,
truncateText: true,
width: '25%',
},
{
field: 'docsCount',
@ -66,7 +67,6 @@ export const SyncJobs: React.FC = () => {
}),
sortable: true,
truncateText: true,
width: '25%',
},
{
field: 'status',
@ -77,25 +77,50 @@ export const SyncJobs: React.FC = () => {
<EuiBadge color={syncStatusToColor(syncStatus)}>{syncStatusToText(syncStatus)}</EuiBadge>
),
truncateText: true,
width: '25%',
},
{
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',
},
],
},
];
return (
<EuiBasicTable
items={syncJobs}
columns={columns}
onChange={({ page: { index, size } }: { page: { index: number; size: number } }) => {
if (connectorId) {
fetchSyncJobs({ connectorId, page: index, size });
}
}}
pagination={{
...syncJobsPagination,
totalItemCount: syncJobsPagination.total,
}}
tableLayout="fixed"
loading={syncJobsLoading}
/>
<>
<SyncJobFlyout onClose={() => setSyncJobFlyout(undefined)} syncJob={syncJobFlyout} />
<EuiBasicTable
items={syncJobs}
columns={columns}
hasActions
onChange={({ page: { index, size } }: { page: { index: number; size: number } }) => {
if (connectorId) {
fetchSyncJobs({ connectorId, page: index, size });
}
}}
pagination={{
...syncJobsPagination,
totalItemCount: syncJobsPagination.total,
}}
tableLayout="fixed"
loading={syncJobsLoading}
/>
</>
);
};

View file

@ -5,18 +5,23 @@
* 2.0.
*/
import { LogicMounter, mockFlashMessageHelpers } from '../../../__mocks__/kea_logic';
import { LogicMounter, mockFlashMessageHelpers } from '../../../../__mocks__/kea_logic';
import moment from 'moment';
import { nextTick } from '@kbn/test-jest-helpers';
import { HttpError, Status } from '../../../../../common/types/api';
import { HttpError, Status } from '../../../../../../common/types/api';
import { SyncStatus } from '../../../../../common/types/connectors';
import { FetchSyncJobsApiLogic } from '../../api/connector/fetch_sync_jobs_api_logic';
import {
ConnectorSyncJob,
SyncStatus,
TriggerMethod,
} from '../../../../../../common/types/connectors';
import { FetchSyncJobsApiLogic } from '../../../api/connector/fetch_sync_jobs_api_logic';
import { IndexViewLogic } from '../index_view_logic';
import { IndexViewLogic } from './index_view_logic';
import { SyncJobView, SyncJobsViewLogic } from './sync_jobs_view_logic';
// We can't test fetchTimeOutId because this will get set whenever the logic is created
@ -54,21 +59,31 @@ describe('SyncJobsViewLogic', () => {
describe('actions', () => {
describe('FetchIndexApiLogic.apiSuccess', () => {
const syncJob = {
const syncJob: ConnectorSyncJob = {
canceled_at: null,
cancelation_requested_at: null,
completed_at: '2022-09-05T15:59:39.816+00:00',
connector_id: 'we2284IBjobuR2-lAuXh',
created_at: '2022-09-05T14:59:39.816+00:00',
deleted_document_count: 20,
error: null,
filtering: null,
id: 'id',
index_name: 'indexName',
indexed_document_count: 50,
indexed_document_volume: 40,
last_seen: '2022-09-05T15:59:39.816+00:00',
metadata: {},
pipeline: null,
trigger_method: TriggerMethod.ON_DEMAND,
started_at: '2022-09-05T14:59:39.816+00:00',
status: SyncStatus.COMPLETED,
worker_hostname: 'hostname_fake',
};
const syncJobView: SyncJobView = {
docsCount: 30,
...syncJob,
duration: moment.duration(1, 'hour'),
lastSync: syncJob.completed_at,
lastSync: syncJob.completed_at ?? '',
status: SyncStatus.COMPLETED,
};
it('should update values', async () => {
@ -124,8 +139,10 @@ describe('SyncJobsViewLogic', () => {
...DEFAULT_VALUES,
syncJobs: [
{
docsCount: 50,
duration: undefined,
...syncJob,
completed_at: null,
deleted_document_count: 0,
duration: expect.anything(),
lastSync: syncJob.created_at,
status: SyncStatus.IN_PROGRESS,
},

View file

@ -9,25 +9,23 @@ import { kea, MakeLogicType } from 'kea';
import moment from 'moment';
import { Status } from '../../../../../common/types/api';
import { Status } from '../../../../../../common/types/api';
import { ConnectorSyncJob, SyncStatus } from '../../../../../common/types/connectors';
import { Paginate } from '../../../../../common/types/pagination';
import { Actions } from '../../../shared/api_logic/create_api_logic';
import { clearFlashMessages, flashAPIErrors } from '../../../shared/flash_messages';
import { ConnectorSyncJob } from '../../../../../../common/types/connectors';
import { Paginate } from '../../../../../../common/types/pagination';
import { Actions } from '../../../../shared/api_logic/create_api_logic';
import { clearFlashMessages, flashAPIErrors } from '../../../../shared/flash_messages';
import {
FetchSyncJobsApiLogic,
FetchSyncJobsArgs,
FetchSyncJobsResponse,
} from '../../api/connector/fetch_sync_jobs_api_logic';
} from '../../../api/connector/fetch_sync_jobs_api_logic';
import { IndexViewLogic } from './index_view_logic';
import { IndexViewLogic } from '../index_view_logic';
export interface SyncJobView {
docsCount: number;
export interface SyncJobView extends ConnectorSyncJob {
duration: moment.Duration;
lastSync: string;
status: SyncStatus;
}
export interface IndexViewActions {
@ -74,14 +72,13 @@ export const SyncJobsViewLogic = kea<MakeLogicType<IndexViewValues, IndexViewAct
(data?: Paginate<ConnectorSyncJob>) =>
data?.data.map((syncJob) => {
return {
docsCount: syncJob.deleted_document_count
? syncJob.indexed_document_count - syncJob.deleted_document_count
: syncJob.indexed_document_count,
...syncJob,
duration: syncJob.completed_at
? moment.duration(moment(syncJob.completed_at).diff(moment(syncJob.created_at)))
: syncJob.started_at
? moment.duration(moment(new Date()).diff(moment(syncJob.started_at)))
: undefined,
lastSync: syncJob.completed_at ?? syncJob.created_at,
status: syncJob.status,
};
}) ?? [],
],

View file

@ -135,10 +135,24 @@ export const IndicesTable: React.FC<IndicesTableProps> = ({
{
actions: [
{
description: 'View this index',
description: i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.actions.viewIndex.title',
{
defaultMessage: 'View this index',
}
),
icon: 'eye',
isPrimary: false,
name: (index) => `View ${index.name}`,
name: (index) =>
i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.actions.viewIndex.caption',
{
defaultMessage: 'View index {indexName}',
values: {
indexName: index.name,
},
}
),
onClick: (index) =>
navigateToUrl(
generateEncodedPath(SEARCH_INDEX_PATH, {
@ -149,10 +163,24 @@ export const IndicesTable: React.FC<IndicesTableProps> = ({
},
{
color: 'danger',
description: 'Delete this index',
description: i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.actions.deleteIndex.title',
{
defaultMessage: 'Delete this index',
}
),
icon: 'trash',
isPrimary: false,
name: (index) => `Delete ${index.name}`,
name: (index) =>
i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.actions.deleteIndex.caption',
{
defaultMessage: 'Delete index {indexName}',
values: {
indexName: index.name,
},
}
),
onClick: (index) => onDelete(index),
type: 'icon',
},

View file

@ -0,0 +1,19 @@
/*
* 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 moment from 'moment';
export function dateToString(input?: string): string {
if (!input) {
return '';
}
try {
return moment(input).toLocaleString();
} catch {
return '';
}
}

View file

@ -0,0 +1,78 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { FilteringPolicy, FilteringRuleRule } from '../../../../common/types/connectors';
const filteringRuleStringMap: Record<FilteringRuleRule, string> = {
[FilteringRuleRule.CONTAINS]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.rules.contains',
{
defaultMessage: 'Contains',
}
),
[FilteringRuleRule.ENDS_WITH]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.rules.endsWith',
{
defaultMessage: 'Ends with',
}
),
[FilteringRuleRule.EQUALS]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.rules.equals',
{
defaultMessage: 'Equals',
}
),
[FilteringRuleRule.GT]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.rules.greaterThan',
{
defaultMessage: 'Greater than',
}
),
[FilteringRuleRule.LT]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.rules.lessThan',
{
defaultMessage: 'Less than',
}
),
[FilteringRuleRule.REGEX]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.rules.regEx',
{
defaultMessage: 'Regular expression',
}
),
[FilteringRuleRule.STARTS_WITH]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.rules.startsWith',
{
defaultMessage: 'Starts with',
}
),
};
export function filteringRuleToText(filteringRule: FilteringRuleRule): string {
return filteringRuleStringMap[filteringRule];
}
const filteringPolicyStringMap: Record<FilteringPolicy, string> = {
[FilteringPolicy.EXCLUDE]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.policy.exclude',
{
defaultMessage: 'Exclude',
}
),
[FilteringPolicy.INCLUDE]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.policy.include',
{
defaultMessage: 'Include',
}
),
};
export function filteringPolicyToText(filteringPolicy: FilteringPolicy): string {
return filteringPolicyStringMap[filteringPolicy];
}

View file

@ -49,7 +49,10 @@ export const fetchSyncJobsByConnectorId = async (
const total = totalToPaginateTotal(result.hits.total);
// If we get fewer results than the target page, make sure we return correct page we're on
const resultPageIndex = Math.min(pageIndex, Math.trunc(total.total / size));
const data = result.hits.hits.map((hit) => hit._source).filter(isNotNullish) ?? [];
const data =
result.hits.hits
.map((hit) => (hit._source ? { ...hit._source, id: hit._id } : null))
.filter(isNotNullish) ?? [];
return {
data,
pageIndex: resultPageIndex,