mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[ML] Transforms: Improve transform list reloading behavior. (#164296)
## Summary - If the transform list fails to load, it does no longer show a non-refreshable full page error. Instead the error is shown in an inline callout and the refresh button is still present. - `AuthorizationProvider` is gone and has been replaced by a custom hook `useTransformCapabilities`. All client side code no longer relies on `privileges` being present but makes use of `capabilities` (like `canGetTransform` etc.). The custom route to fetch privileges and capabilities is also gone, instead capabilities are retrieved from Kibana's own `application.capabilities.transform` which we register server side. - Refactors all remote data fetching to use `react-query`. This gets rid of the custom code to refresh the transform list using observables, instead all CRUD actions now make use of `react-query`'s `useMutation` and trigger a cache invalidation of the transform list data to initiate a refetch. The `useApi` hook is gone, instead we now have specific hooks for data fetching that wrap `useQuery` (`useGetTransform`, `useGetTransformStats` etc.) and the existing hooks for actions have been refactored to use `useMutation` (`useStartTransforms`, `useStopTransforms` etc.). - Toasts for "success" messages have been removed. - All tests that made use of `toMatchSnapshot` have been refactored away from `enzyme` to `react-testing-lib` and no longer rely on snapshots, instead we make basic assertions on the rendered components. ### Checklist - [x] [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] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
e52dd715ca
commit
0b705eba71
163 changed files with 2415 additions and 4106 deletions
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { schema, type TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import type { ES_FIELD_TYPES } from '@kbn/field-types';
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import { getTransformsRequestSchema } from './transforms';
|
|||
|
||||
export const getTransformsStatsRequestSchema = getTransformsRequestSchema;
|
||||
|
||||
export type GetTransformsRequestSchema = TypeOf<typeof getTransformsStatsRequestSchema>;
|
||||
export type GetTransformsStatsRequestSchema = TypeOf<typeof getTransformsStatsRequestSchema>;
|
||||
|
||||
export interface GetTransformsStatsResponseSchema {
|
||||
node_failures?: object;
|
||||
|
|
|
@ -1,147 +0,0 @@
|
|||
/*
|
||||
* 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 type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
||||
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
||||
|
||||
import type { EsIndex } from '../types/es_index';
|
||||
import type { EsIngestPipeline } from '../types/es_ingest_pipeline';
|
||||
|
||||
// To be able to use the type guards on the client side, we need to make sure we don't import
|
||||
// the code of '@kbn/config-schema' but just its types, otherwise the client side code will
|
||||
// fail to build.
|
||||
import type { FieldHistogramsResponseSchema } from './field_histograms';
|
||||
import type { GetTransformsAuditMessagesResponseSchema } from './audit_messages';
|
||||
import type { DeleteTransformsResponseSchema } from './delete_transforms';
|
||||
import type { ResetTransformsResponseSchema } from './reset_transforms';
|
||||
import type { StartTransformsResponseSchema } from './start_transforms';
|
||||
import type { StopTransformsResponseSchema } from './stop_transforms';
|
||||
import type { ScheduleNowTransformsResponseSchema } from './schedule_now_transforms';
|
||||
import type {
|
||||
GetTransformNodesResponseSchema,
|
||||
GetTransformsResponseSchema,
|
||||
PostTransformsPreviewResponseSchema,
|
||||
PutTransformsResponseSchema,
|
||||
} from './transforms';
|
||||
import type { GetTransformsStatsResponseSchema } from './transforms_stats';
|
||||
import type { PostTransformsUpdateResponseSchema } from './update_transforms';
|
||||
|
||||
const isGenericResponseSchema = <T>(arg: any): arg is T => {
|
||||
return isPopulatedObject(arg, ['count', 'transforms']) && Array.isArray(arg.transforms);
|
||||
};
|
||||
|
||||
export const isGetTransformNodesResponseSchema = (
|
||||
arg: unknown
|
||||
): arg is GetTransformNodesResponseSchema => {
|
||||
return isPopulatedObject(arg, ['count']) && typeof arg.count === 'number';
|
||||
};
|
||||
|
||||
export const isGetTransformsResponseSchema = (arg: unknown): arg is GetTransformsResponseSchema => {
|
||||
return isGenericResponseSchema<GetTransformsResponseSchema>(arg);
|
||||
};
|
||||
|
||||
export const isGetTransformsStatsResponseSchema = (
|
||||
arg: unknown
|
||||
): arg is GetTransformsStatsResponseSchema => {
|
||||
return isGenericResponseSchema<GetTransformsStatsResponseSchema>(arg);
|
||||
};
|
||||
|
||||
export const isDeleteTransformsResponseSchema = (
|
||||
arg: unknown
|
||||
): arg is DeleteTransformsResponseSchema => {
|
||||
return (
|
||||
isPopulatedObject(arg) &&
|
||||
Object.values(arg).every((d) => isPopulatedObject(d, ['transformDeleted']))
|
||||
);
|
||||
};
|
||||
|
||||
export const isResetTransformsResponseSchema = (
|
||||
arg: unknown
|
||||
): arg is ResetTransformsResponseSchema => {
|
||||
return (
|
||||
isPopulatedObject(arg) &&
|
||||
Object.values(arg).every((d) => isPopulatedObject(d, ['transformReset']))
|
||||
);
|
||||
};
|
||||
|
||||
export const isEsIndices = (arg: unknown): arg is EsIndex[] => {
|
||||
return Array.isArray(arg);
|
||||
};
|
||||
|
||||
export const isEsIngestPipelines = (arg: unknown): arg is EsIngestPipeline[] => {
|
||||
return Array.isArray(arg) && arg.every((d) => isPopulatedObject(d, ['name']));
|
||||
};
|
||||
|
||||
export const isEsSearchResponse = (arg: unknown): arg is estypes.SearchResponse => {
|
||||
return isPopulatedObject(arg, ['hits']);
|
||||
};
|
||||
|
||||
type SearchResponseWithAggregations = Required<Pick<estypes.SearchResponse, 'aggregations'>> &
|
||||
estypes.SearchResponse;
|
||||
export const isEsSearchResponseWithAggregations = (
|
||||
arg: unknown
|
||||
): arg is SearchResponseWithAggregations => {
|
||||
return isEsSearchResponse(arg) && {}.hasOwnProperty.call(arg, 'aggregations');
|
||||
};
|
||||
|
||||
export const isFieldHistogramsResponseSchema = (
|
||||
arg: unknown
|
||||
): arg is FieldHistogramsResponseSchema => {
|
||||
return Array.isArray(arg);
|
||||
};
|
||||
|
||||
export const isGetTransformsAuditMessagesResponseSchema = (
|
||||
arg: unknown
|
||||
): arg is GetTransformsAuditMessagesResponseSchema => {
|
||||
return isPopulatedObject(arg, ['messages', 'total']);
|
||||
};
|
||||
|
||||
export const isPostTransformsPreviewResponseSchema = (
|
||||
arg: unknown
|
||||
): arg is PostTransformsPreviewResponseSchema => {
|
||||
return (
|
||||
isPopulatedObject(arg, ['generated_dest_index', 'preview']) &&
|
||||
typeof arg.generated_dest_index !== undefined &&
|
||||
Array.isArray(arg.preview)
|
||||
);
|
||||
};
|
||||
|
||||
export const isPostTransformsUpdateResponseSchema = (
|
||||
arg: unknown
|
||||
): arg is PostTransformsUpdateResponseSchema => {
|
||||
return isPopulatedObject(arg, ['id']) && typeof arg.id === 'string';
|
||||
};
|
||||
|
||||
export const isPutTransformsResponseSchema = (arg: unknown): arg is PutTransformsResponseSchema => {
|
||||
return (
|
||||
isPopulatedObject(arg, ['transformsCreated', 'errors']) &&
|
||||
Array.isArray(arg.transformsCreated) &&
|
||||
Array.isArray(arg.errors)
|
||||
);
|
||||
};
|
||||
|
||||
const isGenericSuccessResponseSchema = (arg: unknown) =>
|
||||
isPopulatedObject(arg) && Object.values(arg).every((d) => isPopulatedObject(d, ['success']));
|
||||
|
||||
export const isStartTransformsResponseSchema = (
|
||||
arg: unknown
|
||||
): arg is StartTransformsResponseSchema => {
|
||||
return isGenericSuccessResponseSchema(arg);
|
||||
};
|
||||
|
||||
export const isStopTransformsResponseSchema = (
|
||||
arg: unknown
|
||||
): arg is StopTransformsResponseSchema => {
|
||||
return isGenericSuccessResponseSchema(arg);
|
||||
};
|
||||
|
||||
export const isScheduleNowTransformsResponseSchema = (
|
||||
arg: unknown
|
||||
): arg is ScheduleNowTransformsResponseSchema => {
|
||||
return isGenericSuccessResponseSchema(arg);
|
||||
};
|
|
@ -32,6 +32,21 @@ const EXTERNAL_API_BASE_PATH = '/api/transform/';
|
|||
export const addInternalBasePath = (uri: string): string => `${INTERNAL_API_BASE_PATH}${uri}`;
|
||||
export const addExternalBasePath = (uri: string): string => `${EXTERNAL_API_BASE_PATH}${uri}`;
|
||||
|
||||
export const TRANSFORM_REACT_QUERY_KEYS = {
|
||||
DATA_SEARCH: 'transform.data_search',
|
||||
DATA_VIEW_EXISTS: 'transform.data_view_exists',
|
||||
GET_DATA_VIEW_TITLES: 'transform.get_data_view_titles',
|
||||
GET_ES_INDICES: 'transform.get_es_indices',
|
||||
GET_ES_INGEST_PIPELINES: 'transform.get_es_ingest_pipelines',
|
||||
GET_HISTOGRAMS_FOR_FIELDS: 'transform.get_histograms_for_fields',
|
||||
GET_TRANSFORM: 'transform.get_transform',
|
||||
GET_TRANSFORM_NODES: 'transform.get_transform_nodes',
|
||||
GET_TRANSFORM_AUDIT_MESSAGES: 'transform.get_transform_audit_messages',
|
||||
GET_TRANSFORM_STATS: 'transform.get_transform_stats',
|
||||
GET_TRANSFORMS: 'transform.get_transforms',
|
||||
GET_TRANSFORMS_PREVIEW: 'transform.get_transforms_preview',
|
||||
} as const;
|
||||
|
||||
// In order to create a transform, the API requires the following privileges:
|
||||
// - transform_admin (builtin)
|
||||
// - cluster privileges: manage_transform
|
||||
|
@ -71,22 +86,6 @@ export const APP_CLUSTER_PRIVILEGES = [
|
|||
// Minimum privileges required to return transform node count
|
||||
export const NODES_INFO_PRIVILEGES = ['cluster:monitor/transform/get'];
|
||||
|
||||
// Equivalent of capabilities.canGetTransform
|
||||
export const APP_GET_TRANSFORM_CLUSTER_PRIVILEGES = [
|
||||
'cluster.cluster:monitor/transform/get',
|
||||
'cluster.cluster:monitor/transform/stats/get',
|
||||
];
|
||||
|
||||
// Equivalent of capabilities.canCreateTransform
|
||||
export const APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES = [
|
||||
'cluster.cluster:monitor/transform/get',
|
||||
'cluster.cluster:monitor/transform/stats/get',
|
||||
'cluster.cluster:admin/transform/preview',
|
||||
'cluster.cluster:admin/transform/put',
|
||||
'cluster.cluster:admin/transform/start',
|
||||
'cluster.cluster:admin/transform/start_task',
|
||||
];
|
||||
|
||||
export const APP_INDEX_PRIVILEGES = ['monitor'];
|
||||
|
||||
// reflects https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformStats.java#L214
|
||||
|
|
|
@ -1,217 +0,0 @@
|
|||
/*
|
||||
* 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 { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { APP_INDEX_PRIVILEGES } from '../constants';
|
||||
import { Privileges } from '../types/privileges';
|
||||
|
||||
export interface PrivilegesAndCapabilities {
|
||||
privileges: Privileges;
|
||||
capabilities: Capabilities;
|
||||
}
|
||||
|
||||
export interface TransformCapabilities {
|
||||
canGetTransform: boolean;
|
||||
canDeleteTransform: boolean;
|
||||
canPreviewTransform: boolean;
|
||||
canCreateTransform: boolean;
|
||||
canReauthorizeTransform: boolean;
|
||||
canScheduleNowTransform: boolean;
|
||||
canStartStopTransform: boolean;
|
||||
canCreateTransformAlerts: boolean;
|
||||
canUseTransformAlerts: boolean;
|
||||
canResetTransform: boolean;
|
||||
}
|
||||
export type Capabilities = { [k in keyof TransformCapabilities]: boolean };
|
||||
|
||||
export const INITIAL_CAPABILITIES = Object.freeze<Capabilities>({
|
||||
canGetTransform: false,
|
||||
canDeleteTransform: false,
|
||||
canPreviewTransform: false,
|
||||
canCreateTransform: false,
|
||||
canReauthorizeTransform: false,
|
||||
canScheduleNowTransform: false,
|
||||
canStartStopTransform: false,
|
||||
canCreateTransformAlerts: false,
|
||||
canUseTransformAlerts: false,
|
||||
canResetTransform: false,
|
||||
});
|
||||
|
||||
export type Privilege = [string, string];
|
||||
|
||||
function isPrivileges(arg: unknown): arg is Privileges {
|
||||
return (
|
||||
isPopulatedObject(arg, ['hasAllPrivileges', 'missingPrivileges']) &&
|
||||
typeof arg.hasAllPrivileges === 'boolean' &&
|
||||
typeof arg.missingPrivileges === 'object' &&
|
||||
arg.missingPrivileges !== null
|
||||
);
|
||||
}
|
||||
|
||||
export const toArray = (value: string | string[]): string[] =>
|
||||
Array.isArray(value) ? value : [value];
|
||||
|
||||
export const hasPrivilegeFactory =
|
||||
(privileges: Privileges | undefined | null) => (privilege: Privilege) => {
|
||||
const [section, requiredPrivilege] = privilege;
|
||||
if (isPrivileges(privileges) && !privileges.missingPrivileges[section]) {
|
||||
// if the section does not exist in our missingPrivileges, everything is OK
|
||||
return true;
|
||||
}
|
||||
if (isPrivileges(privileges) && privileges.missingPrivileges[section]!.length === 0) {
|
||||
return true;
|
||||
}
|
||||
if (requiredPrivilege === '*') {
|
||||
// If length > 0 and we require them all... KO
|
||||
return false;
|
||||
}
|
||||
// If we require _some_ privilege, we make sure that the one
|
||||
// we require is *not* in the missingPrivilege array
|
||||
return (
|
||||
isPrivileges(privileges) &&
|
||||
!privileges.missingPrivileges[section]!.includes(requiredPrivilege)
|
||||
);
|
||||
};
|
||||
|
||||
export const extractMissingPrivileges = (
|
||||
privilegesObject: { [key: string]: boolean } = {}
|
||||
): string[] =>
|
||||
Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => {
|
||||
if (!privilegesObject[privilegeName]) {
|
||||
privileges.push(privilegeName);
|
||||
}
|
||||
return privileges;
|
||||
}, []);
|
||||
|
||||
export const getPrivilegesAndCapabilities = (
|
||||
clusterPrivileges: Record<string, boolean>,
|
||||
hasOneIndexWithAllPrivileges: boolean,
|
||||
hasAllPrivileges: boolean
|
||||
): PrivilegesAndCapabilities => {
|
||||
const privilegesResult: Privileges = {
|
||||
hasAllPrivileges: true,
|
||||
missingPrivileges: {
|
||||
cluster: [],
|
||||
index: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Find missing cluster privileges and set overall app privileges
|
||||
privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(clusterPrivileges);
|
||||
privilegesResult.hasAllPrivileges = hasAllPrivileges;
|
||||
|
||||
if (!hasOneIndexWithAllPrivileges) {
|
||||
privilegesResult.missingPrivileges.index = [...APP_INDEX_PRIVILEGES];
|
||||
}
|
||||
|
||||
const hasPrivilege = hasPrivilegeFactory(privilegesResult);
|
||||
|
||||
const capabilities = cloneDeep<Capabilities>(INITIAL_CAPABILITIES);
|
||||
capabilities.canGetTransform =
|
||||
hasPrivilege(['cluster', 'cluster:monitor/transform/get']) &&
|
||||
hasPrivilege(['cluster', 'cluster:monitor/transform/stats/get']);
|
||||
|
||||
capabilities.canCreateTransform = hasPrivilege(['cluster', 'cluster:admin/transform/put']);
|
||||
|
||||
capabilities.canDeleteTransform = hasPrivilege(['cluster', 'cluster:admin/transform/delete']);
|
||||
|
||||
capabilities.canResetTransform = hasPrivilege(['cluster', 'cluster:admin/transform/reset']);
|
||||
|
||||
capabilities.canPreviewTransform = hasPrivilege(['cluster', 'cluster:admin/transform/preview']);
|
||||
|
||||
capabilities.canStartStopTransform =
|
||||
hasPrivilege(['cluster', 'cluster:admin/transform/start']) &&
|
||||
hasPrivilege(['cluster', 'cluster:admin/transform/start_task']) &&
|
||||
hasPrivilege(['cluster', 'cluster:admin/transform/stop']);
|
||||
|
||||
capabilities.canCreateTransformAlerts = capabilities.canCreateTransform;
|
||||
|
||||
capabilities.canUseTransformAlerts = capabilities.canGetTransform;
|
||||
|
||||
capabilities.canScheduleNowTransform = capabilities.canStartStopTransform;
|
||||
|
||||
capabilities.canReauthorizeTransform = capabilities.canStartStopTransform;
|
||||
|
||||
return { privileges: privilegesResult, capabilities };
|
||||
};
|
||||
// create the text for button's tooltips if the user
|
||||
// doesn't have the permission to press that button
|
||||
export function createCapabilityFailureMessage(
|
||||
capability: keyof TransformCapabilities | 'noTransformNodes'
|
||||
) {
|
||||
let message = '';
|
||||
|
||||
switch (capability) {
|
||||
case 'canCreateTransform':
|
||||
message = i18n.translate('xpack.transform.capability.noPermission.createTransformTooltip', {
|
||||
defaultMessage: 'You do not have permission to create transforms.',
|
||||
});
|
||||
break;
|
||||
case 'canCreateTransformAlerts':
|
||||
message = i18n.translate(
|
||||
'xpack.transform.capability.noPermission.canCreateTransformAlertsTooltip',
|
||||
{
|
||||
defaultMessage: 'You do not have permission to create transform alert rules.',
|
||||
}
|
||||
);
|
||||
break;
|
||||
case 'canScheduleNowTransform':
|
||||
message = i18n.translate(
|
||||
'xpack.transform.capability.noPermission.scheduleNowTransformTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'You do not have permission to schedule transforms to process data instantly.',
|
||||
}
|
||||
);
|
||||
break;
|
||||
case 'canStartStopTransform':
|
||||
message = i18n.translate(
|
||||
'xpack.transform.capability.noPermission.startOrStopTransformTooltip',
|
||||
{
|
||||
defaultMessage: 'You do not have permission to start or stop transforms.',
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
||||
case 'canReauthorizeTransform':
|
||||
message = i18n.translate(
|
||||
'xpack.transform.capability.noPermission.reauthorizeTransformTooltip',
|
||||
{
|
||||
defaultMessage: 'You do not have permission to reauthorize transforms.',
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
||||
case 'canDeleteTransform':
|
||||
message = i18n.translate('xpack.transform.capability.noPermission.deleteTransformTooltip', {
|
||||
defaultMessage: 'You do not have permission to delete transforms.',
|
||||
});
|
||||
break;
|
||||
|
||||
case 'canResetTransform':
|
||||
message = i18n.translate('xpack.transform.capability.noPermission.resetTransformTooltip', {
|
||||
defaultMessage: 'You do not have permission to reset transforms.',
|
||||
});
|
||||
break;
|
||||
|
||||
case 'noTransformNodes':
|
||||
message = i18n.translate('xpack.transform.capability.noPermission.noTransformNodesTooltip', {
|
||||
defaultMessage: 'There are no transform nodes available.',
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return i18n.translate('xpack.transform.capability.pleaseContactAdministratorTooltip', {
|
||||
defaultMessage: '{message} Please contact your administrator.',
|
||||
values: {
|
||||
message,
|
||||
},
|
||||
});
|
||||
}
|
48
x-pack/plugins/transform/common/types/capabilities.ts
Normal file
48
x-pack/plugins/transform/common/types/capabilities.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
||||
|
||||
export const getInitialTransformCapabilities = (initialSetting = false) => ({
|
||||
canCreateTransform: initialSetting,
|
||||
canCreateTransformAlerts: initialSetting,
|
||||
canDeleteIndex: initialSetting,
|
||||
canDeleteTransform: initialSetting,
|
||||
canGetTransform: initialSetting,
|
||||
canPreviewTransform: initialSetting,
|
||||
canReauthorizeTransform: initialSetting,
|
||||
canResetTransform: initialSetting,
|
||||
canScheduleNowTransform: initialSetting,
|
||||
canStartStopTransform: initialSetting,
|
||||
canUseTransformAlerts: initialSetting,
|
||||
});
|
||||
|
||||
export const isTransformCapabilities = (arg: unknown): arg is TransformCapabilities => {
|
||||
return (
|
||||
isPopulatedObject(arg, Object.keys(getInitialTransformCapabilities())) &&
|
||||
Object.values(arg).every((d) => typeof d === 'boolean')
|
||||
);
|
||||
};
|
||||
|
||||
export type TransformCapabilities = ReturnType<typeof getInitialTransformCapabilities>;
|
||||
export type TransformCapability = keyof TransformCapabilities;
|
||||
|
||||
export interface PrivilegesAndCapabilities {
|
||||
privileges: Privileges;
|
||||
capabilities: TransformCapabilities;
|
||||
}
|
||||
|
||||
export type Privilege = [string, string];
|
||||
|
||||
export interface MissingPrivileges {
|
||||
[key: string]: string[] | undefined;
|
||||
}
|
||||
|
||||
export interface Privileges {
|
||||
hasAllPrivileges: boolean;
|
||||
missingPrivileges: MissingPrivileges;
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* 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 interface MissingPrivileges {
|
||||
[key: string]: string[] | undefined;
|
||||
}
|
||||
|
||||
export interface Privileges {
|
||||
hasAllPrivileges: boolean;
|
||||
missingPrivileges: MissingPrivileges;
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 type { TransformCapabilities } from '../types/capabilities';
|
||||
|
||||
// create the text for button's tooltips if the user
|
||||
// doesn't have the permission to press that button
|
||||
export function createCapabilityFailureMessage(
|
||||
capability: keyof TransformCapabilities | 'noTransformNodes'
|
||||
) {
|
||||
let message = '';
|
||||
|
||||
switch (capability) {
|
||||
case 'canCreateTransform':
|
||||
message = i18n.translate('xpack.transform.capability.noPermission.createTransformTooltip', {
|
||||
defaultMessage: 'You do not have permission to create transforms.',
|
||||
});
|
||||
break;
|
||||
case 'canCreateTransformAlerts':
|
||||
message = i18n.translate(
|
||||
'xpack.transform.capability.noPermission.canCreateTransformAlertsTooltip',
|
||||
{
|
||||
defaultMessage: 'You do not have permission to create transform alert rules.',
|
||||
}
|
||||
);
|
||||
break;
|
||||
case 'canScheduleNowTransform':
|
||||
message = i18n.translate(
|
||||
'xpack.transform.capability.noPermission.scheduleNowTransformTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'You do not have permission to schedule transforms to process data instantly.',
|
||||
}
|
||||
);
|
||||
break;
|
||||
case 'canStartStopTransform':
|
||||
message = i18n.translate(
|
||||
'xpack.transform.capability.noPermission.startOrStopTransformTooltip',
|
||||
{
|
||||
defaultMessage: 'You do not have permission to start or stop transforms.',
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
||||
case 'canReauthorizeTransform':
|
||||
message = i18n.translate(
|
||||
'xpack.transform.capability.noPermission.reauthorizeTransformTooltip',
|
||||
{
|
||||
defaultMessage: 'You do not have permission to reauthorize transforms.',
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
||||
case 'canDeleteTransform':
|
||||
message = i18n.translate('xpack.transform.capability.noPermission.deleteTransformTooltip', {
|
||||
defaultMessage: 'You do not have permission to delete transforms.',
|
||||
});
|
||||
break;
|
||||
|
||||
case 'canResetTransform':
|
||||
message = i18n.translate('xpack.transform.capability.noPermission.resetTransformTooltip', {
|
||||
defaultMessage: 'You do not have permission to reset transforms.',
|
||||
});
|
||||
break;
|
||||
|
||||
case 'noTransformNodes':
|
||||
message = i18n.translate('xpack.transform.capability.noPermission.noTransformNodesTooltip', {
|
||||
defaultMessage: 'There are no transform nodes available.',
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return i18n.translate('xpack.transform.capability.pleaseContactAdministratorTooltip', {
|
||||
defaultMessage: '{message} Please contact your administrator.',
|
||||
values: {
|
||||
message,
|
||||
},
|
||||
});
|
||||
}
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './components';
|
||||
export const toArray = <T>(value: T | T[]): T[] => (Array.isArray(value) ? value : [value]);
|
|
@ -6,16 +6,15 @@
|
|||
*/
|
||||
|
||||
import { EuiForm, EuiSpacer } from '@elastic/eui';
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { FC, useCallback, useEffect, useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { TransformHealthRuleParams } from '../../../common/types/alerting';
|
||||
import { TestsSelectionControl } from './tests_selection_control';
|
||||
import { TransformSelectorControl } from './transform_selector_control';
|
||||
import { useApi } from '../../app/hooks';
|
||||
import { useGetTransforms } from '../../app/hooks';
|
||||
import { useToastNotifications } from '../../app/app_dependencies';
|
||||
import { GetTransformsResponseSchema } from '../../../common/api_schemas/transforms';
|
||||
import { ALL_TRANSFORMS_SELECTION } from '../../../common/constants';
|
||||
|
||||
export type TransformHealthRuleTriggerProps =
|
||||
|
@ -29,9 +28,12 @@ const TransformHealthRuleTrigger: FC<TransformHealthRuleTriggerProps> = ({
|
|||
const formErrors = Object.values(errors).flat();
|
||||
const isFormInvalid = formErrors.length > 0;
|
||||
|
||||
const api = useApi();
|
||||
const toast = useToastNotifications();
|
||||
const [transformOptions, setTransformOptions] = useState<string[]>([]);
|
||||
const { error, data } = useGetTransforms();
|
||||
const transformOptions = useMemo(
|
||||
() => data?.transforms.filter((v) => v.config.sync).map((v) => v.id) ?? [],
|
||||
[data]
|
||||
);
|
||||
|
||||
const onAlertParamChange = useCallback(
|
||||
<T extends keyof TransformHealthRuleParams>(param: T) =>
|
||||
|
@ -41,34 +43,18 @@ const TransformHealthRuleTrigger: FC<TransformHealthRuleTriggerProps> = ({
|
|||
[setRuleParams]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function fetchTransforms() {
|
||||
let unmounted = false;
|
||||
api
|
||||
.getTransforms()
|
||||
.then((r) => {
|
||||
if (!unmounted) {
|
||||
setTransformOptions(
|
||||
(r as GetTransformsResponseSchema).transforms.filter((v) => v.sync).map((v) => v.id)
|
||||
);
|
||||
useEffect(() => {
|
||||
if (error !== null) {
|
||||
toast.addError(error, {
|
||||
title: i18n.translate(
|
||||
'xpack.transform.alertingRuleTypes.transformHealth.fetchErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Unable to fetch transforms',
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.addError(e, {
|
||||
title: i18n.translate(
|
||||
'xpack.transform.alertingRuleTypes.transformHealth.fetchErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Unable to fetch transforms',
|
||||
}
|
||||
),
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
unmounted = true;
|
||||
};
|
||||
},
|
||||
[api, toast]
|
||||
);
|
||||
),
|
||||
});
|
||||
}
|
||||
}, [error, toast]);
|
||||
|
||||
const excludeTransformOptions = useMemo(() => {
|
||||
if (ruleParams.includeTransforms?.some((v) => v === ALL_TRANSFORMS_SELECTION)) {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useContext, FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
|
@ -13,68 +13,50 @@ import { EuiErrorBoundary } from '@elastic/eui';
|
|||
|
||||
import { Router, Routes, Route } from '@kbn/shared-ux-router';
|
||||
import { ScopedHistory } from '@kbn/core/public';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import { addInternalBasePath } from '../../common/constants';
|
||||
|
||||
import { SectionError } from './components';
|
||||
import { SECTION_SLUG } from './common/constants';
|
||||
import { AuthorizationContext, AuthorizationProvider } from './lib/authorization';
|
||||
import { AppDependencies } from './app_dependencies';
|
||||
import { CloneTransformSection } from './sections/clone_transform';
|
||||
import { CreateTransformSection } from './sections/create_transform';
|
||||
import { TransformManagementSection } from './sections/transform_management';
|
||||
|
||||
export const App: FC<{ history: ScopedHistory }> = ({ history }) => {
|
||||
const { apiError } = useContext(AuthorizationContext);
|
||||
if (apiError !== null) {
|
||||
return (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.transform.app.checkingPrivilegesErrorMessage"
|
||||
defaultMessage="Error fetching user privileges from the server"
|
||||
/>
|
||||
}
|
||||
error={apiError}
|
||||
export const App: FC<{ history: ScopedHistory }> = ({ history }) => (
|
||||
<Router history={history}>
|
||||
<Routes>
|
||||
<Route
|
||||
path={`/${SECTION_SLUG.CLONE_TRANSFORM}/:transformId`}
|
||||
component={CloneTransformSection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Router history={history}>
|
||||
<Routes>
|
||||
<Route
|
||||
path={`/${SECTION_SLUG.CLONE_TRANSFORM}/:transformId`}
|
||||
component={CloneTransformSection}
|
||||
/>
|
||||
<Route
|
||||
path={`/${SECTION_SLUG.CREATE_TRANSFORM}/:savedObjectId`}
|
||||
component={CreateTransformSection}
|
||||
/>
|
||||
<Route path={`/`} component={TransformManagementSection} />
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
<Route
|
||||
path={`/${SECTION_SLUG.CREATE_TRANSFORM}/:savedObjectId`}
|
||||
component={CreateTransformSection}
|
||||
/>
|
||||
<Route path={`/`} component={TransformManagementSection} />
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
|
||||
export const renderApp = (element: HTMLElement, appDependencies: AppDependencies) => {
|
||||
const I18nContext = appDependencies.i18n.Context;
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: Infinity,
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<EuiErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<KibanaThemeProvider theme$={appDependencies.theme.theme$}>
|
||||
<KibanaContextProvider services={appDependencies}>
|
||||
<AuthorizationProvider
|
||||
privilegesEndpoint={{ path: addInternalBasePath(`privileges`), version: '1' }}
|
||||
>
|
||||
<I18nContext>
|
||||
<App history={appDependencies.history} />
|
||||
</I18nContext>
|
||||
</AuthorizationProvider>
|
||||
<I18nContext>
|
||||
<App history={appDependencies.history} />
|
||||
</I18nContext>
|
||||
</KibanaContextProvider>
|
||||
</KibanaThemeProvider>
|
||||
</QueryClientProvider>
|
||||
|
|
|
@ -19,12 +19,7 @@ export {
|
|||
toggleSelectedField,
|
||||
} from './fields';
|
||||
export type { DropDownLabel, DropDownOption, Label } from './dropdown';
|
||||
export {
|
||||
isTransformIdValid,
|
||||
refreshTransformList$,
|
||||
useRefreshTransformList,
|
||||
REFRESH_TRANSFORM_LIST_STATE,
|
||||
} from './transform';
|
||||
export { isTransformIdValid } from './transform';
|
||||
export type { TransformListAction, TransformListRow } from './transform_list';
|
||||
export { TRANSFORM_LIST_COLUMN } from './transform_list';
|
||||
export { getTransformProgress, isCompletedBatchTransform } from './transform_stats';
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { SECTION_SLUG } from './constants';
|
||||
|
|
|
@ -5,11 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { filter, distinctUntilChanged } from 'rxjs/operators';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import type { TransformConfigUnion, TransformId } from '../../../common/types/transform';
|
||||
|
||||
// Via https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/utils/TransformStrings.java#L24
|
||||
|
@ -23,64 +20,6 @@ export function isTransformIdValid(transformId: TransformId) {
|
|||
export const TRANSFORM_ERROR_TYPE = {
|
||||
DANGLING_TASK: 'dangling_task',
|
||||
} as const;
|
||||
export enum REFRESH_TRANSFORM_LIST_STATE {
|
||||
ERROR = 'error',
|
||||
IDLE = 'idle',
|
||||
LOADING = 'loading',
|
||||
REFRESH = 'refresh',
|
||||
}
|
||||
export const refreshTransformList$ = new BehaviorSubject<REFRESH_TRANSFORM_LIST_STATE>(
|
||||
REFRESH_TRANSFORM_LIST_STATE.IDLE
|
||||
);
|
||||
|
||||
export const useRefreshTransformList = (
|
||||
callback: {
|
||||
isLoading?(d: boolean): void;
|
||||
onRefresh?(): void;
|
||||
} = {}
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const distinct$ = refreshTransformList$.pipe(distinctUntilChanged());
|
||||
|
||||
const subscriptions: Subscription[] = [];
|
||||
|
||||
if (typeof callback.onRefresh === 'function') {
|
||||
// initial call to refresh
|
||||
callback.onRefresh();
|
||||
|
||||
subscriptions.push(
|
||||
distinct$
|
||||
.pipe(filter((state) => state === REFRESH_TRANSFORM_LIST_STATE.REFRESH))
|
||||
.subscribe(() => typeof callback.onRefresh === 'function' && callback.onRefresh())
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof callback.isLoading === 'function') {
|
||||
subscriptions.push(
|
||||
distinct$.subscribe(
|
||||
(state) =>
|
||||
typeof callback.isLoading === 'function' &&
|
||||
callback.isLoading(state === REFRESH_TRANSFORM_LIST_STATE.LOADING)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
subscriptions.map((sub) => sub.unsubscribe());
|
||||
};
|
||||
// The effect should only be called once.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return {
|
||||
refresh: () => {
|
||||
// A refresh is followed immediately by setting the state to loading
|
||||
// to trigger data fetching and loading indicators in one go.
|
||||
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
|
||||
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.LOADING);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const overrideTransformForCloning = (originalConfig: TransformConfigUnion) => {
|
||||
// 'Managed' means job is preconfigured and deployed by other solutions
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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, { type FC } from 'react';
|
||||
|
||||
import { EuiFlexItem, EuiFlexGroup, EuiPageTemplate, EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type { TransformCapability } from '../../../common/types/capabilities';
|
||||
import { toArray } from '../../../common/utils/to_array';
|
||||
|
||||
import { useTransformCapabilities } from '../hooks';
|
||||
|
||||
interface Props {
|
||||
title: React.ReactNode;
|
||||
message: React.ReactNode | string;
|
||||
}
|
||||
|
||||
export const NotAuthorizedSection = ({ title, message }: Props) => (
|
||||
<EuiEmptyPrompt iconType="securityApp" title={<h2>{title}</h2>} body={<p>{message}</p>} />
|
||||
);
|
||||
|
||||
const MissingCapabilities: FC = () => (
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPageTemplate.EmptyPrompt color="danger">
|
||||
<NotAuthorizedSection
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.transform.app.missingCapabilitiesTitle"
|
||||
defaultMessage="Missing permission"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id="xpack.transform.app.missingCapabilitiesDescription"
|
||||
defaultMessage="You're missing permissions to use this section of Transforms."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiPageTemplate.EmptyPrompt>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
export const CapabilitiesWrapper: FC<{
|
||||
requiredCapabilities: TransformCapability | TransformCapability[];
|
||||
}> = ({ children, requiredCapabilities }) => {
|
||||
const capabilities = useTransformCapabilities();
|
||||
|
||||
const hasCapabilities = toArray(requiredCapabilities).every((c) => capabilities[c]);
|
||||
|
||||
return hasCapabilities ? <>{children}</> : <MissingCapabilities />;
|
||||
};
|
|
@ -5,6 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { SectionError } from './section_error';
|
||||
export { SectionLoading } from './section_loading';
|
||||
export { ToastNotificationText } from './toast_notification_text';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { EuiIcon, EuiToolTip } from '@elastic/eui';
|
||||
import { AuditMessageBase } from '../../../common/types/messages';
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* 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 { EuiPageTemplate } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
title: React.ReactNode;
|
||||
error: Error | null;
|
||||
actions?: JSX.Element;
|
||||
}
|
||||
|
||||
export const SectionError: React.FunctionComponent<Props> = ({
|
||||
title,
|
||||
error,
|
||||
actions,
|
||||
...rest
|
||||
}) => {
|
||||
const errorMessage = error?.message ?? JSON.stringify(error, null, 2);
|
||||
|
||||
return (
|
||||
<EuiPageTemplate.EmptyPrompt
|
||||
color={'danger'}
|
||||
iconType="warning"
|
||||
title={<h2>{title}</h2>}
|
||||
body={
|
||||
<p>
|
||||
<pre>{errorMessage}</pre>
|
||||
{actions ? actions : null}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiEmptyPrompt,
|
||||
EuiLoadingSpinner,
|
||||
EuiText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
inline?: boolean;
|
||||
children: React.ReactNode;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const SectionLoading: React.FunctionComponent<Props> = ({ inline, children, ...rest }) => {
|
||||
if (inline) {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="flexStart" alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText {...rest}>
|
||||
<EuiTextColor color="subdued">{children}</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={<EuiLoadingSpinner size="xl" />}
|
||||
body={<EuiText color="subdued">{children}</EuiText>}
|
||||
data-test-subj="sectionLoading"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
|
@ -19,41 +19,45 @@ import {
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
|
||||
const MAX_SIMPLE_MESSAGE_LENGTH = 140;
|
||||
|
||||
// Because of the use of `toMountPoint`, `useKibanaContext` doesn't work via `useAppDependencies`.
|
||||
// That's why we need to pass in `overlays` as a prop cannot get it via context.
|
||||
interface ToastNotificationTextProps {
|
||||
overlays: CoreStart['overlays'];
|
||||
theme: CoreStart['theme'];
|
||||
text: any;
|
||||
previewTextLength?: number;
|
||||
inline?: boolean;
|
||||
forceModal?: boolean;
|
||||
}
|
||||
|
||||
export const ToastNotificationText: FC<ToastNotificationTextProps> = ({
|
||||
overlays,
|
||||
text,
|
||||
theme,
|
||||
previewTextLength,
|
||||
inline = false,
|
||||
forceModal = false,
|
||||
}) => {
|
||||
if (typeof text === 'string' && text.length <= MAX_SIMPLE_MESSAGE_LENGTH) {
|
||||
const { overlays, theme, i18n: i18nStart } = useAppDependencies();
|
||||
|
||||
if (!forceModal && typeof text === 'string' && text.length <= MAX_SIMPLE_MESSAGE_LENGTH) {
|
||||
return text;
|
||||
}
|
||||
|
||||
if (
|
||||
!forceModal &&
|
||||
typeof text === 'object' &&
|
||||
text !== null &&
|
||||
typeof text.message === 'string' &&
|
||||
text.message.length <= MAX_SIMPLE_MESSAGE_LENGTH
|
||||
) {
|
||||
return text.message;
|
||||
}
|
||||
|
||||
const unformattedText = text.message ? text.message : text;
|
||||
const formattedText = typeof unformattedText === 'object' ? JSON.stringify(text, null, 2) : text;
|
||||
const unformattedText =
|
||||
typeof text === 'object' && text !== null && text.message ? text.message : text;
|
||||
const formattedText =
|
||||
typeof unformattedText === 'object' ? JSON.stringify(text, null, 2) : unformattedText;
|
||||
const textLength = previewTextLength ?? 140;
|
||||
const previewText = `${formattedText.substring(0, textLength)}${
|
||||
formattedText.length > textLength ? ' ...' : ''
|
||||
|
@ -83,15 +87,19 @@ export const ToastNotificationText: FC<ToastNotificationTextProps> = ({
|
|||
</EuiButtonEmpty>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>,
|
||||
{ theme$: theme.theme$ }
|
||||
{ theme, i18n: i18nStart }
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<pre>{previewText}</pre>
|
||||
<EuiButtonEmpty onClick={openModal}>
|
||||
{!inline && <pre>{previewText}</pre>}
|
||||
<EuiButtonEmpty
|
||||
onClick={openModal}
|
||||
css={inline ? { blockSize: 0 } : {}}
|
||||
size={inline ? 's' : undefined}
|
||||
>
|
||||
{i18n.translate('xpack.transform.toastText.openModalButtonText', {
|
||||
defaultMessage: 'View details',
|
||||
})}
|
||||
|
|
|
@ -1,150 +0,0 @@
|
|||
/*
|
||||
* 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 type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
|
||||
import { KBN_FIELD_TYPES } from '@kbn/field-types';
|
||||
import { DEFAULT_SAMPLER_SHARD_SIZE } from '@kbn/ml-agg-utils';
|
||||
|
||||
import type { TransformId } from '../../../../common/types/transform';
|
||||
import type { FieldHistogramsResponseSchema } from '../../../../common/api_schemas/field_histograms';
|
||||
import type { GetTransformsAuditMessagesResponseSchema } from '../../../../common/api_schemas/audit_messages';
|
||||
import type {
|
||||
DeleteTransformsRequestSchema,
|
||||
DeleteTransformsResponseSchema,
|
||||
} from '../../../../common/api_schemas/delete_transforms';
|
||||
import type {
|
||||
StartTransformsRequestSchema,
|
||||
StartTransformsResponseSchema,
|
||||
} from '../../../../common/api_schemas/start_transforms';
|
||||
import type {
|
||||
StopTransformsRequestSchema,
|
||||
StopTransformsResponseSchema,
|
||||
} from '../../../../common/api_schemas/stop_transforms';
|
||||
import type {
|
||||
GetTransformsResponseSchema,
|
||||
PostTransformsPreviewRequestSchema,
|
||||
PostTransformsPreviewResponseSchema,
|
||||
PutTransformsRequestSchema,
|
||||
PutTransformsResponseSchema,
|
||||
} from '../../../../common/api_schemas/transforms';
|
||||
import type { GetTransformsStatsResponseSchema } from '../../../../common/api_schemas/transforms_stats';
|
||||
import type {
|
||||
PostTransformsUpdateRequestSchema,
|
||||
PostTransformsUpdateResponseSchema,
|
||||
} from '../../../../common/api_schemas/update_transforms';
|
||||
|
||||
import type { EsIndex } from '../../../../common/types/es_index';
|
||||
|
||||
import type { SavedSearchQuery } from '../use_search_items';
|
||||
|
||||
export interface FieldHistogramRequestConfig {
|
||||
fieldName: string;
|
||||
type?: KBN_FIELD_TYPES;
|
||||
}
|
||||
|
||||
const apiFactory = () => ({
|
||||
async getTransform(
|
||||
transformId: TransformId
|
||||
): Promise<GetTransformsResponseSchema | IHttpFetchError> {
|
||||
return Promise.resolve({ count: 0, transforms: [] });
|
||||
},
|
||||
async getTransforms(): Promise<GetTransformsResponseSchema | IHttpFetchError> {
|
||||
return Promise.resolve({ count: 0, transforms: [] });
|
||||
},
|
||||
async getTransformStats(
|
||||
transformId: TransformId
|
||||
): Promise<GetTransformsStatsResponseSchema | IHttpFetchError> {
|
||||
return Promise.resolve({ count: 0, transforms: [] });
|
||||
},
|
||||
async getTransformsStats(): Promise<GetTransformsStatsResponseSchema | IHttpFetchError> {
|
||||
return Promise.resolve({ count: 0, transforms: [] });
|
||||
},
|
||||
async createTransform(
|
||||
transformId: TransformId,
|
||||
transformConfig: PutTransformsRequestSchema
|
||||
): Promise<PutTransformsResponseSchema | IHttpFetchError> {
|
||||
return Promise.resolve({ transformsCreated: [], errors: [] });
|
||||
},
|
||||
async updateTransform(
|
||||
transformId: TransformId,
|
||||
transformConfig: PostTransformsUpdateRequestSchema
|
||||
): Promise<PostTransformsUpdateResponseSchema | IHttpFetchError> {
|
||||
return Promise.resolve({
|
||||
id: 'the-test-id',
|
||||
source: { index: ['the-index-name'], query: { match_all: {} } },
|
||||
dest: { index: 'user-the-destination-index-name' },
|
||||
frequency: '10m',
|
||||
pivot: {
|
||||
group_by: { the_group: { terms: { field: 'the-group-by-field' } } },
|
||||
aggregations: { the_agg: { value_count: { field: 'the-agg-field' } } },
|
||||
},
|
||||
description: 'the-description',
|
||||
settings: { docs_per_second: null },
|
||||
version: '8.0.0',
|
||||
create_time: 1598860879097,
|
||||
});
|
||||
},
|
||||
async deleteTransforms(
|
||||
reqBody: DeleteTransformsRequestSchema
|
||||
): Promise<DeleteTransformsResponseSchema | IHttpFetchError> {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
async getTransformsPreview(
|
||||
obj: PostTransformsPreviewRequestSchema
|
||||
): Promise<PostTransformsPreviewResponseSchema | IHttpFetchError> {
|
||||
return Promise.resolve({
|
||||
generated_dest_index: {
|
||||
mappings: {
|
||||
_meta: {
|
||||
_transform: {
|
||||
transform: 'the-transform',
|
||||
version: { create: 'the-version' },
|
||||
creation_date_in_millis: 0,
|
||||
},
|
||||
created_by: 'mock',
|
||||
},
|
||||
properties: {},
|
||||
},
|
||||
settings: { index: { number_of_shards: '1', auto_expand_replicas: '0-1' } },
|
||||
aliases: {},
|
||||
},
|
||||
preview: [],
|
||||
});
|
||||
},
|
||||
async startTransforms(
|
||||
reqBody: StartTransformsRequestSchema
|
||||
): Promise<StartTransformsResponseSchema | IHttpFetchError> {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
async stopTransforms(
|
||||
transformsInfo: StopTransformsRequestSchema
|
||||
): Promise<StopTransformsResponseSchema | IHttpFetchError> {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
async getTransformAuditMessages(
|
||||
transformId: TransformId
|
||||
): Promise<GetTransformsAuditMessagesResponseSchema | IHttpFetchError> {
|
||||
return Promise.resolve({ messages: [], total: 0 });
|
||||
},
|
||||
|
||||
async getEsIndices(): Promise<EsIndex[] | IHttpFetchError> {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
async getHistogramsForFields(
|
||||
dataViewTitle: string,
|
||||
fields: FieldHistogramRequestConfig[],
|
||||
query: string | SavedSearchQuery,
|
||||
samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE
|
||||
): Promise<FieldHistogramsResponseSchema | IHttpFetchError> {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
});
|
||||
|
||||
export const useApi = () => {
|
||||
return apiFactory();
|
||||
};
|
|
@ -5,10 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { useApi } from './use_api';
|
||||
export { useCreateTransform } from './use_create_transform';
|
||||
export { useDocumentationLinks } from './use_documentation_links';
|
||||
export { useGetDataViewTitles } from './use_get_data_view_titles';
|
||||
export { useGetEsIndices } from './use_get_es_indices';
|
||||
export { useGetEsIngestPipelines } from './use_get_es_ingest_pipelines';
|
||||
export { useGetTransformAuditMessages } from './use_get_transform_audit_messages';
|
||||
export { useGetTransform } from './use_get_transform';
|
||||
export { useGetTransformNodes } from './use_get_transform_nodes';
|
||||
export { useGetTransforms } from './use_get_transforms';
|
||||
export { useGetTransformsPreview } from './use_get_transforms_preview';
|
||||
export { useGetTransformStats } from './use_get_transform_stats';
|
||||
export { useDeleteTransforms, useDeleteIndexAndTargetIndex } from './use_delete_transform';
|
||||
export { useRefreshTransformList } from './use_refresh_transform_list';
|
||||
export { useResetTransforms } from './use_reset_transform';
|
||||
export { useSearchItems } from './use_search_items';
|
||||
export { useScheduleNowTransforms } from './use_schedule_now_transform';
|
||||
export { useStartTransforms } from './use_start_transform';
|
||||
export { useStopTransforms } from './use_stop_transform';
|
||||
export { useTransformCapabilities } from './use_transform_capabilities';
|
||||
export { useUpdateTransform } from './use_update_transform';
|
||||
|
|
|
@ -1,301 +0,0 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
|
||||
import { KBN_FIELD_TYPES } from '@kbn/field-types';
|
||||
import { DEFAULT_SAMPLER_SHARD_SIZE } from '@kbn/ml-agg-utils';
|
||||
|
||||
import {
|
||||
ReauthorizeTransformsRequestSchema,
|
||||
ReauthorizeTransformsResponseSchema,
|
||||
} from '../../../common/api_schemas/reauthorize_transforms';
|
||||
import type { GetTransformsAuditMessagesResponseSchema } from '../../../common/api_schemas/audit_messages';
|
||||
import type {
|
||||
DeleteTransformsRequestSchema,
|
||||
DeleteTransformsResponseSchema,
|
||||
} from '../../../common/api_schemas/delete_transforms';
|
||||
import type {
|
||||
FieldHistogramsRequestSchema,
|
||||
FieldHistogramsResponseSchema,
|
||||
} from '../../../common/api_schemas/field_histograms';
|
||||
import type {
|
||||
ResetTransformsRequestSchema,
|
||||
ResetTransformsResponseSchema,
|
||||
} from '../../../common/api_schemas/reset_transforms';
|
||||
import type {
|
||||
StartTransformsRequestSchema,
|
||||
StartTransformsResponseSchema,
|
||||
} from '../../../common/api_schemas/start_transforms';
|
||||
import type {
|
||||
StopTransformsRequestSchema,
|
||||
StopTransformsResponseSchema,
|
||||
} from '../../../common/api_schemas/stop_transforms';
|
||||
import type {
|
||||
ScheduleNowTransformsRequestSchema,
|
||||
ScheduleNowTransformsResponseSchema,
|
||||
} from '../../../common/api_schemas/schedule_now_transforms';
|
||||
import type {
|
||||
GetTransformNodesResponseSchema,
|
||||
GetTransformsResponseSchema,
|
||||
PostTransformsPreviewRequestSchema,
|
||||
PostTransformsPreviewResponseSchema,
|
||||
PutTransformsRequestSchema,
|
||||
PutTransformsResponseSchema,
|
||||
} from '../../../common/api_schemas/transforms';
|
||||
import type {
|
||||
PostTransformsUpdateRequestSchema,
|
||||
PostTransformsUpdateResponseSchema,
|
||||
} from '../../../common/api_schemas/update_transforms';
|
||||
import type { GetTransformsStatsResponseSchema } from '../../../common/api_schemas/transforms_stats';
|
||||
import type { TransformId } from '../../../common/types/transform';
|
||||
import { addInternalBasePath } from '../../../common/constants';
|
||||
import type { EsIndex } from '../../../common/types/es_index';
|
||||
import type { EsIngestPipeline } from '../../../common/types/es_ingest_pipeline';
|
||||
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
|
||||
import type { SavedSearchQuery } from './use_search_items';
|
||||
|
||||
export interface FieldHistogramRequestConfig {
|
||||
fieldName: string;
|
||||
type?: KBN_FIELD_TYPES;
|
||||
}
|
||||
|
||||
interface FetchOptions {
|
||||
asSystemRequest?: boolean;
|
||||
}
|
||||
|
||||
export const useApi = () => {
|
||||
const { http } = useAppDependencies();
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
async getTransformNodes(): Promise<GetTransformNodesResponseSchema | IHttpFetchError> {
|
||||
try {
|
||||
return await http.get(addInternalBasePath(`transforms/_nodes`), { version: '1' });
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async getTransform(
|
||||
transformId: TransformId
|
||||
): Promise<GetTransformsResponseSchema | IHttpFetchError> {
|
||||
try {
|
||||
return await http.get(addInternalBasePath(`transforms/${transformId}`), {
|
||||
version: '1',
|
||||
});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async getTransforms(
|
||||
fetchOptions: FetchOptions = {}
|
||||
): Promise<GetTransformsResponseSchema | IHttpFetchError> {
|
||||
try {
|
||||
return await http.get(addInternalBasePath(`transforms`), {
|
||||
...fetchOptions,
|
||||
version: '1',
|
||||
});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async getTransformStats(
|
||||
transformId: TransformId
|
||||
): Promise<GetTransformsStatsResponseSchema | IHttpFetchError> {
|
||||
try {
|
||||
return await http.get(addInternalBasePath(`transforms/${transformId}/_stats`), {
|
||||
version: '1',
|
||||
});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async getTransformsStats(
|
||||
fetchOptions: FetchOptions = {}
|
||||
): Promise<GetTransformsStatsResponseSchema | IHttpFetchError> {
|
||||
try {
|
||||
return await http.get(addInternalBasePath(`transforms/_stats`), {
|
||||
...fetchOptions,
|
||||
version: '1',
|
||||
});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async createTransform(
|
||||
transformId: TransformId,
|
||||
transformConfig: PutTransformsRequestSchema
|
||||
): Promise<PutTransformsResponseSchema | IHttpFetchError> {
|
||||
try {
|
||||
return await http.put(addInternalBasePath(`transforms/${transformId}`), {
|
||||
body: JSON.stringify(transformConfig),
|
||||
version: '1',
|
||||
});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async updateTransform(
|
||||
transformId: TransformId,
|
||||
transformConfig: PostTransformsUpdateRequestSchema
|
||||
): Promise<PostTransformsUpdateResponseSchema | IHttpFetchError> {
|
||||
try {
|
||||
return await http.post(addInternalBasePath(`transforms/${transformId}/_update`), {
|
||||
body: JSON.stringify(transformConfig),
|
||||
version: '1',
|
||||
});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async deleteTransforms(
|
||||
reqBody: DeleteTransformsRequestSchema
|
||||
): Promise<DeleteTransformsResponseSchema | IHttpFetchError> {
|
||||
try {
|
||||
return await http.post(addInternalBasePath(`delete_transforms`), {
|
||||
body: JSON.stringify(reqBody),
|
||||
version: '1',
|
||||
});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async getTransformsPreview(
|
||||
obj: PostTransformsPreviewRequestSchema
|
||||
): Promise<PostTransformsPreviewResponseSchema | IHttpFetchError> {
|
||||
try {
|
||||
return await http.post(addInternalBasePath(`transforms/_preview`), {
|
||||
body: JSON.stringify(obj),
|
||||
version: '1',
|
||||
});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async reauthorizeTransforms(
|
||||
reqBody: ReauthorizeTransformsRequestSchema
|
||||
): Promise<ReauthorizeTransformsResponseSchema | IHttpFetchError> {
|
||||
try {
|
||||
return await http.post(addInternalBasePath(`reauthorize_transforms`), {
|
||||
body: JSON.stringify(reqBody),
|
||||
version: '1',
|
||||
});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
|
||||
async resetTransforms(
|
||||
reqBody: ResetTransformsRequestSchema
|
||||
): Promise<ResetTransformsResponseSchema | IHttpFetchError> {
|
||||
try {
|
||||
return await http.post(addInternalBasePath(`reset_transforms`), {
|
||||
body: JSON.stringify(reqBody),
|
||||
version: '1',
|
||||
});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async startTransforms(
|
||||
reqBody: StartTransformsRequestSchema
|
||||
): Promise<StartTransformsResponseSchema | IHttpFetchError> {
|
||||
try {
|
||||
return await http.post(addInternalBasePath(`start_transforms`), {
|
||||
body: JSON.stringify(reqBody),
|
||||
version: '1',
|
||||
});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async stopTransforms(
|
||||
transformsInfo: StopTransformsRequestSchema
|
||||
): Promise<StopTransformsResponseSchema | IHttpFetchError> {
|
||||
try {
|
||||
return await http.post(addInternalBasePath(`stop_transforms`), {
|
||||
body: JSON.stringify(transformsInfo),
|
||||
version: '1',
|
||||
});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async scheduleNowTransforms(
|
||||
transformsInfo: ScheduleNowTransformsRequestSchema
|
||||
): Promise<ScheduleNowTransformsResponseSchema | IHttpFetchError> {
|
||||
try {
|
||||
return await http.post(addInternalBasePath(`schedule_now_transforms`), {
|
||||
body: JSON.stringify(transformsInfo),
|
||||
version: '1',
|
||||
});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async getTransformAuditMessages(
|
||||
transformId: TransformId,
|
||||
sortField: string,
|
||||
sortDirection: 'asc' | 'desc'
|
||||
): Promise<
|
||||
{ messages: GetTransformsAuditMessagesResponseSchema; total: number } | IHttpFetchError
|
||||
> {
|
||||
try {
|
||||
return await http.get(addInternalBasePath(`transforms/${transformId}/messages`), {
|
||||
query: {
|
||||
sortField,
|
||||
sortDirection,
|
||||
},
|
||||
version: '1',
|
||||
});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async getEsIndices(): Promise<EsIndex[] | IHttpFetchError> {
|
||||
try {
|
||||
return await http.get(`/api/index_management/indices`, { version: '1' });
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async getEsIngestPipelines(): Promise<EsIngestPipeline[] | IHttpFetchError> {
|
||||
try {
|
||||
return await http.get('/api/ingest_pipelines', { version: '1' });
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
async getHistogramsForFields(
|
||||
dataViewTitle: string,
|
||||
fields: FieldHistogramRequestConfig[],
|
||||
query: string | SavedSearchQuery,
|
||||
runtimeMappings?: FieldHistogramsRequestSchema['runtimeMappings'],
|
||||
samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE
|
||||
): Promise<FieldHistogramsResponseSchema | IHttpFetchError> {
|
||||
try {
|
||||
return await http.post(addInternalBasePath(`field_histograms/${dataViewTitle}`), {
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
fields,
|
||||
samplerShardSize,
|
||||
...(runtimeMappings !== undefined ? { runtimeMappings } : {}),
|
||||
}),
|
||||
version: '1',
|
||||
});
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
}),
|
||||
[http]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
|
||||
import type {
|
||||
PutTransformsRequestSchema,
|
||||
PutTransformsResponseSchema,
|
||||
} from '../../../common/api_schemas/transforms';
|
||||
import { addInternalBasePath } from '../../../common/constants';
|
||||
import type { TransformId } from '../../../common/types/transform';
|
||||
import { getErrorMessage } from '../../../common/utils/errors';
|
||||
|
||||
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
||||
import { ToastNotificationText } from '../components';
|
||||
|
||||
import { useRefreshTransformList } from './use_refresh_transform_list';
|
||||
|
||||
interface CreateTransformArgs {
|
||||
transformId: TransformId;
|
||||
transformConfig: PutTransformsRequestSchema;
|
||||
}
|
||||
|
||||
export const useCreateTransform = () => {
|
||||
const { http, i18n: i18nStart, theme } = useAppDependencies();
|
||||
const refreshTransformList = useRefreshTransformList();
|
||||
const toastNotifications = useToastNotifications();
|
||||
|
||||
function errorToast(error: unknown, { transformId }: CreateTransformArgs) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.stepCreateForm.createTransformErrorMessage', {
|
||||
defaultMessage: 'An error occurred creating the transform {transformId}:',
|
||||
values: { transformId },
|
||||
}),
|
||||
text: toMountPoint(<ToastNotificationText text={getErrorMessage(error)} />, {
|
||||
theme,
|
||||
i18n: i18nStart,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({ transformId, transformConfig }: CreateTransformArgs) => {
|
||||
return http.put<PutTransformsResponseSchema>(
|
||||
addInternalBasePath(`transforms/${transformId}`),
|
||||
{
|
||||
body: JSON.stringify(transformConfig),
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
},
|
||||
onError: errorToast,
|
||||
onSuccess: (resp, options) => {
|
||||
if (resp.errors.length > 0) {
|
||||
errorToast(resp.errors.length === 1 ? resp.errors[0] : resp.errors, options);
|
||||
}
|
||||
|
||||
refreshTransformList();
|
||||
},
|
||||
});
|
||||
|
||||
return mutation.mutate;
|
||||
};
|
|
@ -5,37 +5,37 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
||||
import type { IKibanaSearchRequest } from '@kbn/data-plugin/common';
|
||||
|
||||
import { TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants';
|
||||
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
|
||||
export const useDataSearch = () => {
|
||||
export const useDataSearch = (
|
||||
esSearchRequestParams: IKibanaSearchRequest['params'],
|
||||
enabled?: boolean
|
||||
) => {
|
||||
const { data } = useAppDependencies();
|
||||
|
||||
return useCallback(
|
||||
async (esSearchRequestParams: IKibanaSearchRequest['params'], abortSignal?: AbortSignal) => {
|
||||
try {
|
||||
const { rawResponse: resp } = await lastValueFrom(
|
||||
data.search.search(
|
||||
{
|
||||
params: esSearchRequestParams,
|
||||
},
|
||||
{ abortSignal }
|
||||
)
|
||||
);
|
||||
return useQuery<estypes.SearchResponse>(
|
||||
[TRANSFORM_REACT_QUERY_KEYS.DATA_SEARCH, esSearchRequestParams],
|
||||
async ({ signal }) => {
|
||||
const { rawResponse: resp } = await lastValueFrom(
|
||||
data.search.search(
|
||||
{
|
||||
params: esSearchRequestParams,
|
||||
},
|
||||
{ abortSignal: signal }
|
||||
)
|
||||
);
|
||||
|
||||
return resp;
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
// ignore abort errors
|
||||
} else {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
return resp;
|
||||
},
|
||||
[data]
|
||||
{ enabled }
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { ErrorType } from '@kbn/ml-error-utils';
|
||||
|
||||
import { TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants';
|
||||
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
import type { TransformListRow } from '../common';
|
||||
|
||||
export const useDataViewExists = (items: TransformListRow[]) => {
|
||||
const {
|
||||
data: { dataViews: dataViewsContract },
|
||||
} = useAppDependencies();
|
||||
|
||||
return useQuery<boolean, ErrorType>(
|
||||
[TRANSFORM_REACT_QUERY_KEYS.DATA_VIEW_EXISTS, items],
|
||||
async () => {
|
||||
if (items.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
const config = items[0].config;
|
||||
const indexName = Array.isArray(config.dest.index) ? config.dest.index[0] : config.dest.index;
|
||||
|
||||
if (indexName === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (await dataViewsContract.find(indexName)).some(({ title }) => title === indexName);
|
||||
}
|
||||
);
|
||||
};
|
|
@ -6,34 +6,40 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { extractErrorMessage } from '@kbn/ml-error-utils';
|
||||
|
||||
import { addInternalBasePath } from '../../../common/constants';
|
||||
import type {
|
||||
DeleteTransformStatus,
|
||||
DeleteTransformsRequestSchema,
|
||||
DeleteTransformsResponseSchema,
|
||||
} from '../../../common/api_schemas/delete_transforms';
|
||||
import { isDeleteTransformsResponseSchema } from '../../../common/api_schemas/type_guards';
|
||||
import { getErrorMessage } from '../../../common/utils/errors';
|
||||
|
||||
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
||||
import { REFRESH_TRANSFORM_LIST_STATE, refreshTransformList$, TransformListRow } from '../common';
|
||||
import { type TransformListRow } from '../common';
|
||||
import { ToastNotificationText } from '../components';
|
||||
import { useApi } from './use_api';
|
||||
import { indexService } from '../services/es_index_service';
|
||||
|
||||
import { useTransformCapabilities } from './use_transform_capabilities';
|
||||
import { useDataViewExists } from './use_data_view_exists';
|
||||
import { useRefreshTransformList } from './use_refresh_transform_list';
|
||||
|
||||
export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => {
|
||||
const {
|
||||
http,
|
||||
data: { dataViews: dataViewsContract },
|
||||
application: { capabilities },
|
||||
} = useAppDependencies();
|
||||
const toastNotifications = useToastNotifications();
|
||||
const { canDeleteIndex: userCanDeleteIndex } = useTransformCapabilities();
|
||||
|
||||
const userCanDeleteDataView =
|
||||
capabilities.savedObjectsManagement?.delete === true ||
|
||||
capabilities.indexPatterns?.save === true;
|
||||
|
||||
const [deleteDestIndex, setDeleteDestIndex] = useState<boolean>(true);
|
||||
const [deleteDataView, setDeleteDataView] = useState<boolean>(true);
|
||||
const [userCanDeleteIndex, setUserCanDeleteIndex] = useState<boolean>(false);
|
||||
const [dataViewExists, setDataViewExists] = useState<boolean>(false);
|
||||
const [userCanDeleteDataView, setUserCanDeleteDataView] = useState<boolean>(false);
|
||||
const [deleteDataView, setDeleteDataView] = useState<boolean>(userCanDeleteDataView);
|
||||
|
||||
const toggleDeleteIndex = useCallback(
|
||||
() => setDeleteDestIndex(!deleteDestIndex),
|
||||
|
@ -43,67 +49,31 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => {
|
|||
() => setDeleteDataView(!deleteDataView),
|
||||
[deleteDataView]
|
||||
);
|
||||
const checkDataViewExists = useCallback(
|
||||
async (indexName: string) => {
|
||||
try {
|
||||
const dvExists = await indexService.dataViewExists(dataViewsContract, indexName);
|
||||
setDataViewExists(dvExists);
|
||||
} catch (e) {
|
||||
const error = extractErrorMessage(e);
|
||||
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate(
|
||||
'xpack.transform.deleteTransform.errorWithCheckingIfDataViewExistsNotificationErrorMessage',
|
||||
{
|
||||
defaultMessage: 'An error occurred checking if data view {dataView} exists: {error}',
|
||||
values: { dataView: indexName, error },
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
[dataViewsContract, toastNotifications]
|
||||
);
|
||||
const { error: dataViewExistsError, data: dataViewExists = items.length !== 1 } =
|
||||
useDataViewExists(items);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataViewExistsError !== null && items.length === 1) {
|
||||
const config = items[0].config;
|
||||
const indexName = Array.isArray(config.dest.index) ? config.dest.index[0] : config.dest.index;
|
||||
|
||||
const checkUserIndexPermission = useCallback(async () => {
|
||||
try {
|
||||
const userCanDelete = await indexService.canDeleteIndex(http);
|
||||
if (userCanDelete) {
|
||||
setUserCanDeleteIndex(true);
|
||||
}
|
||||
const canDeleteDataView =
|
||||
capabilities.savedObjectsManagement.delete === true ||
|
||||
capabilities.indexPatterns.save === true;
|
||||
setUserCanDeleteDataView(canDeleteDataView);
|
||||
if (canDeleteDataView === false) {
|
||||
setDeleteDataView(false);
|
||||
}
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate(
|
||||
'xpack.transform.transformList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage',
|
||||
'xpack.transform.deleteTransform.errorWithCheckingIfDataViewExistsNotificationErrorMessage',
|
||||
{
|
||||
defaultMessage: 'An error occurred checking if user can delete destination index',
|
||||
defaultMessage: 'An error occurred checking if data view {dataView} exists: {error}',
|
||||
values: {
|
||||
dataView: indexName,
|
||||
error: extractErrorMessage(dataViewExistsError),
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [http, toastNotifications, capabilities]);
|
||||
|
||||
useEffect(() => {
|
||||
checkUserIndexPermission();
|
||||
|
||||
// if user only deleting one transform
|
||||
if (items.length === 1) {
|
||||
const config = items[0].config;
|
||||
const destinationIndex = Array.isArray(config.dest.index)
|
||||
? config.dest.index[0]
|
||||
: config.dest.index;
|
||||
checkDataViewExists(destinationIndex);
|
||||
} else {
|
||||
setDataViewExists(true);
|
||||
}
|
||||
}, [checkDataViewExists, checkUserIndexPermission, items]);
|
||||
// custom comparison
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataViewExistsError]);
|
||||
|
||||
return {
|
||||
userCanDeleteIndex,
|
||||
|
@ -116,183 +86,87 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => {
|
|||
};
|
||||
};
|
||||
|
||||
type SuccessCountField = keyof Omit<DeleteTransformStatus, 'destinationIndex'>;
|
||||
|
||||
export const useDeleteTransforms = () => {
|
||||
const { overlays, theme } = useAppDependencies();
|
||||
const { http, i18n: i18nStart, theme } = useAppDependencies();
|
||||
const refreshTransformList = useRefreshTransformList();
|
||||
const toastNotifications = useToastNotifications();
|
||||
const api = useApi();
|
||||
|
||||
return async (reqBody: DeleteTransformsRequestSchema) => {
|
||||
const results = await api.deleteTransforms(reqBody);
|
||||
|
||||
if (!isDeleteTransformsResponseSchema(results)) {
|
||||
const mutation = useMutation({
|
||||
mutationFn: (reqBody: DeleteTransformsRequestSchema) =>
|
||||
http.post<DeleteTransformsResponseSchema>(addInternalBasePath('delete_transforms'), {
|
||||
body: JSON.stringify(reqBody),
|
||||
version: '1',
|
||||
}),
|
||||
onError: (error) =>
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.transformList.deleteTransformGenericErrorMessage', {
|
||||
defaultMessage: 'An error occurred calling the API endpoint to delete transforms.',
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
previewTextLength={50}
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={getErrorMessage(results)}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
<ToastNotificationText previewTextLength={50} text={getErrorMessage(error)} />,
|
||||
{ theme, i18n: i18nStart }
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}),
|
||||
onSuccess: (results) => {
|
||||
for (const transformId in results) {
|
||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||
if (results.hasOwnProperty(transformId)) {
|
||||
const status = results[transformId];
|
||||
const destinationIndex = status.destinationIndex;
|
||||
|
||||
const isBulk = Object.keys(results).length > 1;
|
||||
const successCount: Record<SuccessCountField, number> = {
|
||||
transformDeleted: 0,
|
||||
destIndexDeleted: 0,
|
||||
destDataViewDeleted: 0,
|
||||
};
|
||||
for (const transformId in results) {
|
||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||
if (results.hasOwnProperty(transformId)) {
|
||||
const status = results[transformId];
|
||||
const destinationIndex = status.destinationIndex;
|
||||
|
||||
// if we are only deleting one transform, show the success toast messages
|
||||
if (!isBulk && status.transformDeleted) {
|
||||
if (status.transformDeleted?.success) {
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.transform.transformList.deleteTransformSuccessMessage', {
|
||||
defaultMessage: 'Request to delete transform {transformId} acknowledged.',
|
||||
if (status.transformDeleted?.error) {
|
||||
const error = status.transformDeleted.error.reason;
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.transformList.deleteTransformErrorMessage', {
|
||||
defaultMessage: 'An error occurred deleting the transform {transformId}',
|
||||
values: { transformId },
|
||||
})
|
||||
);
|
||||
}),
|
||||
text: toMountPoint(<ToastNotificationText previewTextLength={50} text={error} />, {
|
||||
theme,
|
||||
i18n: i18nStart,
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (status.destIndexDeleted?.success) {
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate(
|
||||
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage',
|
||||
|
||||
if (status.destIndexDeleted?.error) {
|
||||
const error = status.destIndexDeleted.error.reason;
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'Request to delete destination index {destinationIndex} acknowledged.',
|
||||
defaultMessage: 'An error occurred deleting destination index {destinationIndex}',
|
||||
values: { destinationIndex },
|
||||
}
|
||||
)
|
||||
);
|
||||
),
|
||||
text: toMountPoint(<ToastNotificationText previewTextLength={50} text={error} />, {
|
||||
theme,
|
||||
i18n: i18nStart,
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (status.destDataViewDeleted?.success) {
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate(
|
||||
'xpack.transform.deleteTransform.deleteAnalyticsWithDataViewSuccessMessage',
|
||||
|
||||
if (status.destDataViewDeleted?.error) {
|
||||
const error = status.destDataViewDeleted.error.reason;
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.transform.deleteTransform.deleteAnalyticsWithDataViewErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Request to delete data view {destinationIndex} acknowledged.',
|
||||
defaultMessage: 'An error occurred deleting data view {destinationIndex}',
|
||||
values: { destinationIndex },
|
||||
}
|
||||
)
|
||||
);
|
||||
),
|
||||
text: toMountPoint(<ToastNotificationText previewTextLength={50} text={error} />, {
|
||||
theme,
|
||||
i18n: i18nStart,
|
||||
}),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
(Object.keys(successCount) as SuccessCountField[]).forEach((key) => {
|
||||
if (status[key]?.success) {
|
||||
successCount[key] = successCount[key] + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (status.transformDeleted?.error) {
|
||||
const error = status.transformDeleted.error.reason;
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.transformList.deleteTransformErrorMessage', {
|
||||
defaultMessage: 'An error occurred deleting the transform {transformId}',
|
||||
values: { transformId },
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
previewTextLength={50}
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={error}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (status.destIndexDeleted?.error) {
|
||||
const error = status.destIndexDeleted.error.reason;
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage',
|
||||
{
|
||||
defaultMessage: 'An error occurred deleting destination index {destinationIndex}',
|
||||
values: { destinationIndex },
|
||||
}
|
||||
),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
previewTextLength={50}
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={error}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (status.destDataViewDeleted?.error) {
|
||||
const error = status.destDataViewDeleted.error.reason;
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.transform.deleteTransform.deleteAnalyticsWithDataViewErrorMessage',
|
||||
{
|
||||
defaultMessage: 'An error occurred deleting data view {destinationIndex}',
|
||||
values: { destinationIndex },
|
||||
}
|
||||
),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
previewTextLength={50}
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={error}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we are deleting multiple transforms, combine the success messages
|
||||
if (isBulk) {
|
||||
if (successCount.transformDeleted > 0) {
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.transform.transformList.bulkDeleteTransformSuccessMessage', {
|
||||
defaultMessage:
|
||||
'Successfully deleted {count} {count, plural, one {transform} other {transforms}}.',
|
||||
values: { count: successCount.transformDeleted },
|
||||
})
|
||||
);
|
||||
}
|
||||
refreshTransformList();
|
||||
},
|
||||
});
|
||||
|
||||
if (successCount.destIndexDeleted > 0) {
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage', {
|
||||
defaultMessage:
|
||||
'Successfully deleted {count} destination {count, plural, one {index} other {indices}}.',
|
||||
values: { count: successCount.destIndexDeleted },
|
||||
})
|
||||
);
|
||||
}
|
||||
if (successCount.destDataViewDeleted > 0) {
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.transform.transformList.bulkDeleteDestDataViewSuccessMessage', {
|
||||
defaultMessage:
|
||||
'Successfully deleted {count} destination data {count, plural, one {view} other {views}}.',
|
||||
values: { count: successCount.destDataViewDeleted },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
|
||||
};
|
||||
return mutation.mutate;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
|
||||
import { TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants';
|
||||
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
|
||||
export const useGetDataViewTitles = () => {
|
||||
const { data } = useAppDependencies();
|
||||
|
||||
return useQuery<string[], IHttpFetchError>(
|
||||
[TRANSFORM_REACT_QUERY_KEYS.GET_DATA_VIEW_TITLES],
|
||||
() => data.dataViews.getTitles()
|
||||
);
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
|
||||
import { TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants';
|
||||
import type { EsIndex } from '../../../common/types/es_index';
|
||||
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
|
||||
export const useGetEsIndices = () => {
|
||||
const { http } = useAppDependencies();
|
||||
|
||||
return useQuery<EsIndex[], IHttpFetchError>(
|
||||
[TRANSFORM_REACT_QUERY_KEYS.GET_ES_INDICES],
|
||||
({ signal }) =>
|
||||
http.get<EsIndex[]>('/api/index_management/indices', {
|
||||
version: '1',
|
||||
signal,
|
||||
})
|
||||
);
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
|
||||
import { TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants';
|
||||
import type { EsIngestPipeline } from '../../../common/types/es_ingest_pipeline';
|
||||
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
|
||||
export const useGetEsIngestPipelines = () => {
|
||||
const { http } = useAppDependencies();
|
||||
|
||||
return useQuery<EsIngestPipeline[], IHttpFetchError>(
|
||||
[TRANSFORM_REACT_QUERY_KEYS.GET_ES_INGEST_PIPELINES],
|
||||
({ signal }) =>
|
||||
http.get<EsIngestPipeline[]>('/api/ingest_pipelines', {
|
||||
version: '1',
|
||||
signal,
|
||||
})
|
||||
);
|
||||
};
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { KBN_FIELD_TYPES } from '@kbn/field-types';
|
||||
import { DEFAULT_SAMPLER_SHARD_SIZE } from '@kbn/ml-agg-utils';
|
||||
|
||||
import { addInternalBasePath, TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants';
|
||||
import type {
|
||||
FieldHistogramsRequestSchema,
|
||||
FieldHistogramsResponseSchema,
|
||||
} from '../../../common/api_schemas/field_histograms';
|
||||
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
|
||||
import type { SavedSearchQuery } from './use_search_items';
|
||||
|
||||
export interface FieldHistogramRequestConfig {
|
||||
fieldName: string;
|
||||
type?: KBN_FIELD_TYPES;
|
||||
}
|
||||
|
||||
export const useGetHistogramsForFields = (
|
||||
dataViewTitle: string,
|
||||
fields: FieldHistogramRequestConfig[],
|
||||
query: string | SavedSearchQuery,
|
||||
runtimeMappings?: FieldHistogramsRequestSchema['runtimeMappings'],
|
||||
enabled?: boolean,
|
||||
samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE
|
||||
) => {
|
||||
const { http } = useAppDependencies();
|
||||
|
||||
return useQuery<FieldHistogramsResponseSchema, IHttpFetchError>(
|
||||
[
|
||||
TRANSFORM_REACT_QUERY_KEYS.GET_HISTOGRAMS_FOR_FIELDS,
|
||||
{
|
||||
dataViewTitle,
|
||||
fields,
|
||||
query,
|
||||
runtimeMappings,
|
||||
samplerShardSize,
|
||||
},
|
||||
],
|
||||
({ signal }) =>
|
||||
http.post<FieldHistogramsResponseSchema>(
|
||||
addInternalBasePath(`field_histograms/${dataViewTitle}`),
|
||||
{
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
fields,
|
||||
samplerShardSize,
|
||||
...(runtimeMappings !== undefined ? { runtimeMappings } : {}),
|
||||
}),
|
||||
version: '1',
|
||||
signal,
|
||||
}
|
||||
),
|
||||
{ enabled }
|
||||
);
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
|
||||
import type { GetTransformsResponseSchema } from '../../../common/api_schemas/transforms';
|
||||
import { addInternalBasePath, TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants';
|
||||
import type { TransformId } from '../../../common/types/transform';
|
||||
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
|
||||
export const useGetTransform = (transformId: TransformId, enabled?: boolean) => {
|
||||
const { http } = useAppDependencies();
|
||||
|
||||
return useQuery<GetTransformsResponseSchema, IHttpFetchError>(
|
||||
[TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORM, transformId],
|
||||
({ signal }) =>
|
||||
http.get<GetTransformsResponseSchema>(addInternalBasePath(`transforms/${transformId}`), {
|
||||
version: '1',
|
||||
signal,
|
||||
}),
|
||||
{ enabled }
|
||||
);
|
||||
};
|
|
@ -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 { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
|
||||
import type { GetTransformsAuditMessagesResponseSchema } from '../../../common/api_schemas/audit_messages';
|
||||
import { addInternalBasePath, TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants';
|
||||
import { TransformMessage } from '../../../common/types/messages';
|
||||
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
|
||||
export const useGetTransformAuditMessages = (
|
||||
transformId: string,
|
||||
sortField: keyof TransformMessage,
|
||||
sortDirection: 'asc' | 'desc'
|
||||
) => {
|
||||
const { http } = useAppDependencies();
|
||||
|
||||
const query = { sortField, sortDirection };
|
||||
|
||||
return useQuery<GetTransformsAuditMessagesResponseSchema, IHttpFetchError>(
|
||||
[TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORM_AUDIT_MESSAGES, transformId, query],
|
||||
({ signal }) =>
|
||||
http.get<GetTransformsAuditMessagesResponseSchema>(
|
||||
addInternalBasePath(`transforms/${transformId}/messages`),
|
||||
{
|
||||
query,
|
||||
version: '1',
|
||||
signal,
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
|
||||
import type { GetTransformNodesResponseSchema } from '../../../common/api_schemas/transforms';
|
||||
import {
|
||||
addInternalBasePath,
|
||||
DEFAULT_REFRESH_INTERVAL_MS,
|
||||
TRANSFORM_REACT_QUERY_KEYS,
|
||||
} from '../../../common/constants';
|
||||
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
|
||||
export const useGetTransformNodes = () => {
|
||||
const { http } = useAppDependencies();
|
||||
|
||||
return useQuery<number, IHttpFetchError>(
|
||||
[TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORM_NODES],
|
||||
async ({ signal }) => {
|
||||
const transformNodes = await http.get<GetTransformNodesResponseSchema>(
|
||||
addInternalBasePath('transforms/_nodes'),
|
||||
{
|
||||
version: '1',
|
||||
signal,
|
||||
}
|
||||
);
|
||||
|
||||
return transformNodes.count;
|
||||
},
|
||||
{
|
||||
refetchInterval: DEFAULT_REFRESH_INTERVAL_MS,
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
|
||||
import type { GetTransformsStatsResponseSchema } from '../../../common/api_schemas/transforms_stats';
|
||||
import { addInternalBasePath, TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants';
|
||||
import type { TransformId } from '../../../common/types/transform';
|
||||
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
|
||||
export const useGetTransformStats = (
|
||||
transformId: TransformId,
|
||||
enabled?: boolean,
|
||||
refetchInterval?: number | false
|
||||
) => {
|
||||
const { http } = useAppDependencies();
|
||||
|
||||
return useQuery<GetTransformsStatsResponseSchema, IHttpFetchError>(
|
||||
[TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORM_STATS, transformId],
|
||||
({ signal }) =>
|
||||
http.get<GetTransformsStatsResponseSchema>(
|
||||
addInternalBasePath(`transforms/${transformId}/_stats`),
|
||||
{
|
||||
version: '1',
|
||||
signal,
|
||||
}
|
||||
),
|
||||
{ enabled, refetchInterval }
|
||||
);
|
||||
};
|
|
@ -5,75 +5,64 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { isDefined } from '@kbn/ml-is-defined';
|
||||
|
||||
import type { GetTransformsResponseSchema } from '../../../common/api_schemas/transforms';
|
||||
import type { GetTransformsStatsResponseSchema } from '../../../common/api_schemas/transforms_stats';
|
||||
import {
|
||||
isGetTransformNodesResponseSchema,
|
||||
isGetTransformsResponseSchema,
|
||||
isGetTransformsStatsResponseSchema,
|
||||
} from '../../../common/api_schemas/type_guards';
|
||||
import { TRANSFORM_MODE } from '../../../common/constants';
|
||||
addInternalBasePath,
|
||||
DEFAULT_REFRESH_INTERVAL_MS,
|
||||
TRANSFORM_REACT_QUERY_KEYS,
|
||||
TRANSFORM_MODE,
|
||||
} from '../../../common/constants';
|
||||
import { isTransformStats } from '../../../common/types/transform_stats';
|
||||
|
||||
import {
|
||||
type TransformListRow,
|
||||
refreshTransformList$,
|
||||
REFRESH_TRANSFORM_LIST_STATE,
|
||||
} from '../common';
|
||||
|
||||
import { useApi } from './use_api';
|
||||
import { type TransformListRow } from '../common';
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
import { TRANSFORM_ERROR_TYPE } from '../common/transform';
|
||||
|
||||
export type GetTransforms = (forceRefresh?: boolean) => void;
|
||||
interface UseGetTransformsResponse {
|
||||
transforms: TransformListRow[];
|
||||
transformIds: string[];
|
||||
transformIdsWithoutConfig?: string[];
|
||||
}
|
||||
|
||||
export const useGetTransforms = (
|
||||
setTransforms: React.Dispatch<React.SetStateAction<TransformListRow[]>>,
|
||||
setTransformNodes: React.Dispatch<React.SetStateAction<number>>,
|
||||
setErrorMessage: React.Dispatch<React.SetStateAction<IHttpFetchError | undefined>>,
|
||||
setTransformIdsWithoutConfig: React.Dispatch<React.SetStateAction<string[] | undefined>>,
|
||||
setIsInitialized: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
blockRefresh: boolean
|
||||
): GetTransforms => {
|
||||
const api = useApi();
|
||||
const getInitialData = (): UseGetTransformsResponse => ({
|
||||
transforms: [],
|
||||
transformIds: [],
|
||||
});
|
||||
|
||||
let concurrentLoads = 0;
|
||||
interface UseGetTransformsOptions {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
const getTransforms = async (forceRefresh = false) => {
|
||||
if (forceRefresh === true || blockRefresh === false) {
|
||||
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.LOADING);
|
||||
concurrentLoads++;
|
||||
export const useGetTransforms = ({ enabled }: UseGetTransformsOptions = {}) => {
|
||||
const { http } = useAppDependencies();
|
||||
|
||||
if (concurrentLoads > 1) {
|
||||
return;
|
||||
}
|
||||
const { data = getInitialData(), ...rest } = useQuery<UseGetTransformsResponse, IHttpFetchError>(
|
||||
[TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORMS],
|
||||
async ({ signal }) => {
|
||||
const update = getInitialData();
|
||||
|
||||
const fetchOptions = { asSystemRequest: true };
|
||||
const transformNodes = await api.getTransformNodes();
|
||||
const transformConfigs = await api.getTransforms(fetchOptions);
|
||||
const transformStats = await api.getTransformsStats(fetchOptions);
|
||||
|
||||
if (
|
||||
!isGetTransformsResponseSchema(transformConfigs) ||
|
||||
!isGetTransformsStatsResponseSchema(transformStats) ||
|
||||
!isGetTransformNodesResponseSchema(transformNodes)
|
||||
) {
|
||||
// An error is followed immediately by setting the state to idle.
|
||||
// This way we're able to treat ERROR as a one-time-event like REFRESH.
|
||||
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.ERROR);
|
||||
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE);
|
||||
setTransformNodes(0);
|
||||
setTransforms([]);
|
||||
|
||||
setIsInitialized(true);
|
||||
|
||||
if (!isGetTransformsResponseSchema(transformConfigs)) {
|
||||
setErrorMessage(transformConfigs);
|
||||
} else if (!isGetTransformsStatsResponseSchema(transformStats)) {
|
||||
setErrorMessage(transformStats);
|
||||
const transformConfigs = await http.get<GetTransformsResponseSchema>(
|
||||
addInternalBasePath('transforms'),
|
||||
{
|
||||
version: '1',
|
||||
asSystemRequest: true,
|
||||
signal,
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
);
|
||||
const transformStats = await http.get<GetTransformsStatsResponseSchema>(
|
||||
addInternalBasePath(`transforms/_stats`),
|
||||
{
|
||||
version: '1',
|
||||
asSystemRequest: true,
|
||||
signal,
|
||||
}
|
||||
);
|
||||
|
||||
// There might be some errors with fetching certain transforms
|
||||
// For example, when task exists and is running but the config is deleted
|
||||
|
@ -87,17 +76,12 @@ export const useGetTransforms = (
|
|||
})
|
||||
.filter(isDefined);
|
||||
|
||||
setTransformIdsWithoutConfig(
|
||||
danglingTaskIdMatches.length > 0 ? danglingTaskIdMatches : undefined
|
||||
);
|
||||
} else {
|
||||
setTransformIdsWithoutConfig(undefined);
|
||||
update.transformIdsWithoutConfig =
|
||||
danglingTaskIdMatches.length > 0 ? danglingTaskIdMatches : undefined;
|
||||
}
|
||||
|
||||
const tableRows = transformConfigs.transforms.reduce((reducedtableRows, config) => {
|
||||
const stats = isGetTransformsStatsResponseSchema(transformStats)
|
||||
? transformStats.transforms.find((d) => config.id === d.id)
|
||||
: undefined;
|
||||
update.transforms = transformConfigs.transforms.reduce((reducedtableRows, config) => {
|
||||
const stats = transformStats.transforms.find((d) => config.id === d.id);
|
||||
|
||||
// A newly created transform might not have corresponding stats yet.
|
||||
// If that's the case we just skip the transform and don't add it to the transform list yet.
|
||||
|
@ -117,21 +101,15 @@ export const useGetTransforms = (
|
|||
return reducedtableRows;
|
||||
}, [] as TransformListRow[]);
|
||||
|
||||
setTransformNodes(transformNodes.count);
|
||||
setTransforms(tableRows);
|
||||
setErrorMessage(undefined);
|
||||
setIsInitialized(true);
|
||||
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE);
|
||||
update.transformIds = update.transforms.map(({ id }) => id);
|
||||
|
||||
concurrentLoads--;
|
||||
|
||||
if (concurrentLoads > 0) {
|
||||
concurrentLoads = 0;
|
||||
getTransforms(true);
|
||||
return;
|
||||
}
|
||||
return update;
|
||||
},
|
||||
{
|
||||
enabled,
|
||||
refetchInterval: DEFAULT_REFRESH_INTERVAL_MS,
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
return getTransforms;
|
||||
return { data, ...rest };
|
||||
};
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
|
||||
import type {
|
||||
PostTransformsPreviewRequestSchema,
|
||||
PostTransformsPreviewResponseSchema,
|
||||
} from '../../../common/api_schemas/transforms';
|
||||
import { addInternalBasePath, TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants';
|
||||
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
|
||||
export const useGetTransformsPreview = (
|
||||
obj: PostTransformsPreviewRequestSchema,
|
||||
enabled?: boolean
|
||||
) => {
|
||||
const { http } = useAppDependencies();
|
||||
|
||||
return useQuery<PostTransformsPreviewResponseSchema, IHttpFetchError>(
|
||||
[TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORMS_PREVIEW, obj],
|
||||
({ signal }) =>
|
||||
http.post<PostTransformsPreviewResponseSchema>(addInternalBasePath('transforms/_preview'), {
|
||||
body: JSON.stringify(obj),
|
||||
version: '1',
|
||||
signal,
|
||||
}),
|
||||
{ enabled }
|
||||
);
|
||||
};
|
|
@ -5,13 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
|
||||
import React, { type FC } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { CoreSetup } from '@kbn/core/public';
|
||||
import { DataGrid, type UseIndexDataReturnType } from '@kbn/ml-data-grid';
|
||||
import type { RuntimeMappings } from '@kbn/ml-runtime-field-utils';
|
||||
|
@ -25,7 +25,6 @@ import { useIndexData } from './use_index_data';
|
|||
|
||||
jest.mock('../../shared_imports');
|
||||
jest.mock('../app_dependencies');
|
||||
jest.mock('./use_api');
|
||||
|
||||
import { MlSharedContext } from '../__mocks__/shared_context';
|
||||
|
||||
|
@ -45,13 +44,17 @@ const runtimeMappings: RuntimeMappings = {
|
|||
},
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
describe('Transform: useIndexData()', () => {
|
||||
test('dataView set triggers loading', async () => {
|
||||
const mlShared = await getMlSharedImports();
|
||||
const wrapper: FC = ({ children }) => (
|
||||
<IntlProvider locale="en">
|
||||
<MlSharedContext.Provider value={mlShared}>{children}</MlSharedContext.Provider>
|
||||
</IntlProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<IntlProvider locale="en">
|
||||
<MlSharedContext.Provider value={mlShared}>{children}</MlSharedContext.Provider>
|
||||
</IntlProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(
|
||||
|
@ -102,11 +105,13 @@ describe('Transform: <DataGrid /> with useIndexData()', () => {
|
|||
};
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<MlSharedContext.Provider value={mlSharedImports}>
|
||||
<Wrapper />
|
||||
</MlSharedContext.Provider>
|
||||
</IntlProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<IntlProvider locale="en">
|
||||
<MlSharedContext.Provider value={mlSharedImports}>
|
||||
<Wrapper />
|
||||
</MlSharedContext.Provider>
|
||||
</IntlProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
// Act
|
||||
|
@ -142,11 +147,13 @@ describe('Transform: <DataGrid /> with useIndexData()', () => {
|
|||
};
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<MlSharedContext.Provider value={mlSharedImports}>
|
||||
<Wrapper />
|
||||
</MlSharedContext.Provider>
|
||||
</IntlProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<IntlProvider locale="en">
|
||||
<MlSharedContext.Provider value={mlSharedImports}>
|
||||
<Wrapper />
|
||||
</MlSharedContext.Provider>
|
||||
</IntlProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
// Act
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { EuiDataGridColumn } from '@elastic/eui';
|
||||
|
@ -28,10 +28,6 @@ import {
|
|||
} from '@kbn/ml-data-grid';
|
||||
import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
|
||||
|
||||
import {
|
||||
isEsSearchResponse,
|
||||
isFieldHistogramsResponseSchema,
|
||||
} from '../../../common/api_schemas/type_guards';
|
||||
import {
|
||||
hasKeywordDuplicate,
|
||||
isKeywordDuplicate,
|
||||
|
@ -44,7 +40,7 @@ import { useToastNotifications, useAppDependencies } from '../app_dependencies';
|
|||
import type { StepDefineExposedState } from '../sections/create_transform/components/step_define/common';
|
||||
|
||||
import { SearchItems } from './use_search_items';
|
||||
import { useApi } from './use_api';
|
||||
import { useGetHistogramsForFields } from './use_get_histograms_for_fields';
|
||||
import { useDataSearch } from './use_data_search';
|
||||
|
||||
export const useIndexData = (
|
||||
|
@ -52,7 +48,7 @@ export const useIndexData = (
|
|||
query: TransformConfigQuery,
|
||||
combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings'],
|
||||
timeRangeMs?: TimeRangeMs,
|
||||
populatedFields?: Set<string> | null
|
||||
populatedFields?: string[]
|
||||
): UseIndexDataReturnType => {
|
||||
const { analytics } = useAppDependencies();
|
||||
|
||||
|
@ -61,13 +57,8 @@ export const useIndexData = (
|
|||
const loadIndexDataStartTime = useRef<number | undefined>(window.performance.now());
|
||||
|
||||
const indexPattern = useMemo(() => dataView.getIndexPattern(), [dataView]);
|
||||
|
||||
const api = useApi();
|
||||
const dataSearch = useDataSearch();
|
||||
const toastNotifications = useToastNotifications();
|
||||
|
||||
const [dataViewFields, setDataViewFields] = useState<string[]>();
|
||||
|
||||
const baseFilterCriteria = buildBaseFilterCriteria(
|
||||
dataView.timeFieldName,
|
||||
timeRangeMs?.from,
|
||||
|
@ -86,73 +77,73 @@ export const useIndexData = (
|
|||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dataView.timeFieldName !== undefined && timeRangeMs === undefined) {
|
||||
return;
|
||||
}
|
||||
const abortController = new AbortController();
|
||||
|
||||
// Fetch 500 random documents to determine populated fields.
|
||||
// This is a workaround to avoid passing potentially thousands of unpopulated fields
|
||||
// (for example, as part of filebeat/metricbeat/ECS based indices)
|
||||
// to the data grid component which would significantly slow down the page.
|
||||
const fetchDataGridSampleDocuments = async function () {
|
||||
let populatedDataViewFields = populatedFields ? [...populatedFields] : [];
|
||||
let isMissingFields = populatedDataViewFields.length === 0;
|
||||
|
||||
// If populatedFields are not provided, make own request to calculate
|
||||
if (populatedFields === undefined) {
|
||||
setErrorMessage('');
|
||||
setStatus(INDEX_STATUS.LOADING);
|
||||
|
||||
const esSearchRequest = {
|
||||
index: indexPattern,
|
||||
body: {
|
||||
fields: ['*'],
|
||||
_source: false,
|
||||
query: {
|
||||
function_score: {
|
||||
query: defaultQuery,
|
||||
random_score: {},
|
||||
},
|
||||
},
|
||||
size: 500,
|
||||
// Fetch 500 random documents to determine populated fields.
|
||||
// This is a workaround to avoid passing potentially thousands of unpopulated fields
|
||||
// (for example, as part of filebeat/metricbeat/ECS based indices)
|
||||
// to the data grid component which would significantly slow down the page.
|
||||
const {
|
||||
error: dataViewFieldsError,
|
||||
data: dataViewFieldsData,
|
||||
isError: dataViewFieldsIsError,
|
||||
isLoading: dataViewFieldsIsLoading,
|
||||
} = useDataSearch(
|
||||
{
|
||||
index: indexPattern,
|
||||
body: {
|
||||
fields: ['*'],
|
||||
_source: false,
|
||||
query: {
|
||||
function_score: {
|
||||
query: defaultQuery,
|
||||
random_score: {},
|
||||
},
|
||||
};
|
||||
},
|
||||
size: 500,
|
||||
},
|
||||
},
|
||||
// Check whether fetching should be enabled
|
||||
// If populatedFields are not provided, make own request to calculate
|
||||
!Array.isArray(populatedFields) &&
|
||||
!(dataView.timeFieldName !== undefined && timeRangeMs === undefined)
|
||||
);
|
||||
|
||||
const resp = await dataSearch(esSearchRequest, abortController.signal);
|
||||
|
||||
if (!isEsSearchResponse(resp)) {
|
||||
setErrorMessage(getErrorMessage(resp));
|
||||
setStatus(INDEX_STATUS.ERROR);
|
||||
return;
|
||||
}
|
||||
const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields ?? {}));
|
||||
isMissingFields = resp.hits.hits.every((d) => typeof d.fields === 'undefined');
|
||||
|
||||
populatedDataViewFields = [...new Set(docs.map(Object.keys).flat(1))];
|
||||
}
|
||||
useEffect(() => {
|
||||
if (dataViewFieldsIsLoading && !dataViewFieldsIsError) {
|
||||
setErrorMessage('');
|
||||
setStatus(INDEX_STATUS.LOADING);
|
||||
} else if (dataViewFieldsError !== null) {
|
||||
setErrorMessage(getErrorMessage(dataViewFieldsError));
|
||||
setStatus(INDEX_STATUS.ERROR);
|
||||
} else if (
|
||||
!dataViewFieldsIsLoading &&
|
||||
!dataViewFieldsIsError &&
|
||||
dataViewFieldsData !== undefined
|
||||
) {
|
||||
const isCrossClusterSearch = indexPattern.includes(':');
|
||||
|
||||
// Get all field names for each returned doc and flatten it
|
||||
// to a list of unique field names used across all docs.
|
||||
const allDataViewFields = getFieldsFromKibanaIndexPattern(dataView);
|
||||
const filteredDataViewFields = populatedDataViewFields
|
||||
.filter((d) => allDataViewFields.includes(d))
|
||||
.sort();
|
||||
const isMissingFields = dataViewFieldsData.hits.hits.every(
|
||||
(d) => typeof d.fields === 'undefined'
|
||||
);
|
||||
|
||||
setCcsWarning(isCrossClusterSearch && isMissingFields);
|
||||
setStatus(INDEX_STATUS.LOADED);
|
||||
setDataViewFields(filteredDataViewFields);
|
||||
};
|
||||
|
||||
fetchDataGridSampleDocuments();
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [timeRangeMs, populatedFields?.size]);
|
||||
}, [dataViewFieldsData, dataViewFieldsError, dataViewFieldsIsError, dataViewFieldsIsLoading]);
|
||||
|
||||
const dataViewFields = useMemo(() => {
|
||||
let allPopulatedFields = Array.isArray(populatedFields) ? populatedFields : [];
|
||||
|
||||
if (populatedFields === undefined && dataViewFieldsData) {
|
||||
// Get all field names for each returned doc and flatten it
|
||||
// to a list of unique field names used across all docs.
|
||||
const docs = dataViewFieldsData.hits.hits.map((d) => getProcessedFields(d.fields ?? {}));
|
||||
allPopulatedFields = [...new Set(docs.map(Object.keys).flat(1))];
|
||||
}
|
||||
|
||||
const allDataViewFields = getFieldsFromKibanaIndexPattern(dataView);
|
||||
return allPopulatedFields.filter((d) => allDataViewFields.includes(d)).sort();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataViewFieldsData, populatedFields]);
|
||||
|
||||
const columns: EuiDataGridColumn[] = useMemo(() => {
|
||||
if (typeof dataViewFields === 'undefined') {
|
||||
|
@ -206,132 +197,113 @@ export const useIndexData = (
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify([query, timeRangeMs])]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof dataViewFields === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const abortController = new AbortController();
|
||||
const sort: EsSorting = sortingColumns.reduce((s, column) => {
|
||||
s[column.id] = { order: column.direction };
|
||||
return s;
|
||||
}, {} as EsSorting);
|
||||
|
||||
const fetchDataGridData = async function () {
|
||||
const {
|
||||
error: dataGridDataError,
|
||||
data: dataGridData,
|
||||
isError: dataGridDataIsError,
|
||||
isLoading: dataGridDataIsLoading,
|
||||
} = useDataSearch(
|
||||
{
|
||||
index: indexPattern,
|
||||
body: {
|
||||
fields: ['*'],
|
||||
_source: false,
|
||||
query: isDefaultQuery(query) ? defaultQuery : queryWithBaseFilterCriteria,
|
||||
from: pagination.pageIndex * pagination.pageSize,
|
||||
size: pagination.pageSize,
|
||||
...(Object.keys(sort).length > 0 ? { sort } : {}),
|
||||
...(isRuntimeMappings(combinedRuntimeMappings)
|
||||
? { runtime_mappings: combinedRuntimeMappings }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
// Check whether fetching should be enabled
|
||||
dataViewFields !== undefined
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataGridDataIsLoading && !dataGridDataIsError) {
|
||||
setErrorMessage('');
|
||||
setStatus(INDEX_STATUS.LOADING);
|
||||
|
||||
const sort: EsSorting = sortingColumns.reduce((s, column) => {
|
||||
s[column.id] = { order: column.direction };
|
||||
return s;
|
||||
}, {} as EsSorting);
|
||||
|
||||
const esSearchRequest = {
|
||||
index: indexPattern,
|
||||
body: {
|
||||
fields: ['*'],
|
||||
_source: false,
|
||||
query: isDefaultQuery(query) ? defaultQuery : queryWithBaseFilterCriteria,
|
||||
from: pagination.pageIndex * pagination.pageSize,
|
||||
size: pagination.pageSize,
|
||||
...(Object.keys(sort).length > 0 ? { sort } : {}),
|
||||
...(isRuntimeMappings(combinedRuntimeMappings)
|
||||
? { runtime_mappings: combinedRuntimeMappings }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
const resp = await dataSearch(esSearchRequest, abortController.signal);
|
||||
|
||||
if (!isEsSearchResponse(resp)) {
|
||||
setErrorMessage(getErrorMessage(resp));
|
||||
setStatus(INDEX_STATUS.ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (dataGridDataError !== null) {
|
||||
setErrorMessage(getErrorMessage(dataGridDataError));
|
||||
setStatus(INDEX_STATUS.ERROR);
|
||||
} else if (!dataGridDataIsLoading && !dataGridDataIsError && dataGridData !== undefined) {
|
||||
const isCrossClusterSearch = indexPattern.includes(':');
|
||||
const isMissingFields = resp.hits.hits.every((d) => typeof d.fields === 'undefined');
|
||||
const isMissingFields = dataGridData.hits.hits.every((d) => typeof d.fields === 'undefined');
|
||||
|
||||
const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields ?? {}));
|
||||
const docs = dataGridData.hits.hits.map((d) => getProcessedFields(d.fields ?? {}));
|
||||
|
||||
setCcsWarning(isCrossClusterSearch && isMissingFields);
|
||||
setRowCountInfo({
|
||||
rowCount: typeof resp.hits.total === 'number' ? resp.hits.total : resp.hits.total!.value,
|
||||
rowCount:
|
||||
typeof dataGridData.hits.total === 'number'
|
||||
? dataGridData.hits.total
|
||||
: dataGridData.hits.total!.value,
|
||||
rowCountRelation:
|
||||
typeof resp.hits.total === 'number'
|
||||
typeof dataGridData.hits.total === 'number'
|
||||
? ('eq' as estypes.SearchTotalHitsRelation)
|
||||
: resp.hits.total!.relation,
|
||||
: dataGridData.hits.total!.relation,
|
||||
});
|
||||
setTableItems(docs);
|
||||
setStatus(INDEX_STATUS.LOADED);
|
||||
};
|
||||
|
||||
fetchDataGridData();
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
// custom comparison
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
indexPattern,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
JSON.stringify([
|
||||
query,
|
||||
pagination,
|
||||
sortingColumns,
|
||||
dataViewFields,
|
||||
}, [dataGridDataError, dataGridDataIsError, dataGridDataIsLoading]);
|
||||
|
||||
const allDataViewFieldNames = new Set(dataView.fields.map((f) => f.name));
|
||||
const { error: histogramsForFieldsError, data: histogramsForFieldsData } =
|
||||
useGetHistogramsForFields(
|
||||
indexPattern,
|
||||
columns
|
||||
.filter((cT) => dataGrid.visibleColumns.includes(cT.id))
|
||||
.map((cT) => {
|
||||
// If a column field name has a corresponding keyword field,
|
||||
// fetch the keyword field instead to be able to do aggregations.
|
||||
const fieldName = cT.id;
|
||||
return hasKeywordDuplicate(fieldName, allDataViewFieldNames)
|
||||
? {
|
||||
fieldName: `${fieldName}.keyword`,
|
||||
type: getFieldType(undefined),
|
||||
}
|
||||
: {
|
||||
fieldName,
|
||||
type: getFieldType(cT.schema),
|
||||
};
|
||||
}),
|
||||
isDefaultQuery(query) ? defaultQuery : queryWithBaseFilterCriteria,
|
||||
combinedRuntimeMappings,
|
||||
timeRangeMs,
|
||||
]),
|
||||
]);
|
||||
chartsVisible
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchColumnChartsData = async function () {
|
||||
const allDataViewFieldNames = new Set(dataView.fields.map((f) => f.name));
|
||||
const columnChartsData = await api.getHistogramsForFields(
|
||||
indexPattern,
|
||||
columns
|
||||
.filter((cT) => dataGrid.visibleColumns.includes(cT.id))
|
||||
.map((cT) => {
|
||||
// If a column field name has a corresponding keyword field,
|
||||
// fetch the keyword field instead to be able to do aggregations.
|
||||
const fieldName = cT.id;
|
||||
return hasKeywordDuplicate(fieldName, allDataViewFieldNames)
|
||||
? {
|
||||
fieldName: `${fieldName}.keyword`,
|
||||
type: getFieldType(undefined),
|
||||
}
|
||||
: {
|
||||
fieldName,
|
||||
type: getFieldType(cT.schema),
|
||||
};
|
||||
}),
|
||||
isDefaultQuery(query) ? defaultQuery : queryWithBaseFilterCriteria,
|
||||
combinedRuntimeMappings
|
||||
);
|
||||
|
||||
if (!isFieldHistogramsResponseSchema(columnChartsData)) {
|
||||
showDataGridColumnChartErrorMessageToast(columnChartsData, toastNotifications);
|
||||
return;
|
||||
}
|
||||
if (histogramsForFieldsError !== null) {
|
||||
showDataGridColumnChartErrorMessageToast(histogramsForFieldsError, toastNotifications);
|
||||
}
|
||||
// custom comparison
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [histogramsForFieldsError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (histogramsForFieldsData) {
|
||||
setColumnCharts(
|
||||
// revert field names with `.keyword` used to do aggregations to their original column name
|
||||
columnChartsData.map((d) => ({
|
||||
histogramsForFieldsData.map((d) => ({
|
||||
...d,
|
||||
...(isKeywordDuplicate(d.id, allDataViewFieldNames)
|
||||
? { id: removeKeywordPostfix(d.id) }
|
||||
: {}),
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
if (chartsVisible) {
|
||||
fetchColumnChartsData();
|
||||
}
|
||||
// custom comparison
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
chartsVisible,
|
||||
indexPattern,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
JSON.stringify([query, dataGrid.visibleColumns, combinedRuntimeMappings, timeRangeMs]),
|
||||
]);
|
||||
}, [histogramsForFieldsData]);
|
||||
|
||||
const renderCellValue = useRenderCellValue(dataView, pagination, tableItems);
|
||||
|
||||
|
|
|
@ -6,31 +6,39 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import type { StartTransformsRequestSchema } from '../../../common/api_schemas/start_transforms';
|
||||
import { isStartTransformsResponseSchema } from '../../../common/api_schemas/type_guards';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
|
||||
import { addInternalBasePath } from '../../../common/constants';
|
||||
import { getErrorMessage } from '../../../common/utils/errors';
|
||||
import type {
|
||||
ReauthorizeTransformsRequestSchema,
|
||||
ReauthorizeTransformsResponseSchema,
|
||||
} from '../../../common/api_schemas/reauthorize_transforms';
|
||||
|
||||
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
||||
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
|
||||
import { ToastNotificationText } from '../components';
|
||||
|
||||
import { useApi } from './use_api';
|
||||
import { useRefreshTransformList } from './use_refresh_transform_list';
|
||||
|
||||
export const useReauthorizeTransforms = () => {
|
||||
const { overlays, theme } = useAppDependencies();
|
||||
const { http, i18n: i18nStart, theme } = useAppDependencies();
|
||||
const refreshTransformList = useRefreshTransformList();
|
||||
const toastNotifications = useToastNotifications();
|
||||
const api = useApi();
|
||||
|
||||
return async (transformsInfo: StartTransformsRequestSchema) => {
|
||||
const results = await api.reauthorizeTransforms(transformsInfo);
|
||||
|
||||
if (!isStartTransformsResponseSchema(results)) {
|
||||
const mutation = useMutation({
|
||||
mutationFn: (reqBody: ReauthorizeTransformsRequestSchema) =>
|
||||
http.post<ReauthorizeTransformsResponseSchema>(
|
||||
addInternalBasePath('reauthorize_transforms'),
|
||||
{
|
||||
body: JSON.stringify(reqBody),
|
||||
version: '1',
|
||||
}
|
||||
),
|
||||
onError: (error) =>
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.transform.stepCreateForm.reauthorizeTransformResponseSchemaErrorMessage',
|
||||
|
@ -38,44 +46,37 @@ export const useReauthorizeTransforms = () => {
|
|||
defaultMessage: 'An error occurred calling the reauthorize transforms request.',
|
||||
}
|
||||
),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={getErrorMessage(results)}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const transformId in results) {
|
||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||
if (results.hasOwnProperty(transformId)) {
|
||||
const result = results[transformId];
|
||||
if (result.success === true) {
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.transform.transformList.reauthorizeTransformSuccessMessage', {
|
||||
defaultMessage: 'Request to reauthorize transform {transformId} acknowledged.',
|
||||
values: { transformId },
|
||||
})
|
||||
);
|
||||
} else {
|
||||
toastNotifications.addError(new Error(JSON.stringify(result.error!.caused_by, null, 2)), {
|
||||
title: i18n.translate(
|
||||
'xpack.transform.transformList.reauthorizeTransformErrorMessage',
|
||||
text: toMountPoint(<ToastNotificationText text={getErrorMessage(error)} />, {
|
||||
theme,
|
||||
i18n: i18nStart,
|
||||
}),
|
||||
}),
|
||||
onSuccess: (results) => {
|
||||
for (const transformId in results) {
|
||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||
if (results.hasOwnProperty(transformId)) {
|
||||
const result = results[transformId];
|
||||
if (!result.success) {
|
||||
toastNotifications.addError(
|
||||
new Error(JSON.stringify(result.error!.caused_by, null, 2)),
|
||||
{
|
||||
defaultMessage: 'An error occurred reauthorizing the transform {transformId}',
|
||||
values: { transformId },
|
||||
title: i18n.translate(
|
||||
'xpack.transform.transformList.reauthorizeTransformErrorMessage',
|
||||
{
|
||||
defaultMessage: 'An error occurred reauthorizing the transform {transformId}',
|
||||
values: { transformId },
|
||||
}
|
||||
),
|
||||
toastMessage: result.error!.reason,
|
||||
}
|
||||
),
|
||||
toastMessage: result.error!.reason,
|
||||
});
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
|
||||
};
|
||||
refreshTransformList();
|
||||
},
|
||||
});
|
||||
|
||||
return mutation.mutate;
|
||||
};
|
||||
|
|
|
@ -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 { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants';
|
||||
|
||||
export const useRefreshTransformList = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return () => {
|
||||
queryClient.invalidateQueries([TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORM_NODES]);
|
||||
queryClient.invalidateQueries([TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORMS]);
|
||||
queryClient.invalidateQueries([TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORM_AUDIT_MESSAGES]);
|
||||
};
|
||||
};
|
|
@ -6,107 +6,72 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import type {
|
||||
ResetTransformStatus,
|
||||
ResetTransformsRequestSchema,
|
||||
} from '../../../common/api_schemas/reset_transforms';
|
||||
import { isResetTransformsResponseSchema } from '../../../common/api_schemas/type_guards';
|
||||
import { getErrorMessage } from '../../../common/utils/errors';
|
||||
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
||||
import { REFRESH_TRANSFORM_LIST_STATE, refreshTransformList$ } from '../common';
|
||||
import { ToastNotificationText } from '../components';
|
||||
import { useApi } from './use_api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
type SuccessCountField = keyof Omit<ResetTransformStatus, 'destinationIndex'>;
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
|
||||
import type {
|
||||
ResetTransformsRequestSchema,
|
||||
ResetTransformsResponseSchema,
|
||||
} from '../../../common/api_schemas/reset_transforms';
|
||||
import { addInternalBasePath } from '../../../common/constants';
|
||||
import { getErrorMessage } from '../../../common/utils/errors';
|
||||
|
||||
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
||||
import { ToastNotificationText } from '../components';
|
||||
|
||||
import { useRefreshTransformList } from './use_refresh_transform_list';
|
||||
|
||||
export const useResetTransforms = () => {
|
||||
const { overlays, theme } = useAppDependencies();
|
||||
const { http, i18n: i18nStart, theme } = useAppDependencies();
|
||||
const refreshTransformList = useRefreshTransformList();
|
||||
const toastNotifications = useToastNotifications();
|
||||
const api = useApi();
|
||||
|
||||
return async (reqBody: ResetTransformsRequestSchema) => {
|
||||
const results = await api.resetTransforms(reqBody);
|
||||
|
||||
if (!isResetTransformsResponseSchema(results)) {
|
||||
const mutation = useMutation({
|
||||
mutationFn: (reqBody: ResetTransformsRequestSchema) =>
|
||||
http.post<ResetTransformsResponseSchema>(addInternalBasePath('reset_transforms'), {
|
||||
body: JSON.stringify(reqBody),
|
||||
version: '1',
|
||||
}),
|
||||
onError: (error) =>
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.transformList.resetTransformGenericErrorMessage', {
|
||||
defaultMessage: 'An error occurred calling the API endpoint to reset transforms.',
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
previewTextLength={50}
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={getErrorMessage(results)}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const isBulk = Object.keys(results).length > 1;
|
||||
const successCount: Record<SuccessCountField, number> = {
|
||||
transformReset: 0,
|
||||
};
|
||||
for (const transformId in results) {
|
||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||
if (results.hasOwnProperty(transformId)) {
|
||||
const status = results[transformId];
|
||||
|
||||
// if we are only resetting one transform, show the success toast messages
|
||||
if (!isBulk && status.transformReset) {
|
||||
if (status.transformReset?.success) {
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.transform.transformList.resetTransformSuccessMessage', {
|
||||
defaultMessage: 'Request to reset transform {transformId} acknowledged.',
|
||||
values: { transformId },
|
||||
})
|
||||
);
|
||||
<ToastNotificationText previewTextLength={50} text={getErrorMessage(error)} />,
|
||||
{
|
||||
theme,
|
||||
i18n: i18nStart,
|
||||
}
|
||||
),
|
||||
}),
|
||||
onSuccess: (results) => {
|
||||
for (const transformId in results) {
|
||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||
if (results.hasOwnProperty(transformId)) {
|
||||
const status = results[transformId];
|
||||
|
||||
if (status.transformReset?.error) {
|
||||
const error = status.transformReset.error.reason;
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.transformList.resetTransformErrorMessage', {
|
||||
defaultMessage: 'An error occurred resetting the transform {transformId}',
|
||||
values: { transformId },
|
||||
}),
|
||||
text: toMountPoint(<ToastNotificationText previewTextLength={50} text={error} />, {
|
||||
theme,
|
||||
i18n: i18nStart,
|
||||
}),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
(Object.keys(successCount) as SuccessCountField[]).forEach((key) => {
|
||||
if (status[key]?.success) {
|
||||
successCount[key] = successCount[key] + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (status.transformReset?.error) {
|
||||
const error = status.transformReset.error.reason;
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.transformList.resetTransformErrorMessage', {
|
||||
defaultMessage: 'An error occurred resetting the transform {transformId}',
|
||||
values: { transformId },
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
previewTextLength={50}
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={error}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we are deleting multiple transforms, combine the success messages
|
||||
if (isBulk) {
|
||||
if (successCount.transformReset > 0) {
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.transform.transformList.bulkResetTransformSuccessMessage', {
|
||||
defaultMessage:
|
||||
'Successfully reset {count} {count, plural, one {transform} other {transforms}}.',
|
||||
values: { count: successCount.transformReset },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
refreshTransformList();
|
||||
},
|
||||
});
|
||||
|
||||
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
|
||||
};
|
||||
return mutation.mutate;
|
||||
};
|
||||
|
|
|
@ -6,31 +6,38 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import type { ScheduleNowTransformsRequestSchema } from '../../../common/api_schemas/schedule_now_transforms';
|
||||
import { isScheduleNowTransformsResponseSchema } from '../../../common/api_schemas/type_guards';
|
||||
|
||||
import { addInternalBasePath } from '../../../common/constants';
|
||||
import type {
|
||||
ScheduleNowTransformsRequestSchema,
|
||||
ScheduleNowTransformsResponseSchema,
|
||||
} from '../../../common/api_schemas/schedule_now_transforms';
|
||||
import { getErrorMessage } from '../../../common/utils/errors';
|
||||
|
||||
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
||||
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
|
||||
import { ToastNotificationText } from '../components';
|
||||
|
||||
import { useApi } from './use_api';
|
||||
import { useRefreshTransformList } from './use_refresh_transform_list';
|
||||
|
||||
export const useScheduleNowTransforms = () => {
|
||||
const { overlays, theme } = useAppDependencies();
|
||||
const { http, i18n: i18nStart, theme } = useAppDependencies();
|
||||
const refreshTransformList = useRefreshTransformList();
|
||||
const toastNotifications = useToastNotifications();
|
||||
const api = useApi();
|
||||
|
||||
return async (transformsInfo: ScheduleNowTransformsRequestSchema) => {
|
||||
const results = await api.scheduleNowTransforms(transformsInfo);
|
||||
|
||||
if (!isScheduleNowTransformsResponseSchema(results)) {
|
||||
const mutation = useMutation({
|
||||
mutationFn: (reqBody: ScheduleNowTransformsRequestSchema) =>
|
||||
http.post<ScheduleNowTransformsResponseSchema>(
|
||||
addInternalBasePath('schedule_now_transforms'),
|
||||
{
|
||||
body: JSON.stringify(reqBody),
|
||||
version: '1',
|
||||
}
|
||||
),
|
||||
onError: (error) =>
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.transform.stepCreateForm.scheduleNowTransformResponseSchemaErrorMessage',
|
||||
|
@ -39,46 +46,38 @@ export const useScheduleNowTransforms = () => {
|
|||
'An error occurred calling the request to schedule the transform to process data instantly.',
|
||||
}
|
||||
),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={getErrorMessage(results)}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const transformId in results) {
|
||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||
if (results.hasOwnProperty(transformId)) {
|
||||
const result = results[transformId];
|
||||
if (result.success === true) {
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.transform.transformList.scheduleNowTransformSuccessMessage', {
|
||||
defaultMessage:
|
||||
'Request to schedule transform {transformId} to process data instantly acknowledged.',
|
||||
values: { transformId },
|
||||
})
|
||||
);
|
||||
} else {
|
||||
toastNotifications.addError(new Error(JSON.stringify(result.error!.caused_by, null, 2)), {
|
||||
title: i18n.translate(
|
||||
'xpack.transform.transformList.scheduleNowTransformErrorMessage',
|
||||
text: toMountPoint(<ToastNotificationText text={getErrorMessage(error)} />, {
|
||||
theme,
|
||||
i18n: i18nStart,
|
||||
}),
|
||||
}),
|
||||
onSuccess: (results) => {
|
||||
for (const transformId in results) {
|
||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||
if (results.hasOwnProperty(transformId)) {
|
||||
const result = results[transformId];
|
||||
if (!result.success) {
|
||||
toastNotifications.addError(
|
||||
new Error(JSON.stringify(result.error!.caused_by, null, 2)),
|
||||
{
|
||||
defaultMessage:
|
||||
'An error occurred scheduling transform {transformId} to process data instantly.',
|
||||
values: { transformId },
|
||||
title: i18n.translate(
|
||||
'xpack.transform.transformList.scheduleNowTransformErrorMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'An error occurred scheduling transform {transformId} to process data instantly.',
|
||||
values: { transformId },
|
||||
}
|
||||
),
|
||||
toastMessage: result.error!.reason,
|
||||
}
|
||||
),
|
||||
toastMessage: result.error!.reason,
|
||||
});
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
|
||||
};
|
||||
refreshTransformList();
|
||||
},
|
||||
});
|
||||
|
||||
return mutation.mutate;
|
||||
};
|
||||
|
|
|
@ -6,31 +6,35 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import type { StartTransformsRequestSchema } from '../../../common/api_schemas/start_transforms';
|
||||
import { isStartTransformsResponseSchema } from '../../../common/api_schemas/type_guards';
|
||||
|
||||
import { addInternalBasePath } from '../../../common/constants';
|
||||
import type {
|
||||
StartTransformsRequestSchema,
|
||||
StartTransformsResponseSchema,
|
||||
} from '../../../common/api_schemas/start_transforms';
|
||||
import { getErrorMessage } from '../../../common/utils/errors';
|
||||
|
||||
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
||||
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
|
||||
import { ToastNotificationText } from '../components';
|
||||
|
||||
import { useApi } from './use_api';
|
||||
import { useRefreshTransformList } from './use_refresh_transform_list';
|
||||
|
||||
export const useStartTransforms = () => {
|
||||
const { overlays, theme } = useAppDependencies();
|
||||
const { http, i18n: i18nStart, theme } = useAppDependencies();
|
||||
const refreshTransformList = useRefreshTransformList();
|
||||
const toastNotifications = useToastNotifications();
|
||||
const api = useApi();
|
||||
|
||||
return async (transformsInfo: StartTransformsRequestSchema) => {
|
||||
const results = await api.startTransforms(transformsInfo);
|
||||
|
||||
if (!isStartTransformsResponseSchema(results)) {
|
||||
const mutation = useMutation({
|
||||
mutationFn: (reqBody: StartTransformsRequestSchema) =>
|
||||
http.post<StartTransformsResponseSchema>(addInternalBasePath('start_transforms'), {
|
||||
body: JSON.stringify(reqBody),
|
||||
version: '1',
|
||||
}),
|
||||
onError: (error) =>
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.transform.stepCreateForm.startTransformResponseSchemaErrorMessage',
|
||||
|
@ -38,41 +42,34 @@ export const useStartTransforms = () => {
|
|||
defaultMessage: 'An error occurred calling the start transforms request.',
|
||||
}
|
||||
),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={getErrorMessage(results)}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const transformId in results) {
|
||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||
if (results.hasOwnProperty(transformId)) {
|
||||
const result = results[transformId];
|
||||
if (result.success === true) {
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.transform.transformList.startTransformSuccessMessage', {
|
||||
defaultMessage: 'Request to start transform {transformId} acknowledged.',
|
||||
values: { transformId },
|
||||
})
|
||||
);
|
||||
} else {
|
||||
toastNotifications.addError(new Error(JSON.stringify(result.error!.caused_by, null, 2)), {
|
||||
title: i18n.translate('xpack.transform.transformList.startTransformErrorMessage', {
|
||||
defaultMessage: 'An error occurred starting the transform {transformId}',
|
||||
values: { transformId },
|
||||
}),
|
||||
toastMessage: result.error!.reason,
|
||||
});
|
||||
text: toMountPoint(<ToastNotificationText text={getErrorMessage(error)} />, {
|
||||
theme,
|
||||
i18n: i18nStart,
|
||||
}),
|
||||
}),
|
||||
onSuccess: (results) => {
|
||||
for (const transformId in results) {
|
||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||
if (results.hasOwnProperty(transformId)) {
|
||||
const result = results[transformId];
|
||||
if (!result.success) {
|
||||
toastNotifications.addError(
|
||||
new Error(JSON.stringify(result.error!.caused_by, null, 2)),
|
||||
{
|
||||
title: i18n.translate('xpack.transform.transformList.startTransformErrorMessage', {
|
||||
defaultMessage: 'An error occurred starting the transform {transformId}',
|
||||
values: { transformId },
|
||||
}),
|
||||
toastMessage: result.error!.reason,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
|
||||
};
|
||||
refreshTransformList();
|
||||
},
|
||||
});
|
||||
|
||||
return mutation.mutate;
|
||||
};
|
||||
|
|
|
@ -6,31 +6,36 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import type { StopTransformsRequestSchema } from '../../../common/api_schemas/stop_transforms';
|
||||
import { isStopTransformsResponseSchema } from '../../../common/api_schemas/type_guards';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
|
||||
import { addInternalBasePath } from '../../../common/constants';
|
||||
import type {
|
||||
StopTransformsRequestSchema,
|
||||
StopTransformsResponseSchema,
|
||||
} from '../../../common/api_schemas/stop_transforms';
|
||||
import { getErrorMessage } from '../../../common/utils/errors';
|
||||
|
||||
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
||||
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
|
||||
import { ToastNotificationText } from '../components';
|
||||
|
||||
import { useApi } from './use_api';
|
||||
import { useRefreshTransformList } from './use_refresh_transform_list';
|
||||
|
||||
export const useStopTransforms = () => {
|
||||
const { overlays, theme } = useAppDependencies();
|
||||
const { http, i18n: i18nStart, theme } = useAppDependencies();
|
||||
const refreshTransformList = useRefreshTransformList();
|
||||
const toastNotifications = useToastNotifications();
|
||||
const api = useApi();
|
||||
|
||||
return async (transformsInfo: StopTransformsRequestSchema) => {
|
||||
const results = await api.stopTransforms(transformsInfo);
|
||||
|
||||
if (!isStopTransformsResponseSchema(results)) {
|
||||
const mutation = useMutation({
|
||||
mutationFn: (reqBody: StopTransformsRequestSchema) =>
|
||||
http.post<StopTransformsResponseSchema>(addInternalBasePath('stop_transforms'), {
|
||||
body: JSON.stringify(reqBody),
|
||||
version: '1',
|
||||
}),
|
||||
onError: (error) =>
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.transform.transformList.stopTransformResponseSchemaErrorMessage',
|
||||
|
@ -38,39 +43,29 @@ export const useStopTransforms = () => {
|
|||
defaultMessage: 'An error occurred called the stop transforms request.',
|
||||
}
|
||||
),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={getErrorMessage(results)}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const transformId in results) {
|
||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||
if (results.hasOwnProperty(transformId)) {
|
||||
if (results[transformId].success === true) {
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.transform.transformList.stopTransformSuccessMessage', {
|
||||
defaultMessage: 'Request to stop data frame transform {transformId} acknowledged.',
|
||||
values: { transformId },
|
||||
})
|
||||
);
|
||||
} else {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.transform.transformList.stopTransformErrorMessage', {
|
||||
defaultMessage: 'An error occurred stopping the data frame transform {transformId}',
|
||||
values: { transformId },
|
||||
})
|
||||
);
|
||||
text: toMountPoint(<ToastNotificationText text={getErrorMessage(error)} />, {
|
||||
theme,
|
||||
i18n: i18nStart,
|
||||
}),
|
||||
}),
|
||||
onSuccess: (results) => {
|
||||
for (const transformId in results) {
|
||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||
if (results.hasOwnProperty(transformId)) {
|
||||
if (!results[transformId].success) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.transform.transformList.stopTransformErrorMessage', {
|
||||
defaultMessage: 'An error occurred stopping the data frame transform {transformId}',
|
||||
values: { transformId },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
|
||||
};
|
||||
refreshTransformList();
|
||||
},
|
||||
});
|
||||
|
||||
return mutation.mutate;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 {
|
||||
getInitialTransformCapabilities,
|
||||
isTransformCapabilities,
|
||||
} from '../../../common/types/capabilities';
|
||||
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
|
||||
export const useTransformCapabilities = () => {
|
||||
const { application } = useAppDependencies();
|
||||
|
||||
if (isTransformCapabilities(application?.capabilities?.transform)) {
|
||||
return application.capabilities.transform;
|
||||
}
|
||||
|
||||
return getInitialTransformCapabilities();
|
||||
};
|
|
@ -29,14 +29,13 @@ import {
|
|||
} from '@kbn/ml-data-grid';
|
||||
|
||||
import type { PreviewMappingsProperties } from '../../../common/api_schemas/transforms';
|
||||
import { isPostTransformsPreviewResponseSchema } from '../../../common/api_schemas/type_guards';
|
||||
|
||||
import { getErrorMessage } from '../../../common/utils/errors';
|
||||
|
||||
import { getPreviewTransformRequestBody, type TransformConfigQuery } from '../common';
|
||||
|
||||
import { SearchItems } from './use_search_items';
|
||||
import { useApi } from './use_api';
|
||||
import { useGetTransformsPreview } from './use_get_transforms_preview';
|
||||
import { StepDefineExposedState } from '../sections/create_transform/components/step_define';
|
||||
import {
|
||||
isLatestPartialRequest,
|
||||
|
@ -111,7 +110,6 @@ export const useTransformConfigData = (
|
|||
): UseIndexDataReturnType => {
|
||||
const [previewMappingsProperties, setPreviewMappingsProperties] =
|
||||
useState<PreviewMappingsProperties>({});
|
||||
const api = useApi();
|
||||
|
||||
// Filters mapping properties of type `object`, which get returned for nested field parents.
|
||||
const columnKeys = Object.keys(previewMappingsProperties).filter(
|
||||
|
@ -147,32 +145,32 @@ export const useTransformConfigData = (
|
|||
tableItems,
|
||||
} = dataGrid;
|
||||
|
||||
const getPreviewData = async () => {
|
||||
if (!validationStatus.isValid) {
|
||||
setTableItems([]);
|
||||
setRowCountInfo({
|
||||
rowCount: 0,
|
||||
rowCountRelation: ES_CLIENT_TOTAL_HITS_RELATION.EQ,
|
||||
});
|
||||
setNoDataMessage(validationStatus.errorMessage!);
|
||||
return;
|
||||
}
|
||||
const previewRequest = useMemo(
|
||||
() =>
|
||||
getPreviewTransformRequestBody(
|
||||
dataView,
|
||||
query,
|
||||
requestPayload,
|
||||
combinedRuntimeMappings,
|
||||
timeRangeMs
|
||||
),
|
||||
[dataView, query, requestPayload, combinedRuntimeMappings, timeRangeMs]
|
||||
);
|
||||
|
||||
setErrorMessage('');
|
||||
setNoDataMessage('');
|
||||
setStatus(INDEX_STATUS.LOADING);
|
||||
const {
|
||||
error: previewError,
|
||||
data: previewData,
|
||||
isError,
|
||||
isLoading,
|
||||
} = useGetTransformsPreview(previewRequest, validationStatus.isValid);
|
||||
|
||||
const previewRequest = getPreviewTransformRequestBody(
|
||||
dataView,
|
||||
query,
|
||||
requestPayload,
|
||||
combinedRuntimeMappings,
|
||||
timeRangeMs
|
||||
);
|
||||
const resp = await api.getTransformsPreview(previewRequest);
|
||||
|
||||
if (!isPostTransformsPreviewResponseSchema(resp)) {
|
||||
setErrorMessage(getErrorMessage(resp));
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
setErrorMessage('');
|
||||
setNoDataMessage('');
|
||||
setStatus(INDEX_STATUS.LOADING);
|
||||
} else if (isError) {
|
||||
setErrorMessage(getErrorMessage(previewError));
|
||||
setTableItems([]);
|
||||
setRowCountInfo({
|
||||
rowCount: 0,
|
||||
|
@ -180,51 +178,67 @@ export const useTransformConfigData = (
|
|||
});
|
||||
setPreviewMappingsProperties({});
|
||||
setStatus(INDEX_STATUS.ERROR);
|
||||
return;
|
||||
} else if (!isLoading && !isError && previewData !== undefined) {
|
||||
// To improve UI performance with a latest configuration for indices with a large number
|
||||
// of fields, we reduce the number of available columns to those populated with values.
|
||||
|
||||
// 1. Flatten the returned object structure object documents to match mapping properties
|
||||
const docs = previewData.preview.map(getFlattenedObject);
|
||||
|
||||
// 2. Get all field names for each returned doc and flatten it
|
||||
// to a list of unique field names used across all docs.
|
||||
const populatedFields = [...new Set(docs.map(Object.keys).flat(1))];
|
||||
|
||||
// 3. Filter mapping properties by populated fields
|
||||
let populatedProperties: PreviewMappingsProperties = Object.entries(
|
||||
previewData.generated_dest_index.mappings.properties
|
||||
)
|
||||
.filter(([key]) => populatedFields.includes(key))
|
||||
.reduce(
|
||||
(p, [key, value]) => ({
|
||||
...p,
|
||||
[key]: value,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
populatedProperties = getCombinedProperties(populatedProperties, docs);
|
||||
|
||||
setTableItems(docs);
|
||||
setRowCountInfo({
|
||||
rowCount: docs.length,
|
||||
rowCountRelation: ES_CLIENT_TOTAL_HITS_RELATION.EQ,
|
||||
});
|
||||
setPreviewMappingsProperties(populatedProperties);
|
||||
setStatus(INDEX_STATUS.LOADED);
|
||||
|
||||
if (docs.length === 0) {
|
||||
setNoDataMessage(
|
||||
i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', {
|
||||
defaultMessage:
|
||||
'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.',
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setNoDataMessage('');
|
||||
}
|
||||
}
|
||||
// custom comparison
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isError, isLoading, previewData]);
|
||||
|
||||
// To improve UI performance with a latest configuration for indices with a large number
|
||||
// of fields, we reduce the number of available columns to those populated with values.
|
||||
|
||||
// 1. Flatten the returned object structure object documents to match mapping properties
|
||||
const docs = resp.preview.map(getFlattenedObject);
|
||||
|
||||
// 2. Get all field names for each returned doc and flatten it
|
||||
// to a list of unique field names used across all docs.
|
||||
const populatedFields = [...new Set(docs.map(Object.keys).flat(1))];
|
||||
|
||||
// 3. Filter mapping properties by populated fields
|
||||
let populatedProperties: PreviewMappingsProperties = Object.entries(
|
||||
resp.generated_dest_index.mappings.properties
|
||||
)
|
||||
.filter(([key]) => populatedFields.includes(key))
|
||||
.reduce(
|
||||
(p, [key, value]) => ({
|
||||
...p,
|
||||
[key]: value,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
populatedProperties = getCombinedProperties(populatedProperties, docs);
|
||||
|
||||
setTableItems(docs);
|
||||
setRowCountInfo({
|
||||
rowCount: docs.length,
|
||||
rowCountRelation: ES_CLIENT_TOTAL_HITS_RELATION.EQ,
|
||||
});
|
||||
setPreviewMappingsProperties(populatedProperties);
|
||||
setStatus(INDEX_STATUS.LOADED);
|
||||
|
||||
if (docs.length === 0) {
|
||||
setNoDataMessage(
|
||||
i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', {
|
||||
defaultMessage:
|
||||
'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.',
|
||||
})
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!validationStatus.isValid) {
|
||||
setTableItems([]);
|
||||
setRowCountInfo({
|
||||
rowCount: 0,
|
||||
rowCountRelation: ES_CLIENT_TOTAL_HITS_RELATION.EQ,
|
||||
});
|
||||
setNoDataMessage(validationStatus.errorMessage!);
|
||||
}
|
||||
};
|
||||
// custom comparison
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [validationStatus.isValid]);
|
||||
|
||||
useEffect(() => {
|
||||
resetPagination();
|
||||
|
@ -232,15 +246,6 @@ export const useTransformConfigData = (
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(query)]);
|
||||
|
||||
useEffect(() => {
|
||||
getPreviewData();
|
||||
// custom comparison
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
}, [
|
||||
dataView.getIndexPattern(),
|
||||
JSON.stringify([requestPayload, query, combinedRuntimeMappings, timeRangeMs]),
|
||||
]);
|
||||
|
||||
if (sortingColumns.length > 0) {
|
||||
const sortingColumnsWithTypes = sortingColumns.map((c) => {
|
||||
// Since items might contain undefined/null values, we want to accurate find the data type
|
||||
|
@ -291,13 +296,7 @@ export const useTransformConfigData = (
|
|||
|
||||
return cellValue;
|
||||
};
|
||||
}, [
|
||||
pageData,
|
||||
pagination.pageIndex,
|
||||
pagination.pageSize,
|
||||
previewMappingsProperties,
|
||||
formatHumanReadableDateTimeSeconds,
|
||||
]);
|
||||
}, [pageData, pagination.pageIndex, pagination.pageSize, previewMappingsProperties]);
|
||||
|
||||
return {
|
||||
...dataGrid,
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import type {
|
||||
PostTransformsUpdateRequestSchema,
|
||||
PostTransformsUpdateResponseSchema,
|
||||
} from '../../../common/api_schemas/update_transforms';
|
||||
import { addInternalBasePath } from '../../../common/constants';
|
||||
import type { TransformId } from '../../../common/types/transform';
|
||||
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
|
||||
import { useRefreshTransformList } from './use_refresh_transform_list';
|
||||
|
||||
export const useUpdateTransform = (
|
||||
transformId: TransformId,
|
||||
transformConfig: PostTransformsUpdateRequestSchema
|
||||
) => {
|
||||
const { http } = useAppDependencies();
|
||||
const refreshTransformList = useRefreshTransformList();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
http.post<PostTransformsUpdateResponseSchema>(
|
||||
addInternalBasePath(`transforms/${transformId}/_update`),
|
||||
{
|
||||
body: JSON.stringify(transformConfig),
|
||||
version: '1',
|
||||
}
|
||||
),
|
||||
onSuccess: () => refreshTransformList(),
|
||||
});
|
||||
|
||||
return mutation.mutate;
|
||||
};
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
* 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, { createContext } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
|
||||
import type { Privileges } from '../../../../../common/types/privileges';
|
||||
|
||||
import {
|
||||
type PrivilegesAndCapabilities,
|
||||
type TransformCapabilities,
|
||||
INITIAL_CAPABILITIES,
|
||||
} from '../../../../../common/privilege/has_privilege_factory';
|
||||
|
||||
import { useAppDependencies } from '../../../app_dependencies';
|
||||
|
||||
interface Authorization {
|
||||
isLoading: boolean;
|
||||
apiError: Error | null;
|
||||
privileges: Privileges;
|
||||
capabilities: TransformCapabilities;
|
||||
}
|
||||
|
||||
const initialValue: Authorization = {
|
||||
isLoading: true,
|
||||
apiError: null,
|
||||
privileges: {
|
||||
hasAllPrivileges: false,
|
||||
missingPrivileges: {},
|
||||
},
|
||||
capabilities: INITIAL_CAPABILITIES,
|
||||
};
|
||||
|
||||
export const AuthorizationContext = createContext<Authorization>({ ...initialValue });
|
||||
|
||||
interface Props {
|
||||
privilegesEndpoint: { path: string; version: string };
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) => {
|
||||
const { http } = useAppDependencies();
|
||||
|
||||
const { path, version } = privilegesEndpoint;
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
data: privilegesData,
|
||||
} = useQuery<PrivilegesAndCapabilities, IHttpFetchError>(
|
||||
['transform-privileges-and-capabilities'],
|
||||
async ({ signal }) => {
|
||||
return await http.fetch<PrivilegesAndCapabilities>(path, {
|
||||
version,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const value = {
|
||||
isLoading,
|
||||
privileges:
|
||||
isLoading || privilegesData === undefined
|
||||
? { ...initialValue.privileges }
|
||||
: privilegesData.privileges,
|
||||
capabilities:
|
||||
isLoading || privilegesData === undefined
|
||||
? { ...INITIAL_CAPABILITIES }
|
||||
: privilegesData.capabilities,
|
||||
apiError: error ? error : null,
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthorizationContext.Provider value={{ ...value }}>{children}</AuthorizationContext.Provider>
|
||||
);
|
||||
};
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* 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 { createCapabilityFailureMessage } from '../../../../../common/privilege/has_privilege_factory';
|
||||
export { AuthorizationProvider, AuthorizationContext } from './authorization_provider';
|
||||
export { PrivilegesWrapper } from './with_privileges';
|
||||
export { NotAuthorizedSection } from './not_authorized_section';
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
* 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 { EuiPageTemplate } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
title: React.ReactNode;
|
||||
message: React.ReactNode | string;
|
||||
}
|
||||
|
||||
export const NotAuthorizedSection = ({ title, message }: Props) => (
|
||||
<EuiPageTemplate.EmptyPrompt
|
||||
color="danger"
|
||||
iconType="securityApp"
|
||||
title={<h2>{title}</h2>}
|
||||
body={<p>{message}</p>}
|
||||
/>
|
||||
);
|
|
@ -1,125 +0,0 @@
|
|||
/*
|
||||
* 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, { useContext, FC } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { MissingPrivileges } from '../../../../../common/types/privileges';
|
||||
import { SectionLoading } from '../../../components';
|
||||
import { AuthorizationContext } from './authorization_provider';
|
||||
import { NotAuthorizedSection } from './not_authorized_section';
|
||||
import {
|
||||
hasPrivilegeFactory,
|
||||
toArray,
|
||||
Privilege,
|
||||
} from '../../../../../common/privilege/has_privilege_factory';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Each required privilege must have the format "section.privilege".
|
||||
* To indicate that *all* privileges from a section are required, we can use the asterix
|
||||
* e.g. "index.*"
|
||||
*/
|
||||
privileges: string | string[];
|
||||
children: (childrenProps: {
|
||||
isLoading: boolean;
|
||||
hasPrivileges: boolean;
|
||||
privilegesMissing: MissingPrivileges;
|
||||
}) => JSX.Element;
|
||||
}
|
||||
|
||||
export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Props) => {
|
||||
const { isLoading, privileges } = useContext(AuthorizationContext);
|
||||
|
||||
const privilegesToArray: Privilege[] = toArray(requiredPrivileges).map((p) => {
|
||||
const [section, privilege] = p.split('.');
|
||||
if (!privilege) {
|
||||
// Oh! we forgot to use the dot "." notation.
|
||||
throw new Error('Required privilege must have the format "section.privilege"');
|
||||
}
|
||||
return [section, privilege];
|
||||
});
|
||||
|
||||
const hasPrivilege = hasPrivilegeFactory(privileges);
|
||||
const hasPrivileges = isLoading ? false : privilegesToArray.every(hasPrivilege);
|
||||
|
||||
const privilegesMissing = privilegesToArray.reduce((acc, [section, privilege]) => {
|
||||
if (privilege === '*') {
|
||||
acc[section] = privileges.missingPrivileges[section] || [];
|
||||
} else if (
|
||||
privileges.missingPrivileges[section] &&
|
||||
privileges.missingPrivileges[section]!.includes(privilege)
|
||||
) {
|
||||
const missing: string[] = acc[section] || [];
|
||||
acc[section] = [...missing, privilege];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as MissingPrivileges);
|
||||
|
||||
return children({ isLoading, hasPrivileges, privilegesMissing });
|
||||
};
|
||||
|
||||
interface MissingClusterPrivilegesProps {
|
||||
missingPrivileges: string;
|
||||
privilegesCount: number;
|
||||
}
|
||||
|
||||
const MissingClusterPrivileges: FC<MissingClusterPrivilegesProps> = ({
|
||||
missingPrivileges,
|
||||
privilegesCount,
|
||||
}) => (
|
||||
<NotAuthorizedSection
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.transform.app.deniedPrivilegeTitle"
|
||||
defaultMessage="You're missing cluster privileges"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id="xpack.transform.app.deniedPrivilegeDescription"
|
||||
defaultMessage="To use this section of Transforms, you must have {privilegesCount,
|
||||
plural, one {this cluster privilege} other {these cluster privileges}}: {missingPrivileges}."
|
||||
values={{
|
||||
missingPrivileges,
|
||||
privilegesCount,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export const PrivilegesWrapper: FC<{ privileges: string | string[] }> = ({
|
||||
children,
|
||||
privileges,
|
||||
}) => (
|
||||
<WithPrivileges privileges={privileges}>
|
||||
{({ isLoading, hasPrivileges, privilegesMissing }) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.transform.app.checkingPrivilegesDescription"
|
||||
defaultMessage="Checking privileges…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasPrivileges) {
|
||||
return (
|
||||
<MissingClusterPrivileges
|
||||
missingPrivileges={privilegesMissing.cluster!.join(', ')}
|
||||
privilegesCount={privilegesMissing.cluster!.length}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}}
|
||||
</WithPrivileges>
|
||||
);
|
|
@ -13,16 +13,15 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiButtonEmpty, EuiCallOut, EuiPageTemplate, EuiSpacer } from '@elastic/eui';
|
||||
import { isHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants';
|
||||
|
||||
import { TransformConfigUnion } from '../../../../common/types/transform';
|
||||
|
||||
import { useApi } from '../../hooks/use_api';
|
||||
import { useGetTransform } from '../../hooks';
|
||||
import { useDocumentationLinks } from '../../hooks/use_documentation_links';
|
||||
import { useSearchItems } from '../../hooks/use_search_items';
|
||||
|
||||
import { BREADCRUMB_SECTION, breadcrumbService, docTitleService } from '../../services/navigation';
|
||||
import { PrivilegesWrapper } from '../../lib/authorization';
|
||||
import { CapabilitiesWrapper } from '../../components/capabilities_wrapper';
|
||||
|
||||
import { Wizard } from '../create_transform/components/wizard';
|
||||
import { overrideTransformForCloning } from '../../common/transform';
|
||||
|
@ -39,8 +38,6 @@ export const CloneTransformSection: FC<Props> = ({ match, location }) => {
|
|||
docTitleService.setTitle('createTransform');
|
||||
}, []);
|
||||
|
||||
const api = useApi();
|
||||
|
||||
const { esTransform } = useDocumentationLinks();
|
||||
|
||||
const transformId = match.params.transformId;
|
||||
|
@ -50,52 +47,55 @@ export const CloneTransformSection: FC<Props> = ({ match, location }) => {
|
|||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const { error: searchItemsError, searchItems, setSavedObjectId } = useSearchItems(undefined);
|
||||
|
||||
const fetchTransformConfig = async () => {
|
||||
useEffect(() => {
|
||||
if (dataViewId === undefined) {
|
||||
setErrorMessage(
|
||||
i18n.translate('xpack.transform.clone.fetchErrorPromptText', {
|
||||
defaultMessage: 'Could not fetch the Kibana data view ID.',
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setSavedObjectId(dataViewId);
|
||||
}
|
||||
}, [dataViewId, setSavedObjectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchItemsError !== undefined) {
|
||||
setTransformConfig(undefined);
|
||||
setErrorMessage(searchItemsError);
|
||||
setIsInitialized(true);
|
||||
return;
|
||||
}
|
||||
}, [searchItemsError]);
|
||||
|
||||
const transformConfigs = await api.getTransform(transformId);
|
||||
if (isHttpFetchError(transformConfigs)) {
|
||||
setTransformConfig(undefined);
|
||||
setErrorMessage(transformConfigs.message);
|
||||
setIsInitialized(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (dataViewId === undefined) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.transform.clone.fetchErrorPromptText', {
|
||||
defaultMessage: 'Could not fetch the Kibana data view ID.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setSavedObjectId(dataViewId);
|
||||
|
||||
setTransformConfig(overrideTransformForCloning(transformConfigs.transforms[0]));
|
||||
setErrorMessage(undefined);
|
||||
setIsInitialized(true);
|
||||
} catch (e) {
|
||||
setTransformConfig(undefined);
|
||||
if (e.message !== undefined) {
|
||||
setErrorMessage(e.message);
|
||||
} else {
|
||||
setErrorMessage(JSON.stringify(e, null, 2));
|
||||
}
|
||||
setIsInitialized(true);
|
||||
}
|
||||
};
|
||||
const { data: transformConfigs, error } = useGetTransform(
|
||||
transformId,
|
||||
searchItemsError === undefined
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTransformConfig();
|
||||
// The effect should only be called once.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
if (error !== null && error.message !== errorMessage) {
|
||||
setTransformConfig(undefined);
|
||||
setErrorMessage(error.message);
|
||||
setIsInitialized(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (transformConfigs !== undefined) {
|
||||
try {
|
||||
setTransformConfig(overrideTransformForCloning(transformConfigs.transforms[0]));
|
||||
setErrorMessage(undefined);
|
||||
setIsInitialized(true);
|
||||
} catch (e) {
|
||||
setTransformConfig(undefined);
|
||||
if (e.message !== undefined) {
|
||||
setErrorMessage(e.message);
|
||||
} else {
|
||||
setErrorMessage(JSON.stringify(e, null, 2));
|
||||
}
|
||||
setIsInitialized(true);
|
||||
}
|
||||
}
|
||||
}, [error, errorMessage, transformConfigs]);
|
||||
|
||||
const docsLink = (
|
||||
<EuiButtonEmpty
|
||||
|
@ -112,7 +112,14 @@ export const CloneTransformSection: FC<Props> = ({ match, location }) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<PrivilegesWrapper privileges={APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES}>
|
||||
<CapabilitiesWrapper
|
||||
requiredCapabilities={[
|
||||
'canGetTransform',
|
||||
'canPreviewTransform',
|
||||
'canCreateTransform',
|
||||
'canStartStopTransform',
|
||||
]}
|
||||
>
|
||||
<EuiPageTemplate.Header
|
||||
pageTitle={
|
||||
<FormattedMessage
|
||||
|
@ -147,6 +154,6 @@ export const CloneTransformSection: FC<Props> = ({ match, location }) => {
|
|||
<Wizard cloneConfig={transformConfig} searchItems={searchItems} />
|
||||
)}
|
||||
</EuiPageTemplate.Section>
|
||||
</PrivilegesWrapper>
|
||||
</CapabilitiesWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSwitch } from '@elastic/eui';
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { EuiSwitch } from '@elastic/eui';
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { EuiSwitch } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SwitchModal } from './switch_modal';
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { EuiConfirmModal } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Transform: <AggLabelForm /> Date histogram aggregation 1`] = `
|
||||
<Fragment>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="transform__AggregationLabel--text"
|
||||
>
|
||||
<span
|
||||
className="eui-textTruncate"
|
||||
data-test-subj="transformAggregationEntryLabel"
|
||||
>
|
||||
the-group-by-agg-name
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--button"
|
||||
grow={false}
|
||||
>
|
||||
<EuiPopover
|
||||
anchorPosition="downCenter"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label="Edit aggregation"
|
||||
data-test-subj="transformAggregationEntryEditButton_the-group-by-agg-name"
|
||||
iconType="pencil"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
/>
|
||||
}
|
||||
closePopover={[Function]}
|
||||
display="inline-block"
|
||||
hasArrow={true}
|
||||
id="transformFormPopover"
|
||||
isOpen={false}
|
||||
ownFocus={true}
|
||||
panelPaddingSize="m"
|
||||
>
|
||||
<PopoverForm
|
||||
defaultData={
|
||||
Object {
|
||||
"agg": "cardinality",
|
||||
"aggName": "the-group-by-agg-name",
|
||||
"dropDownName": "the-group-by-drop-down-name",
|
||||
"field": "the-group-by-field",
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
options={Object {}}
|
||||
otherAggNames={Array []}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--button"
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label="Delete item"
|
||||
data-test-subj="transformAggregationEntryDeleteButton"
|
||||
iconType="cross"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
`;
|
|
@ -1,28 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Transform: <AggListForm /> Minimal initialization 1`] = `
|
||||
<Fragment>
|
||||
<EuiPanel
|
||||
data-test-subj="transformAggregationEntry_0"
|
||||
paddingSize="s"
|
||||
>
|
||||
<AggLabelForm
|
||||
deleteHandler={[Function]}
|
||||
item={
|
||||
Object {
|
||||
"agg": "avg",
|
||||
"aggName": "the-group-by-agg-name",
|
||||
"dropDownName": "the-group-by-drop-down-name",
|
||||
"field": "the-field",
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
options={Object {}}
|
||||
otherAggNames={Array []}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
|
@ -1,18 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Transform: <AggListSummary /> Minimal initialization 1`] = `
|
||||
<EuiForm>
|
||||
<EuiPanel
|
||||
paddingSize="s"
|
||||
>
|
||||
<div
|
||||
className="eui-textTruncate"
|
||||
>
|
||||
the-agg
|
||||
</div>
|
||||
</EuiPanel>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
</EuiForm>
|
||||
`;
|
|
@ -1,48 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Transform: Aggregation <PopoverForm /> Minimal initialization 1`] = `
|
||||
<EuiForm
|
||||
css={
|
||||
Object {
|
||||
"width": "300px",
|
||||
}
|
||||
}
|
||||
data-test-subj="transformAggPopoverForm_the-group-by-agg-name"
|
||||
>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
error={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
helpText=""
|
||||
isInvalid={false}
|
||||
label="Aggregation name"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="transformAggName"
|
||||
isInvalid={false}
|
||||
onChange={[Function]}
|
||||
value="the-group-by-agg-name"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={true}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="transformApplyAggChanges"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
>
|
||||
Apply
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
`;
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { AggName } from '../../../../../../common/types/aggregations';
|
||||
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
|
||||
|
@ -31,8 +31,8 @@ describe('Transform: <AggLabelForm />', () => {
|
|||
onChange() {},
|
||||
};
|
||||
|
||||
const wrapper = shallow(<AggLabelForm {...props} />);
|
||||
const { container } = render(<AggLabelForm {...props} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(container.textContent).toBe('the-group-by-agg-name');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
|
||||
|
||||
|
@ -29,8 +29,8 @@ describe('Transform: <AggListForm />', () => {
|
|||
onChange() {},
|
||||
};
|
||||
|
||||
const wrapper = shallow(<AggListForm {...props} />);
|
||||
const { container } = render(<AggListForm {...props} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(container.textContent).toBe('the-group-by-agg-name');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
|
||||
|
||||
|
@ -26,8 +26,8 @@ describe('Transform: <AggListSummary />', () => {
|
|||
list: { 'the-agg': item },
|
||||
};
|
||||
|
||||
const wrapper = shallow(<AggListSummary {...props} />);
|
||||
const { container } = render(<AggListSummary {...props} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(container.textContent).toBe('the-agg');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { AggName } from '../../../../../../common/types/aggregations';
|
||||
|
@ -29,7 +28,7 @@ describe('Transform: Aggregation <PopoverForm />', () => {
|
|||
const otherAggNames: AggName[] = [];
|
||||
const onChange = (item: PivotAggsConfig) => {};
|
||||
|
||||
const wrapper = shallow(
|
||||
const { getByTestId } = render(
|
||||
<PopoverForm
|
||||
defaultData={defaultData}
|
||||
otherAggNames={otherAggNames}
|
||||
|
@ -38,7 +37,8 @@ describe('Transform: Aggregation <PopoverForm />', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const input = getByTestId('transformAggName');
|
||||
expect(input).toHaveValue('the-group-by-agg-name');
|
||||
});
|
||||
|
||||
test('preserves the field for unsupported aggs', async () => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { EuiSwitch } from '@elastic/eui';
|
||||
|
||||
|
|
|
@ -1,234 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Transform: <GroupByLabelForm /> Date histogram aggregation 1`] = `
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--text"
|
||||
>
|
||||
<span
|
||||
className="eui-textTruncate"
|
||||
data-test-subj="transformGroupByEntryLabel"
|
||||
>
|
||||
the-group-by-agg-name
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--text transform__GroupByLabel--interval"
|
||||
grow={false}
|
||||
>
|
||||
<EuiTextColor
|
||||
className="eui-textTruncate"
|
||||
color="subdued"
|
||||
data-test-subj="transformGroupByEntryIntervalLabel"
|
||||
>
|
||||
1m
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--button"
|
||||
grow={false}
|
||||
>
|
||||
<EuiPopover
|
||||
anchorPosition="downCenter"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label="Edit interval"
|
||||
data-test-subj="transformGroupByEntryEditButton"
|
||||
iconType="pencil"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
/>
|
||||
}
|
||||
closePopover={[Function]}
|
||||
display="inline-block"
|
||||
hasArrow={true}
|
||||
id="transformIntervalFormPopover"
|
||||
isOpen={false}
|
||||
ownFocus={true}
|
||||
panelPaddingSize="m"
|
||||
>
|
||||
<PopoverForm
|
||||
defaultData={
|
||||
Object {
|
||||
"agg": "date_histogram",
|
||||
"aggName": "the-group-by-agg-name",
|
||||
"calendar_interval": "1m",
|
||||
"dropDownName": "the-group-by-drop-down-name",
|
||||
"field": "the-group-by-field",
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
options={Object {}}
|
||||
otherAggNames={Array []}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--button"
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label="Delete item"
|
||||
data-test-subj="transformGroupByEntryDeleteButton"
|
||||
iconType="cross"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
||||
|
||||
exports[`Transform: <GroupByLabelForm /> Histogram aggregation 1`] = `
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--text"
|
||||
>
|
||||
<span
|
||||
className="eui-textTruncate"
|
||||
data-test-subj="transformGroupByEntryLabel"
|
||||
>
|
||||
the-group-by-agg-name
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--text transform__GroupByLabel--interval"
|
||||
grow={false}
|
||||
>
|
||||
<EuiTextColor
|
||||
className="eui-textTruncate"
|
||||
color="subdued"
|
||||
data-test-subj="transformGroupByEntryIntervalLabel"
|
||||
>
|
||||
100
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--button"
|
||||
grow={false}
|
||||
>
|
||||
<EuiPopover
|
||||
anchorPosition="downCenter"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label="Edit interval"
|
||||
data-test-subj="transformGroupByEntryEditButton"
|
||||
iconType="pencil"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
/>
|
||||
}
|
||||
closePopover={[Function]}
|
||||
display="inline-block"
|
||||
hasArrow={true}
|
||||
id="transformIntervalFormPopover"
|
||||
isOpen={false}
|
||||
ownFocus={true}
|
||||
panelPaddingSize="m"
|
||||
>
|
||||
<PopoverForm
|
||||
defaultData={
|
||||
Object {
|
||||
"agg": "histogram",
|
||||
"aggName": "the-group-by-agg-name",
|
||||
"dropDownName": "the-group-by-drop-down-name",
|
||||
"field": "the-group-by-field",
|
||||
"interval": "100",
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
options={Object {}}
|
||||
otherAggNames={Array []}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--button"
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label="Delete item"
|
||||
data-test-subj="transformGroupByEntryDeleteButton"
|
||||
iconType="cross"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
||||
|
||||
exports[`Transform: <GroupByLabelForm /> Terms aggregation 1`] = `
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--text"
|
||||
>
|
||||
<span
|
||||
className="eui-textTruncate"
|
||||
data-test-subj="transformGroupByEntryLabel"
|
||||
>
|
||||
the-group-by-agg-name
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--button"
|
||||
grow={false}
|
||||
>
|
||||
<EuiPopover
|
||||
anchorPosition="downCenter"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label="Edit interval"
|
||||
data-test-subj="transformGroupByEntryEditButton"
|
||||
iconType="pencil"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
/>
|
||||
}
|
||||
closePopover={[Function]}
|
||||
display="inline-block"
|
||||
hasArrow={true}
|
||||
id="transformIntervalFormPopover"
|
||||
isOpen={false}
|
||||
ownFocus={true}
|
||||
panelPaddingSize="m"
|
||||
>
|
||||
<PopoverForm
|
||||
defaultData={
|
||||
Object {
|
||||
"agg": "terms",
|
||||
"aggName": "the-group-by-agg-name",
|
||||
"dropDownName": "the-group-by-drop-down-name",
|
||||
"field": "the-group-by-field",
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
options={Object {}}
|
||||
otherAggNames={Array []}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--button"
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label="Delete item"
|
||||
data-test-subj="transformGroupByEntryDeleteButton"
|
||||
iconType="cross"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
|
@ -1,77 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Transform: <GroupByLabelSummary /> Date histogram aggregation 1`] = `
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--text"
|
||||
>
|
||||
<span
|
||||
className="eui-textTruncate"
|
||||
>
|
||||
the-options-data-id
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--text transform__GroupByLabel--interval"
|
||||
grow={false}
|
||||
>
|
||||
<EuiTextColor
|
||||
className="eui-textTruncate"
|
||||
color="subdued"
|
||||
>
|
||||
1m
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
||||
|
||||
exports[`Transform: <GroupByLabelSummary /> Histogram aggregation 1`] = `
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--text"
|
||||
>
|
||||
<span
|
||||
className="eui-textTruncate"
|
||||
>
|
||||
the-options-data-id
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--text transform__GroupByLabel--interval"
|
||||
grow={false}
|
||||
>
|
||||
<EuiTextColor
|
||||
className="eui-textTruncate"
|
||||
color="subdued"
|
||||
>
|
||||
100
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
||||
|
||||
exports[`Transform: <GroupByLabelSummary /> Terms aggregation 1`] = `
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="transform__GroupByLabel--text"
|
||||
>
|
||||
<span
|
||||
className="eui-textTruncate"
|
||||
>
|
||||
the-options-data-id
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
|
@ -1,28 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Transform: <GroupByListForm /> Minimal initialization 1`] = `
|
||||
<Fragment>
|
||||
<EuiPanel
|
||||
data-test-subj="transformGroupByEntry 0"
|
||||
paddingSize="s"
|
||||
>
|
||||
<GroupByLabelForm
|
||||
deleteHandler={[Function]}
|
||||
item={
|
||||
Object {
|
||||
"agg": "terms",
|
||||
"aggName": "the-group-by-agg-name",
|
||||
"dropDownName": "the-group-by-drop-down-name",
|
||||
"field": "the-group-by-field",
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
options={Object {}}
|
||||
otherAggNames={Array []}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
|
@ -1,24 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Transform: <GroupByListSummary /> Minimal initialization 1`] = `
|
||||
<Fragment>
|
||||
<EuiPanel
|
||||
paddingSize="s"
|
||||
>
|
||||
<GroupByLabelSummary
|
||||
item={
|
||||
Object {
|
||||
"agg": "terms",
|
||||
"aggName": "the-group-by-agg-name",
|
||||
"dropDownName": "the-group-by-drop-down-name",
|
||||
"field": "the-group-by-field",
|
||||
}
|
||||
}
|
||||
optionsDataId="the-options-data-id"
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
|
@ -1,18 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Transform: Group By <PopoverForm /> Minimal initialization 1`] = `
|
||||
<PopoverForm
|
||||
defaultData={
|
||||
Object {
|
||||
"agg": "date_histogram",
|
||||
"aggName": "the-agg-name",
|
||||
"calendar_interval": "1m",
|
||||
"dropDownName": "the-drop-down-name",
|
||||
"field": "the-field",
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
options={Object {}}
|
||||
otherAggNames={Array []}
|
||||
/>
|
||||
`;
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common';
|
||||
|
||||
|
@ -29,9 +29,9 @@ describe('Transform: <GroupByLabelForm />', () => {
|
|||
onChange() {},
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GroupByLabelForm {...props} />);
|
||||
const { container } = render(<GroupByLabelForm {...props} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(container.textContent).toContain('the-group-by-agg-name');
|
||||
});
|
||||
|
||||
test('Histogram aggregation', () => {
|
||||
|
@ -50,9 +50,9 @@ describe('Transform: <GroupByLabelForm />', () => {
|
|||
onChange() {},
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GroupByLabelForm {...props} />);
|
||||
const { container } = render(<GroupByLabelForm {...props} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(container.textContent).toContain('the-group-by-agg-name');
|
||||
});
|
||||
|
||||
test('Terms aggregation', () => {
|
||||
|
@ -70,8 +70,8 @@ describe('Transform: <GroupByLabelForm />', () => {
|
|||
onChange() {},
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GroupByLabelForm {...props} />);
|
||||
const { container } = render(<GroupByLabelForm {...props} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(container.textContent).toContain('the-group-by-agg-name');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common';
|
||||
|
||||
|
@ -26,9 +26,9 @@ describe('Transform: <GroupByLabelSummary />', () => {
|
|||
optionsDataId: 'the-options-data-id',
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GroupByLabelSummary {...props} />);
|
||||
const { container } = render(<GroupByLabelSummary {...props} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(container.textContent).toContain('the-options-data-id');
|
||||
});
|
||||
|
||||
test('Histogram aggregation', () => {
|
||||
|
@ -44,9 +44,9 @@ describe('Transform: <GroupByLabelSummary />', () => {
|
|||
optionsDataId: 'the-options-data-id',
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GroupByLabelSummary {...props} />);
|
||||
const { container } = render(<GroupByLabelSummary {...props} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(container.textContent).toContain('the-options-data-id');
|
||||
});
|
||||
|
||||
test('Terms aggregation', () => {
|
||||
|
@ -61,8 +61,8 @@ describe('Transform: <GroupByLabelSummary />', () => {
|
|||
optionsDataId: 'the-options-data-id',
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GroupByLabelSummary {...props} />);
|
||||
const { container } = render(<GroupByLabelSummary {...props} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(container.textContent).toContain('the-options-data-id');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common';
|
||||
|
||||
|
@ -27,8 +27,8 @@ describe('Transform: <GroupByListForm />', () => {
|
|||
onChange() {},
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GroupByListForm {...props} />);
|
||||
const { container } = render(<GroupByListForm {...props} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(container.textContent).toContain('the-group-by-agg-name');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common';
|
||||
|
||||
|
@ -24,8 +24,8 @@ describe('Transform: <GroupByListSummary />', () => {
|
|||
list: { 'the-options-data-id': item },
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GroupByListSummary {...props} />);
|
||||
const { container } = render(<GroupByListSummary {...props} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(container.textContent).toContain('the-options-data-id');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
|
@ -101,7 +101,7 @@ describe('Transform: Group By <PopoverForm />', () => {
|
|||
appName: 'the-test-app',
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
const { getByDisplayValue } = render(
|
||||
<KibanaContextProvider services={services}>
|
||||
<PopoverForm
|
||||
defaultData={defaultData}
|
||||
|
@ -112,6 +112,7 @@ describe('Transform: Group By <PopoverForm />', () => {
|
|||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find(PopoverForm)).toMatchSnapshot();
|
||||
expect(getByDisplayValue('the-agg-name')).toBeInTheDocument();
|
||||
expect(getByDisplayValue('1m')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { EuiCode, EuiInputPopover } from '@elastic/eui';
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { StepCreateForm, StepCreateFormProps } from './step_create_form';
|
||||
|
||||
|
@ -16,6 +17,7 @@ jest.mock('../../../../app_dependencies');
|
|||
describe('Transform: <StepCreateForm />', () => {
|
||||
test('Minimal initialization', () => {
|
||||
// Arrange
|
||||
const queryClient = new QueryClient();
|
||||
const props: StepCreateFormProps = {
|
||||
createDataView: false,
|
||||
transformId: 'the-transform-id',
|
||||
|
@ -35,7 +37,11 @@ describe('Transform: <StepCreateForm />', () => {
|
|||
onChange() {},
|
||||
};
|
||||
|
||||
const { getByText } = render(<StepCreateForm {...props} />);
|
||||
const { getByText } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StepCreateForm {...props} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
// Act
|
||||
// Assert
|
||||
|
|
|
@ -24,25 +24,19 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
|
||||
import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common';
|
||||
|
||||
import { DuplicateDataViewError } from '@kbn/data-plugin/public';
|
||||
import type { RuntimeField } from '@kbn/data-views-plugin/common';
|
||||
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
||||
import type { PutTransformsResponseSchema } from '../../../../../../common/api_schemas/transforms';
|
||||
import {
|
||||
isGetTransformsStatsResponseSchema,
|
||||
isPutTransformsResponseSchema,
|
||||
isStartTransformsResponseSchema,
|
||||
} from '../../../../../../common/api_schemas/type_guards';
|
||||
import { PROGRESS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants';
|
||||
|
||||
import { getErrorMessage } from '../../../../../../common/utils/errors';
|
||||
|
||||
import { getTransformProgress } from '../../../../common';
|
||||
import { useApi } from '../../../../hooks/use_api';
|
||||
import { useCreateTransform, useGetTransformStats, useStartTransforms } from '../../../../hooks';
|
||||
import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies';
|
||||
import { RedirectToTransformManagement } from '../../../../common/navigation';
|
||||
import { ToastNotificationText } from '../../../../components';
|
||||
|
@ -92,11 +86,10 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
|||
);
|
||||
const [discoverLink, setDiscoverLink] = useState<string>();
|
||||
|
||||
const deps = useAppDependencies();
|
||||
const { share } = deps;
|
||||
const dataViews = deps.data.dataViews;
|
||||
const toastNotifications = useToastNotifications();
|
||||
const isDiscoverAvailable = deps.application.capabilities.discover?.show ?? false;
|
||||
const { application, data, i18n: i18nStart, share, theme } = useAppDependencies();
|
||||
const dataViews = data.dataViews;
|
||||
const isDiscoverAvailable = application.capabilities.discover?.show ?? false;
|
||||
|
||||
useEffect(() => {
|
||||
let unmounted = false;
|
||||
|
@ -128,104 +121,38 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [created, started, dataViewId]);
|
||||
|
||||
const { overlays, theme } = useAppDependencies();
|
||||
const api = useApi();
|
||||
const startTransforms = useStartTransforms();
|
||||
const createTransform = useCreateTransform();
|
||||
|
||||
async function createTransform() {
|
||||
function createTransformHandler(startAfterCreation = false) {
|
||||
setLoading(true);
|
||||
|
||||
const resp = await api.createTransform(transformId, transformConfig);
|
||||
|
||||
if (!isPutTransformsResponseSchema(resp) || resp.errors.length > 0) {
|
||||
let respErrors:
|
||||
| PutTransformsResponseSchema['errors']
|
||||
| PutTransformsResponseSchema['errors'][number]
|
||||
| undefined;
|
||||
|
||||
if (isPutTransformsResponseSchema(resp) && resp.errors.length > 0) {
|
||||
respErrors = resp.errors.length === 1 ? resp.errors[0] : resp.errors;
|
||||
createTransform(
|
||||
{ transformId, transformConfig },
|
||||
{
|
||||
onError: () => setCreated(false),
|
||||
onSuccess: () => {
|
||||
setCreated(true);
|
||||
if (createDataView) {
|
||||
createKibanaDataView();
|
||||
}
|
||||
if (startAfterCreation) {
|
||||
startTransform();
|
||||
}
|
||||
},
|
||||
onSettled: () => setLoading(false),
|
||||
}
|
||||
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.stepCreateForm.createTransformErrorMessage', {
|
||||
defaultMessage: 'An error occurred creating the transform {transformId}:',
|
||||
values: { transformId },
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={getErrorMessage(isPutTransformsResponseSchema(resp) ? respErrors : resp)}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
setCreated(false);
|
||||
setLoading(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.transform.stepCreateForm.createTransformSuccessMessage', {
|
||||
defaultMessage: 'Request to create transform {transformId} acknowledged.',
|
||||
values: { transformId },
|
||||
})
|
||||
);
|
||||
setCreated(true);
|
||||
setLoading(false);
|
||||
|
||||
if (createDataView) {
|
||||
createKibanaDataView();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function startTransform() {
|
||||
function startTransform() {
|
||||
setLoading(true);
|
||||
|
||||
const resp = await api.startTransforms([{ id: transformId }]);
|
||||
|
||||
if (isStartTransformsResponseSchema(resp) && resp[transformId]?.success === true) {
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.transform.stepCreateForm.startTransformSuccessMessage', {
|
||||
defaultMessage: 'Request to start transform {transformId} acknowledged.',
|
||||
values: { transformId },
|
||||
})
|
||||
);
|
||||
setStarted(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const errorMessage =
|
||||
isStartTransformsResponseSchema(resp) && resp[transformId]?.success === false
|
||||
? resp[transformId].error
|
||||
: resp;
|
||||
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.stepCreateForm.startTransformErrorMessage', {
|
||||
defaultMessage: 'An error occurred starting the transform {transformId}:',
|
||||
values: { transformId },
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={getErrorMessage(errorMessage)}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
startTransforms([{ id: transformId }], {
|
||||
onError: () => setStarted(false),
|
||||
onSuccess: (resp) => setStarted(resp[transformId]?.success === true),
|
||||
onSettled: () => setLoading(false),
|
||||
});
|
||||
setStarted(false);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function createAndStartTransform() {
|
||||
const acknowledged = await createTransform();
|
||||
if (acknowledged) {
|
||||
await startTransform();
|
||||
}
|
||||
}
|
||||
|
||||
const createKibanaDataView = async () => {
|
||||
|
@ -250,13 +177,6 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
|||
true
|
||||
);
|
||||
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.transform.stepCreateForm.createDataViewSuccessMessage', {
|
||||
defaultMessage: 'Kibana data view {dataViewName} created successfully.',
|
||||
values: { dataViewName },
|
||||
})
|
||||
);
|
||||
|
||||
setDataViewId(newDataView.id);
|
||||
setLoading(false);
|
||||
return true;
|
||||
|
@ -275,10 +195,10 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
|||
defaultMessage: 'An error occurred creating the Kibana data view {dataViewName}:',
|
||||
values: { dataViewName },
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText overlays={overlays} theme={theme} text={getErrorMessage(e)} />,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
text: toMountPoint(<ToastNotificationText text={getErrorMessage(e)} />, {
|
||||
theme,
|
||||
i18n: i18nStart,
|
||||
}),
|
||||
});
|
||||
setLoading(false);
|
||||
return false;
|
||||
|
@ -288,57 +208,59 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
|||
|
||||
const isBatchTransform = typeof transformConfig.sync === 'undefined';
|
||||
|
||||
if (
|
||||
loading === false &&
|
||||
started === true &&
|
||||
progressPercentComplete === undefined &&
|
||||
isBatchTransform
|
||||
) {
|
||||
// wrapping in function so we can keep the interval id in local scope
|
||||
function startProgressBar() {
|
||||
const interval = setInterval(async () => {
|
||||
const stats = await api.getTransformStats(transformId);
|
||||
|
||||
if (
|
||||
isGetTransformsStatsResponseSchema(stats) &&
|
||||
Array.isArray(stats.transforms) &&
|
||||
stats.transforms.length > 0
|
||||
) {
|
||||
const percent =
|
||||
getTransformProgress({
|
||||
id: transformId,
|
||||
config: {
|
||||
...transformConfig,
|
||||
id: transformId,
|
||||
},
|
||||
stats: stats.transforms[0],
|
||||
}) || 0;
|
||||
setProgressPercentComplete(percent);
|
||||
if (percent >= 100) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
} else {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.stepCreateForm.progressErrorMessage', {
|
||||
defaultMessage: 'An error occurred getting the progress percentage:',
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={getErrorMessage(stats)}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, PROGRESS_REFRESH_INTERVAL_MS);
|
||||
useEffect(() => {
|
||||
if (
|
||||
loading === false &&
|
||||
started === true &&
|
||||
progressPercentComplete === undefined &&
|
||||
isBatchTransform
|
||||
) {
|
||||
setProgressPercentComplete(0);
|
||||
}
|
||||
}, [loading, started, progressPercentComplete, isBatchTransform]);
|
||||
|
||||
startProgressBar();
|
||||
}
|
||||
const progressBarRefetchEnabled =
|
||||
isBatchTransform &&
|
||||
typeof progressPercentComplete === 'number' &&
|
||||
progressPercentComplete < 100;
|
||||
const progressBarRefetchInterval = progressBarRefetchEnabled
|
||||
? PROGRESS_REFRESH_INTERVAL_MS
|
||||
: false;
|
||||
|
||||
const { data: stats } = useGetTransformStats(
|
||||
transformId,
|
||||
progressBarRefetchEnabled,
|
||||
progressBarRefetchInterval
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (stats === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stats && Array.isArray(stats.transforms) && stats.transforms.length > 0) {
|
||||
const percent =
|
||||
getTransformProgress({
|
||||
id: transformId,
|
||||
config: {
|
||||
...transformConfig,
|
||||
id: transformId,
|
||||
},
|
||||
stats: stats.transforms[0],
|
||||
}) || 0;
|
||||
setProgressPercentComplete(percent);
|
||||
} else {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.stepCreateForm.progressErrorMessage', {
|
||||
defaultMessage: 'An error occurred getting the progress percentage:',
|
||||
}),
|
||||
text: toMountPoint(<ToastNotificationText text={getErrorMessage(stats)} />, {
|
||||
theme,
|
||||
i18n: i18nStart,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}, [i18nStart, stats, theme, toastNotifications, transformConfig, transformId]);
|
||||
|
||||
function getTransformConfigDevConsoleStatement() {
|
||||
return `PUT _transform/${transformId}\n${JSON.stringify(transformConfig, null, 2)}\n\n`;
|
||||
|
@ -362,7 +284,7 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
|||
<EuiButton
|
||||
fill
|
||||
isDisabled={loading || (created && started)}
|
||||
onClick={createAndStartTransform}
|
||||
onClick={() => createTransformHandler(true)}
|
||||
data-test-subj="transformWizardCreateAndStartButton"
|
||||
>
|
||||
{i18n.translate('xpack.transform.stepCreateForm.createAndStartTransformButton', {
|
||||
|
@ -436,7 +358,7 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
|||
<EuiFlexItem grow={false} style={FLEX_ITEM_STYLE}>
|
||||
<EuiButton
|
||||
isDisabled={loading || created}
|
||||
onClick={createTransform}
|
||||
onClick={() => createTransformHandler()}
|
||||
data-test-subj="transformWizardCreateButton"
|
||||
>
|
||||
{i18n.translate('xpack.transform.stepCreateForm.createTransformButton', {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
export const StepCreateSummary: FC = React.memo(() => {
|
||||
return null;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import useUpdateEffect from 'react-use/lib/useUpdateEffect';
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
@ -18,7 +18,6 @@ import { i18n } from '@kbn/i18n';
|
|||
import { isMultiBucketAggregate } from '@kbn/ml-agg-utils';
|
||||
|
||||
import { useDataSearch } from '../../../../../../../hooks/use_data_search';
|
||||
import { isEsSearchResponseWithAggregations } from '../../../../../../../../../common/api_schemas/type_guards';
|
||||
import { CreateTransformWizardContext } from '../../../../wizard/wizard';
|
||||
import { useToastNotifications } from '../../../../../../../app_dependencies';
|
||||
|
||||
|
@ -33,16 +32,22 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm
|
|||
selectedField,
|
||||
}) => {
|
||||
const { dataView, runtimeMappings } = useContext(CreateTransformWizardContext);
|
||||
const dataSearch = useDataSearch();
|
||||
const toastNotifications = useToastNotifications();
|
||||
|
||||
const [options, setOptions] = useState<EuiComboBoxOptionOption[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const debouncedOnSearchChange = useMemo(
|
||||
() => debounce((d: string) => setSearchValue(d), 600),
|
||||
[]
|
||||
);
|
||||
|
||||
const onSearchChange = (newSearchValue: string) => {
|
||||
setSearchValue(newSearchValue);
|
||||
};
|
||||
useEffect(() => {
|
||||
// Simulate initial load.
|
||||
debouncedOnSearchChange('');
|
||||
// Cancel debouncing when unmounting
|
||||
return () => debouncedOnSearchChange.cancel();
|
||||
// Only call on mount
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
}, []);
|
||||
|
||||
const updateConfig = useCallback(
|
||||
(update) => {
|
||||
|
@ -56,80 +61,53 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm
|
|||
[config, onChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
const fetchOptions = debounce(async () => {
|
||||
if (selectedField === undefined) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setOptions([]);
|
||||
|
||||
const esSearchRequest = {
|
||||
index: dataView!.title,
|
||||
body: {
|
||||
...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}),
|
||||
query: {
|
||||
wildcard: {
|
||||
[selectedField!]: {
|
||||
value: `*${searchValue}*`,
|
||||
},
|
||||
const { data, isError, isLoading } = useDataSearch(
|
||||
{
|
||||
index: dataView!.title,
|
||||
body: {
|
||||
...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}),
|
||||
query: {
|
||||
wildcard: {
|
||||
[selectedField!]: {
|
||||
value: `*${searchValue}*`,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
field_values: {
|
||||
terms: {
|
||||
field: selectedField,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
},
|
||||
};
|
||||
aggs: {
|
||||
field_values: {
|
||||
terms: {
|
||||
field: selectedField,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
},
|
||||
},
|
||||
// Check whether fetching should be enabled
|
||||
selectedField !== undefined
|
||||
);
|
||||
|
||||
const response = await dataSearch(esSearchRequest, abortController.signal);
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
toastNotifications.addWarning(
|
||||
i18n.translate('xpack.transform.agg.popoverForm.filerAgg.term.errorFetchSuggestions', {
|
||||
defaultMessage: 'Unable to fetch suggestions',
|
||||
})
|
||||
);
|
||||
}
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
}, [isError]);
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
if (
|
||||
!(
|
||||
isEsSearchResponseWithAggregations(response) &&
|
||||
isMultiBucketAggregate<estypes.AggregationsSignificantLongTermsBucket>(
|
||||
response.aggregations.field_values
|
||||
)
|
||||
)
|
||||
) {
|
||||
toastNotifications.addWarning(
|
||||
i18n.translate('xpack.transform.agg.popoverForm.filerAgg.term.errorFetchSuggestions', {
|
||||
defaultMessage: 'Unable to fetch suggestions',
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setOptions(
|
||||
(
|
||||
response.aggregations.field_values
|
||||
const options: EuiComboBoxOptionOption[] =
|
||||
isMultiBucketAggregate<estypes.AggregationsSignificantLongTermsBucket>(
|
||||
data?.aggregations?.field_values
|
||||
)
|
||||
? (
|
||||
data?.aggregations?.field_values
|
||||
.buckets as estypes.AggregationsSignificantLongTermsBucket[]
|
||||
).map((value) => ({ label: value.key + '' }))
|
||||
);
|
||||
}, 600);
|
||||
|
||||
fetchOptions();
|
||||
|
||||
return () => {
|
||||
// make sure the ongoing request is canceled
|
||||
fetchOptions.cancel();
|
||||
abortController.abort();
|
||||
};
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
}, [selectedField]);
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate initial load.
|
||||
onSearchChange('');
|
||||
}, []);
|
||||
: [];
|
||||
|
||||
useUpdateEffect(() => {
|
||||
// Reset value control on field change
|
||||
|
@ -168,7 +146,7 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm
|
|||
onCreateOption={(value) => {
|
||||
updateConfig({ value });
|
||||
}}
|
||||
onSearchChange={onSearchChange}
|
||||
onSearchChange={debouncedOnSearchChange}
|
||||
data-test-subj="transformFilterTermValueSelector"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiButtonIcon, EuiCallOut, EuiComboBox, EuiCopy, EuiFormRow } from '@elastic/eui';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker';
|
||||
|
@ -66,6 +67,7 @@ const createMockStorage = () => ({
|
|||
describe('Transform: <DefinePivotForm />', () => {
|
||||
test('Minimal initialization', async () => {
|
||||
// Arrange
|
||||
const queryClient = new QueryClient();
|
||||
const mlSharedImports = await getMlSharedImports();
|
||||
|
||||
const searchItems = {
|
||||
|
@ -87,13 +89,15 @@ describe('Transform: <DefinePivotForm />', () => {
|
|||
|
||||
const { getByText } = render(
|
||||
<I18nProvider>
|
||||
<KibanaContextProvider services={services}>
|
||||
<MlSharedContext.Provider value={mlSharedImports}>
|
||||
<DatePickerContextProvider {...getMockedDatePickerDependencies()}>
|
||||
<StepDefineForm onChange={mockOnChange} searchItems={searchItems as SearchItems} />
|
||||
</DatePickerContextProvider>
|
||||
</MlSharedContext.Provider>
|
||||
</KibanaContextProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<KibanaContextProvider services={services}>
|
||||
<MlSharedContext.Provider value={mlSharedImports}>
|
||||
<DatePickerContextProvider {...getMockedDatePickerDependencies()}>
|
||||
<StepDefineForm onChange={mockOnChange} searchItems={searchItems as SearchItems} />
|
||||
</DatePickerContextProvider>
|
||||
</MlSharedContext.Provider>
|
||||
</KibanaContextProvider>
|
||||
</QueryClientProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
|
|
|
@ -78,6 +78,9 @@ const ALLOW_TIME_RANGE_ON_TRANSFORM_CONFIG = false;
|
|||
|
||||
const advancedEditorsSidebarWidth = '220px';
|
||||
|
||||
type PopulatedFields = Set<string>;
|
||||
const isPopulatedFields = (arg: unknown): arg is PopulatedFields => arg instanceof Set;
|
||||
|
||||
export const ConfigSectionTitle: FC<{ title: string }> = ({ title }) => (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
|
@ -132,7 +135,9 @@ export const StepDefineForm: FC<StepDefineFormProps> = React.memo((props) => {
|
|||
transformConfigQuery,
|
||||
runtimeMappings,
|
||||
timeRangeMs,
|
||||
fieldStatsContext?.populatedFields ?? null
|
||||
isPopulatedFields(fieldStatsContext?.populatedFields)
|
||||
? [...fieldStatsContext.populatedFields]
|
||||
: []
|
||||
),
|
||||
dataTestSubj: 'transformIndexPreview',
|
||||
toastNotifications,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
|
||||
|
||||
|
@ -30,6 +31,7 @@ describe('Transform: <DefinePivotSummary />', () => {
|
|||
// Using the async/await wait()/done() pattern to avoid act() errors.
|
||||
test('Minimal initialization', async () => {
|
||||
// Arrange
|
||||
const queryClient = new QueryClient();
|
||||
const mlSharedImports = await getMlSharedImports();
|
||||
|
||||
const searchItems = {
|
||||
|
@ -78,9 +80,11 @@ describe('Transform: <DefinePivotSummary />', () => {
|
|||
};
|
||||
|
||||
const { queryByText } = render(
|
||||
<MlSharedContext.Provider value={mlSharedImports}>
|
||||
<StepDefineSummary formState={formState} searchItems={searchItems as SearchItems} />
|
||||
</MlSharedContext.Provider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MlSharedContext.Provider value={mlSharedImports}>
|
||||
<StepDefineSummary formState={formState} searchItems={searchItems as SearchItems} />
|
||||
</MlSharedContext.Provider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
// Act
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui';
|
||||
import { TRANSFORM_FUNCTION, TransformFunction } from '../../../../../../common/constants';
|
||||
|
|
|
@ -25,15 +25,9 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { KBN_FIELD_TYPES } from '@kbn/field-types';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
|
||||
import { isHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { retentionPolicyMaxAgeInvalidErrorMessage } from '../../../../common/constants/validation_messages';
|
||||
import {
|
||||
isEsIndices,
|
||||
isEsIngestPipelines,
|
||||
isPostTransformsPreviewResponseSchema,
|
||||
} from '../../../../../../common/api_schemas/type_guards';
|
||||
import { DEFAULT_TRANSFORM_FREQUENCY } from '../../../../../../common/constants';
|
||||
import { TransformId } from '../../../../../../common/types/transform';
|
||||
import { isValidIndexName } from '../../../../../../common/utils/es_utils';
|
||||
|
@ -42,16 +36,22 @@ import { getErrorMessage } from '../../../../../../common/utils/errors';
|
|||
|
||||
import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies';
|
||||
import { ToastNotificationText } from '../../../../components';
|
||||
import { useDocumentationLinks } from '../../../../hooks/use_documentation_links';
|
||||
import {
|
||||
useDocumentationLinks,
|
||||
useGetDataViewTitles,
|
||||
useGetEsIndices,
|
||||
useGetEsIngestPipelines,
|
||||
useGetTransforms,
|
||||
useGetTransformsPreview,
|
||||
} from '../../../../hooks';
|
||||
import { SearchItems } from '../../../../hooks/use_search_items';
|
||||
import { useApi } from '../../../../hooks/use_api';
|
||||
import { StepDetailsTimeField } from './step_details_time_field';
|
||||
import {
|
||||
getTransformConfigQuery,
|
||||
getPreviewTransformRequestBody,
|
||||
isTransformIdValid,
|
||||
} from '../../../../common';
|
||||
import { EsIndexName, DataViewTitle } from './common';
|
||||
import { EsIndexName } from './common';
|
||||
import {
|
||||
continuousModeDelayValidator,
|
||||
integerRangeMinus1To100Validator,
|
||||
|
@ -73,8 +73,8 @@ interface StepDetailsFormProps {
|
|||
|
||||
export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
|
||||
({ overrides = {}, onChange, searchItems, stepDefineState }) => {
|
||||
const deps = useAppDependencies();
|
||||
const { capabilities } = deps.application;
|
||||
const { application, i18n: i18nStart, theme } = useAppDependencies();
|
||||
const { capabilities } = application;
|
||||
const toastNotifications = useToastNotifications();
|
||||
const { esIndicesCreateIndex } = useDocumentationLinks();
|
||||
|
||||
|
@ -90,19 +90,15 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
|
|||
const [destinationIngestPipeline, setDestinationIngestPipeline] = useState<string>(
|
||||
defaults.destinationIngestPipeline
|
||||
);
|
||||
const [transformIds, setTransformIds] = useState<TransformId[]>([]);
|
||||
const [indexNames, setIndexNames] = useState<EsIndexName[]>([]);
|
||||
const [ingestPipelineNames, setIngestPipelineNames] = useState<string[]>([]);
|
||||
|
||||
const canCreateDataView = useMemo(
|
||||
() =>
|
||||
capabilities.savedObjectsManagement.edit === true ||
|
||||
capabilities.indexPatterns.save === true,
|
||||
capabilities.savedObjectsManagement?.edit === true ||
|
||||
capabilities.indexPatterns?.save === true,
|
||||
[capabilities]
|
||||
);
|
||||
|
||||
// Index pattern state
|
||||
const [dataViewTitles, setDataViewTitles] = useState<DataViewTitle[]>([]);
|
||||
const [createDataView, setCreateDataView] = useState(
|
||||
canCreateDataView === false ? false : defaults.createDataView
|
||||
);
|
||||
|
@ -125,126 +121,122 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
|
|||
[setDataViewTimeField, dataViewAvailableTimeFields]
|
||||
);
|
||||
|
||||
const { overlays, theme } = useAppDependencies();
|
||||
const api = useApi();
|
||||
const {
|
||||
error: transformsError,
|
||||
data: { transformIds },
|
||||
} = useGetTransforms();
|
||||
|
||||
// fetch existing transform IDs and indices once for form validation
|
||||
useEffect(() => {
|
||||
// use an IIFE to avoid returning a Promise to useEffect.
|
||||
(async function () {
|
||||
const { searchQuery, previewRequest: partialPreviewRequest } = stepDefineState;
|
||||
const transformConfigQuery = getTransformConfigQuery(searchQuery);
|
||||
const previewRequest = getPreviewTransformRequestBody(
|
||||
searchItems.dataView,
|
||||
transformConfigQuery,
|
||||
partialPreviewRequest,
|
||||
stepDefineState.runtimeMappings
|
||||
if (transformsError !== null) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformList', {
|
||||
defaultMessage: 'An error occurred getting the existing transform IDs:',
|
||||
}),
|
||||
text: toMountPoint(<ToastNotificationText text={getErrorMessage(transformsError)} />, {
|
||||
theme,
|
||||
i18n: i18nStart,
|
||||
}),
|
||||
});
|
||||
}
|
||||
// custom comparison
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [transformsError]);
|
||||
|
||||
const previewRequest = useMemo(() => {
|
||||
const { searchQuery, previewRequest: partialPreviewRequest } = stepDefineState;
|
||||
const transformConfigQuery = getTransformConfigQuery(searchQuery);
|
||||
return getPreviewTransformRequestBody(
|
||||
searchItems.dataView,
|
||||
transformConfigQuery,
|
||||
partialPreviewRequest,
|
||||
stepDefineState.runtimeMappings
|
||||
);
|
||||
}, [searchItems.dataView, stepDefineState]);
|
||||
const { error: transformsPreviewError, data: transformPreview } =
|
||||
useGetTransformsPreview(previewRequest);
|
||||
|
||||
useEffect(() => {
|
||||
if (transformPreview) {
|
||||
const properties = transformPreview.generated_dest_index.mappings.properties;
|
||||
const timeFields: string[] = Object.keys(properties).filter(
|
||||
(col) => properties[col].type === 'date'
|
||||
);
|
||||
|
||||
const transformPreview = await api.getTransformsPreview(previewRequest);
|
||||
setDataViewAvailableTimeFields(timeFields);
|
||||
setDataViewTimeField(timeFields[0]);
|
||||
}
|
||||
}, [transformPreview]);
|
||||
|
||||
if (isPostTransformsPreviewResponseSchema(transformPreview)) {
|
||||
const properties = transformPreview.generated_dest_index.mappings.properties;
|
||||
const timeFields: string[] = Object.keys(properties).filter(
|
||||
(col) => properties[col].type === 'date'
|
||||
);
|
||||
|
||||
setDataViewAvailableTimeFields(timeFields);
|
||||
setDataViewTimeField(timeFields[0]);
|
||||
} else {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformPreview', {
|
||||
defaultMessage: 'An error occurred fetching the transform preview',
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={getErrorMessage(transformPreview)}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const resp = await api.getTransforms();
|
||||
|
||||
if (isHttpFetchError(resp)) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformList', {
|
||||
defaultMessage: 'An error occurred getting the existing transform IDs:',
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={getErrorMessage(resp)}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
} else {
|
||||
setTransformIds(resp.transforms.map((transform) => transform.id));
|
||||
}
|
||||
|
||||
const [indices, ingestPipelines] = await Promise.all([
|
||||
api.getEsIndices(),
|
||||
api.getEsIngestPipelines(),
|
||||
]);
|
||||
|
||||
if (isEsIndices(indices)) {
|
||||
setIndexNames(indices.map((index) => index.name));
|
||||
} else {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexNames', {
|
||||
defaultMessage: 'An error occurred getting the existing index names:',
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={getErrorMessage(indices)}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (isEsIngestPipelines(ingestPipelines)) {
|
||||
setIngestPipelineNames(ingestPipelines.map(({ name }) => name));
|
||||
} else {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIngestPipelines', {
|
||||
defaultMessage: 'An error occurred getting the existing ingest pipeline names:',
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
overlays={overlays}
|
||||
theme={theme}
|
||||
text={getErrorMessage(ingestPipelines)}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
setDataViewTitles(await deps.data.dataViews.getTitles());
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingDataViewTitles', {
|
||||
defaultMessage: 'An error occurred getting the existing data view titles:',
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText overlays={overlays} theme={theme} text={getErrorMessage(e)} />,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
});
|
||||
}
|
||||
})();
|
||||
// run once
|
||||
useEffect(() => {
|
||||
if (transformsPreviewError !== null) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformPreview', {
|
||||
defaultMessage: 'An error occurred fetching the transform preview',
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText text={getErrorMessage(transformsPreviewError)} />,
|
||||
{ theme, i18n: i18nStart }
|
||||
),
|
||||
});
|
||||
}
|
||||
// custom comparison
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [transformsPreviewError]);
|
||||
|
||||
const { error: esIndicesError, data: esIndicesData } = useGetEsIndices();
|
||||
const indexNames = esIndicesData?.map((index) => index.name) ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
if (esIndicesError !== null) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexNames', {
|
||||
defaultMessage: 'An error occurred getting the existing index names:',
|
||||
}),
|
||||
text: toMountPoint(<ToastNotificationText text={getErrorMessage(esIndicesError)} />, {
|
||||
theme,
|
||||
i18n: i18nStart,
|
||||
}),
|
||||
});
|
||||
}
|
||||
// custom comparison
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
}, [esIndicesError]);
|
||||
|
||||
const { error: esIngestPipelinesError, data: esIngestPipelinesData } =
|
||||
useGetEsIngestPipelines();
|
||||
const ingestPipelineNames = esIngestPipelinesData?.map(({ name }) => name) ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
if (esIngestPipelinesError !== null) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIngestPipelines', {
|
||||
defaultMessage: 'An error occurred getting the existing ingest pipeline names:',
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText text={getErrorMessage(esIngestPipelinesError)} />,
|
||||
{ theme, i18n: i18nStart }
|
||||
),
|
||||
});
|
||||
}
|
||||
// custom comparison
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
}, [esIngestPipelinesError]);
|
||||
|
||||
const { error: dataViewTitlesError, data: dataViewTitles } = useGetDataViewTitles();
|
||||
|
||||
useEffect(() => {
|
||||
if (dataViewTitlesError !== null) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingDataViewTitles', {
|
||||
defaultMessage: 'An error occurred getting the existing data view titles:',
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText text={getErrorMessage(dataViewTitlesError)} />,
|
||||
{ theme, i18n: i18nStart }
|
||||
),
|
||||
});
|
||||
}
|
||||
}, [dataViewTitlesError]);
|
||||
|
||||
const dateFieldNames = searchItems.dataView.fields
|
||||
.filter((f) => f.type === KBN_FIELD_TYPES.DATE)
|
||||
|
@ -284,7 +276,6 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
|
|||
);
|
||||
setRetentionPolicyMaxAge('');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isRetentionPolicyEnabled]);
|
||||
|
||||
const transformIdExists = transformIds.some((id) => transformId === id);
|
||||
|
@ -294,7 +285,7 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
|
|||
const indexNameExists = indexNames.some((name) => destinationIndex === name);
|
||||
const indexNameEmpty = destinationIndex === '';
|
||||
const indexNameValid = isValidIndexName(destinationIndex);
|
||||
const dataViewTitleExists = dataViewTitles.some((name) => destinationIndex === name);
|
||||
const dataViewTitleExists = dataViewTitles?.some((name) => destinationIndex === name) ?? false;
|
||||
|
||||
const [transformFrequency, setTransformFrequency] = useState(defaults.transformFrequency);
|
||||
const isTransformFrequencyValid = transformFrequencyValidator(transformFrequency);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiConfirmModal } from '@elastic/eui';
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
|
|
|
@ -8,12 +8,14 @@
|
|||
import React, { FC, useEffect } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { EuiButtonEmpty, EuiCallOut, EuiPageTemplate, EuiSpacer } from '@elastic/eui';
|
||||
import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants';
|
||||
|
||||
import { useDocumentationLinks } from '../../hooks/use_documentation_links';
|
||||
import { useSearchItems } from '../../hooks/use_search_items';
|
||||
import { BREADCRUMB_SECTION, breadcrumbService, docTitleService } from '../../services/navigation';
|
||||
import { PrivilegesWrapper } from '../../lib/authorization';
|
||||
import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation';
|
||||
import { CapabilitiesWrapper } from '../../components/capabilities_wrapper';
|
||||
|
||||
import { Wizard } from './components/wizard';
|
||||
|
||||
type Props = RouteComponentProps<{ savedObjectId: string }>;
|
||||
|
@ -43,7 +45,14 @@ export const CreateTransformSection: FC<Props> = ({ match }) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<PrivilegesWrapper privileges={APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES}>
|
||||
<CapabilitiesWrapper
|
||||
requiredCapabilities={[
|
||||
'canGetTransform',
|
||||
'canPreviewTransform',
|
||||
'canCreateTransform',
|
||||
'canStartStopTransform',
|
||||
]}
|
||||
>
|
||||
<EuiPageTemplate.Header
|
||||
pageTitle={
|
||||
<FormattedMessage
|
||||
|
@ -67,6 +76,6 @@ export const CreateTransformSection: FC<Props> = ({ match }) => {
|
|||
)}
|
||||
{searchItems !== undefined && <Wizard searchItems={searchItems} />}
|
||||
</EuiPageTemplate.Section>
|
||||
</PrivilegesWrapper>
|
||||
</CapabilitiesWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Transform: <TransformManagementSection /> Minimal initialization 1`] = `
|
||||
<PrivilegesWrapper
|
||||
privileges={
|
||||
Array [
|
||||
"cluster.cluster:monitor/transform/get",
|
||||
"cluster.cluster:monitor/transform/stats/get",
|
||||
]
|
||||
}
|
||||
>
|
||||
<TransformManagement />
|
||||
</PrivilegesWrapper>
|
||||
`;
|
|
@ -5,11 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { createCapabilityFailureMessage } from '../../../../lib/authorization';
|
||||
import { createCapabilityFailureMessage } from '../../../../../../common/utils/create_capability_failure_message';
|
||||
|
||||
export const cloneActionNameText = i18n.translate(
|
||||
'xpack.transform.transformList.cloneActionNameText',
|
||||
|
|
|
@ -5,14 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { AuthorizationContext } from '../../../../lib/authorization';
|
||||
import { TransformListAction, TransformListRow } from '../../../../common';
|
||||
import { SECTION_SLUG } from '../../../../common/constants';
|
||||
import { useSearchItems } from '../../../../hooks/use_search_items';
|
||||
import { useTransformCapabilities, useSearchItems } from '../../../../hooks';
|
||||
import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies';
|
||||
|
||||
import { cloneActionNameText, CloneActionName } from './clone_action_name';
|
||||
|
@ -26,7 +25,7 @@ export const useCloneAction = (forceDisable: boolean, transformNodes: number) =>
|
|||
|
||||
const { getDataViewIdByTitle, loadDataViews } = useSearchItems(undefined);
|
||||
|
||||
const { canCreateTransform } = useContext(AuthorizationContext).capabilities;
|
||||
const { canCreateTransform } = useTransformCapabilities();
|
||||
|
||||
const clickHandler = useCallback(
|
||||
async (item: TransformListRow) => {
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createCapabilityFailureMessage } from '../../../../lib/authorization';
|
||||
|
||||
import { createCapabilityFailureMessage } from '../../../../../../common/utils/create_capability_failure_message';
|
||||
|
||||
interface CreateAlertRuleActionProps {
|
||||
disabled: boolean;
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import { AuthorizationContext } from '../../../../lib/authorization';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTransformCapabilities } from '../../../../hooks';
|
||||
import { TransformListAction, TransformListRow } from '../../../../common';
|
||||
import {
|
||||
crateAlertRuleActionNameText,
|
||||
|
@ -17,7 +17,7 @@ import { isContinuousTransform } from '../../../../../../common/types/transform'
|
|||
|
||||
export type CreateAlertRuleAction = ReturnType<typeof useCreateAlertRuleAction>;
|
||||
export const useCreateAlertRuleAction = (forceDisable: boolean) => {
|
||||
const { canCreateTransformAlerts } = useContext(AuthorizationContext).capabilities;
|
||||
const { canCreateTransformAlerts } = useTransformCapabilities();
|
||||
const { setCreateAlertRule } = useAlertRuleFlyout();
|
||||
|
||||
const clickHandler = useCallback(
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue