Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] Adds DF Transform Analytics list to Kibana management #43151

Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ function stringMatch(str: string | undefined, substr: string) {
);
}

export const DataFrameAnalyticsList: FC = () => {
interface Props {
isManagementTable?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming nit: I'm afraid isManagementTable could be a bit confusing. It refers to the management section in Kibana, but seen alone here like this "management" could mean the opposite as in the capability to manage jobs where the table in Kibana management is view only. I'd like to suggest that we rename it to something that refers to the feature that the flag will enable/disable, something like isReadOnly or hideActions, what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. 🤔 I'm not sure isReadOnly or something like that would fit since it's more that we're showing a different level of detail as well as removing actions when it's for the Kibana management view. This is the same prop name used in for the job management table so it would be consistent. Happy to change to something that makes more sense. Maybe in a small follow-up once we've decided on a name?

}
// isManagementTable - for use in Kibana managagement ML section
export const DataFrameAnalyticsList: FC<Props> = ({ isManagementTable }) => {
const [isInitialized, setIsInitialized] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [blockRefresh, setBlockRefresh] = useState(false);
Expand Down Expand Up @@ -225,7 +229,7 @@ export const DataFrameAnalyticsList: FC = () => {
);
}

const columns = getColumns(expandedRowItemIds, setExpandedRowItemIds);
const columns = getColumns(expandedRowItemIds, setExpandedRowItemIds, isManagementTable);

const sorting = {
sort: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export const getTaskStateBadge = (

export const getColumns = (
expandedRowItemIds: DataFrameAnalyticsId[],
setExpandedRowItemIds: React.Dispatch<React.SetStateAction<DataFrameAnalyticsId[]>>
setExpandedRowItemIds: React.Dispatch<React.SetStateAction<DataFrameAnalyticsId[]>>,
isManagementTable: boolean = false
) => {
const actions = getActions();

Expand All @@ -75,8 +76,8 @@ export const getColumns = (
// spread to a new array otherwise the component wouldn't re-render
setExpandedRowItemIds([...expandedRowItemIds]);
}

return [
// update possible column types to something like (FieldDataColumn | ComputedColumn | ActionsColumn)[] when they have been added to EUI
const columns: any[] = [
{
align: RIGHT_ALIGNMENT,
width: '40px',
Expand Down Expand Up @@ -205,12 +206,17 @@ export const getColumns = (
},
width: '100px',
},
{
];

if (isManagementTable === false) {
columns.push({
name: i18n.translate('xpack.ml.dataframe.analyticsList.tableActionLabel', {
defaultMessage: 'Actions',
}),
actions,
width: '200px',
},
];
});
}

return columns;
};
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ class JobsListUI extends Component {
};

getJobIdLink(id) {
// Don't allow link to job if ML is not enabled in current space
if (this.props.isMlEnabledInSpace === false) {
return id;
}

return (
<EuiLink href={getJobIdUrl(id)}>
{id}
Expand Down Expand Up @@ -253,6 +258,10 @@ class JobsListUI extends Component {
<EuiBadge color={'hollow'}>{'all'}</EuiBadge>
)
});
// Remove actions if Ml not enabled in current space
if (this.props.isMlEnabledInSpace === false) {
columns.pop();
}
} else {
// insert before last column
columns.splice(columns.length - 1, 0, {
Expand Down Expand Up @@ -344,6 +353,8 @@ class JobsListUI extends Component {
JobsListUI.propTypes = {
jobsSummaryList: PropTypes.array.isRequired,
fullJobsList: PropTypes.object.isRequired,
isManagementTable: PropTypes.bool,
isMlEnabledInSpace: PropTypes.bool,
itemIdToExpandedRowMap: PropTypes.object.isRequired,
toggleRow: PropTypes.func.isRequired,
selectJobChange: PropTypes.func.isRequired,
Expand All @@ -355,6 +366,8 @@ JobsListUI.propTypes = {
loading: PropTypes.bool,
};
JobsListUI.defaultProps = {
isManagementTable: false,
isMlEnabledInSpace: true,
loading: false,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,21 +362,22 @@ export class JobsListView extends Component {
}

renderManagementJobsListComponents() {
const { loading } = this.state;
const { loading, itemIdToExpandedRowMap, filteredJobsSummaryList, fullJobsList, selectedJobs } = this.state;
return (
<div className="managementJobsList">
<div>
<JobFilterBar setFilters={this.setFilters} />
</div>
<JobsList
jobsSummaryList={this.state.filteredJobsSummaryList}
fullJobsList={this.state.fullJobsList}
itemIdToExpandedRowMap={this.state.itemIdToExpandedRowMap}
jobsSummaryList={filteredJobsSummaryList}
fullJobsList={fullJobsList}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
toggleRow={this.toggleRow}
selectJobChange={this.selectJobChange}
selectedJobsCount={this.state.selectedJobs.length}
selectedJobsCount={selectedJobs.length}
loading={loading}
isManagementTable={true}
isMlEnabledInSpace={this.props.isMlEnabledInSpace}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { each } from 'lodash';
import { toastNotifications } from 'ui/notify';
import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service';
import rison from 'rison-node';
import chrome from 'ui/chrome'; // TODO: get from context once walter's PR is merged
import chrome from 'ui/chrome';

import { mlJobService } from 'plugins/ml/services/job_service';
import { ml } from 'plugins/ml/services/ml_api_service';
Expand Down
6 changes: 5 additions & 1 deletion x-pack/legacy/plugins/ml/public/management/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ import { management } from 'ui/management';
import { xpackInfo } from 'plugins/xpack_main/services/xpack_info';
import { i18n } from '@kbn/i18n';
import { JOBS_LIST_PATH } from './management_urls';
import { LICENSE_TYPE } from '../../common/constants/license';
import 'plugins/ml/management/jobs_list';

if (xpackInfo.get('features.ml.showLinks', false) === true) {
if (
xpackInfo.get('features.ml.showLinks', false) === true &&
xpackInfo.get('features.ml.licenseType') === LICENSE_TYPE.FULL
) {
management.register('ml', {
display: i18n.translate('xpack.ml.management.mlTitle', {
defaultMessage: 'Machine Learning',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@import './jobs_list_page/stats_bar';
@import './jobs_list_page/buttons';
@import './jobs_list_page/expanded_row';
@import './jobs_list_page/analytics_table';
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

.mlAnalyticsTable {
// Using an override as a last resort because we cannot set custom classes on
// nested upstream components. The opening animation limits the height
// of the expanded row to 1000px which turned out to be not predictable.
// The animation could also result in flickering with expanded rows
// where the inner content would result in the DOM changing the height.
.euiTableRow-isExpandedRow .euiTableCellContent {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make an issue in EUI surrounding the problem you encountered so we can make the fix over there?

animation: none !important;
.euiTableCellContent__text {
width: 100%;
}
}
// Another override: Because an update to the table replaces the DOM, the same
// icon would still again fade in with an animation. If the table refreshes with
// e.g. 1s this would result in a blinking icon effect.
.euiIcon-isLoaded {
animation: none !important;
}
}

.mlAnalyticsProgressBar {
margin-bottom: $euiSizeM;
}

.mlTaskStateBadge {
max-width: 100px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { Fragment } from 'react';
import React, { Fragment, FC } from 'react';
import { i18n } from '@kbn/i18n';
import { I18nContext } from 'ui/i18n';
import {
Expand All @@ -19,8 +19,13 @@ import {

// @ts-ignore undeclared module
import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view';
import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list';

export const JobsListPage = () => {
interface Props {
isMlEnabledInSpace: boolean;
}

export const JobsListPage: FC<Props> = ({ isMlEnabledInSpace }) => {
const tabs = [
{
id: 'anomaly_detection_jobs',
Expand All @@ -30,23 +35,24 @@ export const JobsListPage = () => {
content: (
<Fragment>
<EuiSpacer size="m" />
<JobsListView isManagementTable={true} />
<JobsListView isManagementTable={true} isMlEnabledInSpace={isMlEnabledInSpace} />
</Fragment>
),
},
{
id: 'analytics_jobs',
name: i18n.translate('xpack.ml.management.jobsList.analyticsTab', {
defaultMessage: 'Analytics',
}),
content: (
<Fragment>
<EuiSpacer size="m" />
<DataFrameAnalyticsList isManagementTable={true} />
</Fragment>
),
},
// {
// id: 'analytics_jobs',
// name: i18n.translate('xpack.ml.management.jobsList.analyticsTab', {
// defaultMessage: 'Analytics',
// }),
// content: renderAnalyticsJobs(),
// },
];

// function renderAnalyticsJobs() {
// return <div>Analytics job placeholder</div>;
// }

function renderTabs() {
return <EuiTabbedContent size="s" tabs={tabs} initialSelectedTab={tabs[0]} />;
}
Expand Down
13 changes: 9 additions & 4 deletions x-pack/legacy/plugins/ml/public/management/jobs_list/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

// @ts-ignore no declaration module
import { ReactDOM, render, unmountComponentAtNode } from 'react-dom';
import ReactDOM, { render, unmountComponentAtNode } from 'react-dom';
import React from 'react';
import routes from 'ui/routes';
import { canGetManagementMlJobs } from '../../privilege/check_privilege';
import { JOBS_LIST_PATH, ACCESS_DENIED_PATH } from '../management_urls';
Expand All @@ -22,14 +22,19 @@ routes.when(JOBS_LIST_PATH, {
resolve: {
checkPrivilege: canGetManagementMlJobs,
},
controller($scope) {
controller($scope, checkPrivilege) {
const { mlFeatureEnabledInSpace } = checkPrivilege;

$scope.$on('$destroy', () => {
const elem = document.getElementById('kibanaManagementMLSection');
if (elem) unmountComponentAtNode(elem);
});
$scope.$$postDigest(() => {
const element = document.getElementById('kibanaManagementMLSection');
render(JobsListPage(), element);
ReactDOM.render(
React.createElement(JobsListPage, { isMlEnabledInSpace: mlFeatureEnabledInSpace }),
element
);
});
},
});
Expand Down
22 changes: 12 additions & 10 deletions x-pack/legacy/plugins/ml/public/privilege/check_privilege.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,20 @@ let privileges: Privileges = getDefaultPrivileges();
// manage_ml requires all monitor and admin cluster privileges: https://github.com/elastic/elasticsearch/blob/664a29c8905d8ce9ba8c18aa1ed5c5de93a0eabc/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java#L53
export function canGetManagementMlJobs(kbnUrl: any) {
return new Promise((resolve, reject) => {
getManageMlPrivileges().then(({ capabilities, isPlatinumOrTrialLicense }) => {
privileges = capabilities;
// Loop through all privilages to ensure they are all set to true.
const isManageML = Object.values(privileges).every(p => p === true);
getManageMlPrivileges().then(
({ capabilities, isPlatinumOrTrialLicense, mlFeatureEnabledInSpace }) => {
privileges = capabilities;
// Loop through all privilages to ensure they are all set to true.
const isManageML = Object.values(privileges).every(p => p === true);

if (isManageML === true && isPlatinumOrTrialLicense === true) {
return resolve();
} else {
kbnUrl.redirect(ACCESS_DENIED_PATH);
return reject();
if (isManageML === true && isPlatinumOrTrialLicense === true) {
return resolve({ mlFeatureEnabledInSpace });
} else {
kbnUrl.redirect(ACCESS_DENIED_PATH);
return reject();
}
}
});
);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ interface Response {
export function privilegesProvider(
callWithRequest: callWithRequestType,
xpackMainPlugin: XPackMainPlugin,
isMlEnabledInSpace: () => Promise<boolean>
isMlEnabledInSpace: () => Promise<boolean>,
ignoreSpaces: boolean = false
) {
const { isUpgradeInProgress } = upgradeCheckProvider(callWithRequest);
async function getPrivileges(): Promise<Response> {
Expand All @@ -47,7 +48,7 @@ export function privilegesProvider(
? setFullActionPrivileges
: setBasicActionPrivileges;

if (mlFeatureEnabledInSpace === false) {
if (mlFeatureEnabledInSpace === false && ignoreSpaces === false) {
// if ML isn't enabled in the current space,
// return with the default privileges (all false)
return {
Expand Down
6 changes: 3 additions & 3 deletions x-pack/legacy/plugins/ml/server/routes/system.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,12 @@ export function systemRoutes({
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
try {
const ignoreSpaces = request.query && request.query.ignoreSpaces === 'true';
// if spaces is disabled or ignoreSpace is true force isMlEnabledInSpace to be true
const { isMlEnabledInSpace } = (spacesPlugin !== undefined && ignoreSpaces === false) ?
// if spaces is disabled force isMlEnabledInSpace to be true
const { isMlEnabledInSpace } = spacesPlugin !== undefined ?
spacesUtilsProvider(spacesPlugin, request, config) :
{ isMlEnabledInSpace: async () => true };

const { getPrivileges } = privilegesProvider(callWithRequest, xpackMainPlugin, isMlEnabledInSpace);
const { getPrivileges } = privilegesProvider(callWithRequest, xpackMainPlugin, isMlEnabledInSpace, ignoreSpaces);
return await getPrivileges();
} catch (error) {
return wrapError(error);
Expand Down