mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -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.
|
* 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';
|
import type { ES_FIELD_TYPES } from '@kbn/field-types';
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { getTransformsRequestSchema } from './transforms';
|
||||||
|
|
||||||
export const getTransformsStatsRequestSchema = getTransformsRequestSchema;
|
export const getTransformsStatsRequestSchema = getTransformsRequestSchema;
|
||||||
|
|
||||||
export type GetTransformsRequestSchema = TypeOf<typeof getTransformsStatsRequestSchema>;
|
export type GetTransformsStatsRequestSchema = TypeOf<typeof getTransformsStatsRequestSchema>;
|
||||||
|
|
||||||
export interface GetTransformsStatsResponseSchema {
|
export interface GetTransformsStatsResponseSchema {
|
||||||
node_failures?: object;
|
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 addInternalBasePath = (uri: string): string => `${INTERNAL_API_BASE_PATH}${uri}`;
|
||||||
export const addExternalBasePath = (uri: string): string => `${EXTERNAL_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:
|
// In order to create a transform, the API requires the following privileges:
|
||||||
// - transform_admin (builtin)
|
// - transform_admin (builtin)
|
||||||
// - cluster privileges: manage_transform
|
// - cluster privileges: manage_transform
|
||||||
|
@ -71,22 +86,6 @@ export const APP_CLUSTER_PRIVILEGES = [
|
||||||
// Minimum privileges required to return transform node count
|
// Minimum privileges required to return transform node count
|
||||||
export const NODES_INFO_PRIVILEGES = ['cluster:monitor/transform/get'];
|
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'];
|
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
|
// 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.
|
* 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 { 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 { FormattedMessage } from '@kbn/i18n-react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import type { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
|
import type { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||||
import type { TransformHealthRuleParams } from '../../../common/types/alerting';
|
import type { TransformHealthRuleParams } from '../../../common/types/alerting';
|
||||||
import { TestsSelectionControl } from './tests_selection_control';
|
import { TestsSelectionControl } from './tests_selection_control';
|
||||||
import { TransformSelectorControl } from './transform_selector_control';
|
import { TransformSelectorControl } from './transform_selector_control';
|
||||||
import { useApi } from '../../app/hooks';
|
import { useGetTransforms } from '../../app/hooks';
|
||||||
import { useToastNotifications } from '../../app/app_dependencies';
|
import { useToastNotifications } from '../../app/app_dependencies';
|
||||||
import { GetTransformsResponseSchema } from '../../../common/api_schemas/transforms';
|
|
||||||
import { ALL_TRANSFORMS_SELECTION } from '../../../common/constants';
|
import { ALL_TRANSFORMS_SELECTION } from '../../../common/constants';
|
||||||
|
|
||||||
export type TransformHealthRuleTriggerProps =
|
export type TransformHealthRuleTriggerProps =
|
||||||
|
@ -29,9 +28,12 @@ const TransformHealthRuleTrigger: FC<TransformHealthRuleTriggerProps> = ({
|
||||||
const formErrors = Object.values(errors).flat();
|
const formErrors = Object.values(errors).flat();
|
||||||
const isFormInvalid = formErrors.length > 0;
|
const isFormInvalid = formErrors.length > 0;
|
||||||
|
|
||||||
const api = useApi();
|
|
||||||
const toast = useToastNotifications();
|
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(
|
const onAlertParamChange = useCallback(
|
||||||
<T extends keyof TransformHealthRuleParams>(param: T) =>
|
<T extends keyof TransformHealthRuleParams>(param: T) =>
|
||||||
|
@ -41,20 +43,9 @@ const TransformHealthRuleTrigger: FC<TransformHealthRuleTriggerProps> = ({
|
||||||
[setRuleParams]
|
[setRuleParams]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
function fetchTransforms() {
|
if (error !== null) {
|
||||||
let unmounted = false;
|
toast.addError(error, {
|
||||||
api
|
|
||||||
.getTransforms()
|
|
||||||
.then((r) => {
|
|
||||||
if (!unmounted) {
|
|
||||||
setTransformOptions(
|
|
||||||
(r as GetTransformsResponseSchema).transforms.filter((v) => v.sync).map((v) => v.id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast.addError(e, {
|
|
||||||
title: i18n.translate(
|
title: i18n.translate(
|
||||||
'xpack.transform.alertingRuleTypes.transformHealth.fetchErrorMessage',
|
'xpack.transform.alertingRuleTypes.transformHealth.fetchErrorMessage',
|
||||||
{
|
{
|
||||||
|
@ -62,13 +53,8 @@ const TransformHealthRuleTrigger: FC<TransformHealthRuleTriggerProps> = ({
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
return () => {
|
}, [error, toast]);
|
||||||
unmounted = true;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[api, toast]
|
|
||||||
);
|
|
||||||
|
|
||||||
const excludeTransformOptions = useMemo(() => {
|
const excludeTransformOptions = useMemo(() => {
|
||||||
if (ruleParams.includeTransforms?.some((v) => v === ALL_TRANSFORMS_SELECTION)) {
|
if (ruleParams.includeTransforms?.some((v) => v === ALL_TRANSFORMS_SELECTION)) {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useContext, FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
import { render, unmountComponentAtNode } from 'react-dom';
|
import { render, unmountComponentAtNode } from 'react-dom';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
@ -13,36 +13,15 @@ import { EuiErrorBoundary } from '@elastic/eui';
|
||||||
|
|
||||||
import { Router, Routes, Route } from '@kbn/shared-ux-router';
|
import { Router, Routes, Route } from '@kbn/shared-ux-router';
|
||||||
import { ScopedHistory } from '@kbn/core/public';
|
import { ScopedHistory } from '@kbn/core/public';
|
||||||
import { FormattedMessage } from '@kbn/i18n-react';
|
|
||||||
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
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 { SECTION_SLUG } from './common/constants';
|
||||||
import { AuthorizationContext, AuthorizationProvider } from './lib/authorization';
|
|
||||||
import { AppDependencies } from './app_dependencies';
|
import { AppDependencies } from './app_dependencies';
|
||||||
import { CloneTransformSection } from './sections/clone_transform';
|
import { CloneTransformSection } from './sections/clone_transform';
|
||||||
import { CreateTransformSection } from './sections/create_transform';
|
import { CreateTransformSection } from './sections/create_transform';
|
||||||
import { TransformManagementSection } from './sections/transform_management';
|
import { TransformManagementSection } from './sections/transform_management';
|
||||||
|
|
||||||
export const App: FC<{ history: ScopedHistory }> = ({ history }) => {
|
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
|
@ -57,24 +36,27 @@ export const App: FC<{ history: ScopedHistory }> = ({ history }) => {
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
export const renderApp = (element: HTMLElement, appDependencies: AppDependencies) => {
|
export const renderApp = (element: HTMLElement, appDependencies: AppDependencies) => {
|
||||||
const I18nContext = appDependencies.i18n.Context;
|
const I18nContext = appDependencies.i18n.Context;
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: Infinity,
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<EuiErrorBoundary>
|
<EuiErrorBoundary>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<KibanaThemeProvider theme$={appDependencies.theme.theme$}>
|
<KibanaThemeProvider theme$={appDependencies.theme.theme$}>
|
||||||
<KibanaContextProvider services={appDependencies}>
|
<KibanaContextProvider services={appDependencies}>
|
||||||
<AuthorizationProvider
|
|
||||||
privilegesEndpoint={{ path: addInternalBasePath(`privileges`), version: '1' }}
|
|
||||||
>
|
|
||||||
<I18nContext>
|
<I18nContext>
|
||||||
<App history={appDependencies.history} />
|
<App history={appDependencies.history} />
|
||||||
</I18nContext>
|
</I18nContext>
|
||||||
</AuthorizationProvider>
|
|
||||||
</KibanaContextProvider>
|
</KibanaContextProvider>
|
||||||
</KibanaThemeProvider>
|
</KibanaThemeProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|
|
@ -19,12 +19,7 @@ export {
|
||||||
toggleSelectedField,
|
toggleSelectedField,
|
||||||
} from './fields';
|
} from './fields';
|
||||||
export type { DropDownLabel, DropDownOption, Label } from './dropdown';
|
export type { DropDownLabel, DropDownOption, Label } from './dropdown';
|
||||||
export {
|
export { isTransformIdValid } from './transform';
|
||||||
isTransformIdValid,
|
|
||||||
refreshTransformList$,
|
|
||||||
useRefreshTransformList,
|
|
||||||
REFRESH_TRANSFORM_LIST_STATE,
|
|
||||||
} from './transform';
|
|
||||||
export type { TransformListAction, TransformListRow } from './transform_list';
|
export type { TransformListAction, TransformListRow } from './transform_list';
|
||||||
export { TRANSFORM_LIST_COLUMN } from './transform_list';
|
export { TRANSFORM_LIST_COLUMN } from './transform_list';
|
||||||
export { getTransformProgress, isCompletedBatchTransform } from './transform_stats';
|
export { getTransformProgress, isCompletedBatchTransform } from './transform_stats';
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
|
|
||||||
import { SECTION_SLUG } from './constants';
|
import { SECTION_SLUG } from './constants';
|
||||||
|
|
|
@ -5,11 +5,8 @@
|
||||||
* 2.0.
|
* 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 { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
import type { TransformConfigUnion, TransformId } from '../../../common/types/transform';
|
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
|
// 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 = {
|
export const TRANSFORM_ERROR_TYPE = {
|
||||||
DANGLING_TASK: 'dangling_task',
|
DANGLING_TASK: 'dangling_task',
|
||||||
} as const;
|
} 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) => {
|
export const overrideTransformForCloning = (originalConfig: TransformConfigUnion) => {
|
||||||
// 'Managed' means job is preconfigured and deployed by other solutions
|
// '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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { SectionError } from './section_error';
|
|
||||||
export { SectionLoading } from './section_loading';
|
|
||||||
export { ToastNotificationText } from './toast_notification_text';
|
export { ToastNotificationText } from './toast_notification_text';
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { EuiIcon, EuiToolTip } from '@elastic/eui';
|
import { EuiIcon, EuiToolTip } from '@elastic/eui';
|
||||||
import { AuditMessageBase } from '../../../common/types/messages';
|
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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EuiButtonEmpty,
|
EuiButtonEmpty,
|
||||||
|
@ -19,41 +19,45 @@ import {
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
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;
|
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 {
|
interface ToastNotificationTextProps {
|
||||||
overlays: CoreStart['overlays'];
|
|
||||||
theme: CoreStart['theme'];
|
|
||||||
text: any;
|
text: any;
|
||||||
previewTextLength?: number;
|
previewTextLength?: number;
|
||||||
|
inline?: boolean;
|
||||||
|
forceModal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ToastNotificationText: FC<ToastNotificationTextProps> = ({
|
export const ToastNotificationText: FC<ToastNotificationTextProps> = ({
|
||||||
overlays,
|
|
||||||
text,
|
text,
|
||||||
theme,
|
|
||||||
previewTextLength,
|
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;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
!forceModal &&
|
||||||
typeof text === 'object' &&
|
typeof text === 'object' &&
|
||||||
|
text !== null &&
|
||||||
typeof text.message === 'string' &&
|
typeof text.message === 'string' &&
|
||||||
text.message.length <= MAX_SIMPLE_MESSAGE_LENGTH
|
text.message.length <= MAX_SIMPLE_MESSAGE_LENGTH
|
||||||
) {
|
) {
|
||||||
return text.message;
|
return text.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
const unformattedText = text.message ? text.message : text;
|
const unformattedText =
|
||||||
const formattedText = typeof unformattedText === 'object' ? JSON.stringify(text, null, 2) : text;
|
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 textLength = previewTextLength ?? 140;
|
||||||
const previewText = `${formattedText.substring(0, textLength)}${
|
const previewText = `${formattedText.substring(0, textLength)}${
|
||||||
formattedText.length > textLength ? ' ...' : ''
|
formattedText.length > textLength ? ' ...' : ''
|
||||||
|
@ -83,15 +87,19 @@ export const ToastNotificationText: FC<ToastNotificationTextProps> = ({
|
||||||
</EuiButtonEmpty>
|
</EuiButtonEmpty>
|
||||||
</EuiModalFooter>
|
</EuiModalFooter>
|
||||||
</EuiModal>,
|
</EuiModal>,
|
||||||
{ theme$: theme.theme$ }
|
{ theme, i18n: i18nStart }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<pre>{previewText}</pre>
|
{!inline && <pre>{previewText}</pre>}
|
||||||
<EuiButtonEmpty onClick={openModal}>
|
<EuiButtonEmpty
|
||||||
|
onClick={openModal}
|
||||||
|
css={inline ? { blockSize: 0 } : {}}
|
||||||
|
size={inline ? 's' : undefined}
|
||||||
|
>
|
||||||
{i18n.translate('xpack.transform.toastText.openModalButtonText', {
|
{i18n.translate('xpack.transform.toastText.openModalButtonText', {
|
||||||
defaultMessage: 'View details',
|
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.
|
* 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 { 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 { useDeleteTransforms, useDeleteIndexAndTargetIndex } from './use_delete_transform';
|
||||||
|
export { useRefreshTransformList } from './use_refresh_transform_list';
|
||||||
export { useResetTransforms } from './use_reset_transform';
|
export { useResetTransforms } from './use_reset_transform';
|
||||||
|
export { useSearchItems } from './use_search_items';
|
||||||
export { useScheduleNowTransforms } from './use_schedule_now_transform';
|
export { useScheduleNowTransforms } from './use_schedule_now_transform';
|
||||||
export { useStartTransforms } from './use_start_transform';
|
export { useStartTransforms } from './use_start_transform';
|
||||||
export { useStopTransforms } from './use_stop_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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||||
|
|
||||||
import type { IKibanaSearchRequest } from '@kbn/data-plugin/common';
|
import type { IKibanaSearchRequest } from '@kbn/data-plugin/common';
|
||||||
|
|
||||||
|
import { TRANSFORM_REACT_QUERY_KEYS } from '../../../common/constants';
|
||||||
|
|
||||||
import { useAppDependencies } from '../app_dependencies';
|
import { useAppDependencies } from '../app_dependencies';
|
||||||
|
|
||||||
export const useDataSearch = () => {
|
export const useDataSearch = (
|
||||||
|
esSearchRequestParams: IKibanaSearchRequest['params'],
|
||||||
|
enabled?: boolean
|
||||||
|
) => {
|
||||||
const { data } = useAppDependencies();
|
const { data } = useAppDependencies();
|
||||||
|
|
||||||
return useCallback(
|
return useQuery<estypes.SearchResponse>(
|
||||||
async (esSearchRequestParams: IKibanaSearchRequest['params'], abortSignal?: AbortSignal) => {
|
[TRANSFORM_REACT_QUERY_KEYS.DATA_SEARCH, esSearchRequestParams],
|
||||||
try {
|
async ({ signal }) => {
|
||||||
const { rawResponse: resp } = await lastValueFrom(
|
const { rawResponse: resp } = await lastValueFrom(
|
||||||
data.search.search(
|
data.search.search(
|
||||||
{
|
{
|
||||||
params: esSearchRequestParams,
|
params: esSearchRequestParams,
|
||||||
},
|
},
|
||||||
{ abortSignal }
|
{ abortSignal: signal }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
} catch (error) {
|
|
||||||
if (error.name === 'AbortError') {
|
|
||||||
// ignore abort errors
|
|
||||||
} else {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[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 React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
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 { extractErrorMessage } from '@kbn/ml-error-utils';
|
||||||
|
|
||||||
|
import { addInternalBasePath } from '../../../common/constants';
|
||||||
import type {
|
import type {
|
||||||
DeleteTransformStatus,
|
|
||||||
DeleteTransformsRequestSchema,
|
DeleteTransformsRequestSchema,
|
||||||
|
DeleteTransformsResponseSchema,
|
||||||
} from '../../../common/api_schemas/delete_transforms';
|
} from '../../../common/api_schemas/delete_transforms';
|
||||||
import { isDeleteTransformsResponseSchema } from '../../../common/api_schemas/type_guards';
|
|
||||||
import { getErrorMessage } from '../../../common/utils/errors';
|
import { getErrorMessage } from '../../../common/utils/errors';
|
||||||
|
|
||||||
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
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 { 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[]) => {
|
export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => {
|
||||||
const {
|
const {
|
||||||
http,
|
|
||||||
data: { dataViews: dataViewsContract },
|
|
||||||
application: { capabilities },
|
application: { capabilities },
|
||||||
} = useAppDependencies();
|
} = useAppDependencies();
|
||||||
const toastNotifications = useToastNotifications();
|
const toastNotifications = useToastNotifications();
|
||||||
|
const { canDeleteIndex: userCanDeleteIndex } = useTransformCapabilities();
|
||||||
|
|
||||||
|
const userCanDeleteDataView =
|
||||||
|
capabilities.savedObjectsManagement?.delete === true ||
|
||||||
|
capabilities.indexPatterns?.save === true;
|
||||||
|
|
||||||
const [deleteDestIndex, setDeleteDestIndex] = useState<boolean>(true);
|
const [deleteDestIndex, setDeleteDestIndex] = useState<boolean>(true);
|
||||||
const [deleteDataView, setDeleteDataView] = useState<boolean>(true);
|
const [deleteDataView, setDeleteDataView] = useState<boolean>(userCanDeleteDataView);
|
||||||
const [userCanDeleteIndex, setUserCanDeleteIndex] = useState<boolean>(false);
|
|
||||||
const [dataViewExists, setDataViewExists] = useState<boolean>(false);
|
|
||||||
const [userCanDeleteDataView, setUserCanDeleteDataView] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const toggleDeleteIndex = useCallback(
|
const toggleDeleteIndex = useCallback(
|
||||||
() => setDeleteDestIndex(!deleteDestIndex),
|
() => setDeleteDestIndex(!deleteDestIndex),
|
||||||
|
@ -43,67 +49,31 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => {
|
||||||
() => setDeleteDataView(!deleteDataView),
|
() => setDeleteDataView(!deleteDataView),
|
||||||
[deleteDataView]
|
[deleteDataView]
|
||||||
);
|
);
|
||||||
const checkDataViewExists = useCallback(
|
|
||||||
async (indexName: string) => {
|
const { error: dataViewExistsError, data: dataViewExists = items.length !== 1 } =
|
||||||
try {
|
useDataViewExists(items);
|
||||||
const dvExists = await indexService.dataViewExists(dataViewsContract, indexName);
|
|
||||||
setDataViewExists(dvExists);
|
useEffect(() => {
|
||||||
} catch (e) {
|
if (dataViewExistsError !== null && items.length === 1) {
|
||||||
const error = extractErrorMessage(e);
|
const config = items[0].config;
|
||||||
|
const indexName = Array.isArray(config.dest.index) ? config.dest.index[0] : config.dest.index;
|
||||||
|
|
||||||
toastNotifications.addDanger(
|
toastNotifications.addDanger(
|
||||||
i18n.translate(
|
i18n.translate(
|
||||||
'xpack.transform.deleteTransform.errorWithCheckingIfDataViewExistsNotificationErrorMessage',
|
'xpack.transform.deleteTransform.errorWithCheckingIfDataViewExistsNotificationErrorMessage',
|
||||||
{
|
{
|
||||||
defaultMessage: 'An error occurred checking if data view {dataView} exists: {error}',
|
defaultMessage: 'An error occurred checking if data view {dataView} exists: {error}',
|
||||||
values: { dataView: indexName, error },
|
values: {
|
||||||
}
|
dataView: indexName,
|
||||||
)
|
error: extractErrorMessage(dataViewExistsError),
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[dataViewsContract, toastNotifications]
|
|
||||||
);
|
|
||||||
|
|
||||||
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',
|
|
||||||
{
|
|
||||||
defaultMessage: 'An error occurred checking if user can delete destination index',
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [http, toastNotifications, capabilities]);
|
// custom comparison
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
useEffect(() => {
|
}, [dataViewExistsError]);
|
||||||
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]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userCanDeleteIndex,
|
userCanDeleteIndex,
|
||||||
|
@ -116,86 +86,34 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type SuccessCountField = keyof Omit<DeleteTransformStatus, 'destinationIndex'>;
|
|
||||||
|
|
||||||
export const useDeleteTransforms = () => {
|
export const useDeleteTransforms = () => {
|
||||||
const { overlays, theme } = useAppDependencies();
|
const { http, i18n: i18nStart, theme } = useAppDependencies();
|
||||||
|
const refreshTransformList = useRefreshTransformList();
|
||||||
const toastNotifications = useToastNotifications();
|
const toastNotifications = useToastNotifications();
|
||||||
const api = useApi();
|
|
||||||
|
|
||||||
return async (reqBody: DeleteTransformsRequestSchema) => {
|
const mutation = useMutation({
|
||||||
const results = await api.deleteTransforms(reqBody);
|
mutationFn: (reqBody: DeleteTransformsRequestSchema) =>
|
||||||
|
http.post<DeleteTransformsResponseSchema>(addInternalBasePath('delete_transforms'), {
|
||||||
if (!isDeleteTransformsResponseSchema(results)) {
|
body: JSON.stringify(reqBody),
|
||||||
|
version: '1',
|
||||||
|
}),
|
||||||
|
onError: (error) =>
|
||||||
toastNotifications.addDanger({
|
toastNotifications.addDanger({
|
||||||
title: i18n.translate('xpack.transform.transformList.deleteTransformGenericErrorMessage', {
|
title: i18n.translate('xpack.transform.transformList.deleteTransformGenericErrorMessage', {
|
||||||
defaultMessage: 'An error occurred calling the API endpoint to delete transforms.',
|
defaultMessage: 'An error occurred calling the API endpoint to delete transforms.',
|
||||||
}),
|
}),
|
||||||
text: toMountPoint(
|
text: toMountPoint(
|
||||||
<ToastNotificationText
|
<ToastNotificationText previewTextLength={50} text={getErrorMessage(error)} />,
|
||||||
previewTextLength={50}
|
{ theme, i18n: i18nStart }
|
||||||
overlays={overlays}
|
|
||||||
theme={theme}
|
|
||||||
text={getErrorMessage(results)}
|
|
||||||
/>,
|
|
||||||
{ theme$: theme.theme$ }
|
|
||||||
),
|
),
|
||||||
});
|
}),
|
||||||
return;
|
onSuccess: (results) => {
|
||||||
}
|
|
||||||
|
|
||||||
const isBulk = Object.keys(results).length > 1;
|
|
||||||
const successCount: Record<SuccessCountField, number> = {
|
|
||||||
transformDeleted: 0,
|
|
||||||
destIndexDeleted: 0,
|
|
||||||
destDataViewDeleted: 0,
|
|
||||||
};
|
|
||||||
for (const transformId in results) {
|
for (const transformId in results) {
|
||||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||||
if (results.hasOwnProperty(transformId)) {
|
if (results.hasOwnProperty(transformId)) {
|
||||||
const status = results[transformId];
|
const status = results[transformId];
|
||||||
const destinationIndex = status.destinationIndex;
|
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.',
|
|
||||||
values: { transformId },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (status.destIndexDeleted?.success) {
|
|
||||||
toastNotifications.addSuccess(
|
|
||||||
i18n.translate(
|
|
||||||
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage',
|
|
||||||
{
|
|
||||||
defaultMessage:
|
|
||||||
'Request to delete destination index {destinationIndex} acknowledged.',
|
|
||||||
values: { destinationIndex },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (status.destDataViewDeleted?.success) {
|
|
||||||
toastNotifications.addSuccess(
|
|
||||||
i18n.translate(
|
|
||||||
'xpack.transform.deleteTransform.deleteAnalyticsWithDataViewSuccessMessage',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Request to delete data view {destinationIndex} acknowledged.',
|
|
||||||
values: { destinationIndex },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(Object.keys(successCount) as SuccessCountField[]).forEach((key) => {
|
|
||||||
if (status[key]?.success) {
|
|
||||||
successCount[key] = successCount[key] + 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (status.transformDeleted?.error) {
|
if (status.transformDeleted?.error) {
|
||||||
const error = status.transformDeleted.error.reason;
|
const error = status.transformDeleted.error.reason;
|
||||||
toastNotifications.addDanger({
|
toastNotifications.addDanger({
|
||||||
|
@ -203,15 +121,10 @@ export const useDeleteTransforms = () => {
|
||||||
defaultMessage: 'An error occurred deleting the transform {transformId}',
|
defaultMessage: 'An error occurred deleting the transform {transformId}',
|
||||||
values: { transformId },
|
values: { transformId },
|
||||||
}),
|
}),
|
||||||
text: toMountPoint(
|
text: toMountPoint(<ToastNotificationText previewTextLength={50} text={error} />, {
|
||||||
<ToastNotificationText
|
theme,
|
||||||
previewTextLength={50}
|
i18n: i18nStart,
|
||||||
overlays={overlays}
|
}),
|
||||||
theme={theme}
|
|
||||||
text={error}
|
|
||||||
/>,
|
|
||||||
{ theme$: theme.theme$ }
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,15 +138,10 @@ export const useDeleteTransforms = () => {
|
||||||
values: { destinationIndex },
|
values: { destinationIndex },
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
text: toMountPoint(
|
text: toMountPoint(<ToastNotificationText previewTextLength={50} text={error} />, {
|
||||||
<ToastNotificationText
|
theme,
|
||||||
previewTextLength={50}
|
i18n: i18nStart,
|
||||||
overlays={overlays}
|
}),
|
||||||
theme={theme}
|
|
||||||
text={error}
|
|
||||||
/>,
|
|
||||||
{ theme$: theme.theme$ }
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,52 +155,18 @@ export const useDeleteTransforms = () => {
|
||||||
values: { destinationIndex },
|
values: { destinationIndex },
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
text: toMountPoint(
|
text: toMountPoint(<ToastNotificationText previewTextLength={50} text={error} />, {
|
||||||
<ToastNotificationText
|
theme,
|
||||||
previewTextLength={50}
|
i18n: i18nStart,
|
||||||
overlays={overlays}
|
}),
|
||||||
theme={theme}
|
|
||||||
text={error}
|
|
||||||
/>,
|
|
||||||
{ theme$: theme.theme$ }
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we are deleting multiple transforms, combine the success messages
|
refreshTransformList();
|
||||||
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 },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (successCount.destIndexDeleted > 0) {
|
return mutation.mutate;
|
||||||
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);
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||||
import { isDefined } from '@kbn/ml-is-defined';
|
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 {
|
import {
|
||||||
isGetTransformNodesResponseSchema,
|
addInternalBasePath,
|
||||||
isGetTransformsResponseSchema,
|
DEFAULT_REFRESH_INTERVAL_MS,
|
||||||
isGetTransformsStatsResponseSchema,
|
TRANSFORM_REACT_QUERY_KEYS,
|
||||||
} from '../../../common/api_schemas/type_guards';
|
TRANSFORM_MODE,
|
||||||
import { TRANSFORM_MODE } from '../../../common/constants';
|
} from '../../../common/constants';
|
||||||
import { isTransformStats } from '../../../common/types/transform_stats';
|
import { isTransformStats } from '../../../common/types/transform_stats';
|
||||||
|
|
||||||
import {
|
import { type TransformListRow } from '../common';
|
||||||
type TransformListRow,
|
import { useAppDependencies } from '../app_dependencies';
|
||||||
refreshTransformList$,
|
|
||||||
REFRESH_TRANSFORM_LIST_STATE,
|
|
||||||
} from '../common';
|
|
||||||
|
|
||||||
import { useApi } from './use_api';
|
|
||||||
import { TRANSFORM_ERROR_TYPE } from '../common/transform';
|
import { TRANSFORM_ERROR_TYPE } from '../common/transform';
|
||||||
|
|
||||||
export type GetTransforms = (forceRefresh?: boolean) => void;
|
interface UseGetTransformsResponse {
|
||||||
|
transforms: TransformListRow[];
|
||||||
export const useGetTransforms = (
|
transformIds: string[];
|
||||||
setTransforms: React.Dispatch<React.SetStateAction<TransformListRow[]>>,
|
transformIdsWithoutConfig?: string[];
|
||||||
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();
|
|
||||||
|
|
||||||
let concurrentLoads = 0;
|
|
||||||
|
|
||||||
const getTransforms = async (forceRefresh = false) => {
|
|
||||||
if (forceRefresh === true || blockRefresh === false) {
|
|
||||||
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.LOADING);
|
|
||||||
concurrentLoads++;
|
|
||||||
|
|
||||||
if (concurrentLoads > 1) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchOptions = { asSystemRequest: true };
|
const getInitialData = (): UseGetTransformsResponse => ({
|
||||||
const transformNodes = await api.getTransformNodes();
|
transforms: [],
|
||||||
const transformConfigs = await api.getTransforms(fetchOptions);
|
transformIds: [],
|
||||||
const transformStats = await api.getTransformsStats(fetchOptions);
|
});
|
||||||
|
|
||||||
if (
|
interface UseGetTransformsOptions {
|
||||||
!isGetTransformsResponseSchema(transformConfigs) ||
|
enabled?: boolean;
|
||||||
!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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
export const useGetTransforms = ({ enabled }: UseGetTransformsOptions = {}) => {
|
||||||
|
const { http } = useAppDependencies();
|
||||||
|
|
||||||
|
const { data = getInitialData(), ...rest } = useQuery<UseGetTransformsResponse, IHttpFetchError>(
|
||||||
|
[TRANSFORM_REACT_QUERY_KEYS.GET_TRANSFORMS],
|
||||||
|
async ({ signal }) => {
|
||||||
|
const update = getInitialData();
|
||||||
|
|
||||||
|
const transformConfigs = await http.get<GetTransformsResponseSchema>(
|
||||||
|
addInternalBasePath('transforms'),
|
||||||
|
{
|
||||||
|
version: '1',
|
||||||
|
asSystemRequest: true,
|
||||||
|
signal,
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
const transformStats = await http.get<GetTransformsStatsResponseSchema>(
|
||||||
|
addInternalBasePath(`transforms/_stats`),
|
||||||
|
{
|
||||||
|
version: '1',
|
||||||
|
asSystemRequest: true,
|
||||||
|
signal,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// There might be some errors with fetching certain transforms
|
// There might be some errors with fetching certain transforms
|
||||||
// For example, when task exists and is running but the config is deleted
|
// For example, when task exists and is running but the config is deleted
|
||||||
|
@ -87,17 +76,12 @@ export const useGetTransforms = (
|
||||||
})
|
})
|
||||||
.filter(isDefined);
|
.filter(isDefined);
|
||||||
|
|
||||||
setTransformIdsWithoutConfig(
|
update.transformIdsWithoutConfig =
|
||||||
danglingTaskIdMatches.length > 0 ? danglingTaskIdMatches : undefined
|
danglingTaskIdMatches.length > 0 ? danglingTaskIdMatches : undefined;
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setTransformIdsWithoutConfig(undefined);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableRows = transformConfigs.transforms.reduce((reducedtableRows, config) => {
|
update.transforms = transformConfigs.transforms.reduce((reducedtableRows, config) => {
|
||||||
const stats = isGetTransformsStatsResponseSchema(transformStats)
|
const stats = transformStats.transforms.find((d) => config.id === d.id);
|
||||||
? transformStats.transforms.find((d) => config.id === d.id)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// A newly created transform might not have corresponding stats yet.
|
// 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.
|
// 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;
|
return reducedtableRows;
|
||||||
}, [] as TransformListRow[]);
|
}, [] as TransformListRow[]);
|
||||||
|
|
||||||
setTransformNodes(transformNodes.count);
|
update.transformIds = update.transforms.map(({ id }) => id);
|
||||||
setTransforms(tableRows);
|
|
||||||
setErrorMessage(undefined);
|
|
||||||
setIsInitialized(true);
|
|
||||||
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE);
|
|
||||||
|
|
||||||
concurrentLoads--;
|
return update;
|
||||||
|
},
|
||||||
if (concurrentLoads > 0) {
|
{
|
||||||
concurrentLoads = 0;
|
enabled,
|
||||||
getTransforms(true);
|
refetchInterval: DEFAULT_REFRESH_INTERVAL_MS,
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
};
|
|
||||||
|
|
||||||
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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
import '@testing-library/jest-dom/extend-expect';
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import { renderHook } from '@testing-library/react-hooks';
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
|
||||||
|
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||||
import { CoreSetup } from '@kbn/core/public';
|
import { CoreSetup } from '@kbn/core/public';
|
||||||
import { DataGrid, type UseIndexDataReturnType } from '@kbn/ml-data-grid';
|
import { DataGrid, type UseIndexDataReturnType } from '@kbn/ml-data-grid';
|
||||||
import type { RuntimeMappings } from '@kbn/ml-runtime-field-utils';
|
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('../../shared_imports');
|
||||||
jest.mock('../app_dependencies');
|
jest.mock('../app_dependencies');
|
||||||
jest.mock('./use_api');
|
|
||||||
|
|
||||||
import { MlSharedContext } from '../__mocks__/shared_context';
|
import { MlSharedContext } from '../__mocks__/shared_context';
|
||||||
|
|
||||||
|
@ -45,13 +44,17 @@ const runtimeMappings: RuntimeMappings = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
describe('Transform: useIndexData()', () => {
|
describe('Transform: useIndexData()', () => {
|
||||||
test('dataView set triggers loading', async () => {
|
test('dataView set triggers loading', async () => {
|
||||||
const mlShared = await getMlSharedImports();
|
const mlShared = await getMlSharedImports();
|
||||||
const wrapper: FC = ({ children }) => (
|
const wrapper: FC = ({ children }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<MlSharedContext.Provider value={mlShared}>{children}</MlSharedContext.Provider>
|
<MlSharedContext.Provider value={mlShared}>{children}</MlSharedContext.Provider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderHook(
|
const { result, waitForNextUpdate } = renderHook(
|
||||||
|
@ -102,11 +105,13 @@ describe('Transform: <DataGrid /> with useIndexData()', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
render(
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<MlSharedContext.Provider value={mlSharedImports}>
|
<MlSharedContext.Provider value={mlSharedImports}>
|
||||||
<Wrapper />
|
<Wrapper />
|
||||||
</MlSharedContext.Provider>
|
</MlSharedContext.Provider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
@ -142,11 +147,13 @@ describe('Transform: <DataGrid /> with useIndexData()', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
render(
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<MlSharedContext.Provider value={mlSharedImports}>
|
<MlSharedContext.Provider value={mlSharedImports}>
|
||||||
<Wrapper />
|
<Wrapper />
|
||||||
</MlSharedContext.Provider>
|
</MlSharedContext.Provider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||||
import type { EuiDataGridColumn } from '@elastic/eui';
|
import type { EuiDataGridColumn } from '@elastic/eui';
|
||||||
|
@ -28,10 +28,6 @@ import {
|
||||||
} from '@kbn/ml-data-grid';
|
} from '@kbn/ml-data-grid';
|
||||||
import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
|
import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
|
||||||
|
|
||||||
import {
|
|
||||||
isEsSearchResponse,
|
|
||||||
isFieldHistogramsResponseSchema,
|
|
||||||
} from '../../../common/api_schemas/type_guards';
|
|
||||||
import {
|
import {
|
||||||
hasKeywordDuplicate,
|
hasKeywordDuplicate,
|
||||||
isKeywordDuplicate,
|
isKeywordDuplicate,
|
||||||
|
@ -44,7 +40,7 @@ import { useToastNotifications, useAppDependencies } from '../app_dependencies';
|
||||||
import type { StepDefineExposedState } from '../sections/create_transform/components/step_define/common';
|
import type { StepDefineExposedState } from '../sections/create_transform/components/step_define/common';
|
||||||
|
|
||||||
import { SearchItems } from './use_search_items';
|
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';
|
import { useDataSearch } from './use_data_search';
|
||||||
|
|
||||||
export const useIndexData = (
|
export const useIndexData = (
|
||||||
|
@ -52,7 +48,7 @@ export const useIndexData = (
|
||||||
query: TransformConfigQuery,
|
query: TransformConfigQuery,
|
||||||
combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings'],
|
combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings'],
|
||||||
timeRangeMs?: TimeRangeMs,
|
timeRangeMs?: TimeRangeMs,
|
||||||
populatedFields?: Set<string> | null
|
populatedFields?: string[]
|
||||||
): UseIndexDataReturnType => {
|
): UseIndexDataReturnType => {
|
||||||
const { analytics } = useAppDependencies();
|
const { analytics } = useAppDependencies();
|
||||||
|
|
||||||
|
@ -61,13 +57,8 @@ export const useIndexData = (
|
||||||
const loadIndexDataStartTime = useRef<number | undefined>(window.performance.now());
|
const loadIndexDataStartTime = useRef<number | undefined>(window.performance.now());
|
||||||
|
|
||||||
const indexPattern = useMemo(() => dataView.getIndexPattern(), [dataView]);
|
const indexPattern = useMemo(() => dataView.getIndexPattern(), [dataView]);
|
||||||
|
|
||||||
const api = useApi();
|
|
||||||
const dataSearch = useDataSearch();
|
|
||||||
const toastNotifications = useToastNotifications();
|
const toastNotifications = useToastNotifications();
|
||||||
|
|
||||||
const [dataViewFields, setDataViewFields] = useState<string[]>();
|
|
||||||
|
|
||||||
const baseFilterCriteria = buildBaseFilterCriteria(
|
const baseFilterCriteria = buildBaseFilterCriteria(
|
||||||
dataView.timeFieldName,
|
dataView.timeFieldName,
|
||||||
timeRangeMs?.from,
|
timeRangeMs?.from,
|
||||||
|
@ -86,26 +77,17 @@ export const useIndexData = (
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (dataView.timeFieldName !== undefined && timeRangeMs === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const abortController = new AbortController();
|
|
||||||
|
|
||||||
// Fetch 500 random documents to determine populated fields.
|
// Fetch 500 random documents to determine populated fields.
|
||||||
// This is a workaround to avoid passing potentially thousands of unpopulated fields
|
// This is a workaround to avoid passing potentially thousands of unpopulated fields
|
||||||
// (for example, as part of filebeat/metricbeat/ECS based indices)
|
// (for example, as part of filebeat/metricbeat/ECS based indices)
|
||||||
// to the data grid component which would significantly slow down the page.
|
// to the data grid component which would significantly slow down the page.
|
||||||
const fetchDataGridSampleDocuments = async function () {
|
const {
|
||||||
let populatedDataViewFields = populatedFields ? [...populatedFields] : [];
|
error: dataViewFieldsError,
|
||||||
let isMissingFields = populatedDataViewFields.length === 0;
|
data: dataViewFieldsData,
|
||||||
|
isError: dataViewFieldsIsError,
|
||||||
// If populatedFields are not provided, make own request to calculate
|
isLoading: dataViewFieldsIsLoading,
|
||||||
if (populatedFields === undefined) {
|
} = useDataSearch(
|
||||||
setErrorMessage('');
|
{
|
||||||
setStatus(INDEX_STATUS.LOADING);
|
|
||||||
|
|
||||||
const esSearchRequest = {
|
|
||||||
index: indexPattern,
|
index: indexPattern,
|
||||||
body: {
|
body: {
|
||||||
fields: ['*'],
|
fields: ['*'],
|
||||||
|
@ -118,41 +100,50 @@ export const useIndexData = (
|
||||||
},
|
},
|
||||||
size: 500,
|
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);
|
useEffect(() => {
|
||||||
|
if (dataViewFieldsIsLoading && !dataViewFieldsIsError) {
|
||||||
if (!isEsSearchResponse(resp)) {
|
setErrorMessage('');
|
||||||
setErrorMessage(getErrorMessage(resp));
|
setStatus(INDEX_STATUS.LOADING);
|
||||||
|
} else if (dataViewFieldsError !== null) {
|
||||||
|
setErrorMessage(getErrorMessage(dataViewFieldsError));
|
||||||
setStatus(INDEX_STATUS.ERROR);
|
setStatus(INDEX_STATUS.ERROR);
|
||||||
return;
|
} else if (
|
||||||
}
|
!dataViewFieldsIsLoading &&
|
||||||
const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields ?? {}));
|
!dataViewFieldsIsError &&
|
||||||
isMissingFields = resp.hits.hits.every((d) => typeof d.fields === 'undefined');
|
dataViewFieldsData !== undefined
|
||||||
|
) {
|
||||||
populatedDataViewFields = [...new Set(docs.map(Object.keys).flat(1))];
|
|
||||||
}
|
|
||||||
const isCrossClusterSearch = indexPattern.includes(':');
|
const isCrossClusterSearch = indexPattern.includes(':');
|
||||||
|
const isMissingFields = dataViewFieldsData.hits.hits.every(
|
||||||
// Get all field names for each returned doc and flatten it
|
(d) => typeof d.fields === 'undefined'
|
||||||
// to a list of unique field names used across all docs.
|
);
|
||||||
const allDataViewFields = getFieldsFromKibanaIndexPattern(dataView);
|
|
||||||
const filteredDataViewFields = populatedDataViewFields
|
|
||||||
.filter((d) => allDataViewFields.includes(d))
|
|
||||||
.sort();
|
|
||||||
|
|
||||||
setCcsWarning(isCrossClusterSearch && isMissingFields);
|
setCcsWarning(isCrossClusterSearch && isMissingFields);
|
||||||
setStatus(INDEX_STATUS.LOADED);
|
setStatus(INDEX_STATUS.LOADED);
|
||||||
setDataViewFields(filteredDataViewFields);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
fetchDataGridSampleDocuments();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
abortController.abort();
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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(() => {
|
const columns: EuiDataGridColumn[] = useMemo(() => {
|
||||||
if (typeof dataViewFields === 'undefined') {
|
if (typeof dataViewFields === 'undefined') {
|
||||||
|
@ -206,22 +197,18 @@ export const useIndexData = (
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [JSON.stringify([query, timeRangeMs])]);
|
}, [JSON.stringify([query, timeRangeMs])]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof dataViewFields === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const abortController = new AbortController();
|
|
||||||
|
|
||||||
const fetchDataGridData = async function () {
|
|
||||||
setErrorMessage('');
|
|
||||||
setStatus(INDEX_STATUS.LOADING);
|
|
||||||
|
|
||||||
const sort: EsSorting = sortingColumns.reduce((s, column) => {
|
const sort: EsSorting = sortingColumns.reduce((s, column) => {
|
||||||
s[column.id] = { order: column.direction };
|
s[column.id] = { order: column.direction };
|
||||||
return s;
|
return s;
|
||||||
}, {} as EsSorting);
|
}, {} as EsSorting);
|
||||||
|
|
||||||
const esSearchRequest = {
|
const {
|
||||||
|
error: dataGridDataError,
|
||||||
|
data: dataGridData,
|
||||||
|
isError: dataGridDataIsError,
|
||||||
|
isLoading: dataGridDataIsLoading,
|
||||||
|
} = useDataSearch(
|
||||||
|
{
|
||||||
index: indexPattern,
|
index: indexPattern,
|
||||||
body: {
|
body: {
|
||||||
fields: ['*'],
|
fields: ['*'],
|
||||||
|
@ -234,56 +221,44 @@ export const useIndexData = (
|
||||||
? { runtime_mappings: combinedRuntimeMappings }
|
? { runtime_mappings: combinedRuntimeMappings }
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
const resp = await dataSearch(esSearchRequest, abortController.signal);
|
// Check whether fetching should be enabled
|
||||||
|
dataViewFields !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
if (!isEsSearchResponse(resp)) {
|
useEffect(() => {
|
||||||
setErrorMessage(getErrorMessage(resp));
|
if (dataGridDataIsLoading && !dataGridDataIsError) {
|
||||||
|
setErrorMessage('');
|
||||||
|
setStatus(INDEX_STATUS.LOADING);
|
||||||
|
} else if (dataGridDataError !== null) {
|
||||||
|
setErrorMessage(getErrorMessage(dataGridDataError));
|
||||||
setStatus(INDEX_STATUS.ERROR);
|
setStatus(INDEX_STATUS.ERROR);
|
||||||
return;
|
} else if (!dataGridDataIsLoading && !dataGridDataIsError && dataGridData !== undefined) {
|
||||||
}
|
|
||||||
|
|
||||||
const isCrossClusterSearch = indexPattern.includes(':');
|
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);
|
setCcsWarning(isCrossClusterSearch && isMissingFields);
|
||||||
setRowCountInfo({
|
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:
|
rowCountRelation:
|
||||||
typeof resp.hits.total === 'number'
|
typeof dataGridData.hits.total === 'number'
|
||||||
? ('eq' as estypes.SearchTotalHitsRelation)
|
? ('eq' as estypes.SearchTotalHitsRelation)
|
||||||
: resp.hits.total!.relation,
|
: dataGridData.hits.total!.relation,
|
||||||
});
|
});
|
||||||
setTableItems(docs);
|
setTableItems(docs);
|
||||||
setStatus(INDEX_STATUS.LOADED);
|
setStatus(INDEX_STATUS.LOADED);
|
||||||
};
|
}
|
||||||
|
|
||||||
fetchDataGridData();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
abortController.abort();
|
|
||||||
};
|
|
||||||
// custom comparison
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [dataGridDataError, dataGridDataIsError, dataGridDataIsLoading]);
|
||||||
indexPattern,
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
JSON.stringify([
|
|
||||||
query,
|
|
||||||
pagination,
|
|
||||||
sortingColumns,
|
|
||||||
dataViewFields,
|
|
||||||
combinedRuntimeMappings,
|
|
||||||
timeRangeMs,
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchColumnChartsData = async function () {
|
|
||||||
const allDataViewFieldNames = new Set(dataView.fields.map((f) => f.name));
|
const allDataViewFieldNames = new Set(dataView.fields.map((f) => f.name));
|
||||||
const columnChartsData = await api.getHistogramsForFields(
|
const { error: histogramsForFieldsError, data: histogramsForFieldsData } =
|
||||||
|
useGetHistogramsForFields(
|
||||||
indexPattern,
|
indexPattern,
|
||||||
columns
|
columns
|
||||||
.filter((cT) => dataGrid.visibleColumns.includes(cT.id))
|
.filter((cT) => dataGrid.visibleColumns.includes(cT.id))
|
||||||
|
@ -302,36 +277,33 @@ export const useIndexData = (
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
isDefaultQuery(query) ? defaultQuery : queryWithBaseFilterCriteria,
|
isDefaultQuery(query) ? defaultQuery : queryWithBaseFilterCriteria,
|
||||||
combinedRuntimeMappings
|
combinedRuntimeMappings,
|
||||||
|
chartsVisible
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isFieldHistogramsResponseSchema(columnChartsData)) {
|
useEffect(() => {
|
||||||
showDataGridColumnChartErrorMessageToast(columnChartsData, toastNotifications);
|
if (histogramsForFieldsError !== null) {
|
||||||
return;
|
showDataGridColumnChartErrorMessageToast(histogramsForFieldsError, toastNotifications);
|
||||||
}
|
}
|
||||||
|
// custom comparison
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [histogramsForFieldsError]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (histogramsForFieldsData) {
|
||||||
setColumnCharts(
|
setColumnCharts(
|
||||||
// revert field names with `.keyword` used to do aggregations to their original column name
|
// revert field names with `.keyword` used to do aggregations to their original column name
|
||||||
columnChartsData.map((d) => ({
|
histogramsForFieldsData.map((d) => ({
|
||||||
...d,
|
...d,
|
||||||
...(isKeywordDuplicate(d.id, allDataViewFieldNames)
|
...(isKeywordDuplicate(d.id, allDataViewFieldNames)
|
||||||
? { id: removeKeywordPostfix(d.id) }
|
? { id: removeKeywordPostfix(d.id) }
|
||||||
: {}),
|
: {}),
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
if (chartsVisible) {
|
|
||||||
fetchColumnChartsData();
|
|
||||||
}
|
}
|
||||||
// custom comparison
|
// custom comparison
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [histogramsForFieldsData]);
|
||||||
chartsVisible,
|
|
||||||
indexPattern,
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
JSON.stringify([query, dataGrid.visibleColumns, combinedRuntimeMappings, timeRangeMs]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const renderCellValue = useRenderCellValue(dataView, pagination, tableItems);
|
const renderCellValue = useRenderCellValue(dataView, pagination, tableItems);
|
||||||
|
|
||||||
|
|
|
@ -6,31 +6,39 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||||
|
|
||||||
import type { StartTransformsRequestSchema } from '../../../common/api_schemas/start_transforms';
|
|
||||||
import { isStartTransformsResponseSchema } from '../../../common/api_schemas/type_guards';
|
|
||||||
|
|
||||||
|
import { addInternalBasePath } from '../../../common/constants';
|
||||||
import { getErrorMessage } from '../../../common/utils/errors';
|
import { getErrorMessage } from '../../../common/utils/errors';
|
||||||
|
import type {
|
||||||
|
ReauthorizeTransformsRequestSchema,
|
||||||
|
ReauthorizeTransformsResponseSchema,
|
||||||
|
} from '../../../common/api_schemas/reauthorize_transforms';
|
||||||
|
|
||||||
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
||||||
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
|
|
||||||
import { ToastNotificationText } from '../components';
|
import { ToastNotificationText } from '../components';
|
||||||
|
|
||||||
import { useApi } from './use_api';
|
import { useRefreshTransformList } from './use_refresh_transform_list';
|
||||||
|
|
||||||
export const useReauthorizeTransforms = () => {
|
export const useReauthorizeTransforms = () => {
|
||||||
const { overlays, theme } = useAppDependencies();
|
const { http, i18n: i18nStart, theme } = useAppDependencies();
|
||||||
|
const refreshTransformList = useRefreshTransformList();
|
||||||
const toastNotifications = useToastNotifications();
|
const toastNotifications = useToastNotifications();
|
||||||
const api = useApi();
|
|
||||||
|
|
||||||
return async (transformsInfo: StartTransformsRequestSchema) => {
|
const mutation = useMutation({
|
||||||
const results = await api.reauthorizeTransforms(transformsInfo);
|
mutationFn: (reqBody: ReauthorizeTransformsRequestSchema) =>
|
||||||
|
http.post<ReauthorizeTransformsResponseSchema>(
|
||||||
if (!isStartTransformsResponseSchema(results)) {
|
addInternalBasePath('reauthorize_transforms'),
|
||||||
|
{
|
||||||
|
body: JSON.stringify(reqBody),
|
||||||
|
version: '1',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
onError: (error) =>
|
||||||
toastNotifications.addDanger({
|
toastNotifications.addDanger({
|
||||||
title: i18n.translate(
|
title: i18n.translate(
|
||||||
'xpack.transform.stepCreateForm.reauthorizeTransformResponseSchemaErrorMessage',
|
'xpack.transform.stepCreateForm.reauthorizeTransformResponseSchemaErrorMessage',
|
||||||
|
@ -38,31 +46,20 @@ export const useReauthorizeTransforms = () => {
|
||||||
defaultMessage: 'An error occurred calling the reauthorize transforms request.',
|
defaultMessage: 'An error occurred calling the reauthorize transforms request.',
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
text: toMountPoint(
|
text: toMountPoint(<ToastNotificationText text={getErrorMessage(error)} />, {
|
||||||
<ToastNotificationText
|
theme,
|
||||||
overlays={overlays}
|
i18n: i18nStart,
|
||||||
theme={theme}
|
}),
|
||||||
text={getErrorMessage(results)}
|
}),
|
||||||
/>,
|
onSuccess: (results) => {
|
||||||
{ theme$: theme.theme$ }
|
|
||||||
),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const transformId in results) {
|
for (const transformId in results) {
|
||||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||||
if (results.hasOwnProperty(transformId)) {
|
if (results.hasOwnProperty(transformId)) {
|
||||||
const result = results[transformId];
|
const result = results[transformId];
|
||||||
if (result.success === true) {
|
if (!result.success) {
|
||||||
toastNotifications.addSuccess(
|
toastNotifications.addError(
|
||||||
i18n.translate('xpack.transform.transformList.reauthorizeTransformSuccessMessage', {
|
new Error(JSON.stringify(result.error!.caused_by, null, 2)),
|
||||||
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(
|
title: i18n.translate(
|
||||||
'xpack.transform.transformList.reauthorizeTransformErrorMessage',
|
'xpack.transform.transformList.reauthorizeTransformErrorMessage',
|
||||||
{
|
{
|
||||||
|
@ -71,11 +68,15 @@ export const useReauthorizeTransforms = () => {
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
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,73 +6,53 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
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';
|
|
||||||
|
|
||||||
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 = () => {
|
export const useResetTransforms = () => {
|
||||||
const { overlays, theme } = useAppDependencies();
|
const { http, i18n: i18nStart, theme } = useAppDependencies();
|
||||||
|
const refreshTransformList = useRefreshTransformList();
|
||||||
const toastNotifications = useToastNotifications();
|
const toastNotifications = useToastNotifications();
|
||||||
const api = useApi();
|
|
||||||
|
|
||||||
return async (reqBody: ResetTransformsRequestSchema) => {
|
const mutation = useMutation({
|
||||||
const results = await api.resetTransforms(reqBody);
|
mutationFn: (reqBody: ResetTransformsRequestSchema) =>
|
||||||
|
http.post<ResetTransformsResponseSchema>(addInternalBasePath('reset_transforms'), {
|
||||||
if (!isResetTransformsResponseSchema(results)) {
|
body: JSON.stringify(reqBody),
|
||||||
|
version: '1',
|
||||||
|
}),
|
||||||
|
onError: (error) =>
|
||||||
toastNotifications.addDanger({
|
toastNotifications.addDanger({
|
||||||
title: i18n.translate('xpack.transform.transformList.resetTransformGenericErrorMessage', {
|
title: i18n.translate('xpack.transform.transformList.resetTransformGenericErrorMessage', {
|
||||||
defaultMessage: 'An error occurred calling the API endpoint to reset transforms.',
|
defaultMessage: 'An error occurred calling the API endpoint to reset transforms.',
|
||||||
}),
|
}),
|
||||||
text: toMountPoint(
|
text: toMountPoint(
|
||||||
<ToastNotificationText
|
<ToastNotificationText previewTextLength={50} text={getErrorMessage(error)} />,
|
||||||
previewTextLength={50}
|
{
|
||||||
overlays={overlays}
|
theme,
|
||||||
theme={theme}
|
i18n: i18nStart,
|
||||||
text={getErrorMessage(results)}
|
|
||||||
/>,
|
|
||||||
{ theme$: theme.theme$ }
|
|
||||||
),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
),
|
||||||
const isBulk = Object.keys(results).length > 1;
|
}),
|
||||||
const successCount: Record<SuccessCountField, number> = {
|
onSuccess: (results) => {
|
||||||
transformReset: 0,
|
|
||||||
};
|
|
||||||
for (const transformId in results) {
|
for (const transformId in results) {
|
||||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||||
if (results.hasOwnProperty(transformId)) {
|
if (results.hasOwnProperty(transformId)) {
|
||||||
const status = results[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 },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(Object.keys(successCount) as SuccessCountField[]).forEach((key) => {
|
|
||||||
if (status[key]?.success) {
|
|
||||||
successCount[key] = successCount[key] + 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (status.transformReset?.error) {
|
if (status.transformReset?.error) {
|
||||||
const error = status.transformReset.error.reason;
|
const error = status.transformReset.error.reason;
|
||||||
toastNotifications.addDanger({
|
toastNotifications.addDanger({
|
||||||
|
@ -80,33 +60,18 @@ export const useResetTransforms = () => {
|
||||||
defaultMessage: 'An error occurred resetting the transform {transformId}',
|
defaultMessage: 'An error occurred resetting the transform {transformId}',
|
||||||
values: { transformId },
|
values: { transformId },
|
||||||
}),
|
}),
|
||||||
text: toMountPoint(
|
text: toMountPoint(<ToastNotificationText previewTextLength={50} text={error} />, {
|
||||||
<ToastNotificationText
|
theme,
|
||||||
previewTextLength={50}
|
i18n: i18nStart,
|
||||||
overlays={overlays}
|
}),
|
||||||
theme={theme}
|
|
||||||
text={error}
|
|
||||||
/>,
|
|
||||||
{ theme$: theme.theme$ }
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we are deleting multiple transforms, combine the success messages
|
refreshTransformList();
|
||||||
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$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
|
return mutation.mutate;
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,31 +6,38 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||||
|
|
||||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
import { addInternalBasePath } from '../../../common/constants';
|
||||||
|
import type {
|
||||||
import type { ScheduleNowTransformsRequestSchema } from '../../../common/api_schemas/schedule_now_transforms';
|
ScheduleNowTransformsRequestSchema,
|
||||||
import { isScheduleNowTransformsResponseSchema } from '../../../common/api_schemas/type_guards';
|
ScheduleNowTransformsResponseSchema,
|
||||||
|
} from '../../../common/api_schemas/schedule_now_transforms';
|
||||||
import { getErrorMessage } from '../../../common/utils/errors';
|
import { getErrorMessage } from '../../../common/utils/errors';
|
||||||
|
|
||||||
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
||||||
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
|
|
||||||
import { ToastNotificationText } from '../components';
|
import { ToastNotificationText } from '../components';
|
||||||
|
|
||||||
import { useApi } from './use_api';
|
import { useRefreshTransformList } from './use_refresh_transform_list';
|
||||||
|
|
||||||
export const useScheduleNowTransforms = () => {
|
export const useScheduleNowTransforms = () => {
|
||||||
const { overlays, theme } = useAppDependencies();
|
const { http, i18n: i18nStart, theme } = useAppDependencies();
|
||||||
|
const refreshTransformList = useRefreshTransformList();
|
||||||
const toastNotifications = useToastNotifications();
|
const toastNotifications = useToastNotifications();
|
||||||
const api = useApi();
|
|
||||||
|
|
||||||
return async (transformsInfo: ScheduleNowTransformsRequestSchema) => {
|
const mutation = useMutation({
|
||||||
const results = await api.scheduleNowTransforms(transformsInfo);
|
mutationFn: (reqBody: ScheduleNowTransformsRequestSchema) =>
|
||||||
|
http.post<ScheduleNowTransformsResponseSchema>(
|
||||||
if (!isScheduleNowTransformsResponseSchema(results)) {
|
addInternalBasePath('schedule_now_transforms'),
|
||||||
|
{
|
||||||
|
body: JSON.stringify(reqBody),
|
||||||
|
version: '1',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
onError: (error) =>
|
||||||
toastNotifications.addDanger({
|
toastNotifications.addDanger({
|
||||||
title: i18n.translate(
|
title: i18n.translate(
|
||||||
'xpack.transform.stepCreateForm.scheduleNowTransformResponseSchemaErrorMessage',
|
'xpack.transform.stepCreateForm.scheduleNowTransformResponseSchemaErrorMessage',
|
||||||
|
@ -39,32 +46,20 @@ export const useScheduleNowTransforms = () => {
|
||||||
'An error occurred calling the request to schedule the transform to process data instantly.',
|
'An error occurred calling the request to schedule the transform to process data instantly.',
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
text: toMountPoint(
|
text: toMountPoint(<ToastNotificationText text={getErrorMessage(error)} />, {
|
||||||
<ToastNotificationText
|
theme,
|
||||||
overlays={overlays}
|
i18n: i18nStart,
|
||||||
theme={theme}
|
}),
|
||||||
text={getErrorMessage(results)}
|
}),
|
||||||
/>,
|
onSuccess: (results) => {
|
||||||
{ theme$: theme.theme$ }
|
|
||||||
),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const transformId in results) {
|
for (const transformId in results) {
|
||||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||||
if (results.hasOwnProperty(transformId)) {
|
if (results.hasOwnProperty(transformId)) {
|
||||||
const result = results[transformId];
|
const result = results[transformId];
|
||||||
if (result.success === true) {
|
if (!result.success) {
|
||||||
toastNotifications.addSuccess(
|
toastNotifications.addError(
|
||||||
i18n.translate('xpack.transform.transformList.scheduleNowTransformSuccessMessage', {
|
new Error(JSON.stringify(result.error!.caused_by, null, 2)),
|
||||||
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(
|
title: i18n.translate(
|
||||||
'xpack.transform.transformList.scheduleNowTransformErrorMessage',
|
'xpack.transform.transformList.scheduleNowTransformErrorMessage',
|
||||||
{
|
{
|
||||||
|
@ -74,11 +69,15 @@ export const useScheduleNowTransforms = () => {
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
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 React from 'react';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||||
|
|
||||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
import { addInternalBasePath } from '../../../common/constants';
|
||||||
|
import type {
|
||||||
import type { StartTransformsRequestSchema } from '../../../common/api_schemas/start_transforms';
|
StartTransformsRequestSchema,
|
||||||
import { isStartTransformsResponseSchema } from '../../../common/api_schemas/type_guards';
|
StartTransformsResponseSchema,
|
||||||
|
} from '../../../common/api_schemas/start_transforms';
|
||||||
import { getErrorMessage } from '../../../common/utils/errors';
|
import { getErrorMessage } from '../../../common/utils/errors';
|
||||||
|
|
||||||
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
||||||
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
|
|
||||||
import { ToastNotificationText } from '../components';
|
import { ToastNotificationText } from '../components';
|
||||||
|
|
||||||
import { useApi } from './use_api';
|
import { useRefreshTransformList } from './use_refresh_transform_list';
|
||||||
|
|
||||||
export const useStartTransforms = () => {
|
export const useStartTransforms = () => {
|
||||||
const { overlays, theme } = useAppDependencies();
|
const { http, i18n: i18nStart, theme } = useAppDependencies();
|
||||||
|
const refreshTransformList = useRefreshTransformList();
|
||||||
const toastNotifications = useToastNotifications();
|
const toastNotifications = useToastNotifications();
|
||||||
const api = useApi();
|
|
||||||
|
|
||||||
return async (transformsInfo: StartTransformsRequestSchema) => {
|
const mutation = useMutation({
|
||||||
const results = await api.startTransforms(transformsInfo);
|
mutationFn: (reqBody: StartTransformsRequestSchema) =>
|
||||||
|
http.post<StartTransformsResponseSchema>(addInternalBasePath('start_transforms'), {
|
||||||
if (!isStartTransformsResponseSchema(results)) {
|
body: JSON.stringify(reqBody),
|
||||||
|
version: '1',
|
||||||
|
}),
|
||||||
|
onError: (error) =>
|
||||||
toastNotifications.addDanger({
|
toastNotifications.addDanger({
|
||||||
title: i18n.translate(
|
title: i18n.translate(
|
||||||
'xpack.transform.stepCreateForm.startTransformResponseSchemaErrorMessage',
|
'xpack.transform.stepCreateForm.startTransformResponseSchemaErrorMessage',
|
||||||
|
@ -38,41 +42,34 @@ export const useStartTransforms = () => {
|
||||||
defaultMessage: 'An error occurred calling the start transforms request.',
|
defaultMessage: 'An error occurred calling the start transforms request.',
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
text: toMountPoint(
|
text: toMountPoint(<ToastNotificationText text={getErrorMessage(error)} />, {
|
||||||
<ToastNotificationText
|
theme,
|
||||||
overlays={overlays}
|
i18n: i18nStart,
|
||||||
theme={theme}
|
}),
|
||||||
text={getErrorMessage(results)}
|
}),
|
||||||
/>,
|
onSuccess: (results) => {
|
||||||
{ theme$: theme.theme$ }
|
|
||||||
),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const transformId in results) {
|
for (const transformId in results) {
|
||||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||||
if (results.hasOwnProperty(transformId)) {
|
if (results.hasOwnProperty(transformId)) {
|
||||||
const result = results[transformId];
|
const result = results[transformId];
|
||||||
if (result.success === true) {
|
if (!result.success) {
|
||||||
toastNotifications.addSuccess(
|
toastNotifications.addError(
|
||||||
i18n.translate('xpack.transform.transformList.startTransformSuccessMessage', {
|
new Error(JSON.stringify(result.error!.caused_by, null, 2)),
|
||||||
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', {
|
title: i18n.translate('xpack.transform.transformList.startTransformErrorMessage', {
|
||||||
defaultMessage: 'An error occurred starting the transform {transformId}',
|
defaultMessage: 'An error occurred starting the transform {transformId}',
|
||||||
values: { transformId },
|
values: { transformId },
|
||||||
}),
|
}),
|
||||||
toastMessage: result.error!.reason,
|
toastMessage: result.error!.reason,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
|
refreshTransformList();
|
||||||
};
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return mutation.mutate;
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,31 +6,36 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||||
|
|
||||||
import type { StopTransformsRequestSchema } from '../../../common/api_schemas/stop_transforms';
|
|
||||||
import { isStopTransformsResponseSchema } from '../../../common/api_schemas/type_guards';
|
|
||||||
|
|
||||||
|
import { addInternalBasePath } from '../../../common/constants';
|
||||||
|
import type {
|
||||||
|
StopTransformsRequestSchema,
|
||||||
|
StopTransformsResponseSchema,
|
||||||
|
} from '../../../common/api_schemas/stop_transforms';
|
||||||
import { getErrorMessage } from '../../../common/utils/errors';
|
import { getErrorMessage } from '../../../common/utils/errors';
|
||||||
|
|
||||||
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
||||||
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
|
|
||||||
import { ToastNotificationText } from '../components';
|
import { ToastNotificationText } from '../components';
|
||||||
|
|
||||||
import { useApi } from './use_api';
|
import { useRefreshTransformList } from './use_refresh_transform_list';
|
||||||
|
|
||||||
export const useStopTransforms = () => {
|
export const useStopTransforms = () => {
|
||||||
const { overlays, theme } = useAppDependencies();
|
const { http, i18n: i18nStart, theme } = useAppDependencies();
|
||||||
|
const refreshTransformList = useRefreshTransformList();
|
||||||
const toastNotifications = useToastNotifications();
|
const toastNotifications = useToastNotifications();
|
||||||
const api = useApi();
|
|
||||||
|
|
||||||
return async (transformsInfo: StopTransformsRequestSchema) => {
|
const mutation = useMutation({
|
||||||
const results = await api.stopTransforms(transformsInfo);
|
mutationFn: (reqBody: StopTransformsRequestSchema) =>
|
||||||
|
http.post<StopTransformsResponseSchema>(addInternalBasePath('stop_transforms'), {
|
||||||
if (!isStopTransformsResponseSchema(results)) {
|
body: JSON.stringify(reqBody),
|
||||||
|
version: '1',
|
||||||
|
}),
|
||||||
|
onError: (error) =>
|
||||||
toastNotifications.addDanger({
|
toastNotifications.addDanger({
|
||||||
title: i18n.translate(
|
title: i18n.translate(
|
||||||
'xpack.transform.transformList.stopTransformResponseSchemaErrorMessage',
|
'xpack.transform.transformList.stopTransformResponseSchemaErrorMessage',
|
||||||
|
@ -38,29 +43,16 @@ export const useStopTransforms = () => {
|
||||||
defaultMessage: 'An error occurred called the stop transforms request.',
|
defaultMessage: 'An error occurred called the stop transforms request.',
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
text: toMountPoint(
|
text: toMountPoint(<ToastNotificationText text={getErrorMessage(error)} />, {
|
||||||
<ToastNotificationText
|
theme,
|
||||||
overlays={overlays}
|
i18n: i18nStart,
|
||||||
theme={theme}
|
}),
|
||||||
text={getErrorMessage(results)}
|
}),
|
||||||
/>,
|
onSuccess: (results) => {
|
||||||
{ theme$: theme.theme$ }
|
|
||||||
),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const transformId in results) {
|
for (const transformId in results) {
|
||||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||||
if (results.hasOwnProperty(transformId)) {
|
if (results.hasOwnProperty(transformId)) {
|
||||||
if (results[transformId].success === true) {
|
if (!results[transformId].success) {
|
||||||
toastNotifications.addSuccess(
|
|
||||||
i18n.translate('xpack.transform.transformList.stopTransformSuccessMessage', {
|
|
||||||
defaultMessage: 'Request to stop data frame transform {transformId} acknowledged.',
|
|
||||||
values: { transformId },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
toastNotifications.addDanger(
|
toastNotifications.addDanger(
|
||||||
i18n.translate('xpack.transform.transformList.stopTransformErrorMessage', {
|
i18n.translate('xpack.transform.transformList.stopTransformErrorMessage', {
|
||||||
defaultMessage: 'An error occurred stopping the data frame transform {transformId}',
|
defaultMessage: 'An error occurred stopping the data frame transform {transformId}',
|
||||||
|
@ -71,6 +63,9 @@ export const useStopTransforms = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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';
|
} from '@kbn/ml-data-grid';
|
||||||
|
|
||||||
import type { PreviewMappingsProperties } from '../../../common/api_schemas/transforms';
|
import type { PreviewMappingsProperties } from '../../../common/api_schemas/transforms';
|
||||||
import { isPostTransformsPreviewResponseSchema } from '../../../common/api_schemas/type_guards';
|
|
||||||
|
|
||||||
import { getErrorMessage } from '../../../common/utils/errors';
|
import { getErrorMessage } from '../../../common/utils/errors';
|
||||||
|
|
||||||
import { getPreviewTransformRequestBody, type TransformConfigQuery } from '../common';
|
import { getPreviewTransformRequestBody, type TransformConfigQuery } from '../common';
|
||||||
|
|
||||||
import { SearchItems } from './use_search_items';
|
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 { StepDefineExposedState } from '../sections/create_transform/components/step_define';
|
||||||
import {
|
import {
|
||||||
isLatestPartialRequest,
|
isLatestPartialRequest,
|
||||||
|
@ -111,7 +110,6 @@ export const useTransformConfigData = (
|
||||||
): UseIndexDataReturnType => {
|
): UseIndexDataReturnType => {
|
||||||
const [previewMappingsProperties, setPreviewMappingsProperties] =
|
const [previewMappingsProperties, setPreviewMappingsProperties] =
|
||||||
useState<PreviewMappingsProperties>({});
|
useState<PreviewMappingsProperties>({});
|
||||||
const api = useApi();
|
|
||||||
|
|
||||||
// Filters mapping properties of type `object`, which get returned for nested field parents.
|
// Filters mapping properties of type `object`, which get returned for nested field parents.
|
||||||
const columnKeys = Object.keys(previewMappingsProperties).filter(
|
const columnKeys = Object.keys(previewMappingsProperties).filter(
|
||||||
|
@ -147,32 +145,32 @@ export const useTransformConfigData = (
|
||||||
tableItems,
|
tableItems,
|
||||||
} = dataGrid;
|
} = dataGrid;
|
||||||
|
|
||||||
const getPreviewData = async () => {
|
const previewRequest = useMemo(
|
||||||
if (!validationStatus.isValid) {
|
() =>
|
||||||
setTableItems([]);
|
getPreviewTransformRequestBody(
|
||||||
setRowCountInfo({
|
|
||||||
rowCount: 0,
|
|
||||||
rowCountRelation: ES_CLIENT_TOTAL_HITS_RELATION.EQ,
|
|
||||||
});
|
|
||||||
setNoDataMessage(validationStatus.errorMessage!);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrorMessage('');
|
|
||||||
setNoDataMessage('');
|
|
||||||
setStatus(INDEX_STATUS.LOADING);
|
|
||||||
|
|
||||||
const previewRequest = getPreviewTransformRequestBody(
|
|
||||||
dataView,
|
dataView,
|
||||||
query,
|
query,
|
||||||
requestPayload,
|
requestPayload,
|
||||||
combinedRuntimeMappings,
|
combinedRuntimeMappings,
|
||||||
timeRangeMs
|
timeRangeMs
|
||||||
|
),
|
||||||
|
[dataView, query, requestPayload, combinedRuntimeMappings, timeRangeMs]
|
||||||
);
|
);
|
||||||
const resp = await api.getTransformsPreview(previewRequest);
|
|
||||||
|
|
||||||
if (!isPostTransformsPreviewResponseSchema(resp)) {
|
const {
|
||||||
setErrorMessage(getErrorMessage(resp));
|
error: previewError,
|
||||||
|
data: previewData,
|
||||||
|
isError,
|
||||||
|
isLoading,
|
||||||
|
} = useGetTransformsPreview(previewRequest, validationStatus.isValid);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading) {
|
||||||
|
setErrorMessage('');
|
||||||
|
setNoDataMessage('');
|
||||||
|
setStatus(INDEX_STATUS.LOADING);
|
||||||
|
} else if (isError) {
|
||||||
|
setErrorMessage(getErrorMessage(previewError));
|
||||||
setTableItems([]);
|
setTableItems([]);
|
||||||
setRowCountInfo({
|
setRowCountInfo({
|
||||||
rowCount: 0,
|
rowCount: 0,
|
||||||
|
@ -180,14 +178,12 @@ export const useTransformConfigData = (
|
||||||
});
|
});
|
||||||
setPreviewMappingsProperties({});
|
setPreviewMappingsProperties({});
|
||||||
setStatus(INDEX_STATUS.ERROR);
|
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
|
// 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.
|
// 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
|
// 1. Flatten the returned object structure object documents to match mapping properties
|
||||||
const docs = resp.preview.map(getFlattenedObject);
|
const docs = previewData.preview.map(getFlattenedObject);
|
||||||
|
|
||||||
// 2. Get all field names for each returned doc and flatten it
|
// 2. Get all field names for each returned doc and flatten it
|
||||||
// to a list of unique field names used across all docs.
|
// to a list of unique field names used across all docs.
|
||||||
|
@ -195,7 +191,7 @@ export const useTransformConfigData = (
|
||||||
|
|
||||||
// 3. Filter mapping properties by populated fields
|
// 3. Filter mapping properties by populated fields
|
||||||
let populatedProperties: PreviewMappingsProperties = Object.entries(
|
let populatedProperties: PreviewMappingsProperties = Object.entries(
|
||||||
resp.generated_dest_index.mappings.properties
|
previewData.generated_dest_index.mappings.properties
|
||||||
)
|
)
|
||||||
.filter(([key]) => populatedFields.includes(key))
|
.filter(([key]) => populatedFields.includes(key))
|
||||||
.reduce(
|
.reduce(
|
||||||
|
@ -223,8 +219,26 @@ export const useTransformConfigData = (
|
||||||
'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.',
|
'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]);
|
||||||
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
resetPagination();
|
resetPagination();
|
||||||
|
@ -232,15 +246,6 @@ export const useTransformConfigData = (
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [JSON.stringify(query)]);
|
}, [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) {
|
if (sortingColumns.length > 0) {
|
||||||
const sortingColumnsWithTypes = sortingColumns.map((c) => {
|
const sortingColumnsWithTypes = sortingColumns.map((c) => {
|
||||||
// Since items might contain undefined/null values, we want to accurate find the data type
|
// Since items might contain undefined/null values, we want to accurate find the data type
|
||||||
|
@ -291,13 +296,7 @@ export const useTransformConfigData = (
|
||||||
|
|
||||||
return cellValue;
|
return cellValue;
|
||||||
};
|
};
|
||||||
}, [
|
}, [pageData, pagination.pageIndex, pagination.pageSize, previewMappingsProperties]);
|
||||||
pageData,
|
|
||||||
pagination.pageIndex,
|
|
||||||
pagination.pageSize,
|
|
||||||
previewMappingsProperties,
|
|
||||||
formatHumanReadableDateTimeSeconds,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...dataGrid,
|
...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 { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
import { EuiButtonEmpty, EuiCallOut, EuiPageTemplate, EuiSpacer } from '@elastic/eui';
|
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 { TransformConfigUnion } from '../../../../common/types/transform';
|
||||||
|
|
||||||
import { useApi } from '../../hooks/use_api';
|
import { useGetTransform } from '../../hooks';
|
||||||
import { useDocumentationLinks } from '../../hooks/use_documentation_links';
|
import { useDocumentationLinks } from '../../hooks/use_documentation_links';
|
||||||
import { useSearchItems } from '../../hooks/use_search_items';
|
import { useSearchItems } from '../../hooks/use_search_items';
|
||||||
|
|
||||||
import { BREADCRUMB_SECTION, breadcrumbService, docTitleService } from '../../services/navigation';
|
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 { Wizard } from '../create_transform/components/wizard';
|
||||||
import { overrideTransformForCloning } from '../../common/transform';
|
import { overrideTransformForCloning } from '../../common/transform';
|
||||||
|
@ -39,8 +38,6 @@ export const CloneTransformSection: FC<Props> = ({ match, location }) => {
|
||||||
docTitleService.setTitle('createTransform');
|
docTitleService.setTitle('createTransform');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const api = useApi();
|
|
||||||
|
|
||||||
const { esTransform } = useDocumentationLinks();
|
const { esTransform } = useDocumentationLinks();
|
||||||
|
|
||||||
const transformId = match.params.transformId;
|
const transformId = match.params.transformId;
|
||||||
|
@ -50,33 +47,41 @@ export const CloneTransformSection: FC<Props> = ({ match, location }) => {
|
||||||
const [isInitialized, setIsInitialized] = useState(false);
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
const { error: searchItemsError, searchItems, setSavedObjectId } = useSearchItems(undefined);
|
const { error: searchItemsError, searchItems, setSavedObjectId } = useSearchItems(undefined);
|
||||||
|
|
||||||
const fetchTransformConfig = async () => {
|
useEffect(() => {
|
||||||
if (searchItemsError !== undefined) {
|
|
||||||
setTransformConfig(undefined);
|
|
||||||
setErrorMessage(searchItemsError);
|
|
||||||
setIsInitialized(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const transformConfigs = await api.getTransform(transformId);
|
|
||||||
if (isHttpFetchError(transformConfigs)) {
|
|
||||||
setTransformConfig(undefined);
|
|
||||||
setErrorMessage(transformConfigs.message);
|
|
||||||
setIsInitialized(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (dataViewId === undefined) {
|
if (dataViewId === undefined) {
|
||||||
throw new Error(
|
setErrorMessage(
|
||||||
i18n.translate('xpack.transform.clone.fetchErrorPromptText', {
|
i18n.translate('xpack.transform.clone.fetchErrorPromptText', {
|
||||||
defaultMessage: 'Could not fetch the Kibana data view ID.',
|
defaultMessage: 'Could not fetch the Kibana data view ID.',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
setSavedObjectId(dataViewId);
|
||||||
|
}
|
||||||
|
}, [dataViewId, setSavedObjectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchItemsError !== undefined) {
|
||||||
|
setTransformConfig(undefined);
|
||||||
|
setErrorMessage(searchItemsError);
|
||||||
|
setIsInitialized(true);
|
||||||
|
}
|
||||||
|
}, [searchItemsError]);
|
||||||
|
|
||||||
|
const { data: transformConfigs, error } = useGetTransform(
|
||||||
|
transformId,
|
||||||
|
searchItemsError === undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error !== null && error.message !== errorMessage) {
|
||||||
|
setTransformConfig(undefined);
|
||||||
|
setErrorMessage(error.message);
|
||||||
|
setIsInitialized(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSavedObjectId(dataViewId);
|
if (transformConfigs !== undefined) {
|
||||||
|
try {
|
||||||
setTransformConfig(overrideTransformForCloning(transformConfigs.transforms[0]));
|
setTransformConfig(overrideTransformForCloning(transformConfigs.transforms[0]));
|
||||||
setErrorMessage(undefined);
|
setErrorMessage(undefined);
|
||||||
setIsInitialized(true);
|
setIsInitialized(true);
|
||||||
|
@ -89,13 +94,8 @@ export const CloneTransformSection: FC<Props> = ({ match, location }) => {
|
||||||
}
|
}
|
||||||
setIsInitialized(true);
|
setIsInitialized(true);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
}, [error, errorMessage, transformConfigs]);
|
||||||
useEffect(() => {
|
|
||||||
fetchTransformConfig();
|
|
||||||
// The effect should only be called once.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const docsLink = (
|
const docsLink = (
|
||||||
<EuiButtonEmpty
|
<EuiButtonEmpty
|
||||||
|
@ -112,7 +112,14 @@ export const CloneTransformSection: FC<Props> = ({ match, location }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PrivilegesWrapper privileges={APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES}>
|
<CapabilitiesWrapper
|
||||||
|
requiredCapabilities={[
|
||||||
|
'canGetTransform',
|
||||||
|
'canPreviewTransform',
|
||||||
|
'canCreateTransform',
|
||||||
|
'canStartStopTransform',
|
||||||
|
]}
|
||||||
|
>
|
||||||
<EuiPageTemplate.Header
|
<EuiPageTemplate.Header
|
||||||
pageTitle={
|
pageTitle={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -147,6 +154,6 @@ export const CloneTransformSection: FC<Props> = ({ match, location }) => {
|
||||||
<Wizard cloneConfig={transformConfig} searchItems={searchItems} />
|
<Wizard cloneConfig={transformConfig} searchItems={searchItems} />
|
||||||
)}
|
)}
|
||||||
</EuiPageTemplate.Section>
|
</EuiPageTemplate.Section>
|
||||||
</PrivilegesWrapper>
|
</CapabilitiesWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSwitch } from '@elastic/eui';
|
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSwitch } from '@elastic/eui';
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { EuiSwitch } from '@elastic/eui';
|
import { EuiSwitch } from '@elastic/eui';
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { EuiSwitch } from '@elastic/eui';
|
import { EuiSwitch } from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { SwitchModal } from './switch_modal';
|
import { SwitchModal } from './switch_modal';
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { EuiConfirmModal } from '@elastic/eui';
|
import { EuiConfirmModal } from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EuiButton,
|
EuiButton,
|
||||||
EuiButtonIcon,
|
EuiButtonIcon,
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
import { AggName } from '../../../../../../common/types/aggregations';
|
import { AggName } from '../../../../../../common/types/aggregations';
|
||||||
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
|
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
|
||||||
|
@ -31,8 +31,8 @@ describe('Transform: <AggLabelForm />', () => {
|
||||||
onChange() {},
|
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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
|
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
|
||||||
|
|
||||||
|
@ -29,8 +29,8 @@ describe('Transform: <AggListForm />', () => {
|
||||||
onChange() {},
|
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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
|
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
|
||||||
|
|
||||||
|
@ -26,8 +26,8 @@ describe('Transform: <AggListSummary />', () => {
|
||||||
list: { 'the-agg': item },
|
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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import { fireEvent, render } from '@testing-library/react';
|
import { fireEvent, render } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AggName } from '../../../../../../common/types/aggregations';
|
import { AggName } from '../../../../../../common/types/aggregations';
|
||||||
|
@ -29,7 +28,7 @@ describe('Transform: Aggregation <PopoverForm />', () => {
|
||||||
const otherAggNames: AggName[] = [];
|
const otherAggNames: AggName[] = [];
|
||||||
const onChange = (item: PivotAggsConfig) => {};
|
const onChange = (item: PivotAggsConfig) => {};
|
||||||
|
|
||||||
const wrapper = shallow(
|
const { getByTestId } = render(
|
||||||
<PopoverForm
|
<PopoverForm
|
||||||
defaultData={defaultData}
|
defaultData={defaultData}
|
||||||
otherAggNames={otherAggNames}
|
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 () => {
|
test('preserves the field for unsupported aggs', async () => {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { EuiSwitch } from '@elastic/eui';
|
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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common';
|
import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common';
|
||||||
|
|
||||||
|
@ -29,9 +29,9 @@ describe('Transform: <GroupByLabelForm />', () => {
|
||||||
onChange() {},
|
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', () => {
|
test('Histogram aggregation', () => {
|
||||||
|
@ -50,9 +50,9 @@ describe('Transform: <GroupByLabelForm />', () => {
|
||||||
onChange() {},
|
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', () => {
|
test('Terms aggregation', () => {
|
||||||
|
@ -70,8 +70,8 @@ describe('Transform: <GroupByLabelForm />', () => {
|
||||||
onChange() {},
|
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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common';
|
import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common';
|
||||||
|
|
||||||
|
@ -26,9 +26,9 @@ describe('Transform: <GroupByLabelSummary />', () => {
|
||||||
optionsDataId: 'the-options-data-id',
|
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', () => {
|
test('Histogram aggregation', () => {
|
||||||
|
@ -44,9 +44,9 @@ describe('Transform: <GroupByLabelSummary />', () => {
|
||||||
optionsDataId: 'the-options-data-id',
|
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', () => {
|
test('Terms aggregation', () => {
|
||||||
|
@ -61,8 +61,8 @@ describe('Transform: <GroupByLabelSummary />', () => {
|
||||||
optionsDataId: 'the-options-data-id',
|
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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common';
|
import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common';
|
||||||
|
|
||||||
|
@ -27,8 +27,8 @@ describe('Transform: <GroupByListForm />', () => {
|
||||||
onChange() {},
|
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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common';
|
import { PivotGroupByConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../common';
|
||||||
|
|
||||||
|
@ -24,8 +24,8 @@ describe('Transform: <GroupByListSummary />', () => {
|
||||||
list: { 'the-options-data-id': item },
|
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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ describe('Transform: Group By <PopoverForm />', () => {
|
||||||
appName: 'the-test-app',
|
appName: 'the-test-app',
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallow(
|
const { getByDisplayValue } = render(
|
||||||
<KibanaContextProvider services={services}>
|
<KibanaContextProvider services={services}>
|
||||||
<PopoverForm
|
<PopoverForm
|
||||||
defaultData={defaultData}
|
defaultData={defaultData}
|
||||||
|
@ -112,6 +112,7 @@ describe('Transform: Group By <PopoverForm />', () => {
|
||||||
</KibanaContextProvider>
|
</KibanaContextProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper.find(PopoverForm)).toMatchSnapshot();
|
expect(getByDisplayValue('the-agg-name')).toBeInTheDocument();
|
||||||
|
expect(getByDisplayValue('1m')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { EuiCode, EuiInputPopover } from '@elastic/eui';
|
import { EuiCode, EuiInputPopover } from '@elastic/eui';
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { StepCreateForm, StepCreateFormProps } from './step_create_form';
|
import { StepCreateForm, StepCreateFormProps } from './step_create_form';
|
||||||
|
|
||||||
|
@ -16,6 +17,7 @@ jest.mock('../../../../app_dependencies');
|
||||||
describe('Transform: <StepCreateForm />', () => {
|
describe('Transform: <StepCreateForm />', () => {
|
||||||
test('Minimal initialization', () => {
|
test('Minimal initialization', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
const queryClient = new QueryClient();
|
||||||
const props: StepCreateFormProps = {
|
const props: StepCreateFormProps = {
|
||||||
createDataView: false,
|
createDataView: false,
|
||||||
transformId: 'the-transform-id',
|
transformId: 'the-transform-id',
|
||||||
|
@ -35,7 +37,11 @@ describe('Transform: <StepCreateForm />', () => {
|
||||||
onChange() {},
|
onChange() {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { getByText } = render(<StepCreateForm {...props} />);
|
const { getByText } = render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<StepCreateForm {...props} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
// Assert
|
// Assert
|
||||||
|
|
|
@ -24,25 +24,19 @@ import {
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
|
||||||
import { FormattedMessage } from '@kbn/i18n-react';
|
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 { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common';
|
||||||
|
|
||||||
import { DuplicateDataViewError } from '@kbn/data-plugin/public';
|
import { DuplicateDataViewError } from '@kbn/data-plugin/public';
|
||||||
import type { RuntimeField } from '@kbn/data-views-plugin/common';
|
import type { RuntimeField } from '@kbn/data-views-plugin/common';
|
||||||
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
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 { PROGRESS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants';
|
||||||
|
|
||||||
import { getErrorMessage } from '../../../../../../common/utils/errors';
|
import { getErrorMessage } from '../../../../../../common/utils/errors';
|
||||||
|
|
||||||
import { getTransformProgress } from '../../../../common';
|
import { getTransformProgress } from '../../../../common';
|
||||||
import { useApi } from '../../../../hooks/use_api';
|
import { useCreateTransform, useGetTransformStats, useStartTransforms } from '../../../../hooks';
|
||||||
import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies';
|
import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies';
|
||||||
import { RedirectToTransformManagement } from '../../../../common/navigation';
|
import { RedirectToTransformManagement } from '../../../../common/navigation';
|
||||||
import { ToastNotificationText } from '../../../../components';
|
import { ToastNotificationText } from '../../../../components';
|
||||||
|
@ -92,11 +86,10 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
||||||
);
|
);
|
||||||
const [discoverLink, setDiscoverLink] = useState<string>();
|
const [discoverLink, setDiscoverLink] = useState<string>();
|
||||||
|
|
||||||
const deps = useAppDependencies();
|
|
||||||
const { share } = deps;
|
|
||||||
const dataViews = deps.data.dataViews;
|
|
||||||
const toastNotifications = useToastNotifications();
|
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(() => {
|
useEffect(() => {
|
||||||
let unmounted = false;
|
let unmounted = false;
|
||||||
|
@ -128,104 +121,38 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [created, started, dataViewId]);
|
}, [created, started, dataViewId]);
|
||||||
|
|
||||||
const { overlays, theme } = useAppDependencies();
|
const startTransforms = useStartTransforms();
|
||||||
const api = useApi();
|
const createTransform = useCreateTransform();
|
||||||
|
|
||||||
async function createTransform() {
|
function createTransformHandler(startAfterCreation = false) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const resp = await api.createTransform(transformId, transformConfig);
|
createTransform(
|
||||||
|
{ transformId, transformConfig },
|
||||||
if (!isPutTransformsResponseSchema(resp) || resp.errors.length > 0) {
|
{
|
||||||
let respErrors:
|
onError: () => setCreated(false),
|
||||||
| PutTransformsResponseSchema['errors']
|
onSuccess: () => {
|
||||||
| PutTransformsResponseSchema['errors'][number]
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
if (isPutTransformsResponseSchema(resp) && resp.errors.length > 0) {
|
|
||||||
respErrors = resp.errors.length === 1 ? resp.errors[0] : resp.errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
setCreated(true);
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
if (createDataView) {
|
if (createDataView) {
|
||||||
createKibanaDataView();
|
createKibanaDataView();
|
||||||
}
|
}
|
||||||
|
if (startAfterCreation) {
|
||||||
return true;
|
startTransform();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSettled: () => setLoading(false),
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startTransform() {
|
function startTransform() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const resp = await api.startTransforms([{ id: transformId }]);
|
startTransforms([{ id: transformId }], {
|
||||||
|
onError: () => setStarted(false),
|
||||||
if (isStartTransformsResponseSchema(resp) && resp[transformId]?.success === true) {
|
onSuccess: (resp) => setStarted(resp[transformId]?.success === true),
|
||||||
toastNotifications.addSuccess(
|
onSettled: () => setLoading(false),
|
||||||
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$ }
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
setStarted(false);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createAndStartTransform() {
|
|
||||||
const acknowledged = await createTransform();
|
|
||||||
if (acknowledged) {
|
|
||||||
await startTransform();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createKibanaDataView = async () => {
|
const createKibanaDataView = async () => {
|
||||||
|
@ -250,13 +177,6 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
toastNotifications.addSuccess(
|
|
||||||
i18n.translate('xpack.transform.stepCreateForm.createDataViewSuccessMessage', {
|
|
||||||
defaultMessage: 'Kibana data view {dataViewName} created successfully.',
|
|
||||||
values: { dataViewName },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setDataViewId(newDataView.id);
|
setDataViewId(newDataView.id);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return true;
|
return true;
|
||||||
|
@ -275,10 +195,10 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
||||||
defaultMessage: 'An error occurred creating the Kibana data view {dataViewName}:',
|
defaultMessage: 'An error occurred creating the Kibana data view {dataViewName}:',
|
||||||
values: { dataViewName },
|
values: { dataViewName },
|
||||||
}),
|
}),
|
||||||
text: toMountPoint(
|
text: toMountPoint(<ToastNotificationText text={getErrorMessage(e)} />, {
|
||||||
<ToastNotificationText overlays={overlays} theme={theme} text={getErrorMessage(e)} />,
|
theme,
|
||||||
{ theme$: theme.theme$ }
|
i18n: i18nStart,
|
||||||
),
|
}),
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return false;
|
return false;
|
||||||
|
@ -288,22 +208,37 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
||||||
|
|
||||||
const isBatchTransform = typeof transformConfig.sync === 'undefined';
|
const isBatchTransform = typeof transformConfig.sync === 'undefined';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
loading === false &&
|
loading === false &&
|
||||||
started === true &&
|
started === true &&
|
||||||
progressPercentComplete === undefined &&
|
progressPercentComplete === undefined &&
|
||||||
isBatchTransform
|
isBatchTransform
|
||||||
) {
|
) {
|
||||||
// wrapping in function so we can keep the interval id in local scope
|
setProgressPercentComplete(0);
|
||||||
function startProgressBar() {
|
}
|
||||||
const interval = setInterval(async () => {
|
}, [loading, started, progressPercentComplete, isBatchTransform]);
|
||||||
const stats = await api.getTransformStats(transformId);
|
|
||||||
|
|
||||||
if (
|
const progressBarRefetchEnabled =
|
||||||
isGetTransformsStatsResponseSchema(stats) &&
|
isBatchTransform &&
|
||||||
Array.isArray(stats.transforms) &&
|
typeof progressPercentComplete === 'number' &&
|
||||||
stats.transforms.length > 0
|
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 =
|
const percent =
|
||||||
getTransformProgress({
|
getTransformProgress({
|
||||||
id: transformId,
|
id: transformId,
|
||||||
|
@ -314,31 +249,18 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
||||||
stats: stats.transforms[0],
|
stats: stats.transforms[0],
|
||||||
}) || 0;
|
}) || 0;
|
||||||
setProgressPercentComplete(percent);
|
setProgressPercentComplete(percent);
|
||||||
if (percent >= 100) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
toastNotifications.addDanger({
|
toastNotifications.addDanger({
|
||||||
title: i18n.translate('xpack.transform.stepCreateForm.progressErrorMessage', {
|
title: i18n.translate('xpack.transform.stepCreateForm.progressErrorMessage', {
|
||||||
defaultMessage: 'An error occurred getting the progress percentage:',
|
defaultMessage: 'An error occurred getting the progress percentage:',
|
||||||
}),
|
}),
|
||||||
text: toMountPoint(
|
text: toMountPoint(<ToastNotificationText text={getErrorMessage(stats)} />, {
|
||||||
<ToastNotificationText
|
theme,
|
||||||
overlays={overlays}
|
i18n: i18nStart,
|
||||||
theme={theme}
|
}),
|
||||||
text={getErrorMessage(stats)}
|
|
||||||
/>,
|
|
||||||
{ theme$: theme.theme$ }
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, PROGRESS_REFRESH_INTERVAL_MS);
|
|
||||||
setProgressPercentComplete(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
startProgressBar();
|
|
||||||
}
|
}
|
||||||
|
}, [i18nStart, stats, theme, toastNotifications, transformConfig, transformId]);
|
||||||
|
|
||||||
function getTransformConfigDevConsoleStatement() {
|
function getTransformConfigDevConsoleStatement() {
|
||||||
return `PUT _transform/${transformId}\n${JSON.stringify(transformConfig, null, 2)}\n\n`;
|
return `PUT _transform/${transformId}\n${JSON.stringify(transformConfig, null, 2)}\n\n`;
|
||||||
|
@ -362,7 +284,7 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
||||||
<EuiButton
|
<EuiButton
|
||||||
fill
|
fill
|
||||||
isDisabled={loading || (created && started)}
|
isDisabled={loading || (created && started)}
|
||||||
onClick={createAndStartTransform}
|
onClick={() => createTransformHandler(true)}
|
||||||
data-test-subj="transformWizardCreateAndStartButton"
|
data-test-subj="transformWizardCreateAndStartButton"
|
||||||
>
|
>
|
||||||
{i18n.translate('xpack.transform.stepCreateForm.createAndStartTransformButton', {
|
{i18n.translate('xpack.transform.stepCreateForm.createAndStartTransformButton', {
|
||||||
|
@ -436,7 +358,7 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
||||||
<EuiFlexItem grow={false} style={FLEX_ITEM_STYLE}>
|
<EuiFlexItem grow={false} style={FLEX_ITEM_STYLE}>
|
||||||
<EuiButton
|
<EuiButton
|
||||||
isDisabled={loading || created}
|
isDisabled={loading || created}
|
||||||
onClick={createTransform}
|
onClick={() => createTransformHandler()}
|
||||||
data-test-subj="transformWizardCreateButton"
|
data-test-subj="transformWizardCreateButton"
|
||||||
>
|
>
|
||||||
{i18n.translate('xpack.transform.stepCreateForm.createTransformButton', {
|
{i18n.translate('xpack.transform.stepCreateForm.createTransformButton', {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
export const StepCreateSummary: FC = React.memo(() => {
|
export const StepCreateSummary: FC = React.memo(() => {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { debounce } from 'lodash';
|
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 useUpdateEffect from 'react-use/lib/useUpdateEffect';
|
||||||
|
|
||||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
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 { isMultiBucketAggregate } from '@kbn/ml-agg-utils';
|
||||||
|
|
||||||
import { useDataSearch } from '../../../../../../../hooks/use_data_search';
|
import { useDataSearch } from '../../../../../../../hooks/use_data_search';
|
||||||
import { isEsSearchResponseWithAggregations } from '../../../../../../../../../common/api_schemas/type_guards';
|
|
||||||
import { CreateTransformWizardContext } from '../../../../wizard/wizard';
|
import { CreateTransformWizardContext } from '../../../../wizard/wizard';
|
||||||
import { useToastNotifications } from '../../../../../../../app_dependencies';
|
import { useToastNotifications } from '../../../../../../../app_dependencies';
|
||||||
|
|
||||||
|
@ -33,16 +32,22 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm
|
||||||
selectedField,
|
selectedField,
|
||||||
}) => {
|
}) => {
|
||||||
const { dataView, runtimeMappings } = useContext(CreateTransformWizardContext);
|
const { dataView, runtimeMappings } = useContext(CreateTransformWizardContext);
|
||||||
const dataSearch = useDataSearch();
|
|
||||||
const toastNotifications = useToastNotifications();
|
const toastNotifications = useToastNotifications();
|
||||||
|
|
||||||
const [options, setOptions] = useState<EuiComboBoxOptionOption[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
const debouncedOnSearchChange = useMemo(
|
||||||
|
() => debounce((d: string) => setSearchValue(d), 600),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const onSearchChange = (newSearchValue: string) => {
|
useEffect(() => {
|
||||||
setSearchValue(newSearchValue);
|
// 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(
|
const updateConfig = useCallback(
|
||||||
(update) => {
|
(update) => {
|
||||||
|
@ -56,16 +61,8 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm
|
||||||
[config, onChange]
|
[config, onChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const { data, isError, isLoading } = useDataSearch(
|
||||||
const abortController = new AbortController();
|
{
|
||||||
|
|
||||||
const fetchOptions = debounce(async () => {
|
|
||||||
if (selectedField === undefined) return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
setOptions([]);
|
|
||||||
|
|
||||||
const esSearchRequest = {
|
|
||||||
index: dataView!.title,
|
index: dataView!.title,
|
||||||
body: {
|
body: {
|
||||||
...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}),
|
...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}),
|
||||||
|
@ -86,50 +83,31 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm
|
||||||
},
|
},
|
||||||
size: 0,
|
size: 0,
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
// Check whether fetching should be enabled
|
||||||
|
selectedField !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
const response = await dataSearch(esSearchRequest, abortController.signal);
|
useEffect(() => {
|
||||||
|
if (isError) {
|
||||||
setIsLoading(false);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
isEsSearchResponseWithAggregations(response) &&
|
|
||||||
isMultiBucketAggregate<estypes.AggregationsSignificantLongTermsBucket>(
|
|
||||||
response.aggregations.field_values
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
toastNotifications.addWarning(
|
toastNotifications.addWarning(
|
||||||
i18n.translate('xpack.transform.agg.popoverForm.filerAgg.term.errorFetchSuggestions', {
|
i18n.translate('xpack.transform.agg.popoverForm.filerAgg.term.errorFetchSuggestions', {
|
||||||
defaultMessage: 'Unable to fetch suggestions',
|
defaultMessage: 'Unable to fetch suggestions',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||||
|
}, [isError]);
|
||||||
|
|
||||||
setOptions(
|
const options: EuiComboBoxOptionOption[] =
|
||||||
(
|
isMultiBucketAggregate<estypes.AggregationsSignificantLongTermsBucket>(
|
||||||
response.aggregations.field_values
|
data?.aggregations?.field_values
|
||||||
|
)
|
||||||
|
? (
|
||||||
|
data?.aggregations?.field_values
|
||||||
.buckets as estypes.AggregationsSignificantLongTermsBucket[]
|
.buckets as estypes.AggregationsSignificantLongTermsBucket[]
|
||||||
).map((value) => ({ label: value.key + '' }))
|
).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(() => {
|
useUpdateEffect(() => {
|
||||||
// Reset value control on field change
|
// Reset value control on field change
|
||||||
|
@ -168,7 +146,7 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm
|
||||||
onCreateOption={(value) => {
|
onCreateOption={(value) => {
|
||||||
updateConfig({ value });
|
updateConfig({ value });
|
||||||
}}
|
}}
|
||||||
onSearchChange={onSearchChange}
|
onSearchChange={debouncedOnSearchChange}
|
||||||
data-test-subj="transformFilterTermValueSelector"
|
data-test-subj="transformFilterTermValueSelector"
|
||||||
/>
|
/>
|
||||||
</EuiFormRow>
|
</EuiFormRow>
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { FormattedMessage } from '@kbn/i18n-react';
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
import { EuiButtonIcon, EuiCallOut, EuiComboBox, EuiCopy, EuiFormRow } from '@elastic/eui';
|
import { EuiButtonIcon, EuiCallOut, EuiComboBox, EuiCopy, EuiFormRow } from '@elastic/eui';
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EuiButton,
|
EuiButton,
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, waitFor } from '@testing-library/react';
|
import { render, waitFor } from '@testing-library/react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { I18nProvider } from '@kbn/i18n-react';
|
import { I18nProvider } from '@kbn/i18n-react';
|
||||||
import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker';
|
import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker';
|
||||||
|
@ -66,6 +67,7 @@ const createMockStorage = () => ({
|
||||||
describe('Transform: <DefinePivotForm />', () => {
|
describe('Transform: <DefinePivotForm />', () => {
|
||||||
test('Minimal initialization', async () => {
|
test('Minimal initialization', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
const queryClient = new QueryClient();
|
||||||
const mlSharedImports = await getMlSharedImports();
|
const mlSharedImports = await getMlSharedImports();
|
||||||
|
|
||||||
const searchItems = {
|
const searchItems = {
|
||||||
|
@ -87,6 +89,7 @@ describe('Transform: <DefinePivotForm />', () => {
|
||||||
|
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<KibanaContextProvider services={services}>
|
<KibanaContextProvider services={services}>
|
||||||
<MlSharedContext.Provider value={mlSharedImports}>
|
<MlSharedContext.Provider value={mlSharedImports}>
|
||||||
<DatePickerContextProvider {...getMockedDatePickerDependencies()}>
|
<DatePickerContextProvider {...getMockedDatePickerDependencies()}>
|
||||||
|
@ -94,6 +97,7 @@ describe('Transform: <DefinePivotForm />', () => {
|
||||||
</DatePickerContextProvider>
|
</DatePickerContextProvider>
|
||||||
</MlSharedContext.Provider>
|
</MlSharedContext.Provider>
|
||||||
</KibanaContextProvider>
|
</KibanaContextProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,9 @@ const ALLOW_TIME_RANGE_ON_TRANSFORM_CONFIG = false;
|
||||||
|
|
||||||
const advancedEditorsSidebarWidth = '220px';
|
const advancedEditorsSidebarWidth = '220px';
|
||||||
|
|
||||||
|
type PopulatedFields = Set<string>;
|
||||||
|
const isPopulatedFields = (arg: unknown): arg is PopulatedFields => arg instanceof Set;
|
||||||
|
|
||||||
export const ConfigSectionTitle: FC<{ title: string }> = ({ title }) => (
|
export const ConfigSectionTitle: FC<{ title: string }> = ({ title }) => (
|
||||||
<>
|
<>
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
|
@ -132,7 +135,9 @@ export const StepDefineForm: FC<StepDefineFormProps> = React.memo((props) => {
|
||||||
transformConfigQuery,
|
transformConfigQuery,
|
||||||
runtimeMappings,
|
runtimeMappings,
|
||||||
timeRangeMs,
|
timeRangeMs,
|
||||||
fieldStatsContext?.populatedFields ?? null
|
isPopulatedFields(fieldStatsContext?.populatedFields)
|
||||||
|
? [...fieldStatsContext.populatedFields]
|
||||||
|
: []
|
||||||
),
|
),
|
||||||
dataTestSubj: 'transformIndexPreview',
|
dataTestSubj: 'transformIndexPreview',
|
||||||
toastNotifications,
|
toastNotifications,
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, waitFor } from '@testing-library/react';
|
import { render, waitFor } from '@testing-library/react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
|
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.
|
// Using the async/await wait()/done() pattern to avoid act() errors.
|
||||||
test('Minimal initialization', async () => {
|
test('Minimal initialization', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
const queryClient = new QueryClient();
|
||||||
const mlSharedImports = await getMlSharedImports();
|
const mlSharedImports = await getMlSharedImports();
|
||||||
|
|
||||||
const searchItems = {
|
const searchItems = {
|
||||||
|
@ -78,9 +80,11 @@ describe('Transform: <DefinePivotSummary />', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const { queryByText } = render(
|
const { queryByText } = render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<MlSharedContext.Provider value={mlSharedImports}>
|
<MlSharedContext.Provider value={mlSharedImports}>
|
||||||
<StepDefineSummary formState={formState} searchItems={searchItems as SearchItems} />
|
<StepDefineSummary formState={formState} searchItems={searchItems as SearchItems} />
|
||||||
</MlSharedContext.Provider>
|
</MlSharedContext.Provider>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui';
|
import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui';
|
||||||
import { TRANSFORM_FUNCTION, TransformFunction } from '../../../../../../common/constants';
|
import { TRANSFORM_FUNCTION, TransformFunction } from '../../../../../../common/constants';
|
||||||
|
|
|
@ -25,15 +25,9 @@ import {
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
|
||||||
import { KBN_FIELD_TYPES } from '@kbn/field-types';
|
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 { retentionPolicyMaxAgeInvalidErrorMessage } from '../../../../common/constants/validation_messages';
|
||||||
import {
|
|
||||||
isEsIndices,
|
|
||||||
isEsIngestPipelines,
|
|
||||||
isPostTransformsPreviewResponseSchema,
|
|
||||||
} from '../../../../../../common/api_schemas/type_guards';
|
|
||||||
import { DEFAULT_TRANSFORM_FREQUENCY } from '../../../../../../common/constants';
|
import { DEFAULT_TRANSFORM_FREQUENCY } from '../../../../../../common/constants';
|
||||||
import { TransformId } from '../../../../../../common/types/transform';
|
import { TransformId } from '../../../../../../common/types/transform';
|
||||||
import { isValidIndexName } from '../../../../../../common/utils/es_utils';
|
import { isValidIndexName } from '../../../../../../common/utils/es_utils';
|
||||||
|
@ -42,16 +36,22 @@ import { getErrorMessage } from '../../../../../../common/utils/errors';
|
||||||
|
|
||||||
import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies';
|
import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies';
|
||||||
import { ToastNotificationText } from '../../../../components';
|
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 { SearchItems } from '../../../../hooks/use_search_items';
|
||||||
import { useApi } from '../../../../hooks/use_api';
|
|
||||||
import { StepDetailsTimeField } from './step_details_time_field';
|
import { StepDetailsTimeField } from './step_details_time_field';
|
||||||
import {
|
import {
|
||||||
getTransformConfigQuery,
|
getTransformConfigQuery,
|
||||||
getPreviewTransformRequestBody,
|
getPreviewTransformRequestBody,
|
||||||
isTransformIdValid,
|
isTransformIdValid,
|
||||||
} from '../../../../common';
|
} from '../../../../common';
|
||||||
import { EsIndexName, DataViewTitle } from './common';
|
import { EsIndexName } from './common';
|
||||||
import {
|
import {
|
||||||
continuousModeDelayValidator,
|
continuousModeDelayValidator,
|
||||||
integerRangeMinus1To100Validator,
|
integerRangeMinus1To100Validator,
|
||||||
|
@ -73,8 +73,8 @@ interface StepDetailsFormProps {
|
||||||
|
|
||||||
export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
|
export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
|
||||||
({ overrides = {}, onChange, searchItems, stepDefineState }) => {
|
({ overrides = {}, onChange, searchItems, stepDefineState }) => {
|
||||||
const deps = useAppDependencies();
|
const { application, i18n: i18nStart, theme } = useAppDependencies();
|
||||||
const { capabilities } = deps.application;
|
const { capabilities } = application;
|
||||||
const toastNotifications = useToastNotifications();
|
const toastNotifications = useToastNotifications();
|
||||||
const { esIndicesCreateIndex } = useDocumentationLinks();
|
const { esIndicesCreateIndex } = useDocumentationLinks();
|
||||||
|
|
||||||
|
@ -90,19 +90,15 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
|
||||||
const [destinationIngestPipeline, setDestinationIngestPipeline] = useState<string>(
|
const [destinationIngestPipeline, setDestinationIngestPipeline] = useState<string>(
|
||||||
defaults.destinationIngestPipeline
|
defaults.destinationIngestPipeline
|
||||||
);
|
);
|
||||||
const [transformIds, setTransformIds] = useState<TransformId[]>([]);
|
|
||||||
const [indexNames, setIndexNames] = useState<EsIndexName[]>([]);
|
|
||||||
const [ingestPipelineNames, setIngestPipelineNames] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const canCreateDataView = useMemo(
|
const canCreateDataView = useMemo(
|
||||||
() =>
|
() =>
|
||||||
capabilities.savedObjectsManagement.edit === true ||
|
capabilities.savedObjectsManagement?.edit === true ||
|
||||||
capabilities.indexPatterns.save === true,
|
capabilities.indexPatterns?.save === true,
|
||||||
[capabilities]
|
[capabilities]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Index pattern state
|
// Index pattern state
|
||||||
const [dataViewTitles, setDataViewTitles] = useState<DataViewTitle[]>([]);
|
|
||||||
const [createDataView, setCreateDataView] = useState(
|
const [createDataView, setCreateDataView] = useState(
|
||||||
canCreateDataView === false ? false : defaults.createDataView
|
canCreateDataView === false ? false : defaults.createDataView
|
||||||
);
|
);
|
||||||
|
@ -125,25 +121,42 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
|
||||||
[setDataViewTimeField, dataViewAvailableTimeFields]
|
[setDataViewTimeField, dataViewAvailableTimeFields]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { overlays, theme } = useAppDependencies();
|
const {
|
||||||
const api = useApi();
|
error: transformsError,
|
||||||
|
data: { transformIds },
|
||||||
|
} = useGetTransforms();
|
||||||
|
|
||||||
// fetch existing transform IDs and indices once for form validation
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// use an IIFE to avoid returning a Promise to useEffect.
|
if (transformsError !== null) {
|
||||||
(async function () {
|
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 { searchQuery, previewRequest: partialPreviewRequest } = stepDefineState;
|
||||||
const transformConfigQuery = getTransformConfigQuery(searchQuery);
|
const transformConfigQuery = getTransformConfigQuery(searchQuery);
|
||||||
const previewRequest = getPreviewTransformRequestBody(
|
return getPreviewTransformRequestBody(
|
||||||
searchItems.dataView,
|
searchItems.dataView,
|
||||||
transformConfigQuery,
|
transformConfigQuery,
|
||||||
partialPreviewRequest,
|
partialPreviewRequest,
|
||||||
stepDefineState.runtimeMappings
|
stepDefineState.runtimeMappings
|
||||||
);
|
);
|
||||||
|
}, [searchItems.dataView, stepDefineState]);
|
||||||
|
const { error: transformsPreviewError, data: transformPreview } =
|
||||||
|
useGetTransformsPreview(previewRequest);
|
||||||
|
|
||||||
const transformPreview = await api.getTransformsPreview(previewRequest);
|
useEffect(() => {
|
||||||
|
if (transformPreview) {
|
||||||
if (isPostTransformsPreviewResponseSchema(transformPreview)) {
|
|
||||||
const properties = transformPreview.generated_dest_index.mappings.properties;
|
const properties = transformPreview.generated_dest_index.mappings.properties;
|
||||||
const timeFields: string[] = Object.keys(properties).filter(
|
const timeFields: string[] = Object.keys(properties).filter(
|
||||||
(col) => properties[col].type === 'date'
|
(col) => properties[col].type === 'date'
|
||||||
|
@ -151,100 +164,79 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
|
||||||
|
|
||||||
setDataViewAvailableTimeFields(timeFields);
|
setDataViewAvailableTimeFields(timeFields);
|
||||||
setDataViewTimeField(timeFields[0]);
|
setDataViewTimeField(timeFields[0]);
|
||||||
} else {
|
}
|
||||||
|
}, [transformPreview]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (transformsPreviewError !== null) {
|
||||||
toastNotifications.addDanger({
|
toastNotifications.addDanger({
|
||||||
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformPreview', {
|
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformPreview', {
|
||||||
defaultMessage: 'An error occurred fetching the transform preview',
|
defaultMessage: 'An error occurred fetching the transform preview',
|
||||||
}),
|
}),
|
||||||
text: toMountPoint(
|
text: toMountPoint(
|
||||||
<ToastNotificationText
|
<ToastNotificationText text={getErrorMessage(transformsPreviewError)} />,
|
||||||
overlays={overlays}
|
{ theme, i18n: i18nStart }
|
||||||
theme={theme}
|
|
||||||
text={getErrorMessage(transformPreview)}
|
|
||||||
/>,
|
|
||||||
{ theme$: theme.theme$ }
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// custom comparison
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [transformsPreviewError]);
|
||||||
|
|
||||||
const resp = await api.getTransforms();
|
const { error: esIndicesError, data: esIndicesData } = useGetEsIndices();
|
||||||
|
const indexNames = esIndicesData?.map((index) => index.name) ?? [];
|
||||||
|
|
||||||
if (isHttpFetchError(resp)) {
|
useEffect(() => {
|
||||||
toastNotifications.addDanger({
|
if (esIndicesError !== null) {
|
||||||
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({
|
toastNotifications.addDanger({
|
||||||
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexNames', {
|
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexNames', {
|
||||||
defaultMessage: 'An error occurred getting the existing index names:',
|
defaultMessage: 'An error occurred getting the existing index names:',
|
||||||
}),
|
}),
|
||||||
text: toMountPoint(
|
text: toMountPoint(<ToastNotificationText text={getErrorMessage(esIndicesError)} />, {
|
||||||
<ToastNotificationText
|
theme,
|
||||||
overlays={overlays}
|
i18n: i18nStart,
|
||||||
theme={theme}
|
}),
|
||||||
text={getErrorMessage(indices)}
|
|
||||||
/>,
|
|
||||||
{ theme$: theme.theme$ }
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// custom comparison
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
}, [esIndicesError]);
|
||||||
|
|
||||||
if (isEsIngestPipelines(ingestPipelines)) {
|
const { error: esIngestPipelinesError, data: esIngestPipelinesData } =
|
||||||
setIngestPipelineNames(ingestPipelines.map(({ name }) => name));
|
useGetEsIngestPipelines();
|
||||||
} else {
|
const ingestPipelineNames = esIngestPipelinesData?.map(({ name }) => name) ?? [];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (esIngestPipelinesError !== null) {
|
||||||
toastNotifications.addDanger({
|
toastNotifications.addDanger({
|
||||||
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIngestPipelines', {
|
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIngestPipelines', {
|
||||||
defaultMessage: 'An error occurred getting the existing ingest pipeline names:',
|
defaultMessage: 'An error occurred getting the existing ingest pipeline names:',
|
||||||
}),
|
}),
|
||||||
text: toMountPoint(
|
text: toMountPoint(
|
||||||
<ToastNotificationText
|
<ToastNotificationText text={getErrorMessage(esIngestPipelinesError)} />,
|
||||||
overlays={overlays}
|
{ theme, i18n: i18nStart }
|
||||||
theme={theme}
|
|
||||||
text={getErrorMessage(ingestPipelines)}
|
|
||||||
/>,
|
|
||||||
{ theme$: theme.theme$ }
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// custom comparison
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
}, [esIngestPipelinesError]);
|
||||||
|
|
||||||
try {
|
const { error: dataViewTitlesError, data: dataViewTitles } = useGetDataViewTitles();
|
||||||
setDataViewTitles(await deps.data.dataViews.getTitles());
|
|
||||||
} catch (e) {
|
useEffect(() => {
|
||||||
|
if (dataViewTitlesError !== null) {
|
||||||
toastNotifications.addDanger({
|
toastNotifications.addDanger({
|
||||||
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingDataViewTitles', {
|
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingDataViewTitles', {
|
||||||
defaultMessage: 'An error occurred getting the existing data view titles:',
|
defaultMessage: 'An error occurred getting the existing data view titles:',
|
||||||
}),
|
}),
|
||||||
text: toMountPoint(
|
text: toMountPoint(
|
||||||
<ToastNotificationText overlays={overlays} theme={theme} text={getErrorMessage(e)} />,
|
<ToastNotificationText text={getErrorMessage(dataViewTitlesError)} />,
|
||||||
{ theme$: theme.theme$ }
|
{ theme, i18n: i18nStart }
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
}, [dataViewTitlesError]);
|
||||||
// run once
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const dateFieldNames = searchItems.dataView.fields
|
const dateFieldNames = searchItems.dataView.fields
|
||||||
.filter((f) => f.type === KBN_FIELD_TYPES.DATE)
|
.filter((f) => f.type === KBN_FIELD_TYPES.DATE)
|
||||||
|
@ -284,7 +276,6 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
|
||||||
);
|
);
|
||||||
setRetentionPolicyMaxAge('');
|
setRetentionPolicyMaxAge('');
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [isRetentionPolicyEnabled]);
|
}, [isRetentionPolicyEnabled]);
|
||||||
|
|
||||||
const transformIdExists = transformIds.some((id) => transformId === id);
|
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 indexNameExists = indexNames.some((name) => destinationIndex === name);
|
||||||
const indexNameEmpty = destinationIndex === '';
|
const indexNameEmpty = destinationIndex === '';
|
||||||
const indexNameValid = isValidIndexName(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 [transformFrequency, setTransformFrequency] = useState(defaults.transformFrequency);
|
||||||
const isTransformFrequencyValid = transformFrequencyValidator(transformFrequency);
|
const isTransformFrequencyValid = transformFrequencyValidator(transformFrequency);
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { EuiFormRow, EuiSelect } from '@elastic/eui';
|
import { EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||||
import { FormattedMessage } from '@kbn/i18n-react';
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { EuiConfirmModal } from '@elastic/eui';
|
import { EuiConfirmModal } from '@elastic/eui';
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,14 @@
|
||||||
import React, { FC, useEffect } from 'react';
|
import React, { FC, useEffect } from 'react';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import { FormattedMessage } from '@kbn/i18n-react';
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
|
|
||||||
import { EuiButtonEmpty, EuiCallOut, EuiPageTemplate, EuiSpacer } from '@elastic/eui';
|
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 { useDocumentationLinks } from '../../hooks/use_documentation_links';
|
||||||
import { useSearchItems } from '../../hooks/use_search_items';
|
import { useSearchItems } from '../../hooks/use_search_items';
|
||||||
import { BREADCRUMB_SECTION, breadcrumbService, docTitleService } from '../../services/navigation';
|
import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation';
|
||||||
import { PrivilegesWrapper } from '../../lib/authorization';
|
import { CapabilitiesWrapper } from '../../components/capabilities_wrapper';
|
||||||
|
|
||||||
import { Wizard } from './components/wizard';
|
import { Wizard } from './components/wizard';
|
||||||
|
|
||||||
type Props = RouteComponentProps<{ savedObjectId: string }>;
|
type Props = RouteComponentProps<{ savedObjectId: string }>;
|
||||||
|
@ -43,7 +45,14 @@ export const CreateTransformSection: FC<Props> = ({ match }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PrivilegesWrapper privileges={APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES}>
|
<CapabilitiesWrapper
|
||||||
|
requiredCapabilities={[
|
||||||
|
'canGetTransform',
|
||||||
|
'canPreviewTransform',
|
||||||
|
'canCreateTransform',
|
||||||
|
'canStartStopTransform',
|
||||||
|
]}
|
||||||
|
>
|
||||||
<EuiPageTemplate.Header
|
<EuiPageTemplate.Header
|
||||||
pageTitle={
|
pageTitle={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -67,6 +76,6 @@ export const CreateTransformSection: FC<Props> = ({ match }) => {
|
||||||
)}
|
)}
|
||||||
{searchItems !== undefined && <Wizard searchItems={searchItems} />}
|
{searchItems !== undefined && <Wizard searchItems={searchItems} />}
|
||||||
</EuiPageTemplate.Section>
|
</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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { EuiToolTip } from '@elastic/eui';
|
import { EuiToolTip } from '@elastic/eui';
|
||||||
|
|
||||||
import { createCapabilityFailureMessage } from '../../../../lib/authorization';
|
import { createCapabilityFailureMessage } from '../../../../../../common/utils/create_capability_failure_message';
|
||||||
|
|
||||||
export const cloneActionNameText = i18n.translate(
|
export const cloneActionNameText = i18n.translate(
|
||||||
'xpack.transform.transformList.cloneActionNameText',
|
'xpack.transform.transformList.cloneActionNameText',
|
||||||
|
|
|
@ -5,14 +5,13 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useContext, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
import { AuthorizationContext } from '../../../../lib/authorization';
|
|
||||||
import { TransformListAction, TransformListRow } from '../../../../common';
|
import { TransformListAction, TransformListRow } from '../../../../common';
|
||||||
import { SECTION_SLUG } from '../../../../common/constants';
|
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 { useAppDependencies, useToastNotifications } from '../../../../app_dependencies';
|
||||||
|
|
||||||
import { cloneActionNameText, CloneActionName } from './clone_action_name';
|
import { cloneActionNameText, CloneActionName } from './clone_action_name';
|
||||||
|
@ -26,7 +25,7 @@ export const useCloneAction = (forceDisable: boolean, transformNodes: number) =>
|
||||||
|
|
||||||
const { getDataViewIdByTitle, loadDataViews } = useSearchItems(undefined);
|
const { getDataViewIdByTitle, loadDataViews } = useSearchItems(undefined);
|
||||||
|
|
||||||
const { canCreateTransform } = useContext(AuthorizationContext).capabilities;
|
const { canCreateTransform } = useTransformCapabilities();
|
||||||
|
|
||||||
const clickHandler = useCallback(
|
const clickHandler = useCallback(
|
||||||
async (item: TransformListRow) => {
|
async (item: TransformListRow) => {
|
||||||
|
|
|
@ -5,11 +5,13 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
|
|
||||||
import { EuiToolTip } from '@elastic/eui';
|
import { EuiToolTip } from '@elastic/eui';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { createCapabilityFailureMessage } from '../../../../lib/authorization';
|
|
||||||
|
import { createCapabilityFailureMessage } from '../../../../../../common/utils/create_capability_failure_message';
|
||||||
|
|
||||||
interface CreateAlertRuleActionProps {
|
interface CreateAlertRuleActionProps {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useContext, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { AuthorizationContext } from '../../../../lib/authorization';
|
import { useTransformCapabilities } from '../../../../hooks';
|
||||||
import { TransformListAction, TransformListRow } from '../../../../common';
|
import { TransformListAction, TransformListRow } from '../../../../common';
|
||||||
import {
|
import {
|
||||||
crateAlertRuleActionNameText,
|
crateAlertRuleActionNameText,
|
||||||
|
@ -17,7 +17,7 @@ import { isContinuousTransform } from '../../../../../../common/types/transform'
|
||||||
|
|
||||||
export type CreateAlertRuleAction = ReturnType<typeof useCreateAlertRuleAction>;
|
export type CreateAlertRuleAction = ReturnType<typeof useCreateAlertRuleAction>;
|
||||||
export const useCreateAlertRuleAction = (forceDisable: boolean) => {
|
export const useCreateAlertRuleAction = (forceDisable: boolean) => {
|
||||||
const { canCreateTransformAlerts } = useContext(AuthorizationContext).capabilities;
|
const { canCreateTransformAlerts } = useTransformCapabilities();
|
||||||
const { setCreateAlertRule } = useAlertRuleFlyout();
|
const { setCreateAlertRule } = useAlertRuleFlyout();
|
||||||
|
|
||||||
const clickHandler = useCallback(
|
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