Skip to content

Commit

Permalink
[ML] Persisted URL state for the Data frame analytics jobs and models…
Browse files Browse the repository at this point in the history
… pages (#83439)

* [ML] support table settings from the URL state

* [ML] fix management page

* [ML] models page support

* [ML] update URL generator

* [ML] rename id column

* [ML] update types and tests

* [ML] fix id column name and field

* [ML] remove legacy functions

* [ML] set id key for the job query text

* [ML] fix id column rendering

* [ML] ad jobs with usePageUrlState

* [ML] update unit tests for solutions
  • Loading branch information
darnautov committed Nov 17, 2020
1 parent 7c8b9a1 commit bc22b67
Show file tree
Hide file tree
Showing 24 changed files with 269 additions and 258 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ test('MLLink produces the correct URL', async () => {
);

expect(href).toMatchInlineSnapshot(
`"/app/ml/jobs?_a=(queryText:'id:(something)%20groups:(apm)')&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"`
`"/app/ml/jobs?_a=(jobs:(queryText:'id:(something)%20groups:(apm)'))&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"`
);
});
1 change: 1 addition & 0 deletions x-pack/plugins/ml/common/constants/data_frame_analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const ANALYSIS_CONFIG_TYPE = {
REGRESSION: 'regression',
CLASSIFICATION: 'classification',
} as const;

export const DEFAULT_RESULTS_FIELD = 'ml';

