[transform] Move ML "Data Frame Transforms" to Kibana management section "Transforms". (#45880)

Moves "Data frame transforms" from the ML plugin to its own "transform" plugin within the Kibana management section.
This commit is contained in:
Walter Rafelsberger 2019-10-09 11:13:27 -07:00 committed by GitHub
parent 4f9b17cd8e
commit bcf9ec3662
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
283 changed files with 4741 additions and 3697 deletions

1
.github/CODEOWNERS vendored
View file

@ -35,6 +35,7 @@
/x-pack/test/functional/apps/machine_learning/ @elastic/ml-ui
/x-pack/test/functional/services/machine_learning/ @elastic/ml-ui
/x-pack/test/functional/services/ml.ts @elastic/ml-ui
/x-pack/legacy/plugins/transform/ @elastic/ml-ui
# Operations
/renovate.json5 @elastic/kibana-operations

View file

@ -36,6 +36,7 @@
"xpack.server": "legacy/server",
"xpack.snapshotRestore": "legacy/plugins/snapshot_restore",
"xpack.spaces": "legacy/plugins/spaces",
"xpack.transform": "legacy/plugins/transform",
"xpack.upgradeAssistant": "legacy/plugins/upgrade_assistant",
"xpack.uptime": "legacy/plugins/uptime",
"xpack.watcher": "legacy/plugins/watcher"

View file

@ -41,6 +41,7 @@ import { fileUpload } from './legacy/plugins/file_upload';
import { telemetry } from './legacy/plugins/telemetry';
import { encryptedSavedObjects } from './legacy/plugins/encrypted_saved_objects';
import { snapshotRestore } from './legacy/plugins/snapshot_restore';
import { transform } from './legacy/plugins/transform';
import { actions } from './legacy/plugins/actions';
import { alerting } from './legacy/plugins/alerting';
import { lens } from './legacy/plugins/lens';
@ -75,6 +76,7 @@ module.exports = function (kibana) {
infra(kibana),
taskManager(kibana),
rollup(kibana),
transform(kibana),
siem(kibana),
remoteClusters(kibana),
crossClusterReplication(kibana),

View file

@ -16,10 +16,6 @@ export interface AnalyticsMessage extends AuditMessageBase {
analytics_id: string;
}
export interface TransformMessage extends AuditMessageBase {
transform_id: string;
}
export interface JobMessage extends AuditMessageBase {
job_id: string;
}

View file

@ -28,12 +28,6 @@ export interface Privileges {
canDeleteFilter: boolean;
// File Data Visualizer
canFindFileStructure: boolean;
// Data Frame Transforms
canGetDataFrame: boolean;
canDeleteDataFrame: boolean;
canPreviewDataFrame: boolean;
canCreateDataFrame: boolean;
canStartStopDataFrame: boolean;
// Data Frame Analytics
canGetDataFrameAnalytics: boolean;
canDeleteDataFrameAnalytics: boolean;
@ -65,12 +59,6 @@ export function getDefaultPrivileges(): Privileges {
canDeleteFilter: false,
// File Data Visualizer
canFindFileStructure: false,
// Data Frame Transforms
canGetDataFrame: false,
canDeleteDataFrame: false,
canPreviewDataFrame: false,
canCreateDataFrame: false,
canStartStopDataFrame: false,
// Data Frame Analytics
canGetDataFrameAnalytics: false,
canDeleteDataFrameAnalytics: false,

View file

@ -224,7 +224,7 @@ export function mlFunctionToESAggregation(functionName) {
// Job name must contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores;
// it must also start and end with an alphanumeric character'
export function isJobIdValid(jobId) {
return (jobId.match(/^[a-z0-9\-\_]+$/g) && !jobId.match(/^([_-].*)?(.*[_-])?$/g)) ? true : false;
return /^[a-z0-9\-\_]+$/g.test(jobId) && !/^([_-].*)?(.*[_-])?$/g.test(jobId);
}
// To get median data for jobs and charts we need to use Elasticsearch's

View file

@ -20,7 +20,6 @@ import 'plugins/ml/jobs';
import 'plugins/ml/overview';
import 'plugins/ml/services/calendar_service';
import 'plugins/ml/components/messagebar';
import 'plugins/ml/data_frame';
import 'plugins/ml/data_frame_analytics';
import 'plugins/ml/datavisualizer';
import 'plugins/ml/explorer';

View file

@ -17,28 +17,28 @@ interface Props {
const [INFO, WARNING, ERROR] = ['info', 'warning', 'error'];
export const JobIcon: FC<Props> = ({ message, showTooltip = false }) => {
if (message !== undefined) {
let color = 'primary';
const icon = 'alert';
if (message.level === INFO) {
color = 'primary';
} else if (message.level === WARNING) {
color = 'warning';
} else if (message.level === ERROR) {
color = 'danger';
}
if (showTooltip) {
return (
<EuiToolTip position="bottom" content={message.text}>
<EuiIcon type={icon} color={color} />
</EuiToolTip>
);
} else {
return <EuiIcon type={icon} color={color} />;
}
} else {
if (message === undefined) {
return <span />;
}
let color = 'primary';
const icon = 'alert';
if (message.level === INFO) {
color = 'primary';
} else if (message.level === WARNING) {
color = 'warning';
} else if (message.level === ERROR) {
color = 'danger';
}
if (showTooltip) {
return (
<EuiToolTip position="bottom" content={message.text}>
<EuiIcon type={icon} color={color} />
</EuiToolTip>
);
} else {
return <EuiIcon type={icon} color={color} />;
}
};

View file

@ -37,13 +37,6 @@ function getTabs(disableLinks: boolean): Tab[] {
}),
disabled: disableLinks,
},
{
id: 'data_frames',
name: i18n.translate('xpack.ml.navMenu.dataFrameTabLinkText', {
defaultMessage: 'Transforms',
}),
disabled: false,
},
{
id: 'data_frame_analytics',
name: i18n.translate('xpack.ml.navMenu.dataFrameAnalyticsTabLinkText', {
@ -68,7 +61,6 @@ interface TabData {
const TAB_DATA: Record<TabId, TabData> = {
overview: { testSubject: 'mlMainTab overview', pathId: 'overview' },
anomaly_detection: { testSubject: 'mlMainTab anomalyDetection', pathId: 'jobs' },
data_frames: { testSubject: 'mlMainTab dataFrames' },
data_frame_analytics: { testSubject: 'mlMainTab dataFrameAnalytics' },
datavisualizer: { testSubject: 'mlMainTab dataVisualizer' },
};

View file

@ -21,7 +21,6 @@ const tabSupport: TabSupport = {
overview: null,
jobs: 'anomaly_detection',
settings: 'anomaly_detection',
data_frames: null,
data_frame_analytics: null,
datavisualizer: null,
filedatavisualizer: null,

View file

@ -21,7 +21,6 @@ export function getTabs(tabId: TabId, disableLinks: boolean): Tab[] {
const TAB_MAP: Partial<Record<TabId, Tab[]>> = {
overview: [],
datavisualizer: [],
data_frames: [],
data_frame_analytics: [],
anomaly_detection: [
{

View file

@ -1,7 +1,3 @@
.stat {
margin-right: $euiSizeS;
.stat-value {
font-weight: bold
}
}

View file

@ -4,9 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export {
StatsBar,
TransformStatsBarStats,
AnalyticStatsBarStats,
JobStatsBarStats,
} from './stats_bar';
export { StatsBar, AnalyticStatsBarStats, JobStatsBarStats } from './stats_bar';

View file

@ -18,7 +18,7 @@ interface StatProps {
export const Stat: FC<StatProps> = ({ stat }) => {
return (
<span className="stat">
<span>{stat.label}</span>: <span className="stat-value">{stat.value}</span>
<span>{stat.label}</span>: <strong>{stat.value}</strong>
</span>
);
};

View file

@ -18,18 +18,12 @@ export interface JobStatsBarStats extends Stats {
activeDatafeeds: StatsBarStat;
}
export interface TransformStatsBarStats extends Stats {
batch: StatsBarStat;
continuous: StatsBarStat;
started: StatsBarStat;
}
export interface AnalyticStatsBarStats extends Stats {
started: StatsBarStat;
stopped: StatsBarStat;
}
type StatsBarStats = TransformStatsBarStats | JobStatsBarStats | AnalyticStatsBarStats;
type StatsBarStats = JobStatsBarStats | AnalyticStatsBarStats;
type StatsKey = keyof StatsBarStats;
interface StatsBarProps {

View file

@ -1,3 +0,0 @@
@import 'pages/data_frame_new_pivot/components/aggregation_list/index';
@import 'pages/data_frame_new_pivot/components/group_by_list/index';
@import 'pages/transform_management/components/transform_list/index';

View file

@ -1,61 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
// @ts-ignore
import { ML_BREADCRUMB } from '../breadcrumbs';
export function getJobManagementBreadcrumbs() {
// Whilst top level nav menu with tabs remains,
// use root ML breadcrumb.
return [ML_BREADCRUMB];
}
export function getDataFrameBreadcrumbs() {
return [
ML_BREADCRUMB,
{
text: i18n.translate('xpack.ml.dataFrameBreadcrumbs.dataFrameLabel', {
defaultMessage: 'Transforms',
}),
href: '',
},
];
}
const DATA_FRAME_HOME = {
text: i18n.translate('xpack.ml.dataFrameBreadcrumbs.dataFrameLabel', {
defaultMessage: 'Transforms',
}),
href: '#/data_frames',
};
export function getDataFrameCreateBreadcrumbs() {
return [
ML_BREADCRUMB,
DATA_FRAME_HOME,
{
text: i18n.translate('xpack.ml.dataFrameBreadcrumbs.dataFrameCreateLabel', {
defaultMessage: 'Create transform',
}),
href: '',
},
];
}
export function getDataFrameIndexOrSearchBreadcrumbs() {
return [
ML_BREADCRUMB,
DATA_FRAME_HOME,
{
text: i18n.translate('xpack.ml.dataFrameBreadcrumbs.selectIndexOrSearchLabel', {
defaultMessage: 'Select index or search',
}),
href: '',
},
];
}

View file

@ -1,12 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import './pages/access_denied/directive';
import './pages/access_denied/route';
import './pages/transform_management/directive';
import './pages/transform_management/route';
import './pages/data_frame_new_pivot/directive';
import './pages/data_frame_new_pivot/route';

View file

@ -1,50 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
// @ts-ignore
import { uiModules } from 'ui/modules';
import uiChrome from 'ui/chrome';
const module = uiModules.get('apps/ml', ['react']);
import { I18nContext } from 'ui/i18n';
import { InjectorService } from '../../../../common/types/angular';
import { Page } from './page';
module.directive('mlDataFrameAccessDenied', ($injector: InjectorService) => {
return {
scope: {},
restrict: 'E',
link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => {
const kbnBaseUrl = $injector.get<string>('kbnBaseUrl');
const kbnUrl = $injector.get<any>('kbnUrl');
const goToKibana = () => {
window.location.href = uiChrome.getBasePath() + kbnBaseUrl;
};
const retry = () => {
kbnUrl.redirect('/data_frames');
};
ReactDOM.render(
<I18nContext>
<Page goToKibana={goToKibana} retry={retry} />
</I18nContext>,
element[0]
);
element.on('$destroy', () => {
ReactDOM.unmountComponentAtNode(element[0]);
scope.$destroy();
});
},
};
});

View file

@ -1,42 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { render, fireEvent, cleanup } from 'react-testing-library';
import { I18nProvider } from '@kbn/i18n/react';
import { Page } from './page';
jest.mock('../../../components/navigation_menu/navigation_menu', () => ({
NavigationMenu: () => <div id="mockNavigationMenu" />,
}));
afterEach(cleanup);
describe('Data Frame: Access denied <Page />', () => {
test('Minimal initialization', () => {
const props = {
goToKibana: jest.fn(),
retry: jest.fn(),
};
const tree = (
<I18nProvider>
<Page {...props} />
</I18nProvider>
);
const { getByText } = render(tree);
fireEvent.click(getByText(/Back to Kibana home/i));
fireEvent.click(getByText(/Retry/i));
expect(props.goToKibana).toHaveBeenCalledTimes(1);
expect(props.retry).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,98 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
EuiButton,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiPageBody,
EuiPageContentBody,
EuiPageContentHeader,
EuiPageContentHeaderSection,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { NavigationMenu } from '../../../components/navigation_menu/navigation_menu';
interface PageProps {
goToKibana: () => void;
retry: () => void;
}
export const Page: FC<PageProps> = ({ goToKibana, retry }) => (
<Fragment>
<NavigationMenu tabId="access-denied" />
<EuiPage>
<EuiPageBody>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle>
<h1>
<FormattedMessage
id="xpack.ml.dataframe.accessDeniedTitle"
defaultMessage="Access denied"
/>
</h1>
</EuiTitle>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiSpacer size="m" />
<EuiCallOut
title={i18n.translate('xpack.ml.dataframe.noPermissionToAccessMLLabel', {
defaultMessage: 'You need permission to access Data Frames',
})}
color="danger"
iconType="cross"
>
<EuiText size="s">
<p>
<FormattedMessage
id="xpack.ml.dataframe.noGrantedPrivilegesDescription"
defaultMessage="You must have the privileges granted in the {kibanaUserParam} and {dataFrameUserParam} roles.{br}Your system admin can set these roles on the Management User page."
values={{
kibanaUserParam: <span className="text-monospace">kibana_user</span>,
dataFrameUserParam: (
<span className="text-monospace">data_frame_transforms_user</span>
),
br: <br />,
}}
/>
</p>
</EuiText>
</EuiCallOut>
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton fill onClick={goToKibana} size="s">
<FormattedMessage
id="xpack.ml.dataframe.accessDenied.backToKibanaHomeButtonLabel"
defaultMessage="Back to Kibana home"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill onClick={retry} size="s">
<FormattedMessage
id="xpack.ml.dataframe.accessDenied.retryButtonLabel"
defaultMessage="Retry"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</EuiPageBody>
</EuiPage>
</Fragment>
);

View file

@ -1,17 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import uiRoutes from 'ui/routes';
// @ts-ignore
import { getDataFrameBreadcrumbs } from '../../breadcrumbs';
const template = `<ml-data-frame-access-denied />`;
uiRoutes.when('/data_frames/access-denied', {
template,
k7Breadcrumbs: getDataFrameBreadcrumbs,
});

View file

@ -1,7 +0,0 @@
.mlAggregationLabel--button {
width: $euiSizeL;
}
.mlAggregationLabel--text {
min-width: 0;
}

View file

@ -1,11 +0,0 @@
.mlGroupByLabel--button {
width: $euiSizeL;
}
.mlGroupByLabel--text {
min-width: 0;
}
.mlGroupByLabel--interval {
max-width: 25%;
}

View file

@ -1,59 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Data Frame: <SourceIndexPreview /> Minimal initialization 1`] = `
<div>
<ContextProvider
value={
Object {
"combinedQuery": Object {
"language": "the-query-language",
"query": "the-query-string",
},
"currentIndexPattern": Object {
"fields": Array [],
"id": "the-index-pattern-id",
"title": "the-index-pattern-title",
},
"currentSavedSearch": Object {
"columns": Array [],
"destroy": [Function],
"id": "the-saved-search-id",
"searchSource": Object {},
"sort": Array [],
"title": "the-saved-search-title",
},
"indexPatterns": _temp {
"clearCache": [MockFunction],
"config": Object {},
"fieldFormats": Array [],
"get": [MockFunction],
"getDefault": [MockFunction],
"getFields": [MockFunction],
"getIds": [MockFunction],
"getTitles": [MockFunction],
"make": [MockFunction],
"refreshSavedObjectsCache": Object {},
"savedObjectsClient": Object {},
},
"kbnBaseUrl": "url",
"kibanaConfig": Object {
"get": [Function],
"has": [Function],
"set": [Function],
},
}
}
>
<Component
query={
Object {
"query_string": Object {
"default_operator": "AND",
"query": "the-query",
},
}
}
/>
</ContextProvider>
</div>
`;

View file

@ -1,62 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Data Frame: <StepCreateForm /> Minimal initialization 1`] = `
<div>
<ContextProvider
value={
Object {
"combinedQuery": Object {
"language": "the-query-language",
"query": "the-query-string",
},
"currentIndexPattern": Object {
"fields": Array [],
"id": "the-index-pattern-id",
"title": "the-index-pattern-title",
},
"currentSavedSearch": Object {
"columns": Array [],
"destroy": [Function],
"id": "the-saved-search-id",
"searchSource": Object {},
"sort": Array [],
"title": "the-saved-search-title",
},
"indexPatterns": _temp {
"clearCache": [MockFunction],
"config": Object {},
"fieldFormats": Array [],
"get": [MockFunction],
"getDefault": [MockFunction],
"getFields": [MockFunction],
"getIds": [MockFunction],
"getTitles": [MockFunction],
"make": [MockFunction],
"refreshSavedObjectsCache": Object {},
"savedObjectsClient": Object {},
},
"kbnBaseUrl": "url",
"kibanaConfig": Object {
"get": [Function],
"has": [Function],
"set": [Function],
},
}
}
>
<Component
createIndexPattern={false}
onChange={[Function]}
overrides={
Object {
"created": false,
"indexPatternId": undefined,
"started": false,
}
}
transformConfig={Object {}}
transformId="the-transform-id"
/>
</ContextProvider>
</div>
`;

View file

@ -1,79 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Data Frame: <PivotPreview /> Minimal initialization 1`] = `
<div>
<ContextProvider
value={
Object {
"combinedQuery": Object {
"language": "the-query-language",
"query": "the-query-string",
},
"currentIndexPattern": Object {
"fields": Array [],
"id": "the-index-pattern-id",
"title": "the-index-pattern-title",
},
"currentSavedSearch": Object {
"columns": Array [],
"destroy": [Function],
"id": "the-saved-search-id",
"searchSource": Object {},
"sort": Array [],
"title": "the-saved-search-title",
},
"indexPatterns": _temp {
"clearCache": [MockFunction],
"config": Object {},
"fieldFormats": Array [],
"get": [MockFunction],
"getDefault": [MockFunction],
"getFields": [MockFunction],
"getIds": [MockFunction],
"getTitles": [MockFunction],
"make": [MockFunction],
"refreshSavedObjectsCache": Object {},
"savedObjectsClient": Object {},
},
"kbnBaseUrl": "url",
"kibanaConfig": Object {
"get": [Function],
"has": [Function],
"set": [Function],
},
}
}
>
<Component
aggs={
Object {
"the-agg-name": Object {
"agg": "avg",
"aggName": "the-group-by-agg-name",
"dropDownName": "the-group-by-drop-down-name",
"field": "the-agg-field",
},
}
}
groupBy={
Object {
"the-group-by-name": Object {
"agg": "terms",
"aggName": "the-group-by-agg-name",
"dropDownName": "the-group-by-drop-down-name",
"field": "the-group-by-field",
},
}
}
query={
Object {
"query_string": Object {
"default_operator": "AND",
"query": "the-query",
},
}
}
/>
</ContextProvider>
</div>
`;

View file

@ -1,52 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Data Frame: <DefinePivotForm /> Minimal initialization 1`] = `
<div>
<ContextProvider
value={
Object {
"combinedQuery": Object {
"language": "the-query-language",
"query": "the-query-string",
},
"currentIndexPattern": Object {
"fields": Array [],
"id": "the-index-pattern-id",
"title": "the-index-pattern-title",
},
"currentSavedSearch": Object {
"columns": Array [],
"destroy": [Function],
"id": "the-saved-search-id",
"searchSource": Object {},
"sort": Array [],
"title": "the-saved-search-title",
},
"indexPatterns": _temp {
"clearCache": [MockFunction],
"config": Object {},
"fieldFormats": Array [],
"get": [MockFunction],
"getDefault": [MockFunction],
"getFields": [MockFunction],
"getIds": [MockFunction],
"getTitles": [MockFunction],
"make": [MockFunction],
"refreshSavedObjectsCache": Object {},
"savedObjectsClient": Object {},
},
"kbnBaseUrl": "url",
"kibanaConfig": Object {
"get": [Function],
"has": [Function],
"set": [Function],
},
}
}
>
<Component
onChange={[Function]}
/>
</ContextProvider>
</div>
`;

View file

@ -1,77 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Data Frame: <DefinePivotSummary /> Minimal initialization 1`] = `
<div>
<ContextProvider
value={
Object {
"combinedQuery": Object {
"language": "the-query-language",
"query": "the-query-string",
},
"currentIndexPattern": Object {
"fields": Array [],
"id": "the-index-pattern-id",
"title": "the-index-pattern-title",
},
"currentSavedSearch": Object {
"columns": Array [],
"destroy": [Function],
"id": "the-saved-search-id",
"searchSource": Object {},
"sort": Array [],
"title": "the-saved-search-title",
},
"indexPatterns": _temp {
"clearCache": [MockFunction],
"config": Object {},
"fieldFormats": Array [],
"get": [MockFunction],
"getDefault": [MockFunction],
"getFields": [MockFunction],
"getIds": [MockFunction],
"getTitles": [MockFunction],
"make": [MockFunction],
"refreshSavedObjectsCache": Object {},
"savedObjectsClient": Object {},
},
"kbnBaseUrl": "url",
"kibanaConfig": Object {
"get": [Function],
"has": [Function],
"set": [Function],
},
}
}
>
<StepDefineSummary
aggList={
Object {
"the-agg-name": Object {
"agg": "avg",
"aggName": "the-group-by-agg-name",
"dropDownName": "the-group-by-drop-down-name",
"field": "the-agg-field",
},
}
}
groupByList={
Object {
"the-group-by-name": Object {
"agg": "terms",
"aggName": "the-group-by-agg-name",
"dropDownName": "the-group-by-drop-down-name",
"field": "the-group-by-field",
},
}
}
isAdvancedPivotEditorEnabled={false}
isAdvancedSourceEditorEnabled={false}
searchQuery="the-search-query"
searchString="the-query"
sourceConfigUpdated={false}
valid={true}
/>
</ContextProvider>
</div>
`;

View file

@ -1,134 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, SFC } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiCodeBlock,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiText,
} from '@elastic/eui';
import { useKibanaContext } from '../../../../../contexts/kibana';
import { AggListSummary } from '../aggregation_list';
import { GroupByListSummary } from '../group_by_list';
import { PivotPreview } from './pivot_preview';
import { getPivotQuery } from '../../../../common';
import { StepDefineExposedState } from './step_define_form';
const defaultSearch = '*';
const emptySearch = '';
export const StepDefineSummary: SFC<StepDefineExposedState> = ({
searchString,
searchQuery,
groupByList,
aggList,
}) => {
const kibanaContext = useKibanaContext();
const pivotQuery = getPivotQuery(searchQuery);
let useCodeBlock = false;
let displaySearch;
// searchString set to empty once source config editor used - display query instead
if (searchString === emptySearch) {
displaySearch = JSON.stringify(searchQuery, null, 2);
useCodeBlock = true;
} else if (searchString === defaultSearch) {
displaySearch = emptySearch;
} else {
displaySearch = searchString;
}
return (
<EuiFlexGroup>
<EuiFlexItem grow={false} style={{ minWidth: '420px' }}>
<EuiForm>
{kibanaContext.currentSavedSearch.id === undefined && typeof searchString === 'string' && (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.stepDefineSummary.indexPatternLabel', {
defaultMessage: 'Index pattern',
})}
>
<span>{kibanaContext.currentIndexPattern.title}</span>
</EuiFormRow>
{useCodeBlock === false && displaySearch !== emptySearch && (
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.stepDefineSummary.queryLabel', {
defaultMessage: 'Query',
})}
>
<span>{displaySearch}</span>
</EuiFormRow>
)}
{useCodeBlock === true && displaySearch !== emptySearch && (
<EuiFormRow
label={i18n.translate(
'xpack.ml.dataframe.stepDefineSummary.queryCodeBlockLabel',
{
defaultMessage: 'Query',
}
)}
>
<EuiCodeBlock
language="js"
fontSize="s"
paddingSize="s"
color="light"
overflowHeight={300}
isCopyable
>
{displaySearch}
</EuiCodeBlock>
</EuiFormRow>
)}
</Fragment>
)}
{kibanaContext.currentSavedSearch.id !== undefined && (
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.stepDefineSummary.savedSearchLabel', {
defaultMessage: 'Saved search',
})}
>
<span>{kibanaContext.currentSavedSearch.title}</span>
</EuiFormRow>
)}
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.stepDefineSummary.groupByLabel', {
defaultMessage: 'Group by',
})}
>
<GroupByListSummary list={groupByList} />
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.stepDefineSummary.aggregationsLabel', {
defaultMessage: 'Aggregations',
})}
>
<AggListSummary list={aggList} />
</EuiFormRow>
</EuiForm>
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<PivotPreview aggs={aggList} groupBy={groupByList} query={pivotQuery} />
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -1,65 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
// @ts-ignore
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml', ['react']);
import { IndexPatterns } from 'ui/index_patterns';
import { I18nContext } from 'ui/i18n';
import { IPrivate } from 'ui/private';
import { timefilter } from 'ui/timefilter';
import { InjectorService } from '../../../../common/types/angular';
import { SearchItemsProvider } from '../../../jobs/new_job/utils/new_job_utils';
import { KibanaConfigTypeFix, KibanaContext } from '../../../contexts/kibana';
import { Page } from './page';
module.directive('mlNewDataFrame', ($injector: InjectorService) => {
return {
scope: {},
restrict: 'E',
link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => {
const indexPatterns = $injector.get<IndexPatterns>('indexPatterns');
const kbnBaseUrl = $injector.get<string>('kbnBaseUrl');
const kibanaConfig = $injector.get<KibanaConfigTypeFix>('config');
const Private = $injector.get<IPrivate>('Private');
timefilter.disableTimeRangeSelector();
timefilter.disableAutoRefreshSelector();
const createSearchItems = Private(SearchItemsProvider);
const { indexPattern, savedSearch, combinedQuery } = createSearchItems();
const kibanaContext = {
combinedQuery,
currentIndexPattern: indexPattern,
currentSavedSearch: savedSearch,
indexPatterns,
kbnBaseUrl,
kibanaConfig,
};
ReactDOM.render(
<I18nContext>
<KibanaContext.Provider value={kibanaContext}>
<Page />
</KibanaContext.Provider>
</I18nContext>,
element[0]
);
element.on('$destroy', () => {
ReactDOM.unmountComponentAtNode(element[0]);
scope.$destroy();
});
},
};
});

View file

@ -1,62 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
EuiBetaBadge,
EuiPage,
EuiPageBody,
EuiPageContentBody,
EuiPageContentHeader,
EuiPageContentHeaderSection,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { NavigationMenu } from '../../../components/navigation_menu/navigation_menu';
import { Wizard } from './components/wizard';
export const Page: FC = () => (
<Fragment>
<NavigationMenu tabId="new_data_frame" />
<EuiPage>
<EuiPageBody>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle>
<h1>
<FormattedMessage
id="xpack.ml.dataframe.transformsWizard.newDataFrameTitle"
defaultMessage="New data frame"
/>
<span>&nbsp;</span>
<EuiBetaBadge
label={i18n.translate('xpack.ml.dataframe.transformsWizard.betaBadgeLabel', {
defaultMessage: `Beta`,
})}
tooltipContent={i18n.translate(
'xpack.ml.dataframe.transformsWizard.betaBadgeTooltipContent',
{
defaultMessage: `Data frames are a beta feature. We'd love to hear your feedback.`,
}
)}
/>
</h1>
</EuiTitle>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiSpacer size="l" />
<Wizard />
</EuiPageContentBody>
</EuiPageBody>
</EuiPage>
</Fragment>
);

View file

@ -1,49 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import uiRoutes from 'ui/routes';
// @ts-ignore
import { checkBasicLicense } from '../../../license/check_license';
import { checkCreateDataFrameTransformPrivilege } from '../../../privilege/check_privilege';
import {
loadCurrentIndexPattern,
loadCurrentSavedSearch,
loadIndexPatterns,
} from '../../../util/index_utils';
import {
getDataFrameCreateBreadcrumbs,
getDataFrameIndexOrSearchBreadcrumbs,
} from '../../breadcrumbs';
const wizardTemplate = `<ml-new-data-frame />`;
uiRoutes.when('/data_frames/new_transform/step/pivot?', {
template: wizardTemplate,
k7Breadcrumbs: getDataFrameCreateBreadcrumbs,
resolve: {
CheckLicense: checkBasicLicense,
privileges: checkCreateDataFrameTransformPrivilege,
indexPattern: loadCurrentIndexPattern,
savedSearch: loadCurrentSavedSearch,
},
});
uiRoutes.when('/data_frames/new_transform', {
redirectTo: '/data_frames/new_transform/step/index_or_search',
});
uiRoutes.when('/data_frames/new_transform/step/index_or_search', {
template: '<ml-index-or-search />',
k7Breadcrumbs: getDataFrameIndexOrSearchBreadcrumbs,
resolve: {
CheckLicense: checkBasicLicense,
privileges: checkCreateDataFrameTransformPrivilege,
indexPatterns: loadIndexPatterns,
nextStepPath: () => '#data_frames/new_transform/step/pivot',
},
});

View file

@ -1,74 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Data Frame: Job List <Page /> Minimal initialization 1`] = `
<Fragment>
<NavigationMenu
tabId="data_frames"
/>
<TransformStatsBar
transformsList={Array []}
/>
<EuiPage
data-test-subj="mlPageDataFrame"
restrictWidth={false}
>
<EuiPageBody
restrictWidth={false}
>
<EuiPageContentHeader
responsive={true}
>
<EuiPageContentHeaderSection>
<EuiTitle>
<h1>
<FormattedMessage
defaultMessage="Data frame transforms"
id="xpack.ml.dataframe.transformList.dataFrameTitle"
values={Object {}}
/>
<span>
 
</span>
<EuiBetaBadge
label="Beta"
tooltipContent="Data frames are a beta feature. We'd love to hear your feedback."
/>
</h1>
</EuiTitle>
</EuiPageContentHeaderSection>
<EuiPageContentHeaderSection>
<EuiFlexGroup
alignItems="center"
>
<EuiFlexItem
grow={false}
>
<RefreshTransformListButton
isLoading={false}
onClick={[Function]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<CreateTransformButton />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiSpacer
size="l"
/>
<EuiPanel>
<DataFrameTransformList
isInitialized={false}
transforms={Array []}
transformsLoading={false}
/>
</EuiPanel>
</EuiPageContentBody>
</EuiPageBody>
</EuiPage>
</Fragment>
`;

View file

@ -1,24 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Data Frame: Transform List <CreateTransformButton /> Minimal initialization 1`] = `
<EuiToolTip
content="Your license has expired. Please contact your administrator."
delay="regular"
position="top"
>
<EuiButton
data-test-subj="mlDataFramesButtonCreate"
disabled={true}
fill={true}
iconType="plusInCircle"
onClick={[Function]}
size="s"
>
<FormattedMessage
defaultMessage="Create transform"
id="xpack.ml.dataframe.transformList.createDataFrameButton"
values={Object {}}
/>
</EuiButton>
</EuiToolTip>
`;

View file

@ -1,51 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { SFC } from 'react';
import { EuiButton, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import {
checkPermission,
createPermissionFailureMessage,
} from '../../../../../privilege/check_privilege';
import { moveToDataFrameWizard } from '../../../../common';
export const CreateTransformButton: SFC = () => {
const disabled =
!checkPermission('canCreateDataFrame') ||
!checkPermission('canPreviewDataFrame') ||
!checkPermission('canStartStopDataFrame');
const button = (
<EuiButton
disabled={disabled}
fill
onClick={moveToDataFrameWizard}
iconType="plusInCircle"
size="s"
data-test-subj="mlDataFramesButtonCreate"
>
<FormattedMessage
id="xpack.ml.dataframe.transformList.createDataFrameButton"
defaultMessage="Create transform"
/>
</EuiButton>
);
if (disabled) {
return (
<EuiToolTip position="top" content={createPermissionFailureMessage('canCreateDataFrame')}>
{button}
</EuiToolTip>
);
}
return button;
};

View file

@ -1,28 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { shallow } from 'enzyme';
import React from 'react';
import { DataFrameTransformListRow } from '../../../../common';
import { DeleteAction } from './action_delete';
import dataFrameTransformListRow from '../../../../common/__mocks__/data_frame_transform_list_row.json';
describe('Data Frame: Transform List Actions <DeleteAction />', () => {
test('Minimal initialization', () => {
const item: DataFrameTransformListRow = dataFrameTransformListRow;
const props = {
disabled: false,
items: [item],
deleteTransform(d: DataFrameTransformListRow) {},
};
const wrapper = shallow(<DeleteAction {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -1,28 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { shallow } from 'enzyme';
import React from 'react';
import { DataFrameTransformListRow } from '../../../../common';
import { StartAction } from './action_start';
import dataFrameTransformListRow from '../../../../common/__mocks__/data_frame_transform_list_row.json';
describe('Data Frame: Transform List Actions <StartAction />', () => {
test('Minimal initialization', () => {
const item: DataFrameTransformListRow = dataFrameTransformListRow;
const props = {
disabled: false,
items: [item],
startTransform(d: DataFrameTransformListRow) {},
};
const wrapper = shallow(<StartAction {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -1,28 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { shallow } from 'enzyme';
import React from 'react';
import { DataFrameTransformListRow } from '../../../../common';
import { StopAction } from './action_stop';
import dataFrameTransformListRow from '../../../../common/__mocks__/data_frame_transform_list_row.json';
describe('Data Frame: Transform List Actions <StopAction />', () => {
test('Minimal initialization', () => {
const item: DataFrameTransformListRow = dataFrameTransformListRow;
const props = {
disabled: false,
items: [item],
stopTransform(d: DataFrameTransformListRow) {},
};
const wrapper = shallow(<StopAction {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -1,35 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
// @ts-ignore
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml', ['react']);
import { I18nContext } from 'ui/i18n';
import { Page } from './page';
module.directive('mlDataFramePage', () => {
return {
scope: {},
restrict: 'E',
link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => {
ReactDOM.render(
<I18nContext>
<Page />
</I18nContext>,
element[0]
);
element.on('$destroy', () => {
ReactDOM.unmountComponentAtNode(element[0]);
scope.$destroy();
});
},
};
});

View file

@ -1,28 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import uiRoutes from 'ui/routes';
// @ts-ignore
import { checkBasicLicense } from '../../../license/check_license';
// @ts-ignore
import { checkGetDataFrameTransformsPrivilege } from '../../../privilege/check_privilege';
// @ts-ignore
import { loadIndexPatterns } from '../../../util/index_utils';
// @ts-ignore
import { getDataFrameBreadcrumbs } from '../../breadcrumbs';
const template = `<ml-data-frame-page />`;
uiRoutes.when('/data_frames/?', {
template,
k7Breadcrumbs: getDataFrameBreadcrumbs,
resolve: {
CheckLicense: checkBasicLicense,
privileges: checkGetDataFrameTransformsPrivilege,
indexPatterns: loadIndexPatterns,
},
});

View file

@ -1,54 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import { ml } from '../../../../../services/ml_api_service';
import {
DataFrameTransformListRow,
refreshTransformList$,
REFRESH_TRANSFORM_LIST_STATE,
} from '../../../../common';
import {
DataFrameTransformEndpointRequest,
DataFrameTransformEndpointResult,
} from '../../components/transform_list/common';
import { mlMessageBarService } from '../../../../../../public/components/messagebar/messagebar_service';
export const deleteTransforms = async (dataFrames: DataFrameTransformListRow[]) => {
const dataFramesInfo: DataFrameTransformEndpointRequest[] = dataFrames.map(df => ({
id: df.config.id,
state: df.stats.state,
}));
const results: DataFrameTransformEndpointResult = await ml.dataFrame.deleteDataFrameTransforms(
dataFramesInfo
);
for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
if (results.hasOwnProperty(transformId)) {
if (results[transformId].success === true) {
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.transformList.deleteTransformSuccessMessage', {
defaultMessage: 'Request to delete data frame transform {transformId} acknowledged.',
values: { transformId },
})
);
} else {
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.transformList.deleteTransformErrorMessage', {
defaultMessage: 'An error occurred deleting the data frame transform {transformId}',
values: { transformId },
})
);
mlMessageBarService.notify.error(results[transformId].error);
}
}
}
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
};

View file

@ -1,56 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import { ml } from '../../../../../services/ml_api_service';
import {
DataFrameTransformListRow,
refreshTransformList$,
REFRESH_TRANSFORM_LIST_STATE,
} from '../../../../common';
import {
DataFrameTransformEndpointRequest,
DataFrameTransformEndpointResult,
} from '../../components/transform_list/common';
import { mlMessageBarService } from '../../../../../../public/components/messagebar/messagebar_service';
export const startTransforms = async (dataFrames: DataFrameTransformListRow[]) => {
const dataFramesInfo: DataFrameTransformEndpointRequest[] = dataFrames.map(df => ({
id: df.config.id,
state: df.stats.state,
}));
const results: DataFrameTransformEndpointResult = await ml.dataFrame.startDataFrameTransforms(
dataFramesInfo
);
for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
if (results.hasOwnProperty(transformId)) {
if (results[transformId].success === true) {
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.transformList.startTransformSuccessMessage', {
defaultMessage: 'Request to start data frame transform {transformId} acknowledged.',
values: { transformId },
})
);
} else {
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.transformList.startTransformErrorMessage', {
defaultMessage: 'An error occurred starting the data frame transform {transformId}',
values: { transformId },
})
);
mlMessageBarService.notify.error(results[transformId].error);
}
}
}
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
};

View file

@ -1,56 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import { ml } from '../../../../../services/ml_api_service';
import {
DataFrameTransformListRow,
refreshTransformList$,
REFRESH_TRANSFORM_LIST_STATE,
} from '../../../../common';
import {
DataFrameTransformEndpointRequest,
DataFrameTransformEndpointResult,
} from '../../components/transform_list/common';
import { mlMessageBarService } from '../../../../../../public/components/messagebar/messagebar_service';
export const stopTransforms = async (dataFrames: DataFrameTransformListRow[]) => {
const dataFramesInfo: DataFrameTransformEndpointRequest[] = dataFrames.map(df => ({
id: df.config.id,
state: df.stats.state,
}));
const results: DataFrameTransformEndpointResult = await ml.dataFrame.stopDataFrameTransforms(
dataFramesInfo
);
for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
if (results.hasOwnProperty(transformId)) {
if (results[transformId].success === true) {
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.transformList.stopTransformSuccessMessage', {
defaultMessage: 'Request to stop data frame transform {transformId} acknowledged.',
values: { transformId },
})
);
} else {
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.transformList.stopTransformErrorMessage', {
defaultMessage: 'An error occurred stopping the data frame transform {transformId}',
values: { transformId },
})
);
mlMessageBarService.notify.error(results[transformId].error);
}
}
}
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
};

View file

@ -103,6 +103,12 @@ export const ExpandedRow: FC<Props> = ({ item }) => {
},
*/
];
// Using `expand=false` here so the tabs themselves don't spread
// across the full width. The 100% width is used so the bottom line
// as well as the tab content spans across the full width.
// EuiTabbedContent would do that usually anyway,
// it just doesn't seem to work within certain layouts.
return (
<EuiTabbedContent
size="s"
@ -110,6 +116,7 @@ export const ExpandedRow: FC<Props> = ({ item }) => {
initialSelectedTab={tabs[0]}
onTabClick={() => {}}
expand={false}
style={{ width: '100%' }}
/>
);
};

View file

@ -21,7 +21,6 @@
@import 'app';
// Sub applications
@import 'data_frame/index';
@import 'data_frame_analytics/index';
@import 'datavisualizer/index';
@import 'explorer/index'; // SASSTODO: This file needs to be rewritten

View file

@ -88,42 +88,6 @@ export function checkFindFileStructurePrivilege(kbnUrl: any): Promise<Privileges
});
}
export function checkGetDataFrameTransformsPrivilege(kbnUrl: any): Promise<Privileges> {
return new Promise((resolve, reject) => {
getPrivileges().then(({ capabilities }) => {
privileges = capabilities;
// the minimum privilege for using ML with a basic license is being able to use the data frames.
// all other functionality is controlled by the return privileges object
if (privileges.canGetDataFrame) {
return resolve(privileges);
} else {
kbnUrl.redirect('/data_frames/access-denied');
return reject();
}
});
});
}
export function checkCreateDataFrameTransformPrivilege(kbnUrl: any): Promise<Privileges> {
return new Promise((resolve, reject) => {
getPrivileges().then(({ capabilities }) => {
privileges = capabilities;
if (
privileges.canCreateDataFrame &&
privileges.canPreviewDataFrame &&
privileges.canStartStopDataFrame
) {
return resolve(privileges);
} else {
// if the user has no permission to create a data frame transform,
// redirect them back to the Data Frame Transforms Management page
kbnUrl.redirect('/data_frames');
return reject();
}
});
});
}
// check the privilege type and the license to see whether a user has permission to access a feature.
// takes the name of the privilege variable as specified in get_privileges.js
export function checkPermission(privilegeType: keyof Privileges) {
@ -168,21 +132,6 @@ export function createPermissionFailureMessage(privilegeType: keyof Privileges)
message = i18n.translate('xpack.ml.privilege.noPermission.runForecastsTooltip', {
defaultMessage: 'You do not have permission to run forecasts.',
});
} else if (privilegeType === 'canCreateDataFrame') {
message = i18n.translate('xpack.ml.privilege.noPermission.createDataFrameTransformTooltip', {
defaultMessage: 'You do not have permission to create data frame transforms.',
});
} else if (privilegeType === 'canStartStopDataFrame') {
message = i18n.translate(
'xpack.ml.privilege.noPermission.startOrStopDataFrameTransformTooltip',
{
defaultMessage: 'You do not have permission to start or stop data frame transforms.',
}
);
} else if (privilegeType === 'canDeleteDataFrame') {
message = i18n.translate('xpack.ml.privilege.noPermission.deleteDataFrameTransformTooltip', {
defaultMessage: 'You do not have permission to delete data frame transforms.',
});
}
return i18n.translate('xpack.ml.privilege.pleaseContactAdministratorTooltip', {
defaultMessage: '{message} Please contact your administrator.',

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import chrome from 'ui/chrome';
import { http } from '../../services/http_service';
const basePath = chrome.addBasePath('/api/ml');
export const dataFrame = {
getDataFrameTransforms(transformId) {
const transformIdString = transformId !== undefined ? `/${transformId}` : '';
return http({
url: `${basePath}/_data_frame/transforms${transformIdString}`,
method: 'GET'
});
},
getDataFrameTransformsStats(transformId) {
if (transformId !== undefined) {
return http({
url: `${basePath}/_data_frame/transforms/${transformId}/_stats`,
method: 'GET'
});
}
return http({
url: `${basePath}/_data_frame/transforms/_stats`,
method: 'GET'
});
},
createDataFrameTransform(transformId, transformConfig) {
return http({
url: `${basePath}/_data_frame/transforms/${transformId}`,
method: 'PUT',
data: transformConfig
});
},
deleteDataFrameTransforms(transformsInfo) {
return http({
url: `${basePath}/_data_frame/transforms/delete_transforms`,
method: 'POST',
data: {
transformsInfo
}
});
},
getDataFrameTransformsPreview(obj) {
return http({
url: `${basePath}/_data_frame/transforms/_preview`,
method: 'POST',
data: obj
});
},
startDataFrameTransforms(transformsInfo) {
return http({
url: `${basePath}/_data_frame/transforms/start_transforms`,
method: 'POST',
data: {
transformsInfo,
}
});
},
stopDataFrameTransforms(transformsInfo) {
return http({
url: `${basePath}/_data_frame/transforms/stop_transforms`,
method: 'POST',
data: {
transformsInfo,
}
});
},
getTransformAuditMessages(transformId) {
return http({
url: `${basePath}/_data_frame/transforms/${transformId}/messages`,
method: 'GET',
});
},
};

View file

@ -8,10 +8,6 @@ import { Annotation } from '../../../common/types/annotations';
import { AggFieldNamePair } from '../../../common/types/fields';
import { ExistingJobsAndGroups } from '../job_service';
import { PrivilegesResponse } from '../../../common/types/privileges';
import {
DataFrameTransformEndpointRequest,
DataFrameTransformEndpointResult,
} from '../../data_frame/pages/transform_management/components/transform_list/common';
import { MlSummaryJobs } from '../../../common/types/jobs';
// TODO This is not a complete representation of all methods of `ml.*`.
@ -48,23 +44,6 @@ declare interface Ml {
getAnalyticsAuditMessages(analyticsId: string): Promise<any>;
};
dataFrame: {
getDataFrameTransforms(jobId?: string): Promise<any>;
getDataFrameTransformsStats(jobId?: string): Promise<any>;
createDataFrameTransform(jobId: string, jobConfig: any): Promise<any>;
deleteDataFrameTransforms(
jobsData: DataFrameTransformEndpointRequest[]
): Promise<DataFrameTransformEndpointResult>;
getDataFrameTransformsPreview(payload: any): Promise<any>;
startDataFrameTransforms(
jobsData: DataFrameTransformEndpointRequest[]
): Promise<DataFrameTransformEndpointResult>;
stopDataFrameTransforms(
jobsData: DataFrameTransformEndpointRequest[]
): Promise<DataFrameTransformEndpointResult>;
getTransformAuditMessages(transformId: string): Promise<any>;
};
hasPrivileges(obj: object): Promise<any>;
checkMlPrivileges(): Promise<PrivilegesResponse>;

View file

@ -12,7 +12,6 @@ import chrome from 'ui/chrome';
import { http } from '../../services/http_service';
import { annotations } from './annotations';
import { dataFrame } from './data_frame';
import { dataFrameAnalytics } from './data_frame_analytics';
import { filters } from './filters';
import { results } from './results';
@ -450,7 +449,6 @@ export const ml = {
},
annotations,
dataFrame,
dataFrameAnalytics,
filters,
results,

View file

@ -205,119 +205,6 @@ export const elasticsearchJsPlugin = (Client, config, components) => {
method: 'POST'
});
// Currently the endpoint uses a default size of 100 unless a size is supplied.
// So until paging is supported in the UI, explicitly supply a size of 1000
// to match the max number of docs that the endpoint can return.
ml.getDataFrameTransforms = ca({
urls: [
{
fmt: '/_data_frame/transforms/<%=transformId%>',
req: {
transformId: {
type: 'string'
}
}
},
{
fmt: '/_data_frame/transforms/_all?size=1000',
}
],
method: 'GET'
});
ml.getDataFrameTransformsStats = ca({
urls: [
{
fmt: '/_data_frame/transforms/<%=transformId%>/_stats',
req: {
transformId: {
type: 'string'
}
}
},
{
// Currently the endpoint uses a default size of 100 unless a size is supplied.
// So until paging is supported in the UI, explicitly supply a size of 1000
// to match the max number of docs that the endpoint can return.
fmt: '/_data_frame/transforms/_all/_stats?size=1000',
}
],
method: 'GET'
});
ml.createDataFrameTransform = ca({
urls: [
{
fmt: '/_data_frame/transforms/<%=transformId%>',
req: {
transformId: {
type: 'string'
}
}
}
],
needBody: true,
method: 'PUT'
});
ml.deleteDataFrameTransform = ca({
urls: [
{
fmt: '/_data_frame/transforms/<%=transformId%>',
req: {
transformId: {
type: 'string'
}
}
}
],
method: 'DELETE'
});
ml.getDataFrameTransformsPreview = ca({
urls: [
{
fmt: '/_data_frame/transforms/_preview'
}
],
needBody: true,
method: 'POST'
});
ml.startDataFrameTransform = ca({
urls: [
{
fmt: '/_data_frame/transforms/<%=transformId%>/_start',
req: {
transformId: {
type: 'string'
},
}
}
],
method: 'POST'
});
ml.stopDataFrameTransform = ca({
urls: [
{
fmt: '/_data_frame/transforms/<%=transformId%>/_stop?&force=<%=force%>&wait_for_completion=<%waitForCompletion%>',
req: {
transformId: {
type: 'string'
},
force: {
type: 'boolean'
},
waitForCompletion: {
type: 'boolean'
}
}
}
],
method: 'POST'
});
ml.deleteJob = ca({
urls: [
{

View file

@ -105,27 +105,19 @@ const fullClusterPrivileges = {
'cluster:monitor/xpack/ml/calendars/get': true,
'cluster:admin/xpack/ml/filters/get': true,
'cluster:monitor/xpack/ml/datafeeds/get': true,
'cluster:admin/data_frame/start_task': true,
'cluster:admin/xpack/ml/filters/update': true,
'cluster:admin/xpack/ml/calendars/events/post': true,
'cluster:monitor/data_frame/get': true,
'cluster:admin/xpack/ml/job/close': true,
'cluster:monitor/xpack/ml/datafeeds/stats/get': true,
'cluster:admin/data_frame/preview': true,
'cluster:admin/xpack/ml/calendars/jobs/update': true,
'cluster:admin/data_frame/put': true,
'cluster:admin/xpack/ml/calendars/put': true,
'cluster:monitor/data_frame/stats/get': true,
'cluster:admin/data_frame/stop': true,
'cluster:admin/xpack/ml/calendars/events/delete': true,
'cluster:admin/xpack/ml/datafeeds/put': true,
'cluster:admin/xpack/ml/job/open': true,
'cluster:admin/xpack/ml/job/delete': true,
'cluster:monitor/xpack/ml/job/get': true,
'cluster:admin/data_frame/delete': true,
'cluster:admin/xpack/ml/job/put': true,
'cluster:admin/xpack/ml/job/update': true,
'cluster:admin/data_frame/start': true,
'cluster:admin/xpack/ml/calendars/delete': true,
'cluster:monitor/xpack/ml/findfilestructure': true,
};
@ -144,27 +136,19 @@ const partialClusterPrivileges = {
'cluster:monitor/xpack/ml/calendars/get': true,
'cluster:admin/xpack/ml/filters/get': false,
'cluster:monitor/xpack/ml/datafeeds/get': true,
'cluster:admin/data_frame/start_task': false,
'cluster:admin/xpack/ml/filters/update': false,
'cluster:admin/xpack/ml/calendars/events/post': false,
'cluster:monitor/data_frame/get': false,
'cluster:admin/xpack/ml/job/close': false,
'cluster:monitor/xpack/ml/datafeeds/stats/get': true,
'cluster:admin/data_frame/preview': false,
'cluster:admin/xpack/ml/calendars/jobs/update': false,
'cluster:admin/data_frame/put': false,
'cluster:admin/xpack/ml/calendars/put': false,
'cluster:monitor/data_frame/stats/get': false,
'cluster:admin/data_frame/stop': false,
'cluster:admin/xpack/ml/calendars/events/delete': false,
'cluster:admin/xpack/ml/datafeeds/put': false,
'cluster:admin/xpack/ml/job/open': false,
'cluster:admin/xpack/ml/job/delete': false,
'cluster:monitor/xpack/ml/job/get': true,
'cluster:admin/data_frame/delete': false,
'cluster:admin/xpack/ml/job/put': false,
'cluster:admin/xpack/ml/job/update': false,
'cluster:admin/data_frame/start': false,
'cluster:admin/xpack/ml/calendars/delete': false,
'cluster:monitor/xpack/ml/findfilestructure': true,
};

View file

@ -91,7 +91,7 @@ describe('check_privileges', () => {
describe('getPrivileges() - right number of capabilities', () => {
test('es capabilities count', async done => {
const count = mlPrivileges.cluster.length;
expect(count).toBe(35);
expect(count).toBe(27);
done();
});
@ -104,7 +104,7 @@ describe('check_privileges', () => {
);
const { capabilities } = await getPrivileges();
const count = Object.keys(capabilities).length;
expect(count).toBe(27);
expect(count).toBe(22);
done();
});
});
@ -138,11 +138,6 @@ describe('check_privileges', () => {
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
expect(capabilities.canFindFileStructure).toBe(true);
expect(capabilities.canGetDataFrame).toBe(false);
expect(capabilities.canDeleteDataFrame).toBe(false);
expect(capabilities.canPreviewDataFrame).toBe(false);
expect(capabilities.canCreateDataFrame).toBe(false);
expect(capabilities.canStartStopDataFrame).toBe(false);
expect(capabilities.canGetDataFrameAnalytics).toBe(true);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
@ -178,11 +173,6 @@ describe('check_privileges', () => {
expect(capabilities.canCreateFilter).toBe(true);
expect(capabilities.canDeleteFilter).toBe(true);
expect(capabilities.canFindFileStructure).toBe(true);
expect(capabilities.canGetDataFrame).toBe(true);
expect(capabilities.canDeleteDataFrame).toBe(true);
expect(capabilities.canPreviewDataFrame).toBe(true);
expect(capabilities.canCreateDataFrame).toBe(true);
expect(capabilities.canStartStopDataFrame).toBe(true);
expect(capabilities.canGetDataFrameAnalytics).toBe(true);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(true);
expect(capabilities.canCreateDataFrameAnalytics).toBe(true);
@ -218,11 +208,6 @@ describe('check_privileges', () => {
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
expect(capabilities.canFindFileStructure).toBe(true);
expect(capabilities.canGetDataFrame).toBe(true);
expect(capabilities.canDeleteDataFrame).toBe(false);
expect(capabilities.canPreviewDataFrame).toBe(false);
expect(capabilities.canCreateDataFrame).toBe(false);
expect(capabilities.canStartStopDataFrame).toBe(false);
expect(capabilities.canGetDataFrameAnalytics).toBe(true);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
@ -258,11 +243,6 @@ describe('check_privileges', () => {
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
expect(capabilities.canFindFileStructure).toBe(true);
expect(capabilities.canGetDataFrame).toBe(false);
expect(capabilities.canDeleteDataFrame).toBe(false);
expect(capabilities.canPreviewDataFrame).toBe(false);
expect(capabilities.canCreateDataFrame).toBe(false);
expect(capabilities.canStartStopDataFrame).toBe(false);
expect(capabilities.canGetDataFrameAnalytics).toBe(true);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
@ -298,11 +278,6 @@ describe('check_privileges', () => {
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
expect(capabilities.canFindFileStructure).toBe(true);
expect(capabilities.canGetDataFrame).toBe(false);
expect(capabilities.canDeleteDataFrame).toBe(false);
expect(capabilities.canPreviewDataFrame).toBe(false);
expect(capabilities.canCreateDataFrame).toBe(false);
expect(capabilities.canStartStopDataFrame).toBe(false);
expect(capabilities.canGetDataFrameAnalytics).toBe(false);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
@ -338,11 +313,6 @@ describe('check_privileges', () => {
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
expect(capabilities.canFindFileStructure).toBe(true);
expect(capabilities.canGetDataFrame).toBe(true);
expect(capabilities.canDeleteDataFrame).toBe(true);
expect(capabilities.canPreviewDataFrame).toBe(true);
expect(capabilities.canCreateDataFrame).toBe(true);
expect(capabilities.canStartStopDataFrame).toBe(true);
expect(capabilities.canGetDataFrameAnalytics).toBe(false);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
@ -378,11 +348,6 @@ describe('check_privileges', () => {
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
expect(capabilities.canFindFileStructure).toBe(false);
expect(capabilities.canGetDataFrame).toBe(false);
expect(capabilities.canDeleteDataFrame).toBe(false);
expect(capabilities.canPreviewDataFrame).toBe(false);
expect(capabilities.canCreateDataFrame).toBe(false);
expect(capabilities.canStartStopDataFrame).toBe(false);
expect(capabilities.canGetDataFrameAnalytics).toBe(false);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
@ -420,11 +385,6 @@ describe('check_privileges', () => {
expect(capabilities.canCreateFilter).toBe(true);
expect(capabilities.canDeleteFilter).toBe(true);
expect(capabilities.canFindFileStructure).toBe(true);
expect(capabilities.canGetDataFrame).toBe(true);
expect(capabilities.canDeleteDataFrame).toBe(true);
expect(capabilities.canPreviewDataFrame).toBe(true);
expect(capabilities.canCreateDataFrame).toBe(true);
expect(capabilities.canStartStopDataFrame).toBe(true);
expect(capabilities.canGetDataFrameAnalytics).toBe(true);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(true);
expect(capabilities.canCreateDataFrameAnalytics).toBe(true);
@ -460,11 +420,6 @@ describe('check_privileges', () => {
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
expect(capabilities.canFindFileStructure).toBe(true);
expect(capabilities.canGetDataFrame).toBe(true);
expect(capabilities.canDeleteDataFrame).toBe(false);
expect(capabilities.canPreviewDataFrame).toBe(false);
expect(capabilities.canCreateDataFrame).toBe(false);
expect(capabilities.canStartStopDataFrame).toBe(false);
expect(capabilities.canGetDataFrameAnalytics).toBe(true);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
@ -500,11 +455,6 @@ describe('check_privileges', () => {
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
expect(capabilities.canFindFileStructure).toBe(true);
expect(capabilities.canGetDataFrame).toBe(true);
expect(capabilities.canDeleteDataFrame).toBe(false);
expect(capabilities.canPreviewDataFrame).toBe(false);
expect(capabilities.canCreateDataFrame).toBe(false);
expect(capabilities.canStartStopDataFrame).toBe(false);
expect(capabilities.canGetDataFrameAnalytics).toBe(true);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
@ -540,11 +490,6 @@ describe('check_privileges', () => {
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
expect(capabilities.canFindFileStructure).toBe(true);
expect(capabilities.canGetDataFrame).toBe(true);
expect(capabilities.canDeleteDataFrame).toBe(true);
expect(capabilities.canPreviewDataFrame).toBe(true);
expect(capabilities.canCreateDataFrame).toBe(true);
expect(capabilities.canStartStopDataFrame).toBe(true);
expect(capabilities.canGetDataFrameAnalytics).toBe(false);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
@ -580,11 +525,6 @@ describe('check_privileges', () => {
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
expect(capabilities.canFindFileStructure).toBe(true);
expect(capabilities.canGetDataFrame).toBe(true);
expect(capabilities.canDeleteDataFrame).toBe(true);
expect(capabilities.canPreviewDataFrame).toBe(true);
expect(capabilities.canCreateDataFrame).toBe(true);
expect(capabilities.canStartStopDataFrame).toBe(true);
expect(capabilities.canGetDataFrameAnalytics).toBe(false);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
@ -620,11 +560,6 @@ describe('check_privileges', () => {
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
expect(capabilities.canFindFileStructure).toBe(false);
expect(capabilities.canGetDataFrame).toBe(false);
expect(capabilities.canDeleteDataFrame).toBe(false);
expect(capabilities.canPreviewDataFrame).toBe(false);
expect(capabilities.canCreateDataFrame).toBe(false);
expect(capabilities.canStartStopDataFrame).toBe(false);
expect(capabilities.canGetDataFrameAnalytics).toBe(false);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);

View file

@ -129,14 +129,6 @@ function setFullGettingPrivileges(
privileges.canFindFileStructure = true;
}
// Data Frame Transforms
if (
forceTrue ||
(cluster['cluster:monitor/data_frame/get'] && cluster['cluster:monitor/data_frame/stats/get'])
) {
privileges.canGetDataFrame = true;
}
// Data Frame Analytics
if (
forceTrue ||
@ -234,28 +226,6 @@ function setFullActionPrivileges(
privileges.canDeleteFilter = true;
}
// Data Frame Transforms
if (forceTrue || cluster['cluster:admin/data_frame/put']) {
privileges.canCreateDataFrame = true;
}
if (forceTrue || cluster['cluster:admin/data_frame/delete']) {
privileges.canDeleteDataFrame = true;
}
if (forceTrue || cluster['cluster:admin/data_frame/preview']) {
privileges.canPreviewDataFrame = true;
}
if (
forceTrue ||
(cluster['cluster:admin/data_frame/start'] &&
cluster['cluster:admin/data_frame/start_task'] &&
cluster['cluster:admin/data_frame/stop'])
) {
privileges.canStartStopDataFrame = true;
}
// Data Frame Analytics
if (
forceTrue ||
@ -293,40 +263,10 @@ function setBasicGettingPrivileges(
if (forceTrue || cluster['cluster:monitor/xpack/ml/findfilestructure']) {
privileges.canFindFileStructure = true;
}
// Data Frame Transforms
if (
forceTrue ||
(cluster['cluster:monitor/data_frame/get'] && cluster['cluster:monitor/data_frame/stats/get'])
) {
privileges.canGetDataFrame = true;
}
}
function setBasicActionPrivileges(
cluster: ClusterPrivilege = {},
privileges: Privileges,
forceTrue = false
) {
// Data Frame Transforms
if (forceTrue || cluster['cluster:admin/data_frame/put']) {
privileges.canCreateDataFrame = true;
}
if (forceTrue || cluster['cluster:admin/data_frame/delete']) {
privileges.canDeleteDataFrame = true;
}
if (forceTrue || cluster['cluster:admin/data_frame/preview']) {
privileges.canPreviewDataFrame = true;
}
if (
forceTrue ||
(cluster['cluster:admin/data_frame/start'] &&
cluster['cluster:admin/data_frame/start_task'] &&
cluster['cluster:admin/data_frame/stop'])
) {
privileges.canStartStopDataFrame = true;
}
}
) {}

View file

@ -6,19 +6,11 @@
export const mlPrivileges = {
cluster: [
'cluster:monitor/data_frame/get',
'cluster:monitor/data_frame/stats/get',
'cluster:monitor/xpack/ml/job/get',
'cluster:monitor/xpack/ml/job/stats/get',
'cluster:monitor/xpack/ml/datafeeds/get',
'cluster:monitor/xpack/ml/datafeeds/stats/get',
'cluster:monitor/xpack/ml/calendars/get',
'cluster:admin/data_frame/delete',
'cluster:admin/data_frame/preview',
'cluster:admin/data_frame/put',
'cluster:admin/data_frame/start',
'cluster:admin/data_frame/start_task',
'cluster:admin/data_frame/stop',
'cluster:admin/xpack/ml/job/put',
'cluster:admin/xpack/ml/job/delete',
'cluster:admin/xpack/ml/job/update',

View file

@ -1,145 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { callWithRequestType } from '../../../common/types/kibana';
import { DATA_FRAME_TRANSFORM_STATE } from '../../../public/data_frame/common';
import {
DataFrameTransformEndpointRequest,
DataFrameTransformEndpointResult,
} from '../../../public/data_frame/pages/transform_management/components/transform_list/common';
import { DataFrameTransformId } from '../../../public/data_frame/common/transform';
import { isRequestTimeout, fillResultsWithTimeouts } from './error_utils';
enum TRANSFORM_ACTIONS {
STOP = 'stop',
START = 'start',
DELETE = 'delete',
}
interface StartTransformOptions {
transformId: DataFrameTransformId;
}
interface StopTransformOptions {
transformId: DataFrameTransformId;
force?: boolean;
waitForCompletion?: boolean;
}
export function transformServiceProvider(callWithRequest: callWithRequestType) {
async function deleteTransform(transformId: DataFrameTransformId) {
return callWithRequest('ml.deleteDataFrameTransform', { transformId });
}
async function stopTransform(options: StopTransformOptions) {
return callWithRequest('ml.stopDataFrameTransform', options);
}
async function startTransform(options: StartTransformOptions) {
return callWithRequest('ml.startDataFrameTransform', options);
}
async function deleteTransforms(transformsInfo: DataFrameTransformEndpointRequest[]) {
const results: DataFrameTransformEndpointResult = {};
for (const transformInfo of transformsInfo) {
const transformId = transformInfo.id;
try {
if (transformInfo.state === DATA_FRAME_TRANSFORM_STATE.FAILED) {
try {
await stopTransform({
transformId,
force: true,
waitForCompletion: true,
});
} catch (e) {
if (isRequestTimeout(e)) {
return fillResultsWithTimeouts({
results,
id: transformId,
items: transformsInfo,
action: TRANSFORM_ACTIONS.DELETE,
});
}
}
}
await deleteTransform(transformId);
results[transformId] = { success: true };
} catch (e) {
if (isRequestTimeout(e)) {
return fillResultsWithTimeouts({
results,
id: transformInfo.id,
items: transformsInfo,
action: TRANSFORM_ACTIONS.DELETE,
});
}
results[transformId] = { success: false, error: JSON.stringify(e) };
}
}
return results;
}
async function startTransforms(transformsInfo: DataFrameTransformEndpointRequest[]) {
const results: DataFrameTransformEndpointResult = {};
for (const transformInfo of transformsInfo) {
const transformId = transformInfo.id;
try {
await startTransform({ transformId });
results[transformId] = { success: true };
} catch (e) {
if (isRequestTimeout(e)) {
return fillResultsWithTimeouts({
results,
id: transformId,
items: transformsInfo,
action: TRANSFORM_ACTIONS.START,
});
}
results[transformId] = { success: false, error: JSON.stringify(e) };
}
}
return results;
}
async function stopTransforms(transformsInfo: DataFrameTransformEndpointRequest[]) {
const results: DataFrameTransformEndpointResult = {};
for (const transformInfo of transformsInfo) {
const transformId = transformInfo.id;
try {
await stopTransform({
transformId,
force:
transformInfo.state !== undefined
? transformInfo.state === DATA_FRAME_TRANSFORM_STATE.FAILED
: false,
waitForCompletion: true,
});
results[transformId] = { success: true };
} catch (e) {
if (isRequestTimeout(e)) {
return fillResultsWithTimeouts({
results,
id: transformId,
items: transformsInfo,
action: TRANSFORM_ACTIONS.STOP,
});
}
results[transformId] = { success: false, error: JSON.stringify(e) };
}
}
return results;
}
return {
deleteTransforms,
startTransforms,
stopTransforms,
};
}

View file

@ -37,8 +37,6 @@ import { notificationRoutes } from '../routes/notification_settings';
// @ts-ignore: could not find declaration file for module
import { systemRoutes } from '../routes/system';
// @ts-ignore: could not find declaration file for module
import { dataFrameRoutes } from '../routes/data_frame';
// @ts-ignore: could not find declaration file for module
import { dataFrameAnalyticsRoutes } from '../routes/data_frame_analytics';
// @ts-ignore: could not find declaration file for module
import { dataRecognizer } from '../routes/modules';
@ -222,7 +220,6 @@ export class Plugin {
annotationRoutes(routeInitializationDeps);
jobRoutes(routeInitializationDeps);
dataFeedRoutes(routeInitializationDeps);
dataFrameRoutes(routeInitializationDeps);
dataFrameAnalyticsRoutes(routeInitializationDeps);
indicesRoutes(routeInitializationDeps);
jobValidationRoutes(extendedRouteInitializationDeps);

View file

@ -1,155 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { callWithRequestFactory } from '../client/call_with_request_factory';
import { wrapError } from '../client/errors';
import { transformAuditMessagesProvider } from '../models/data_frame/transform_audit_messages';
import { transformServiceProvider } from '../models/data_frame';
export function dataFrameRoutes({ commonRouteConfig, elasticsearchPlugin, route }) {
route({
method: 'GET',
path: '/api/ml/_data_frame/transforms',
handler(request) {
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
return callWithRequest('ml.getDataFrameTransforms')
.catch(resp => wrapError(resp));
},
config: {
...commonRouteConfig
}
});
route({
method: 'GET',
path: '/api/ml/_data_frame/transforms/{transformId}',
handler(request) {
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
const { transformId } = request.params;
return callWithRequest('ml.getDataFrameTransforms', { transformId })
.catch(resp => wrapError(resp));
},
config: {
...commonRouteConfig
}
});
route({
method: 'GET',
path: '/api/ml/_data_frame/transforms/_stats',
handler(request) {
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
return callWithRequest('ml.getDataFrameTransformsStats')
.catch(resp => wrapError(resp));
},
config: {
...commonRouteConfig
}
});
route({
method: 'GET',
path: '/api/ml/_data_frame/transforms/{transformId}/_stats',
handler(request) {
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
const { transformId } = request.params;
return callWithRequest('ml.getDataFrameTransformsStats', { transformId })
.catch(resp => wrapError(resp));
},
config: {
...commonRouteConfig
}
});
route({
method: 'PUT',
path: '/api/ml/_data_frame/transforms/{transformId}',
handler(request) {
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
const { transformId } = request.params;
return callWithRequest('ml.createDataFrameTransform', { body: request.payload, transformId })
.catch(resp => wrapError(resp));
},
config: {
...commonRouteConfig
}
});
route({
method: 'POST',
path: '/api/ml/_data_frame/transforms/delete_transforms',
handler(request) {
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
const { deleteTransforms } = transformServiceProvider(callWithRequest);
const { transformsInfo } = request.payload;
return deleteTransforms(transformsInfo)
.catch(resp => wrapError(resp));
},
config: {
...commonRouteConfig
}
});
route({
method: 'POST',
path: '/api/ml/_data_frame/transforms/_preview',
handler(request) {
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
return callWithRequest('ml.getDataFrameTransformsPreview', { body: request.payload })
.catch(resp => wrapError(resp));
},
config: {
...commonRouteConfig
}
});
route({
method: 'POST',
path: '/api/ml/_data_frame/transforms/start_transforms',
handler(request) {
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
const { startTransforms } = transformServiceProvider(callWithRequest);
const { transformsInfo } = request.payload;
return startTransforms(transformsInfo)
.catch(resp => wrapError(resp));
},
config: {
...commonRouteConfig
}
});
route({
method: 'POST',
path: '/api/ml/_data_frame/transforms/stop_transforms',
handler(request) {
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
const { stopTransforms } = transformServiceProvider(callWithRequest);
const { transformsInfo } = request.payload;
return stopTransforms(transformsInfo)
.catch(resp => wrapError(resp));
},
config: {
...commonRouteConfig
}
});
route({
method: 'GET',
path: '/api/ml/_data_frame/transforms/{transformId}/messages',
handler(request) {
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
const { getTransformAuditMessages } = transformAuditMessagesProvider(callWithRequest);
const { transformId } = request.params;
return getTransformAuditMessages(transformId)
.catch(resp => wrapError(resp));
},
config: {
...commonRouteConfig
}
});
}

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { LICENSE_TYPE_BASIC, LicenseType } from '../../../common/constants';
export const DEFAULT_REFRESH_INTERVAL_MS = 30000;
export const MINIMUM_REFRESH_INTERVAL_MS = 1000;
export const PROGRESS_REFRESH_INTERVAL_MS = 2000;
export const PLUGIN = {
ID: 'transform',
MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType,
getI18nName: (): string => {
return i18n.translate('xpack.transform.appName', {
defaultMessage: 'Transforms',
});
},
};
export const API_BASE_PATH = '/api/transform/';
// Current df_admin permission requirements:
// 1. `read` on source index
// 2. `all` on source index to create and start transform
// 3. `all` on dest index (could be less tbd)
// 3. `monitor` cluster privilege
// 4. builtin `data_frame_transforms_admin`
// 5. builtin `kibana_user`
// 6. builtin `data_frame_transforms_user` (although this is probably included in the admin)
export const APP_CLUSTER_PRIVILEGES = [
'cluster:monitor/data_frame/get',
'cluster:monitor/data_frame/stats/get',
'cluster:admin/data_frame/delete',
'cluster:admin/data_frame/preview',
'cluster:admin/data_frame/put',
'cluster:admin/data_frame/start',
'cluster:admin/data_frame/start_task',
'cluster:admin/data_frame/stop',
];
// Equivalent of capabilities.canGetTransform
export const APP_GET_TRANSFORM_CLUSTER_PRIVILEGES = [
'cluster.cluster:monitor/data_frame/get',
'cluster.cluster:monitor/data_frame/stats/get',
];
// Equivalent of capabilities.canGetTransform
export const APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES = [
'cluster.cluster:monitor/data_frame/get',
'cluster.cluster:monitor/data_frame/stats/get',
'cluster.cluster:admin/data_frame/preview',
'cluster.cluster:admin/data_frame/put',
'cluster.cluster:admin/data_frame/start',
'cluster.cluster:admin/data_frame/start_task',
];
export const APP_INDEX_PRIVILEGES = ['monitor'];

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export interface Dictionary<TValue> {
[id: string]: TValue;
}
// converts a dictionary to an array. note this loses the dictionary `key` information.
// however it's able to retain the type information of the dictionary elements.
export function dictionaryToArray<TValue>(dict: Dictionary<TValue>): TValue[] {
return Object.keys(dict).map(key => dict[key]);
}
// A recursive partial type to allow passing nested partial attributes.
// Used for example for the optional `jobConfig.dest.results_field` property.
export type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export interface AuditMessageBase {
message: string;
level: string;
timestamp: number;
node_name: string;
text?: string;
}
export interface AuditMessage {
_index: string;
_type: string;
_id: string;
_score: null | number;
_source: TransformMessage;
sort?: any;
}
export interface TransformMessage extends AuditMessageBase {
transform_id: string;
}

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// utility functions for handling dates
// @ts-ignore
import { formatDate } from '@elastic/eui/lib/services/format';
export function formatHumanReadableDate(ts: number) {
return formatDate(ts, 'MMMM Do YYYY');
}
export function formatHumanReadableDateTime(ts: number) {
return formatDate(ts, 'MMMM Do YYYY, HH:mm');
}
export function formatHumanReadableDateTimeSeconds(ts: number) {
return formatDate(ts, 'MMMM Do YYYY, HH:mm:ss');
}

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
interface WindowWithTextEncoder extends Window {
TextEncoder: any;
}
const windowWithTextEncoder = window as WindowWithTextEncoder;
function isValidIndexNameLength(indexName: string) {
if (
windowWithTextEncoder.TextEncoder &&
new windowWithTextEncoder.TextEncoder('utf-8').encode(indexName).length > 255
) {
return false;
}
// If TextEncoder is not available just check for string.length
return indexName.length <= 255;
}
// rules taken from
// https://github.com/elastic/elasticsearch/blob/master/docs/reference/indices/create-index.asciidoc
export function isValidIndexName(indexName: string) {
return (
// Lowercase only
indexName === indexName.toLowerCase() &&
// Cannot include \, /, *, ?, ", <, >, |, space character, comma, #, :
/^[^\*\\/\?"<>|\s,#:]+$/.test(indexName) &&
// Cannot start with -, _, +
/^[^-_\+]+$/.test(indexName.charAt(0)) &&
// Cannot be . or ..
(indexName !== '.' && indexName !== '..') &&
// Cannot be longer than 255 bytes (note it is bytes,
// so multi-byte characters will count towards the 255 limit faster)
isValidIndexNameLength(indexName)
);
}

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { idx } from '@kbn/elastic-idx';
// This is similar to lodash's get() except that it's TypeScript aware and is able to infer return types.
// It splits the attribute key string and uses reduce with an idx check to access nested attributes.
export const getNestedProperty = (
obj: Record<string, any>,
accessor: string,
defaultValue?: any
) => {
return accessor.split('.').reduce((o, i) => idx(o, _ => _[i]), obj) || defaultValue;
};

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Legacy } from 'kibana';
import { resolve } from 'path';
import { PLUGIN } from './common/constants';
import { Plugin as TransformPlugin } from './server/plugin';
import { createServerShim } from './server/shim';
export function transform(kibana: any) {
return new kibana.Plugin({
id: PLUGIN.ID,
configPrefix: 'xpack.transform',
publicDir: resolve(__dirname, 'public'),
require: ['kibana', 'elasticsearch', 'xpack_main'],
uiExports: {
styleSheetPaths: resolve(__dirname, 'public/app/index.scss'),
managementSections: ['plugins/transform'],
},
init(server: Legacy.Server) {
const { core, plugins } = createServerShim(server, PLUGIN.ID);
const transformPlugin = new TransformPlugin();
// Start plugin
transformPlugin.start(core, plugins);
// Register license checker
plugins.license.registerLicenseChecker(
server,
PLUGIN.ID,
PLUGIN.getI18nName(),
PLUGIN.MINIMUM_LICENSE_REQUIRED
);
},
});
}

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useContext, FC } from 'react';
import { render } from 'react-dom';
import { Redirect, Route, Switch } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { SectionError } from './components';
import { CLIENT_BASE_PATH, SECTION_SLUG } from './constants';
import { getAppProviders } from './app_dependencies';
import { AuthorizationContext } from './lib/authorization';
import { AppDependencies } from '../shim';
import { CreateTransformSection } from './sections/create_transform';
import { TransformManagementSection } from './sections/transform_management';
export const App: FC = () => {
const { apiError } = useContext(AuthorizationContext);
if (apiError) {
return (
<SectionError
title={
<FormattedMessage
id="xpack.transform.app.checkingPrivilegesErrorMessage"
defaultMessage="Error fetching user privileges from the server."
/>
}
error={apiError}
/>
);
}
return (
<div data-test-subj="transformApp">
<Switch>
<Route
path={`${CLIENT_BASE_PATH}/${SECTION_SLUG.CREATE_TRANSFORM}/:savedObjectId`}
component={CreateTransformSection}
/>
<Route
exact
path={`${CLIENT_BASE_PATH}/${SECTION_SLUG.HOME}`}
component={TransformManagementSection}
/>
<Redirect from={`${CLIENT_BASE_PATH}`} to={`${CLIENT_BASE_PATH}/${SECTION_SLUG.HOME}`} />
</Switch>
</div>
);
};
export const renderReact = (elem: Element, appDependencies: AppDependencies) => {
const Providers = getAppProviders(appDependencies);
render(
<Providers>
<App />
</Providers>,
elem
);
};

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { createContext, useContext, ReactNode } from 'react';
import { HashRouter } from 'react-router-dom';
import { API_BASE_PATH } from '../../common/constants';
import { AuthorizationProvider } from './lib/authorization';
import { AppDependencies } from '../shim';
let DependenciesContext: React.Context<AppDependencies>;
const setAppDependencies = (deps: AppDependencies) => {
DependenciesContext = createContext<AppDependencies>(deps);
return DependenciesContext.Provider;
};
export const useAppDependencies = () => {
if (!DependenciesContext) {
throw new Error(`The app dependencies Context hasn't been set.
Use the "setAppDependencies()" method when bootstrapping the app.`);
}
return useContext<AppDependencies>(DependenciesContext);
};
export const getAppProviders = (deps: AppDependencies) => {
const I18nContext = deps.core.i18n.Context;
// Create App dependencies context and get its provider
const AppDependenciesProvider = setAppDependencies(deps);
return ({ children }: { children: ReactNode }) => (
<AuthorizationProvider
privilegesEndpoint={deps.core.http.basePath.prepend(`${API_BASE_PATH}privileges`)}
>
<I18nContext>
<HashRouter>
<AppDependenciesProvider value={deps}>{children}</AppDependenciesProvider>
</HashRouter>
</I18nContext>
</AuthorizationProvider>
);
};

View file

@ -6,7 +6,7 @@
import { isAggName } from './aggregations';
describe('Data Frame: Aggregations', () => {
describe('Transform: Aggregations', () => {
test('isAggName()', () => {
expect(isAggName('avg(responsetime)')).toEqual(true);
expect(isAggName('avg_responsetime')).toEqual(true);

View file

@ -22,22 +22,22 @@ export {
useRefreshTransformList,
CreateRequestBody,
PreviewRequestBody,
DataFrameTransformId,
DataFrameTransformPivotConfig,
TransformId,
TransformPivotConfig,
IndexName,
IndexPattern,
REFRESH_TRANSFORM_LIST_STATE,
} from './transform';
export { DATA_FRAME_TRANSFORM_LIST_COLUMN, DataFrameTransformListRow } from './transform_list';
export { TRANSFORM_LIST_COLUMN, TransformListRow } from './transform_list';
export {
getTransformProgress,
isCompletedBatchTransform,
isDataFrameTransformStats,
DataFrameTransformStats,
DATA_FRAME_MODE,
DATA_FRAME_TRANSFORM_STATE,
isTransformStats,
TransformStats,
TRANSFORM_MODE,
TRANSFORM_STATE,
} from './transform_stats';
export { moveToDataFrameWizard, getDiscoverUrl } from './navigation';
export { getDiscoverUrl } from './navigation';
export {
getEsAggFromAggConfig,
isPivotAggsConfigWithUiSupport,

View file

@ -4,15 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import { Redirect } from 'react-router-dom';
import rison from 'rison-node';
export function moveToDataFrameWizard() {
window.location.href = '#/data_frames/new_transform';
}
import { CLIENT_BASE_PATH, SECTION_SLUG } from '../constants';
/**
* Gets a url for navigating to Discover page.
* @param indexPatternId Index pattern id.
* @param indexPatternId Index pattern ID.
* @param baseUrl Base url.
*/
export function getDiscoverUrl(indexPatternId: string, baseUrl: string): string {
@ -27,3 +27,11 @@ export function getDiscoverUrl(indexPatternId: string, baseUrl: string): string
return `${baseUrl}${hash}`;
}
export const RedirectToTransformManagement: FC = () => (
<Redirect from={`${CLIENT_BASE_PATH}`} to={`${CLIENT_BASE_PATH}/${SECTION_SLUG.HOME}`} />
);
export const RedirectToCreateTransform: FC<{ savedObjectId: string }> = ({ savedObjectId }) => (
<Redirect to={`${CLIENT_BASE_PATH}/${SECTION_SLUG.CREATE_TRANSFORM}/${savedObjectId}`} />
);

View file

@ -6,8 +6,8 @@
import { PivotGroupByConfig } from '../common';
import { StepDefineExposedState } from '../pages/data_frame_new_pivot/components/step_define/step_define_form';
import { StepDetailsExposedState } from '../pages/data_frame_new_pivot/components/step_details/step_details_form';
import { StepDefineExposedState } from '../sections/create_transform/components/step_define/step_define_form';
import { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form';
import { PIVOT_SUPPORTED_GROUP_BY_AGGS } from './pivot_group_by';
import { PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from './pivot_aggs';
@ -24,7 +24,7 @@ const defaultQuery: PivotQuery = { query_string: { query: '*' } };
const matchAllQuery: PivotQuery = { match_all: {} };
const simpleQuery: PivotQuery = { query_string: { query: 'airline:AAL' } };
describe('Data Frame: Common', () => {
describe('Transform: Common', () => {
test('isSimpleQuery()', () => {
expect(isSimpleQuery(defaultQuery)).toBe(true);
expect(isSimpleQuery(matchAllQuery)).toBe(false);

View file

@ -9,10 +9,10 @@ import { DefaultOperator } from 'elasticsearch';
import { IndexPattern } from 'ui/index_patterns';
import { dictionaryToArray } from '../../../common/types/common';
import { SavedSearchQuery } from '../../contexts/kibana';
import { SavedSearchQuery } from '../lib/kibana';
import { StepDefineExposedState } from '../pages/data_frame_new_pivot/components/step_define/step_define_form';
import { StepDetailsExposedState } from '../pages/data_frame_new_pivot/components/step_details/step_details_form';
import { StepDefineExposedState } from '../sections/create_transform/components/step_define/step_define_form';
import { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form';
import {
getEsAggFromAggConfig,

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { isTransformIdValid } from './transform';
describe('Transform: isTransformIdValid()', () => {
test('returns true for job ID: "good_job-name"', () => {
expect(isTransformIdValid('good_job-name')).toBe(true);
});
test('returns false for job ID: "_bad_job-name"', () => {
expect(isTransformIdValid('_bad_job-name')).toBe(false);
});
test('returns false for job ID: "bad_job-name_"', () => {
expect(isTransformIdValid('bad_job-name_')).toBe(false);
});
test('returns false for job ID: "-bad_job-name"', () => {
expect(isTransformIdValid('-bad_job-name')).toBe(false);
});
test('returns false for job ID: "bad_job-name-"', () => {
expect(isTransformIdValid('bad_job-name-')).toBe(false);
});
test('returns false for job ID: "bad&job-name"', () => {
expect(isTransformIdValid('bad&job-name')).toBe(false);
});
});

View file

@ -9,17 +9,18 @@ import { BehaviorSubject } from 'rxjs';
import { filter, distinctUntilChanged } from 'rxjs/operators';
import { Subscription } from 'rxjs';
// @ts-ignore
import { isJobIdValid } from '../../../common/util/job_utils';
import { PivotAggDict } from './pivot_aggs';
import { PivotGroupByDict } from './pivot_group_by';
export const isTransformIdValid = isJobIdValid;
export type IndexName = string;
export type IndexPattern = string;
export type DataFrameTransformId = string;
export type TransformId = string;
// Transform name must contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores;
// It must also start and end with an alphanumeric character.
export function isTransformIdValid(transformId: TransformId) {
return /^[a-z0-9\-\_]+$/g.test(transformId) && !/^([_-].*)?(.*[_-])?$/g.test(transformId);
}
export interface PreviewRequestBody {
pivot: {
@ -45,8 +46,8 @@ export interface CreateRequestBody extends PreviewRequestBody {
};
}
export interface DataFrameTransformPivotConfig extends CreateRequestBody {
id: DataFrameTransformId;
export interface TransformPivotConfig extends CreateRequestBody {
id: TransformId;
}
export enum REFRESH_TRANSFORM_LIST_STATE {

View file

@ -4,20 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { DataFrameTransformId, DataFrameTransformPivotConfig } from './transform';
import { DataFrameTransformStats } from './transform_stats';
import { TransformId, TransformPivotConfig } from './transform';
import { TransformStats } from './transform_stats';
// Used to pass on attribute names to table columns
export enum DATA_FRAME_TRANSFORM_LIST_COLUMN {
export enum TRANSFORM_LIST_COLUMN {
CONFIG_DEST_INDEX = 'config.dest.index',
CONFIG_SOURCE_INDEX = 'config.source.index',
DESCRIPTION = 'config.description',
ID = 'id',
}
export interface DataFrameTransformListRow {
id: DataFrameTransformId;
config: DataFrameTransformPivotConfig;
export interface TransformListRow {
id: TransformId;
config: TransformPivotConfig;
mode?: string; // added property on client side to allow filtering by this field
stats: DataFrameTransformStats;
stats: TransformStats;
}

View file

@ -4,24 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import mockDataFrameTransformListRow from './__mocks__/data_frame_transform_list_row.json';
import mockDataFrameTransformStats from './__mocks__/data_frame_transform_stats.json';
import mockTransformListRow from './__mocks__/transform_list_row.json';
import mockTransformStats from './__mocks__/transform_stats.json';
import { DataFrameTransformListRow } from './transform_list';
import { TransformListRow } from './transform_list';
import { getTransformProgress, isCompletedBatchTransform } from './transform_stats';
const getRow = (statsId: string) => {
return {
...(mockDataFrameTransformListRow as DataFrameTransformListRow),
...(mockTransformListRow as TransformListRow),
stats: {
...mockDataFrameTransformStats.transforms.find(
(stats: DataFrameTransformListRow['stats']) => stats.id === statsId
...mockTransformStats.transforms.find(
(stats: TransformListRow['stats']) => stats.id === statsId
),
},
};
};
describe('Data Frame: Transform stats.', () => {
describe('Transform: Transform stats.', () => {
test('getTransformProgress()', () => {
// At the moment, any kind of stopped jobs don't include progress information.
// We cannot infer progress for now from an unfinished job that has been stopped for now.

View file

@ -6,11 +6,11 @@
import { idx } from '@kbn/elastic-idx';
import { DataFrameTransformId } from './transform';
import { DataFrameTransformListRow } from './transform_list';
import { TransformId } from './transform';
import { TransformListRow } from './transform_list';
// reflects https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/dataframe/transforms/DataFrameTransformStats.java#L243
export enum DATA_FRAME_TRANSFORM_STATE {
export enum TRANSFORM_STATE {
ABORTING = 'aborting',
FAILED = 'failed',
INDEXING = 'indexing',
@ -19,13 +19,13 @@ export enum DATA_FRAME_TRANSFORM_STATE {
STOPPING = 'stopping',
}
export enum DATA_FRAME_MODE {
export enum TRANSFORM_MODE {
BATCH = 'batch',
CONTINUOUS = 'continuous',
}
export interface DataFrameTransformStats {
id: DataFrameTransformId;
export interface TransformStats {
id: TransformId;
checkpointing: {
last: {
checkpoint: number;
@ -61,19 +61,19 @@ export interface DataFrameTransformStats {
trigger_count: number;
};
reason?: string;
state: DATA_FRAME_TRANSFORM_STATE;
state: TRANSFORM_STATE;
}
export function isDataFrameTransformStats(arg: any): arg is DataFrameTransformStats {
export function isTransformStats(arg: any): arg is TransformStats {
return (
typeof arg === 'object' &&
arg !== null &&
{}.hasOwnProperty.call(arg, 'state') &&
Object.values(DATA_FRAME_TRANSFORM_STATE).includes(arg.state)
Object.values(TRANSFORM_STATE).includes(arg.state)
);
}
export function getTransformProgress(item: DataFrameTransformListRow) {
export function getTransformProgress(item: TransformListRow) {
if (isCompletedBatchTransform(item)) {
return 100;
}
@ -83,12 +83,12 @@ export function getTransformProgress(item: DataFrameTransformListRow) {
return progress !== undefined ? Math.round(progress) : undefined;
}
export function isCompletedBatchTransform(item: DataFrameTransformListRow) {
export function isCompletedBatchTransform(item: TransformListRow) {
// If `checkpoint=1`, `sync` is missing from the config and state is stopped,
// then this is a completed batch data frame transform.
// then this is a completed batch transform.
return (
item.stats.checkpointing.last.checkpoint === 1 &&
item.config.sync === undefined &&
item.stats.state === DATA_FRAME_TRANSFORM_STATE.STOPPED
item.stats.state === TRANSFORM_STATE.STOPPED
);
}

View file

@ -4,7 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { getTransformsFactory } from './get_transforms';
export { deleteTransforms } from './delete_transform';
export { startTransforms } from './start_transform';
export { stopTransforms } from './stop_transform';
export { SectionError } from './section_error';
export { SectionLoading } from './section_loading';

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import { EuiIcon, EuiToolTip } from '@elastic/eui';
import { AuditMessageBase } from '../../../common/types/messages';
interface Props {
message: AuditMessageBase;
showTooltip?: boolean;
}
const [INFO, WARNING, ERROR] = ['info', 'warning', 'error'];
export const JobIcon: FC<Props> = ({ message, showTooltip = false }) => {
if (message === undefined) {
return <span />;
}
let color = 'primary';
const icon = 'alert';
if (message.level === INFO) {
color = 'primary';
} else if (message.level === WARNING) {
color = 'warning';
} else if (message.level === ERROR) {
color = 'danger';
}
if (showTooltip) {
return (
<EuiToolTip position="bottom" content={message.text}>
<EuiIcon type={icon} color={color} />
</EuiToolTip>
);
} else {
return <EuiIcon type={icon} color={color} />;
}
};

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import React, { Fragment } from 'react';
interface Props {
title: React.ReactNode;
error: {
data: {
error: string;
cause?: string[];
message?: string;
};
};
actions?: JSX.Element;
}
export const SectionError: React.FunctionComponent<Props> = ({
title,
error,
actions,
...rest
}) => {
const {
error: errorString,
cause, // wrapEsError() on the server adds a "cause" array
message,
} = error.data;
return (
<EuiCallOut title={title} color="danger" iconType="alert" {...rest}>
{cause ? message || errorString : <p>{message || errorString}</p>}
{cause && (
<Fragment>
<EuiSpacer size="s" />
<ul>
{cause.map((causeMsg, i) => (
<li key={i}>{causeMsg}</li>
))}
</ul>
</Fragment>
)}
{actions ? actions : null}
</EuiCallOut>
);
};

View file

@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
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"
/>
);
};

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const CLIENT_BASE_PATH = '/management/elasticsearch/transform';
export enum SECTION_SLUG {
HOME = 'transform_management',
CREATE_TRANSFORM = 'create_transform',
}
export enum TRANSFORM_DOC_PATHS {
default = 'docs.html',
plugins = 'plugins.html',
}
// UI Metric constants
export const UIM_APP_NAME = 'transform';
export const UIM_TRANSFORM_LIST_LOAD = 'transform_list_load';
export const UIM_TRANSFORM_CREATE = 'transform_create';
export const UIM_TRANSFORM_DELETE = 'transform_delete';
export const UIM_TRANSFORM_DELETE_MANY = 'transform_delete_many';
export const UIM_TRANSFORM_SHOW_DETAILS_CLICK = 'transform_show_details_click';

View file

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PreviewRequestBody, TransformId } from '../../common';
import { TransformEndpointRequest } from '../use_api_types';
const apiFactory = () => ({
getTransforms(transformId?: TransformId): Promise<any> {
return new Promise((resolve, reject) => {
resolve([]);
});
},
getTransformsStats(transformId?: TransformId): Promise<any> {
if (transformId !== undefined) {
return new Promise((resolve, reject) => {
resolve([]);
});
}
return new Promise((resolve, reject) => {
resolve([]);
});
},
createTransform(transformId: TransformId, transformConfig: any): Promise<any> {
return new Promise((resolve, reject) => {
resolve([]);
});
},
deleteTransforms(transformsInfo: TransformEndpointRequest[]) {
return new Promise((resolve, reject) => {
resolve([]);
});
},
getTransformsPreview(obj: PreviewRequestBody): Promise<any> {
return new Promise((resolve, reject) => {
resolve([]);
});
},
startTransforms(transformsInfo: TransformEndpointRequest[]) {
return new Promise((resolve, reject) => {
resolve([]);
});
},
stopTransforms(transformsInfo: TransformEndpointRequest[]) {
return new Promise((resolve, reject) => {
resolve([]);
});
},
getTransformAuditMessages(transformId: TransformId): Promise<any> {
return new Promise((resolve, reject) => {
resolve([]);
});
},
esSearch(payload: any) {
return new Promise((resolve, reject) => {
resolve([]);
});
},
getIndices() {
return new Promise((resolve, reject) => {
resolve([]);
});
},
});
export const useApi = () => {
return apiFactory();
};

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { useApi } from './use_api';
export { useGetTransforms } from './use_get_transforms';
export { useDeleteTransforms } from './use_delete_transform';
export { useStartTransforms } from './use_start_transform';
export { useStopTransforms } from './use_stop_transform';

View file

@ -0,0 +1,103 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useAppDependencies } from '../app_dependencies';
import { PreviewRequestBody, TransformId } from '../common';
import { http } from '../services/http_service';
import { EsIndex, TransformEndpointRequest, TransformEndpointResult } from './use_api_types';
const apiFactory = (basePath: string, indicesBasePath: string) => ({
getTransforms(transformId?: TransformId): Promise<any> {
const transformIdString = transformId !== undefined ? `/${transformId}` : '';
return http({
url: `${basePath}/transforms${transformIdString}`,
method: 'GET',
});
},
getTransformsStats(transformId?: TransformId): Promise<any> {
if (transformId !== undefined) {
return http({
url: `${basePath}/transforms/${transformId}/_stats`,
method: 'GET',
});
}
return http({
url: `${basePath}/transforms/_stats`,
method: 'GET',
});
},
createTransform(transformId: TransformId, transformConfig: any): Promise<any> {
return http({
url: `${basePath}/transforms/${transformId}`,
method: 'PUT',
data: transformConfig,
});
},
deleteTransforms(transformsInfo: TransformEndpointRequest[]) {
return http({
url: `${basePath}/delete_transforms`,
method: 'POST',
data: transformsInfo,
}) as Promise<TransformEndpointResult>;
},
getTransformsPreview(obj: PreviewRequestBody): Promise<any> {
return http({
url: `${basePath}/transforms/_preview`,
method: 'POST',
data: obj,
});
},
startTransforms(transformsInfo: TransformEndpointRequest[]) {
return http({
url: `${basePath}/start_transforms`,
method: 'POST',
data: {
transformsInfo,
},
}) as Promise<TransformEndpointResult>;
},
stopTransforms(transformsInfo: TransformEndpointRequest[]) {
return http({
url: `${basePath}/stop_transforms`,
method: 'POST',
data: {
transformsInfo,
},
}) as Promise<TransformEndpointResult>;
},
getTransformAuditMessages(transformId: TransformId): Promise<any> {
return http({
url: `${basePath}/transforms/${transformId}/messages`,
method: 'GET',
});
},
esSearch(payload: any) {
return http({
url: `${basePath}/es_search`,
method: 'POST',
data: payload,
}) as Promise<any>;
},
getIndices() {
return http({
url: `${indicesBasePath}/index_management/indices`,
method: 'GET',
}) as Promise<EsIndex[]>;
},
});
export const useApi = () => {
const appDeps = useAppDependencies();
const basePath = appDeps.core.http.basePath.prepend('/api/transform');
const indicesBasePath = appDeps.core.http.basePath.prepend('/api');
return apiFactory(basePath, indicesBasePath);
};

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { TransformId, TRANSFORM_STATE } from '../common';
export interface EsIndex {
name: string;
}
export interface TransformEndpointRequest {
id: TransformId;
state?: TRANSFORM_STATE;
}
export interface ResultData {
success: boolean;
error?: any;
}
export interface TransformEndpointResult {
[key: string]: ResultData;
}

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
import { useApi } from './use_api';
import { TransformEndpointRequest, TransformEndpointResult } from './use_api_types';
export const useDeleteTransforms = () => {
const api = useApi();
return async (transforms: TransformListRow[]) => {
const transformsInfo: TransformEndpointRequest[] = transforms.map(tf => ({
id: tf.config.id,
state: tf.stats.state,
}));
try {
const results: TransformEndpointResult = await api.deleteTransforms(transformsInfo);
for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
if (results.hasOwnProperty(transformId)) {
if (results[transformId].success === true) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.deleteTransformSuccessMessage', {
defaultMessage: 'Request to delete transform {transformId} acknowledged.',
values: { transformId },
})
);
} else {
toastNotifications.addDanger(
i18n.translate('xpack.transform.transformList.deleteTransformErrorMessage', {
defaultMessage: 'An error occurred deleting the transform {transformId}',
values: { transformId },
})
);
}
}
}
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
} catch (e) {
toastNotifications.addDanger(
i18n.translate('xpack.transform.transformList.deleteTransformGenericErrorMessage', {
defaultMessage: 'An error occurred calling the API endpoint to delete transforms.',
})
);
}
};
};

View file

@ -4,31 +4,30 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ml } from '../../../../../services/ml_api_service';
import {
DataFrameTransformListRow,
DataFrameTransformStats,
DATA_FRAME_MODE,
isDataFrameTransformStats,
DataFrameTransformPivotConfig,
TransformListRow,
TransformStats,
TRANSFORM_MODE,
isTransformStats,
TransformPivotConfig,
refreshTransformList$,
REFRESH_TRANSFORM_LIST_STATE,
} from '../../../../common';
} from '../common';
interface GetDataFrameTransformsResponse {
import { useApi } from './use_api';
interface GetTransformsResponse {
count: number;
transforms: DataFrameTransformPivotConfig[];
transforms: TransformPivotConfig[];
}
interface GetDataFrameTransformsStatsResponseOk {
interface GetTransformsStatsResponseOk {
node_failures?: object;
count: number;
transforms: DataFrameTransformStats[];
transforms: TransformStats[];
}
const isGetDataFrameTransformsStatsResponseOk = (
arg: any
): arg is GetDataFrameTransformsStatsResponseOk => {
const isGetTransformsStatsResponseOk = (arg: any): arg is GetTransformsStatsResponseOk => {
return (
{}.hasOwnProperty.call(arg, 'count') &&
{}.hasOwnProperty.call(arg, 'transforms') &&
@ -36,26 +35,26 @@ const isGetDataFrameTransformsStatsResponseOk = (
);
};
interface GetDataFrameTransformsStatsResponseError {
interface GetTransformsStatsResponseError {
statusCode: number;
error: string;
message: string;
}
type GetDataFrameTransformsStatsResponse =
| GetDataFrameTransformsStatsResponseOk
| GetDataFrameTransformsStatsResponseError;
type GetTransformsStatsResponse = GetTransformsStatsResponseOk | GetTransformsStatsResponseError;
export type GetTransforms = (forceRefresh?: boolean) => void;
export const getTransformsFactory = (
setTransforms: React.Dispatch<React.SetStateAction<DataFrameTransformListRow[]>>,
export const useGetTransforms = (
setTransforms: React.Dispatch<React.SetStateAction<TransformListRow[]>>,
setErrorMessage: React.Dispatch<
React.SetStateAction<GetDataFrameTransformsStatsResponseError | undefined>
React.SetStateAction<GetTransformsStatsResponseError | undefined>
>,
setIsInitialized: React.Dispatch<React.SetStateAction<boolean>>,
blockRefresh: boolean
): GetTransforms => {
const api = useApi();
let concurrentLoads = 0;
const getTransforms = async (forceRefresh = false) => {
@ -68,18 +67,18 @@ export const getTransformsFactory = (
}
try {
const transformConfigs: GetDataFrameTransformsResponse = await ml.dataFrame.getDataFrameTransforms();
const transformStats: GetDataFrameTransformsStatsResponse = await ml.dataFrame.getDataFrameTransformsStats();
const transformConfigs: GetTransformsResponse = await api.getTransforms();
const transformStats: GetTransformsStatsResponse = await api.getTransformsStats();
const tableRows = transformConfigs.transforms.reduce(
(reducedtableRows, config) => {
const stats = isGetDataFrameTransformsStatsResponseOk(transformStats)
const stats = isGetTransformsStatsResponseOk(transformStats)
? transformStats.transforms.find(d => config.id === d.id)
: undefined;
// 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 (!isDataFrameTransformStats(stats)) {
if (!isTransformStats(stats)) {
return reducedtableRows;
}
@ -89,13 +88,13 @@ export const getTransformsFactory = (
config,
mode:
typeof config.sync !== 'undefined'
? DATA_FRAME_MODE.CONTINUOUS
: DATA_FRAME_MODE.BATCH,
? TRANSFORM_MODE.CONTINUOUS
: TRANSFORM_MODE.BATCH,
stats,
});
return reducedtableRows;
},
[] as DataFrameTransformListRow[]
[] as TransformListRow[]
);
setTransforms(tableRows);

View 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;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
import { useApi } from './use_api';
import { TransformEndpointRequest, TransformEndpointResult } from './use_api_types';
export const useStartTransforms = () => {
const api = useApi();
return async (transforms: TransformListRow[]) => {
const transformsInfo: TransformEndpointRequest[] = transforms.map(tf => ({
id: tf.config.id,
state: tf.stats.state,
}));
const results: TransformEndpointResult = await api.startTransforms(transformsInfo);
for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
if (results.hasOwnProperty(transformId)) {
if (results[transformId].success === true) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.startTransformSuccessMessage', {
defaultMessage: 'Request to start transform {transformId} acknowledged.',
values: { transformId },
})
);
} else {
toastNotifications.addDanger(
i18n.translate('xpack.transform.transformList.startTransformErrorMessage', {
defaultMessage: 'An error occurred starting the transform {transformId}',
values: { transformId },
})
);
}
}
}
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
};
};

View 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;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
import { useApi } from './use_api';
import { TransformEndpointRequest, TransformEndpointResult } from './use_api_types';
export const useStopTransforms = () => {
const api = useApi();
return async (transforms: TransformListRow[]) => {
const transformsInfo: TransformEndpointRequest[] = transforms.map(df => ({
id: df.config.id,
state: df.stats.state,
}));
const results: TransformEndpointResult = await api.stopTransforms(transformsInfo);
for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
if (results.hasOwnProperty(transformId)) {
if (results[transformId].success === true) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.stopTransformSuccessMessage', {
defaultMessage: 'Request to stop data frame transform {transformId} acknowledged.',
values: { transformId },
})
);
} else {
toastNotifications.addDanger(
i18n.translate('xpack.transform.transformList.stopTransformErrorMessage', {
defaultMessage: 'An error occurred stopping the data frame transform {transformId}',
values: { transformId },
})
);
}
}
}
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
};
};

Some files were not shown because too many files have changed in this diff Show more