Skip to content

Commit

Permalink
[ML] Fix creation of the custom URLs for Kibana Dashboard (#134248)
Browse files Browse the repository at this point in the history
* improve types

* workaround with kibana locaiton

* fix type

* functional test

* remove the getUrl call

* fyx types
  • Loading branch information
darnautov authored Jun 14, 2022
1 parent ccd33c0 commit 43d5907
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*/

// Mock the mlJobService that is used for testing custom URLs.
import { UrlConfig } from '../../../../../common/types/custom_urls';
import { shallow } from 'enzyme';

jest.mock('../../../services/job_service', () => 'mlJobService');
Expand All @@ -18,7 +17,10 @@ import { TIME_RANGE_TYPE, URL_TYPE } from './constants';
import { CustomUrlSettings } from './utils';
import { DataViewListItem } from '@kbn/data-views-plugin/common';

function prepareTest(customUrl: CustomUrlSettings, setEditCustomUrlFn: (url: UrlConfig) => void) {
function prepareTest(
customUrl: CustomUrlSettings,
setEditCustomUrlFn: (url: CustomUrlSettings) => void
) {
const savedCustomUrls = [
{
url_name: 'Show data',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { DataViewListItem } from '@kbn/data-views-plugin/common';
import { isValidCustomUrlSettingsTimeRange } from './utils';
import { CustomUrlSettings, isValidCustomUrlSettingsTimeRange } from './utils';
import { isValidLabel } from '../../../util/custom_url_utils';

import { TIME_RANGE_TYPE, URL_TYPE } from './constants';
import { TIME_RANGE_TYPE, TimeRangeType, URL_TYPE } from './constants';
import { UrlConfig } from '../../../../../common/types/custom_urls';

function getLinkToOptions() {
Expand All @@ -55,10 +55,10 @@ function getLinkToOptions() {
}

interface CustomUrlEditorProps {
customUrl: any;
setEditCustomUrl: (url: any) => void;
customUrl: CustomUrlSettings | undefined;
setEditCustomUrl: (url: CustomUrlSettings) => void;
savedCustomUrls: UrlConfig[];
dashboards: any[];
dashboards: Array<{ id: string; title: string }>;
dataViewListItems: DataViewListItem[];
queryEntityFieldNames: string[];
}
Expand Down Expand Up @@ -142,7 +142,7 @@ export const CustomUrlEditor: FC<CustomUrlEditorProps> = ({
...customUrl,
timeRange: {
...timeRange,
type: e.target.value,
type: e.target.value as TimeRangeType,
},
});
};
Expand Down Expand Up @@ -255,7 +255,7 @@ export const CustomUrlEditor: FC<CustomUrlEditorProps> = ({
>
<EuiSelect
options={dashboardOptions}
value={kibanaSettings.dashboardId}
value={kibanaSettings?.dashboardId}
onChange={onDashboardChange}
data-test-subj="mlJobCustomUrlDashboardNameInput"
compressed
Expand All @@ -275,7 +275,7 @@ export const CustomUrlEditor: FC<CustomUrlEditorProps> = ({
>
<EuiSelect
options={dataViewOptions}
value={kibanaSettings.discoverIndexPatternId}
value={kibanaSettings?.discoverIndexPatternId}
onChange={onDiscoverIndexPatternChange}
data-test-subj="mlJobCustomUrlDiscoverIndexPatternInput"
compressed
Expand Down Expand Up @@ -369,7 +369,7 @@ export const CustomUrlEditor: FC<CustomUrlEditorProps> = ({
<EuiTextArea
fullWidth={true}
rows={2}
value={otherUrlSettings.urlValue}
value={otherUrlSettings?.urlValue}
onChange={onOtherUrlValueChange}
data-test-subj="mlJobCustomUrlOtherTypeUrlInput"
compressed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,23 @@ export interface CustomUrlSettings {
// Note timeRange is only editable in new URLs for Dashboard and Discover URLs,
// as for other URLs we have no way of knowing how the field will be used in the URL.
timeRange: TimeRange;
kibanaSettings?: any;
kibanaSettings?: {
dashboardId?: string;
queryFieldNames?: string[];
discoverIndexPatternId?: string;
};
otherUrlSettings?: {
urlValue: string;
};
}

export function getTestUrl(job: Job, customUrl: UrlConfig): Promise<string>;

export function isValidCustomUrlSettingsTimeRange(timeRangeSettings: any): boolean;
export function isValidCustomUrlSettingsTimeRange(timeRangeSettings: unknown): boolean;

export function getNewCustomUrlDefaults(
job: Job,
dashboards: any[],
dashboards: Array<{ id: string; title: string }>,
dataViews: DataViewListItem[]
): CustomUrlSettings;
export function getQueryEntityFieldNames(job: Job): string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import { ml } from '../../../services/ml_api_service';
import { escapeForElasticsearchQuery } from '../../../util/string_utils';
import { getSavedObjectsClient, getDashboard } from '../../../util/dependency_cache';

import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
import { cleanEmptyKeys } from '@kbn/dashboard-plugin/public';
import { isFilterPinned } from '@kbn/es-query';

export function getNewCustomUrlDefaults(job, dashboards, dataViews) {
// Returns the settings object in the format used by the custom URL editor
// for a new custom URL.
Expand Down Expand Up @@ -121,69 +125,74 @@ export function buildCustomUrlFromSettings(settings) {
}
}

function buildDashboardUrlFromSettings(settings) {
async function buildDashboardUrlFromSettings(settings) {
// Get the complete list of attributes for the selected dashboard (query, filters).
return new Promise((resolve, reject) => {
const { dashboardId, queryFieldNames } = settings.kibanaSettings;

const savedObjectsClient = getSavedObjectsClient();
savedObjectsClient
.get('dashboard', dashboardId)
.then((response) => {
// Use the filters from the saved dashboard if there are any.
let filters = [];

// Use the query from the dashboard only if no job entities are selected.
let query = undefined;

const searchSourceJSON = response.get('kibanaSavedObjectMeta.searchSourceJSON');
if (searchSourceJSON !== undefined) {
const searchSourceData = JSON.parse(searchSourceJSON);
if (searchSourceData.filter !== undefined) {
filters = searchSourceData.filter;
}
query = searchSourceData.query;
}

const queryFromEntityFieldNames = buildAppStateQueryParam(queryFieldNames);
if (queryFromEntityFieldNames !== undefined) {
query = queryFromEntityFieldNames;
}

const dashboard = getDashboard();

dashboard.locator
.getUrl({
dashboardId,
timeRange: {
from: '$earliest$',
to: '$latest$',
mode: 'absolute',
},
filters,
query,
// Don't hash the URL since this string will be 1. shown to the user and 2. used as a
// template to inject the time parameters.
useHash: false,
})
.then((urlValue) => {
const urlToAdd = {
url_name: settings.label,
url_value: decodeURIComponent(`dashboards${url.parse(urlValue).hash}`),
time_range: TIME_RANGE_TYPE.AUTO,
};

if (settings.timeRange.type === TIME_RANGE_TYPE.INTERVAL) {
urlToAdd.time_range = settings.timeRange.interval;
}

resolve(urlToAdd);
});
})
.catch((resp) => {
reject(resp);
});
const { dashboardId, queryFieldNames } = settings.kibanaSettings;

const savedObjectsClient = getSavedObjectsClient();

const response = await savedObjectsClient.get('dashboard', dashboardId);

// Use the filters from the saved dashboard if there are any.
let filters = [];

// Use the query from the dashboard only if no job entities are selected.
let query = undefined;

const searchSourceJSON = response.get('kibanaSavedObjectMeta.searchSourceJSON');
if (searchSourceJSON !== undefined) {
const searchSourceData = JSON.parse(searchSourceJSON);
if (searchSourceData.filter !== undefined) {
filters = searchSourceData.filter;
}
query = searchSourceData.query;
}

const queryFromEntityFieldNames = buildAppStateQueryParam(queryFieldNames);
if (queryFromEntityFieldNames !== undefined) {
query = queryFromEntityFieldNames;
}

const dashboard = getDashboard();

const location = await dashboard.locator.getLocation({
dashboardId,
timeRange: {
from: '$earliest$',
to: '$latest$',
mode: 'absolute',
},
filters,
query,
// Don't hash the URL since this string will be 1. shown to the user and 2. used as a
// template to inject the time parameters.
useHash: false,
});

// Temp workaround
const state = location.state;
const resultPath = setStateToKbnUrl(
'_a',
cleanEmptyKeys({
query: state.query,
filters: state.filters?.filter((f) => !isFilterPinned(f)),
savedQuery: state.savedQuery,
}),
{ useHash: false, storeInHashQuery: true },
location.path
);

const urlToAdd = {
url_name: settings.label,
url_value: decodeURIComponent(`dashboards${url.parse(resultPath).hash}`),
time_range: TIME_RANGE_TYPE.AUTO,
};

if (settings.timeRange.type === TIME_RANGE_TYPE.INTERVAL) {
urlToAdd.time_range = settings.timeRange.interval;
}

return urlToAdd;
}

function buildDiscoverUrlFromSettings(settings) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ interface CustomUrlsProps {

interface CustomUrlsState {
customUrls: UrlConfig[];
dashboards: any[];
dashboards: Array<{ id: string; title: string }>;
dataViewListItems: DataViewListItem[];
queryEntityFieldNames: string[];
editorOpen: boolean;
Expand Down Expand Up @@ -142,7 +142,7 @@ class CustomUrlsUI extends Component<CustomUrlsProps, CustomUrlsState> {
this.props.setCustomUrls(customUrls);
this.setState({ editorOpen: false });
})
.catch((error: any) => {
.catch((error: Error) => {
// eslint-disable-next-line no-console
console.error('Error building custom URL from settings:', error);
const { toasts } = this.props.kibana.services.notifications;
Expand Down
12 changes: 9 additions & 3 deletions x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const testDiscoverCustomUrl: DiscoverUrlConfig = {
const testDashboardCustomUrl: DashboardUrlConfig = {
label: 'Show dashboard',
dashboardName: 'ML Test',
queryEntityFieldNames: [],
queryEntityFieldNames: ['airline'],
timeRange: TIME_RANGE_TYPE.INTERVAL,
timeRangeInterval: '1h',
};
Expand All @@ -64,10 +64,13 @@ export default function ({ getService }: FtrProviderContext) {

describe('custom urls', function () {
this.tags(['ml']);

let testDashboardId: string | null = null;

before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote');
await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp');
await ml.testResources.createMLTestDashboardIfNeeded();
testDashboardId = await ml.testResources.createMLTestDashboardIfNeeded();
await ml.testResources.setKibanaTimeZoneToUTC();

await ml.api.createAndRunAnomalyDetectionLookbackJob(JOB_CONFIG, DATAFEED_CONFIG);
Expand Down Expand Up @@ -95,7 +98,10 @@ export default function ({ getService }: FtrProviderContext) {
});

it('adds a custom URL to Dashboard in the edit job flyout', async () => {
await ml.jobTable.addDashboardCustomUrl(JOB_CONFIG.job_id, testDashboardCustomUrl);
await ml.jobTable.addDashboardCustomUrl(JOB_CONFIG.job_id, testDashboardCustomUrl, {
index: 1,
url: `dashboards#/view/${testDashboardId}?_g=(filters:!(),time:(from:'$earliest$',mode:absolute,to:'$latest$'))&_a=(filters:!(),query:(language:kuery,query:'airline:\"$airline$\"'))`,
});
});

it('adds a custom URL to an external page in the edit job flyout', async () => {
Expand Down
20 changes: 15 additions & 5 deletions x-pack/test/functional/services/ml/job_table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,11 +601,19 @@ export function MachineLearningJobTableProvider(
return existingCustomUrls.length;
}

public async saveCustomUrl(expectedLabel: string, expectedIndex: number) {
public async saveCustomUrl(
expectedLabel: string,
expectedIndex: number,
expectedValue?: string
) {
await retry.tryForTime(5000, async () => {
await testSubjects.click('mlJobAddCustomUrl');
await customUrls.assertCustomUrlLabel(expectedIndex, expectedLabel);
});

if (expectedValue !== undefined) {
await customUrls.assertCustomUrlUrlValue(expectedIndex, expectedValue);
}
}

public async fillInDiscoverUrlForm(customUrl: DiscoverUrlConfig) {
Expand Down Expand Up @@ -671,14 +679,16 @@ export function MachineLearningJobTableProvider(
await this.saveEditJobFlyoutChanges();
}

public async addDashboardCustomUrl(jobId: string, customUrl: DashboardUrlConfig) {
public async addDashboardCustomUrl(
jobId: string,
customUrl: DashboardUrlConfig,
expectedResult: { index: number; url: string }
) {
await retry.tryForTime(30 * 1000, async () => {
await this.closeEditJobFlyout();
await this.openEditCustomUrlsForJobTab(jobId);
const existingCustomUrlCount = await this.getExistingCustomUrlCount();

await this.fillInDashboardUrlForm(customUrl);
await this.saveCustomUrl(customUrl.label, existingCustomUrlCount);
await this.saveCustomUrl(customUrl.label, expectedResult.index, expectedResult.url);
});

// Save the job
Expand Down
6 changes: 3 additions & 3 deletions x-pack/test/functional/services/ml/test_resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,15 +230,15 @@ export function MachineLearningTestResourcesProvider(
await this.createSavedSearchIfNeeded(savedSearches.farequoteFilter, indexPatternTitle);
},

async createMLTestDashboardIfNeeded() {
await this.createDashboardIfNeeded(dashboards.mlTestDashboard);
async createMLTestDashboardIfNeeded(): Promise<string> {
return await this.createDashboardIfNeeded(dashboards.mlTestDashboard);
},

async deleteMLTestDashboard() {
await this.deleteDashboardByTitle(dashboards.mlTestDashboard.requestBody.attributes.title);
},

async createDashboardIfNeeded(dashboard: any) {
async createDashboardIfNeeded(dashboard: { requestBody: any }): Promise<string> {
const title = dashboard.requestBody.attributes.title;
const dashboardId = await this.getDashboardId(title);
if (dashboardId !== undefined) {
Expand Down

0 comments on commit 43d5907

Please sign in to comment.