export const JOB_MAP_NODE_TYPES = {
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/ml/common/constants/ml_url_generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const ML_PAGES = {
ANOMALY_EXPLORER: 'explorer',
SINGLE_METRIC_VIEWER: 'timeseriesexplorer',
DATA_FRAME_ANALYTICS_JOBS_MANAGE: 'data_frame_analytics',
DATA_FRAME_ANALYTICS_MODELS_MANAGE: 'data_frame_analytics/models',
DATA_FRAME_ANALYTICS_EXPLORATION: 'data_frame_analytics/exploration',
DATA_FRAME_ANALYTICS_MAP: 'data_frame_analytics/map',
/**
Expand Down Expand Up @@ -45,3 +46,5 @@ export const ML_PAGES = {
ACCESS_DENIED: 'access-denied',
OVERVIEW: 'overview',
} as const;

export type MlPages = typeof ML_PAGES[keyof typeof ML_PAGES];
14 changes: 14 additions & 0 deletions x-pack/plugins/ml/common/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { MlPages } from '../constants/ml_url_generator';

export interface Dictionary<TValue> {
[id: string]: TValue;
}
Expand Down Expand Up @@ -31,3 +33,15 @@ export type DeepReadonly<T> = T extends Array<infer R>
type DeepReadonlyObject<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
};

export interface ListingPageUrlState {
pageSize: number;
pageIndex: number;
sortField: string;
sortDirection: string;
queryText?: string;
}

export type AppPageState<T> = {
[key in MlPages]?: Partial<T>;
};
2 changes: 1 addition & 1 deletion x-pack/plugins/ml/common/util/string_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ export function getGroupQueryText(groupIds: string[]): string {
}

export function getJobQueryText(jobIds: string | string[]): string {
return Array.isArray(jobIds) ? `id:(${jobIds.join(' OR ')})` : jobIds;
return Array.isArray(jobIds) ? `id:(${jobIds.join(' OR ')})` : `id:${jobIds}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import React, { FC, useCallback, useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiInMemoryTable,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiInMemoryTable,
EuiSearchBar,
EuiSearchBarProps,
EuiSpacer,
Expand All @@ -30,13 +30,12 @@ import { getTaskStateBadge, getJobTypeBadge, useColumns } from './use_columns';
import { ExpandedRow } from './expanded_row';
import { AnalyticStatsBarStats, StatsBar } from '../../../../../components/stats_bar';
import { CreateAnalyticsButton } from '../create_analytics_button';
import { getSelectedIdFromUrl } from '../../../../../jobs/jobs_list/components/utils';
import { SourceSelection } from '../source_selection';
import { filterAnalytics } from '../../../../common/search_bar_filters';
import { AnalyticsEmptyPrompt } from './empty_prompt';
import { useTableSettings } from './use_table_settings';
import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button';
import { getGroupQueryText } from '../../../../../../../common/util/string_utils';
import { ListingPageUrlState } from '../../../../../../../common/types/common';

const filters: EuiSearchBarProps['filters'] = [
{
Expand Down Expand Up @@ -84,27 +83,35 @@ interface Props {
isManagementTable?: boolean;
isMlEnabledInSpace?: boolean;
blockRefresh?: boolean;
pageState: ListingPageUrlState;
updatePageState: (update: Partial<ListingPageUrlState>) => void;
}
export const DataFrameAnalyticsList: FC<Props> = ({
isManagementTable = false,
isMlEnabledInSpace = true,
blockRefresh = false,
pageState,
updatePageState,
}) => {
const searchQueryText = pageState.queryText ?? '';
const setSearchQueryText = useCallback(
(value) => {
updatePageState({ queryText: value });
},
[updatePageState]
);

const [isInitialized, setIsInitialized] = useState(false);
const [isSourceIndexModalVisible, setIsSourceIndexModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [filteredAnalytics, setFilteredAnalytics] = useState<DataFrameAnalyticsListRow[]>([]);
const [searchQueryText, setSearchQueryText] = useState('');
const [searchError, setSearchError] = useState<string | undefined>();
const [analytics, setAnalytics] = useState<DataFrameAnalyticsListRow[]>([]);
const [analyticsStats, setAnalyticsStats] = useState<AnalyticStatsBarStats | undefined>(
undefined
);
const [expandedRowItemIds, setExpandedRowItemIds] = useState<DataFrameAnalyticsId[]>([]);
const [errorMessage, setErrorMessage] = useState<any>(undefined);
// Query text/job_id based on url but only after getAnalytics is done first
// selectedJobIdFromUrlInitialized makes sure the query is only run once since analytics is being refreshed constantly
const [selectedIdFromUrlInitialized, setSelectedIdFromUrlInitialized] = useState(false);

const disabled =
!checkPermission('canCreateDataFrameAnalytics') ||
Expand All @@ -119,17 +126,20 @@ export const DataFrameAnalyticsList: FC<Props> = ({
isManagementTable
);

const updateFilteredItems = (queryClauses: any) => {
if (queryClauses.length) {
const filtered = filterAnalytics(analytics, queryClauses);
setFilteredAnalytics(filtered);
} else {
setFilteredAnalytics(analytics);
}
};
const updateFilteredItems = useCallback(
(queryClauses: any[]) => {
if (queryClauses.length) {
const filtered = filterAnalytics(analytics, queryClauses);
setFilteredAnalytics(filtered);
} else {
setFilteredAnalytics(analytics);
}
},
[analytics]
);

const filterList = () => {
if (searchQueryText !== '' && selectedIdFromUrlInitialized === true) {
if (searchQueryText !== '') {
// trigger table filtering with query for job id to trigger table filter
const query = EuiSearchBar.Query.parse(searchQueryText);
let clauses: any = [];
Expand All @@ -142,27 +152,9 @@ export const DataFrameAnalyticsList: FC<Props> = ({
}
};

useEffect(() => {
if (selectedIdFromUrlInitialized === false && analytics.length > 0) {
const { jobId, groupIds } = getSelectedIdFromUrl(window.location.href);
let queryText = '';

if (groupIds !== undefined) {
queryText = getGroupQueryText(groupIds);
} else if (jobId !== undefined) {
queryText = jobId;
}

setSelectedIdFromUrlInitialized(true);
setSearchQueryText(queryText);
} else {
filterList();
}
}, [selectedIdFromUrlInitialized, analytics]);

useEffect(() => {
filterList();
}, [selectedIdFromUrlInitialized, searchQueryText]);
}, [searchQueryText]);

const getAnalyticsCallback = useCallback(() => getAnalytics(true), []);

Expand All @@ -183,19 +175,19 @@ export const DataFrameAnalyticsList: FC<Props> = ({
);

const { onTableChange, pagination, sorting } = useTableSettings<DataFrameAnalyticsListRow>(
DataFrameAnalyticsListColumn.id,
filteredAnalytics
filteredAnalytics,
pageState,
updatePageState
);

const handleSearchOnChange: EuiSearchBarProps['onChange'] = (search) => {
if (search.error !== null) {
setSearchError(search.error.message);
return false;
return;
}

setSearchError(undefined);
setSearchQueryText(search.queryText);
return true;
};

// Before the analytics have been loaded for the first time, display the loading indicator only.
Expand Down Expand Up @@ -251,6 +243,7 @@ export const DataFrameAnalyticsList: FC<Props> = ({
</EuiFlexGroup>
</EuiFlexItem>
);

const search: EuiSearchBarProps = {
query: searchQueryText,
onChange: handleSearchOnChange,
Expand Down Expand Up @@ -284,15 +277,13 @@ export const DataFrameAnalyticsList: FC<Props> = ({
<div data-test-subj="mlAnalyticsTableContainer">
<EuiInMemoryTable<DataFrameAnalyticsListRow>
allowNeutralSort={false}
className="mlAnalyticsInMemoryTable"
columns={columns}
error={searchError}
hasActions={false}
isExpandable={true}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
isSelectable={false}
items={analytics}
itemId={DataFrameAnalyticsListColumn.id}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
loading={isLoading}
onTableChange={onTableChange}
pagination={pagination}
Expand All @@ -302,6 +293,7 @@ export const DataFrameAnalyticsList: FC<Props> = ({
rowProps={(item) => ({
'data-test-subj': `mlAnalyticsTableRow row-${item.id}`,
})}
error={searchError}
/>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,14 @@ export interface DataFrameAnalyticsListRow {
}

// Used to pass on attribute names to table columns
export enum DataFrameAnalyticsListColumn {
configDestIndex = 'config.dest.index',
configSourceIndex = 'config.source.index',
configCreateTime = 'config.create_time',
description = 'config.description',
id = 'id',
memoryStatus = 'stats.memory_usage.status',
}
export const DataFrameAnalyticsListColumn = {
configDestIndex: 'config.dest.index',
configSourceIndex: 'config.source.index',
configCreateTime: 'config.create_time',
description: 'config.description',
id: 'id',
memoryStatus: 'stats.memory_usage.status',
} as const;

export type ItemIdToExpandedRowMap = Record<string, JSX.Element>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,13 @@ export const progressColumn = {
'data-test-subj': 'mlAnalyticsTableColumnProgress',
};

export const DFAnalyticsJobIdLink = ({ item }: { item: DataFrameAnalyticsListRow }) => {
export const DFAnalyticsJobIdLink = ({ jobId }: { jobId: string }) => {
const href = useMlLink({
page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE,
pageState: { jobId: item.id },
pageState: { jobId },
});

return <EuiLink href={href}>{item.id}</EuiLink>;
return <EuiLink href={href}>{jobId}</EuiLink>;
};

export const useColumns = (
Expand Down Expand Up @@ -199,13 +199,17 @@ export const useColumns = (
'data-test-subj': 'mlAnalyticsTableRowDetailsToggle',
},
{
name: 'ID',
field: DataFrameAnalyticsListColumn.id,
name: i18n.translate('xpack.ml.dataframe.analyticsList.id', {
defaultMessage: 'ID',
}),
sortable: (item: DataFrameAnalyticsListRow) => item.id,
truncateText: true,
'data-test-subj': 'mlAnalyticsTableColumnId',
scope: 'row',
render: (item: DataFrameAnalyticsListRow) =>
isManagementTable ? <DFAnalyticsJobIdLink item={item} /> : item.id,
render: (jobId: string) => {
return isManagementTable ? <DFAnalyticsJobIdLink jobId={jobId} /> : jobId;
},
},
{
field: DataFrameAnalyticsListColumn.description,
Expand Down
Loading

0 comments on commit bc22b67

Please sign in to comment.