Skip to content

Commit

Permalink
[ML] Update route resolvers (#159176)
Browse files Browse the repository at this point in the history
## Summary

Resolves #153932

Updates route resolver callback to track license and ML capabilities
requirements.

- The logic for resolving the data view and saved search has been moved
to the dedicated context `DataSourceContextProvider` and only applies to
pages that need it. It also shows an error callout in case of an error
during the data view fetch.
- ML License class has been updated to track license changes and logic
for redirects has been moved to the route resolver
- `MlCapabilitiesService` has been updated to periodically fetch
capabilities
- Most of the static usages of `checkPermission` have been replaced with
`usePermissionCheck`


### Notes for reviewers 

- Now it's obvious what license and capabilities requirements each route
has. We should carefully review it because I assume legacy resolvers
were not entirely correct in the same cases.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
  • Loading branch information
darnautov committed Jun 13, 2023
1 parent 0c4906a commit bf68488
Show file tree
Hide file tree
Showing 117 changed files with 1,198 additions and 1,393 deletions.
1 change: 0 additions & 1 deletion x-pack/plugins/ml/common/constants/locator.ts
Expand Up @@ -60,7 +60,6 @@ export const ML_PAGES = {
FILTER_LISTS_MANAGE: 'settings/filter_lists',
FILTER_LISTS_NEW: 'settings/filter_lists/new_filter_list',
FILTER_LISTS_EDIT: 'settings/filter_lists/edit_filter_list',
ACCESS_DENIED: 'access-denied',
OVERVIEW: 'overview',
NOTIFICATIONS: 'notifications',
AIOPS: 'aiops',
Expand Down
63 changes: 55 additions & 8 deletions x-pack/plugins/ml/common/license/ml_license.ts
Expand Up @@ -5,8 +5,10 @@
* 2.0.
*/

import { Observable, Subscription } from 'rxjs';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { ILicense } from '@kbn/licensing-plugin/common/types';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { isEqual } from 'lodash';
import { PLUGIN_ID } from '../constants/app';

export const MINIMUM_LICENSE = 'basic';
Expand All @@ -19,6 +21,16 @@ export interface LicenseStatus {
message?: string;
}

export interface MlLicenseInfo {
license: ILicense | null;
isSecurityEnabled: boolean;
hasLicenseExpired: boolean;
isMlEnabled: boolean;
isMinimumLicense: boolean;
isFullLicense: boolean;
isTrialLicense: boolean;
}

export class MlLicense {
private _licenseSubscription: Subscription | null = null;
private _license: ILicense | null = null;
Expand All @@ -29,24 +41,59 @@ export class MlLicense {
private _isFullLicense: boolean = false;
private _isTrialLicense: boolean = false;

private _licenseInfo$ = new BehaviorSubject<MlLicenseInfo>({
license: this._license,
isSecurityEnabled: this._isSecurityEnabled,
hasLicenseExpired: this._hasLicenseExpired,
isMlEnabled: this._isMlEnabled,
isMinimumLicense: this._isMinimumLicense,
isFullLicense: this._isFullLicense,
isTrialLicense: this._isTrialLicense,
});

public licenseInfo$: Observable<MlLicenseInfo> = this._licenseInfo$.pipe(
distinctUntilChanged(isEqual)
);

public isLicenseReady$: Observable<boolean> = this._licenseInfo$.pipe(
map((v) => !!v.license),
distinctUntilChanged()
);

public setup(license$: Observable<ILicense>, callback?: (lic: MlLicense) => void) {
this._licenseSubscription = license$.subscribe(async (license) => {
this._licenseSubscription = license$.subscribe((license) => {
const { isEnabled: securityIsEnabled } = license.getFeature('security');

const mlLicenseUpdate = {
license,
isSecurityEnabled: securityIsEnabled,
hasLicenseExpired: license.status === 'expired',
isMlEnabled: license.getFeature(PLUGIN_ID).isEnabled,
isMinimumLicense: isMinimumLicense(license),
isFullLicense: isFullLicense(license),
isTrialLicense: isTrialLicense(license),
};

this._licenseInfo$.next(mlLicenseUpdate);

this._license = license;
this._isSecurityEnabled = securityIsEnabled;
this._hasLicenseExpired = this._license.status === 'expired';
this._isMlEnabled = this._license.getFeature(PLUGIN_ID).isEnabled;
this._isMinimumLicense = isMinimumLicense(this._license);
this._isFullLicense = isFullLicense(this._license);
this._isTrialLicense = isTrialLicense(this._license);
this._isSecurityEnabled = mlLicenseUpdate.isSecurityEnabled;
this._hasLicenseExpired = mlLicenseUpdate.hasLicenseExpired;
this._isMlEnabled = mlLicenseUpdate.isMlEnabled;
this._isMinimumLicense = mlLicenseUpdate.isMinimumLicense;
this._isFullLicense = mlLicenseUpdate.isFullLicense;
this._isTrialLicense = mlLicenseUpdate.isTrialLicense;

if (callback !== undefined) {
callback(this);
}
});
}

public getLicenseInfo() {
return this._licenseInfo$.getValue();
}

public unsubscribe() {
if (this._licenseSubscription !== null) {
this._licenseSubscription.unsubscribe();
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/ml/common/types/capabilities.ts
Expand Up @@ -39,6 +39,8 @@ export const userMlCapabilities = {
canTestTrainedModels: false,
canGetFieldInfo: false,
canGetMlInfo: false,
// AIOps
canUseAiops: false,
};

export const adminMlCapabilities = {
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/ml/common/types/locator.ts
Expand Up @@ -58,7 +58,6 @@ export type MlGenericUrlState = MLPageState<
| typeof ML_PAGES.FILTER_LISTS_MANAGE
| typeof ML_PAGES.FILTER_LISTS_NEW
| typeof ML_PAGES.SETTINGS
| typeof ML_PAGES.ACCESS_DENIED
| typeof ML_PAGES.DATA_VISUALIZER
| typeof ML_PAGES.DATA_VISUALIZER_FILE
| typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT
Expand Down
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React from 'react';
import React, { type FC } from 'react';

import { FormattedMessage } from '@kbn/i18n-react';

Expand All @@ -16,14 +16,23 @@ import {
EuiPageContent_Deprecated as EuiPageContent,
EuiSpacer,
} from '@elastic/eui';
import { createPermissionFailureMessage } from '../capabilities/check_capabilities';
import { MlCapabilitiesKey } from '../../../common/types/capabilities';
import { HelpMenu } from '../components/help_menu';
import { useMlKibana } from '../contexts/kibana';

export const Page = () => {
export interface AccessDeniedCalloutProps {
missingCapabilities?: MlCapabilitiesKey[];
}

export const AccessDeniedCallout: FC<AccessDeniedCalloutProps> = ({ missingCapabilities }) => {
const {
services: { docLinks },
} = useMlKibana();
const helpLink = docLinks.links.ml.guide;

const errorMessages = (missingCapabilities ?? []).map((c) => createPermissionFailureMessage(c));

return (
<>
<EuiSpacer size="xxl" />
Expand All @@ -41,12 +50,19 @@ export const Page = () => {
</h2>
}
body={
<p>
<div>
<FormattedMessage
id="xpack.ml.accessDenied.description"
defaultMessage="You don’t have permission to view the Machine Learning plugin. Access to the plugin requires the Machine Learning feature to be visible in this space."
defaultMessage="You do not have permission to view this page."
/>
</p>
{errorMessages ? (
<ul>
{errorMessages.map((v) => (
<li key={v}>{v}</li>
))}
</ul>
) : null}
</div>
}
/>
</EuiPageContent>
Expand Down
Expand Up @@ -5,4 +5,4 @@
* 2.0.
*/

export { Page } from './page';
export { AccessDeniedCallout } from './access_denied';
Expand Up @@ -13,8 +13,8 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { ChangePointDetection } from '@kbn/aiops-plugin/public';

import { useDataSource } from '../contexts/ml/data_source_context';
import { useFieldStatsTrigger, FieldStatsFlyoutProvider } from '../components/field_stats_flyout';
import { useMlContext } from '../contexts/ml';
import { useMlKibana } from '../contexts/kibana';
import { HelpMenu } from '../components/help_menu';
import { TechnicalPreviewBadge } from '../components/technical_preview_badge';
Expand All @@ -24,9 +24,7 @@ import { MlPageHeader } from '../components/page_header';
export const ChangePointDetectionPage: FC = () => {
const { services } = useMlKibana();

const context = useMlContext();
const dataView = context.currentDataView;
const savedSearch = context.selectedSavedSearch;
const { currentDataView: dataView, selectedSavedSearch: savedSearch } = useDataSource();

return (
<>
Expand Down
Expand Up @@ -9,23 +9,18 @@ import React, { FC } from 'react';
import { pick } from 'lodash';

import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';

import { FormattedMessage } from '@kbn/i18n-react';
import { ExplainLogRateSpikes } from '@kbn/aiops-plugin/public';

import { useMlContext } from '../contexts/ml';
import { useDataSource } from '../contexts/ml/data_source_context';
import { useMlKibana } from '../contexts/kibana';
import { HelpMenu } from '../components/help_menu';
import { TechnicalPreviewBadge } from '../components/technical_preview_badge';

import { MlPageHeader } from '../components/page_header';

export const ExplainLogRateSpikesPage: FC = () => {
const { services } = useMlKibana();

const context = useMlContext();
const dataView = context.currentDataView;
const savedSearch = context.selectedSavedSearch;
const { currentDataView: dataView, selectedSavedSearch: savedSearch } = useDataSource();

return (
<>
Expand Down
Expand Up @@ -7,25 +7,19 @@

import React, { FC } from 'react';
import { pick } from 'lodash';

import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';

import { FormattedMessage } from '@kbn/i18n-react';
import { LogCategorization } from '@kbn/aiops-plugin/public';

import { useMlContext } from '../contexts/ml';
import { useDataSource } from '../contexts/ml/data_source_context';
import { useMlKibana } from '../contexts/kibana';
import { HelpMenu } from '../components/help_menu';
import { TechnicalPreviewBadge } from '../components/technical_preview_badge';

import { MlPageHeader } from '../components/page_header';

export const LogCategorizationPage: FC = () => {
const { services } = useMlKibana();

const context = useMlContext();
const dataView = context.currentDataView;
const savedSearch = context.selectedSavedSearch;
const { currentDataView: dataView, selectedSavedSearch: savedSearch } = useDataSource();

return (
<>
Expand Down

0 comments on commit bf68488

Please sign in to comment.