[ML] Persisted URL state for Data Frame Analytics Exploration page (#84499)

* [ML] store query string in the URL state

* [ML] query state for the config step

* [ML] pagination in the URL state

* [ML] persisted URL state for outlier results exploration

* [ML] update URL generator

* [ML] do not update the url state when query string hasn't been changed

* [ML] store expandable panels state in the URL

* [ML] fix TS issue

* [ML] fix TS issue
This commit is contained in:
Dima Arnautov 2020-11-30 18:54:15 +01:00 committed by GitHub
parent 647b9914c9
commit f39da3e680
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 340 additions and 155 deletions

View file

@ -7,10 +7,12 @@
export const ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE = 500;
export const ANOMALIES_TABLE_DEFAULT_QUERY_SIZE = 500;
export enum SEARCH_QUERY_LANGUAGE {
KUERY = 'kuery',
LUCENE = 'lucene',
}
export const SEARCH_QUERY_LANGUAGE = {
KUERY: 'kuery',
LUCENE: 'lucene',
} as const;
export type SearchQueryLanguage = typeof SEARCH_QUERY_LANGUAGE[keyof typeof SEARCH_QUERY_LANGUAGE];
export interface ErrorMessage {
query: string;

View file

@ -8,6 +8,8 @@ import { RefreshInterval, TimeRange } from '../../../../../src/plugins/data/comm
import { JobId } from './anomaly_detection_jobs/job';
import { ML_PAGES } from '../constants/ml_url_generator';
import { DataFrameAnalysisConfigType } from './data_frame_analytics';
import { SearchQueryLanguage } from '../constants/search';
import { ListingPageUrlState } from './common';
type OptionalPageState = object | undefined;
@ -182,7 +184,7 @@ export type DataFrameAnalyticsExplorationUrlState = MLPageState<
jobId: JobId;
analysisType: DataFrameAnalysisConfigType;
globalState?: MlCommonGlobalState;
defaultIsTraining?: boolean;
queryText?: string;
modelId?: string;
}
>;
@ -203,6 +205,14 @@ export type FilterEditUrlState = MLPageState<
}
>;
export type ExpandablePanels = 'analysis' | 'evaluation' | 'feature_importance' | 'results';
export type ExplorationPageUrlState = {
queryText: string;
queryLanguage: SearchQueryLanguage;
} & Pick<ListingPageUrlState, 'pageIndex' | 'pageSize'> &
{ [key in ExpandablePanels]: boolean };
/**
* Union type of ML URL state based on page
*/

View file

@ -45,6 +45,9 @@ import { fetchExplainData } from '../shared';
import { useIndexData } from '../../hooks';
import { ExplorationQueryBar } from '../../../analytics_exploration/components/exploration_query_bar';
import { useSavedSearch } from './use_saved_search';
import { SEARCH_QUERY_LANGUAGE } from '../../../../../../../common/constants/search';
import { ExplorationQueryBarProps } from '../../../analytics_exploration/components/exploration_query_bar/exploration_query_bar';
import { Query } from '../../../../../../../../../../src/plugins/data/common/query';
const requiredFieldsErrorText = i18n.translate(
'xpack.ml.dataframe.analytics.createWizard.requiredFieldsErrorMessage',
@ -93,11 +96,18 @@ export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({
trainingPercent,
useEstimatedMml,
} = form;
const [query, setQuery] = useState<Query>({
query: jobConfigQueryString ?? '',
language: SEARCH_QUERY_LANGUAGE.KUERY,
});
const toastNotifications = getToastNotifications();
const setJobConfigQuery = ({ query, queryString }: { query: any; queryString: string }) => {
setFormState({ jobConfigQuery: query, jobConfigQueryString: queryString });
const setJobConfigQuery: ExplorationQueryBarProps['setSearchQuery'] = (update) => {
if (update.query) {
setFormState({ jobConfigQuery: update.query, jobConfigQueryString: update.queryString });
}
setQuery({ query: update.queryString, language: update.language });
};
const indexData = useIndexData(
@ -305,10 +315,8 @@ export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({
>
<ExplorationQueryBar
indexPattern={currentIndexPattern}
// @ts-ignore
setSearchQuery={setJobConfigQuery}
includeQueryString
defaultQueryString={jobConfigQueryString}
query={query}
/>
</EuiFormRow>
)}

View file

@ -23,7 +23,7 @@ export function useSavedSearch() {
if (currentSavedSearch !== null) {
const { query } = getQueryFromSavedSearch(currentSavedSearch);
const queryLanguage = query.language as SEARCH_QUERY_LANGUAGE;
const queryLanguage = query.language;
qryString = query.query;
if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) {

View file

@ -11,12 +11,12 @@ import { i18n } from '@kbn/i18n';
import { ExplorationPageWrapper } from '../exploration_page_wrapper';
import { EvaluatePanel } from './evaluate_panel';
import { FeatureImportanceSummaryPanel } from '../total_feature_importance_summary/feature_importance_summary';
interface Props {
jobId: string;
defaultIsTraining?: boolean;
}
export const ClassificationExploration: FC<Props> = ({ jobId, defaultIsTraining }) => (
export const ClassificationExploration: FC<Props> = ({ jobId }) => (
<div className="mlDataFrameAnalyticsClassification">
<ExplorationPageWrapper
jobId={jobId}
@ -29,7 +29,6 @@ export const ClassificationExploration: FC<Props> = ({ jobId, defaultIsTraining
)}
EvaluatePanel={EvaluatePanel}
FeatureImportanceSummaryPanel={FeatureImportanceSummaryPanel}
defaultIsTraining={defaultIsTraining}
/>
</div>
);

View file

@ -286,6 +286,7 @@ export const EvaluatePanel: FC<EvaluatePanelProps> = ({ jobConfig, jobStatus, se
return (
<>
<ExpandableSection
urlStateKey={'evaluation'}
dataTestId="ClassificationEvaluation"
title={
<FormattedMessage

View file

@ -6,7 +6,7 @@
import './expandable_section.scss';
import React, { useState, FC, ReactNode } from 'react';
import React, { FC, ReactNode, useCallback } from 'react';
import {
EuiBadge,
@ -17,6 +17,11 @@ import {
EuiPanel,
EuiText,
} from '@elastic/eui';
import {
getDefaultExplorationPageUrlState,
useExplorationUrlState,
} from '../../hooks/use_exploration_url_state';
import { ExpandablePanels } from '../../../../../../../common/types/ml_url_generator';
interface HeaderItem {
// id is used as the React key and to construct a data-test-subj
@ -39,25 +44,30 @@ export interface ExpandableSectionProps {
isExpanded?: boolean;
dataTestId: string;
title: ReactNode;
urlStateKey: ExpandablePanels;
}
export const ExpandableSection: FC<ExpandableSectionProps> = ({
headerItems,
// For now we don't have a need for complete external control
// and just want to pass in a default value. If we wanted
// full external control we'd also need to add a onToggleExpanded()
// callback.
isExpanded: isExpandedDefault = true,
content,
isExpanded: isExpandedDefault,
contentPadding = false,
dataTestId,
title,
docsLink,
urlStateKey,
}) => {
const [isExpanded, setIsExpanded] = useState(isExpandedDefault);
const toggleExpanded = () => {
setIsExpanded(!isExpanded);
};
const [pageUrlState, setPageUrlState] = useExplorationUrlState();
const isExpanded =
isExpandedDefault !== undefined &&
pageUrlState[urlStateKey] === getDefaultExplorationPageUrlState()[urlStateKey]
? isExpandedDefault
: pageUrlState[urlStateKey];
const toggleExpanded = useCallback(() => {
setPageUrlState({ [urlStateKey]: !isExpanded });
}, [isExpanded, setPageUrlState, urlStateKey]);
return (
<EuiPanel paddingSize="none" data-test-subj={`mlDFExpandableSection-${dataTestId}`}>

View file

@ -132,7 +132,7 @@ export const ExpandableSectionAnalytics: FC<ExpandableSectionAnalyticsProps> = (
dataTestId="analysis"
content={analyticsSectionContent}
headerItems={analyticsSectionHeaderItems}
isExpanded={false}
urlStateKey={'analysis'}
title={
<FormattedMessage
id="xpack.ml.dataframe.analytics.exploration.analysisSectionTitle"

View file

@ -153,6 +153,7 @@ export const ExpandableSectionResults: FC<ExpandableSectionResultsProps> = ({
return (
<>
<ExpandableSection
urlStateKey={'results'}
dataTestId="results"
content={resultsSectionContent}
headerItems={resultsSectionHeaderItems}

View file

@ -4,16 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, useEffect, useState } from 'react';
import React, { FC, useCallback, useState } from 'react';
import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useUrlState } from '../../../../../util/url_state';
import {
defaultSearchQuery,
getDefaultTrainingFilterQuery,
useResultsViewConfig,
DataFrameAnalyticsConfig,
} from '../../../../common';
@ -27,6 +24,8 @@ import { ExplorationQueryBar } from '../exploration_query_bar';
import { JobConfigErrorCallout } from '../job_config_error_callout';
import { LoadingPanel } from '../loading_panel';
import { FeatureImportanceSummaryPanelProps } from '../total_feature_importance_summary/feature_importance_summary';
import { useExplorationUrlState } from '../../hooks/use_exploration_url_state';
import { ExplorationQueryBarProps } from '../exploration_query_bar/exploration_query_bar';
const filters = {
options: [
@ -58,7 +57,6 @@ interface Props {
title: string;
EvaluatePanel: FC<EvaluatePanelProps>;
FeatureImportanceSummaryPanel: FC<FeatureImportanceSummaryPanelProps>;
defaultIsTraining?: boolean;
}
export const ExplorationPageWrapper: FC<Props> = ({
@ -66,7 +64,6 @@ export const ExplorationPageWrapper: FC<Props> = ({
title,
EvaluatePanel,
FeatureImportanceSummaryPanel,
defaultIsTraining,
}) => {
const {
indexPattern,
@ -81,24 +78,26 @@ export const ExplorationPageWrapper: FC<Props> = ({
totalFeatureImportance,
} = useResultsViewConfig(jobId);
const [searchQuery, setSearchQuery] = useState<ResultsSearchQuery>(defaultSearchQuery);
const [globalState, setGlobalState] = useUrlState('_g');
const [defaultQueryString, setDefaultQueryString] = useState<string | undefined>();
const [pageUrlState, setPageUrlState] = useExplorationUrlState();
useEffect(() => {
if (defaultIsTraining !== undefined && jobConfig !== undefined) {
// Apply defaultIsTraining filter
setSearchQuery(
getDefaultTrainingFilterQuery(jobConfig.dest.results_field, defaultIsTraining)
);
setDefaultQueryString(`${jobConfig.dest.results_field}.is_training : ${defaultIsTraining}`);
// Clear defaultIsTraining from url
setGlobalState('ml', {
analysisType: globalState.ml.analysisType,
jobId: globalState.ml.jobId,
});
}
}, [jobConfig?.dest.results_field]);
const [searchQuery, setSearchQuery] = useState<ResultsSearchQuery>(defaultSearchQuery);
const searchQueryUpdateHandler: ExplorationQueryBarProps['setSearchQuery'] = useCallback(
(update) => {
if (update.query) {
setSearchQuery(update.query);
}
if (update.queryString !== pageUrlState.queryText) {
setPageUrlState({ queryText: update.queryString, queryLanguage: update.language });
}
},
[pageUrlState, setPageUrlState]
);
const query: ExplorationQueryBarProps['query'] = {
query: pageUrlState.queryText,
language: pageUrlState.queryLanguage,
};
if (indexPatternErrorMessage !== undefined) {
return (
@ -144,8 +143,8 @@ export const ExplorationPageWrapper: FC<Props> = ({
<EuiFlexItem>
<ExplorationQueryBar
indexPattern={indexPattern}
setSearchQuery={setSearchQuery}
defaultQueryString={defaultQueryString}
setSearchQuery={searchQueryUpdateHandler}
query={query}
filters={filters}
/>
</EuiFlexItem>

View file

@ -4,10 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Dispatch, FC, SetStateAction, useEffect, useState } from 'react';
import React, { FC, useEffect, useState } from 'react';
import { EuiButtonGroup, EuiCode, EuiFlexGroup, EuiFlexItem, EuiInputPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Dictionary } from '../../../../../../../common/types/common';
@ -19,21 +17,27 @@ import {
QueryStringInput,
} from '../../../../../../../../../../src/plugins/data/public';
import { SEARCH_QUERY_LANGUAGE } from '../../../../../../../common/constants/search';
import { SavedSearchQuery } from '../../../../../contexts/ml';
import {
SEARCH_QUERY_LANGUAGE,
SearchQueryLanguage,
} from '../../../../../../../common/constants/search';
import { removeFilterFromQueryString } from '../../../../../explorer/explorer_utils';
import { SavedSearchQuery } from '../../../../../contexts/ml';
interface ErrorMessage {
query: string;
message: string;
}
interface ExplorationQueryBarProps {
export interface ExplorationQueryBarProps {
indexPattern: IIndexPattern;
setSearchQuery: Dispatch<SetStateAction<SavedSearchQuery>>;
setSearchQuery: (update: {
queryString: string;
query?: SavedSearchQuery;
language: SearchQueryLanguage;
}) => void;
includeQueryString?: boolean;
defaultQueryString?: string;
query: Query;
filters?: {
options: Array<{ id: string; label: string }>;
columnId: string;
@ -44,57 +48,62 @@ interface ExplorationQueryBarProps {
export const ExplorationQueryBar: FC<ExplorationQueryBarProps> = ({
indexPattern,
setSearchQuery,
includeQueryString = false,
defaultQueryString,
filters,
query,
}) => {
// The internal state of the input query bar updated on every key stroke.
const [searchInput, setSearchInput] = useState<Query>({
query: '',
language: SEARCH_QUERY_LANGUAGE.KUERY,
});
const [searchInput, setSearchInput] = useState<Query>(query);
const [idToSelectedMap, setIdToSelectedMap] = useState<{ [id: string]: boolean }>({});
const [errorMessage, setErrorMessage] = useState<ErrorMessage | undefined>(undefined);
useEffect(() => {
if (defaultQueryString !== undefined) {
setSearchInput({ query: defaultQueryString, language: SEARCH_QUERY_LANGUAGE.KUERY });
}
}, [defaultQueryString !== undefined]);
const searchChangeHandler = (q: Query) => setSearchInput(q);
const searchChangeHandler = (query: Query) => setSearchInput(query);
const searchSubmitHandler = (query: Query, filtering?: boolean) => {
/**
* Component is responsible for parsing the query string,
* hence it should sync submitted query string.
*/
useEffect(() => {
try {
let convertedQuery;
switch (query.language) {
case SEARCH_QUERY_LANGUAGE.KUERY:
convertedQuery = esKuery.toElasticsearchQuery(
esKuery.fromKueryExpression(query.query as string),
indexPattern
);
break;
case SEARCH_QUERY_LANGUAGE.LUCENE:
convertedQuery = esQuery.luceneStringToDsl(query.query as string);
break;
default:
setErrorMessage({
query: query.query as string,
message: i18n.translate('xpack.ml.queryBar.queryLanguageNotSupported', {
defaultMessage: 'Query language is not supported',
}),
});
return;
}
setSearchQuery({
queryString: query.query as string,
query: convertedQuery,
language: query.language,
});
} catch (e) {
setErrorMessage({ query: query.query as string, message: e.message });
}
}, [query.query]);
const searchSubmitHandler = (q: Query, filtering?: boolean) => {
// If moved to querying manually, clear filter selection.
if (filtering === undefined) {
setIdToSelectedMap({});
}
try {
switch (query.language) {
case SEARCH_QUERY_LANGUAGE.KUERY:
const convertedKQuery = esKuery.toElasticsearchQuery(
esKuery.fromKueryExpression(query.query as string),
indexPattern
);
setSearchQuery(
includeQueryString
? { queryString: query.query, query: convertedKQuery }
: convertedKQuery
);
return;
case SEARCH_QUERY_LANGUAGE.LUCENE:
const convertedLQuery = esQuery.luceneStringToDsl(query.query as string);
setSearchQuery(
includeQueryString
? { queryString: query.query, query: convertedLQuery }
: convertedLQuery
);
return;
}
} catch (e) {
setErrorMessage({ query: query.query as string, message: e.message });
}
setSearchQuery({
queryString: q.query as string,
language: q.language as SearchQueryLanguage,
});
};
const handleFilterUpdate = (optionId: string, currentIdToSelectedMap: any) => {

View file

@ -0,0 +1,70 @@
/*
* 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 { useCallback, useMemo } from 'react';
import { EuiDataGridColumn } from '@elastic/eui';
import useUpdateEffect from 'react-use/lib/useUpdateEffect';
import { useDataGrid } from '../../../../../components/data_grid';
import {
getDefaultExplorationPageUrlState,
useExplorationUrlState,
} from '../../hooks/use_exploration_url_state';
import { INIT_MAX_COLUMNS } from '../../../../../components/data_grid/common';
export const useExplorationDataGrid = (
columns: EuiDataGridColumn[],
defaultVisibleColumnsCount = INIT_MAX_COLUMNS,
defaultVisibleColumnsFilter?: (id: string) => boolean
) => {
const [pageUrlState, setPageUrlState] = useExplorationUrlState();
const dataGrid = useDataGrid(
columns,
25,
defaultVisibleColumnsCount,
defaultVisibleColumnsFilter
);
// Override dataGrid config to use URL state.
dataGrid.pagination = useMemo(
() => ({
pageSize: pageUrlState.pageSize,
pageIndex: pageUrlState.pageIndex,
}),
[pageUrlState.pageSize, pageUrlState.pageIndex]
);
dataGrid.setPagination = useCallback(
(u) => {
setPageUrlState({ ...u });
},
[setPageUrlState]
);
dataGrid.onChangePage = useCallback(
(pageIndex) => {
setPageUrlState({ pageIndex });
},
[setPageUrlState]
);
dataGrid.onChangeItemsPerPage = useCallback(
(pageSize) => {
setPageUrlState({ pageSize });
},
[setPageUrlState]
);
dataGrid.resetPagination = useCallback(() => {
const a = getDefaultExplorationPageUrlState();
setPageUrlState({ pageSize: a.pageSize, pageIndex: a.pageIndex });
}, [setPageUrlState]);
useUpdateEffect(
function resetPaginationOnQueryChange() {
dataGrid.resetPagination();
},
[pageUrlState.queryText]
);
return dataGrid;
};

View file

@ -20,7 +20,6 @@ import {
getDataGridSchemasFromFieldTypes,
getFieldType,
showDataGridColumnChartErrorMessageToast,
useDataGrid,
useRenderCellValue,
UseIndexDataReturnType,
} from '../../../../../components/data_grid';
@ -38,6 +37,7 @@ import { isRegressionAnalysis } from '../../../../common/analytics';
import { extractErrorMessage } from '../../../../../../../common/util/errors';
import { useTrainedModelsApiService } from '../../../../../services/ml_api_service/trained_models';
import { FeatureImportanceBaseline } from '../../../../../../../common/types/feature_importance';
import { useExplorationDataGrid } from './use_exploration_data_grid';
export const useExplorationResults = (
indexPattern: IndexPattern | undefined,
@ -64,9 +64,8 @@ export const useExplorationResults = (
)
);
}
const dataGrid = useDataGrid(
const dataGrid = useExplorationDataGrid(
columns,
25,
// reduce default selected rows from 20 to 8 for performance reasons.
8,
// by default, hide feature-importance and top-classes columns and the doc id copy
@ -74,10 +73,6 @@ export const useExplorationResults = (
!d.includes(`.${FEATURE_IMPORTANCE}.`) && !d.includes(`.${TOP_CLASSES}.`) && d !== ML__ID_COPY
);
useEffect(() => {
dataGrid.resetPagination();
}, [JSON.stringify(searchQuery)]);
// The pattern using `didCancel` allows us to abort out of date remote request.
// We wrap `didCancel` in a object so we can mutate the value as it's being
// passed on to `getIndexData`.

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState, FC } from 'react';
import React, { useState, FC, useCallback } from 'react';
import { EuiCallOut, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
@ -25,6 +25,8 @@ import { ExplorationQueryBar } from '../exploration_query_bar';
import { getFeatureCount } from './common';
import { useOutlierData } from './use_outlier_data';
import { useExplorationUrlState } from '../../hooks/use_exploration_url_state';
import { ExplorationQueryBarProps } from '../exploration_query_bar/exploration_query_bar';
export type TableItem = Record<string, any>;
@ -39,9 +41,27 @@ export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) =
jobConfig,
needsDestIndexPattern,
} = useResultsViewConfig(jobId);
const [pageUrlState, setPageUrlState] = useExplorationUrlState();
const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery);
const outlierData = useOutlierData(indexPattern, jobConfig, searchQuery);
const searchQueryUpdateHandler: ExplorationQueryBarProps['setSearchQuery'] = useCallback(
(update) => {
if (update.query) {
setSearchQuery(update.query);
}
if (update.queryString !== pageUrlState.queryText) {
setPageUrlState({ queryText: update.queryString, queryLanguage: update.language });
}
},
[pageUrlState, setPageUrlState]
);
const query: ExplorationQueryBarProps['query'] = {
query: pageUrlState.queryText,
language: pageUrlState.queryLanguage,
};
const { columnsWithCharts, tableItems } = outlierData;
const featureCount = getFeatureCount(jobConfig?.dest?.results_field || '', tableItems);
@ -93,7 +113,11 @@ export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) =
{(columnsWithCharts.length > 0 || searchQuery !== defaultSearchQuery) &&
indexPattern !== undefined && (
<>
<ExplorationQueryBar indexPattern={indexPattern} setSearchQuery={setSearchQuery} />
<ExplorationQueryBar
indexPattern={indexPattern}
setSearchQuery={searchQueryUpdateHandler}
query={query}
/>
<EuiSpacer size="m" />
</>
)}

View file

@ -21,7 +21,6 @@ import {
getFieldType,
getDataGridSchemasFromFieldTypes,
showDataGridColumnChartErrorMessageToast,
useDataGrid,
useRenderCellValue,
UseIndexDataReturnType,
} from '../../../../../components/data_grid';
@ -38,6 +37,7 @@ import {
} from '../../../../common/fields';
import { getFeatureCount, getOutlierScoreFieldName } from './common';
import { useExplorationDataGrid } from '../exploration_results_table/use_exploration_data_grid';
export const useOutlierData = (
indexPattern: IndexPattern | undefined,
@ -63,19 +63,14 @@ export const useOutlierData = (
return newColumns;
}, [jobConfig, indexPattern]);
const dataGrid = useDataGrid(
const dataGrid = useExplorationDataGrid(
columns,
25,
// reduce default selected rows from 20 to 8 for performance reasons.
8,
// by default, hide feature-influence columns and the doc id copy
(d) => !d.includes(`.${FEATURE_INFLUENCE}.`) && d !== ML__ID_COPY && d !== ML__INCREMENTAL_ID
);
useEffect(() => {
dataGrid.resetPagination();
}, [JSON.stringify(searchQuery)]);
// initialize sorting: reverse sort on outlier score column
useEffect(() => {
if (jobConfig !== undefined) {

View file

@ -222,6 +222,7 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
return (
<>
<ExpandableSection
urlStateKey={'evaluation'}
dataTestId="RegressionEvaluation"
title={
<FormattedMessage

View file

@ -15,10 +15,9 @@ import { FeatureImportanceSummaryPanel } from '../total_feature_importance_summa
interface Props {
jobId: string;
defaultIsTraining?: boolean;
}
export const RegressionExploration: FC<Props> = ({ jobId, defaultIsTraining }) => (
export const RegressionExploration: FC<Props> = ({ jobId }) => (
<ExplorationPageWrapper
jobId={jobId}
title={i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle', {
@ -27,6 +26,5 @@ export const RegressionExploration: FC<Props> = ({ jobId, defaultIsTraining }) =
})}
EvaluatePanel={EvaluatePanel}
FeatureImportanceSummaryPanel={FeatureImportanceSummaryPanel}
defaultIsTraining={defaultIsTraining}
/>
);

View file

@ -237,11 +237,11 @@ export const FeatureImportanceSummaryPanel: FC<FeatureImportanceSummaryPanelProp
}
}
}
return undefined;
}, [totalFeatureImportance, jobConfig]);
return (
<>
<ExpandableSection
urlStateKey={'feature_importance'}
isExpanded={noDataCallOut === undefined}
dataTestId="FeatureImportanceSummary"
title={

View file

@ -0,0 +1,30 @@
/*
* 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 { usePageUrlState } from '../../../../util/url_state';
import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator';
import { SEARCH_QUERY_LANGUAGE } from '../../../../../../common/constants/search';
import { ExplorationPageUrlState } from '../../../../../../common/types/ml_url_generator';
export function getDefaultExplorationPageUrlState(): ExplorationPageUrlState {
return {
queryText: '',
queryLanguage: SEARCH_QUERY_LANGUAGE.KUERY,
pageIndex: 0,
pageSize: 25,
analysis: false,
evaluation: true,
feature_importance: true,
results: true,
};
}
export function useExplorationUrlState() {
return usePageUrlState<ExplorationPageUrlState>(
ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION,
getDefaultExplorationPageUrlState()
);
}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, FC } from 'react';
import React, { FC } from 'react';
import {
EuiPage,
@ -27,9 +27,8 @@ import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_fr
export const Page: FC<{
jobId: string;
analysisType: DataFrameAnalysisConfigType;
defaultIsTraining?: boolean;
}> = ({ jobId, analysisType, defaultIsTraining }) => (
<Fragment>
}> = ({ jobId, analysisType }) => (
<>
<NavigationMenu tabId="data_frame_analytics" />
<EuiPage data-test-subj="mlPageDataFrameAnalyticsExploration">
<EuiPageBody style={{ maxWidth: 'calc(100% - 0px)' }}>
@ -45,13 +44,13 @@ export const Page: FC<{
<OutlierExploration jobId={jobId} />
)}
{analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION && (
<RegressionExploration jobId={jobId} defaultIsTraining={defaultIsTraining} />
<RegressionExploration jobId={jobId} />
)}
{analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && (
<ClassificationExploration jobId={jobId} defaultIsTraining={defaultIsTraining} />
<ClassificationExploration jobId={jobId} />
)}
</EuiPageContentBody>
</EuiPageBody>
</EuiPage>
</Fragment>
</>
);

View file

@ -313,14 +313,20 @@ export const ModelsList: FC = () => {
onClick: async (item) => {
if (item.metadata?.analytics_config === undefined) return;
const analysisType = getAnalysisType(
item.metadata?.analytics_config.analysis
) as DataFrameAnalysisConfigType;
const url = await urlGenerator.createUrl({
page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION,
pageState: {
jobId: item.metadata?.analytics_config.id as string,
analysisType: getAnalysisType(
item.metadata?.analytics_config.analysis
) as DataFrameAnalysisConfigType,
defaultIsTraining: true,
analysisType,
...(analysisType === 'classification' || analysisType === 'regression'
? {
queryText: `${item.metadata?.analytics_config.dest.results_field}.is_training : true`,
}
: {}),
},
});

View file

@ -21,7 +21,11 @@ import { i18n } from '@kbn/i18n';
import { IndexPattern } from '../../../../../../../../../src/plugins/data/public';
import { SEARCH_QUERY_LANGUAGE, ErrorMessage } from '../../../../../../common/constants/search';
import {
SEARCH_QUERY_LANGUAGE,
ErrorMessage,
SearchQueryLanguage,
} from '../../../../../../common/constants/search';
import {
esKuery,
@ -36,7 +40,7 @@ interface Props {
setSearchString(s: Query['query']): void;
searchQuery: Query['query'];
setSearchQuery(q: Query['query']): void;
searchQueryLanguage: SEARCH_QUERY_LANGUAGE;
searchQueryLanguage: SearchQueryLanguage;
setSearchQueryLanguage(q: any): void;
samplerShardSize: number;
setSamplerShardSize(s: number): void;

View file

@ -33,7 +33,7 @@ import { SavedSearchSavedObject } from '../../../../common/types/kibana';
import { NavigationMenu } from '../../components/navigation_menu';
import { DatePickerWrapper } from '../../components/navigation_menu/date_picker_wrapper';
import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types';
import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search';
import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../../../../common/constants/search';
import { isFullLicense } from '../../license';
import { checkPermission } from '../../capabilities/check_capabilities';
import { mlNodesAvailable } from '../../ml_nodes_check/check_ml_nodes';
@ -55,7 +55,7 @@ import { DataLoader } from './data_loader';
interface DataVisualizerPageState {
searchQuery: Query['query'];
searchString: Query['query'];
searchQueryLanguage: SEARCH_QUERY_LANGUAGE;
searchQueryLanguage: SearchQueryLanguage;
samplerShardSize: number;
overallStats: any;
metricConfigs: FieldVisConfig[];
@ -167,7 +167,9 @@ export const Page: FC = () => {
const [searchString, setSearchString] = useState(initSearchString);
const [searchQuery, setSearchQuery] = useState(initSearchQuery);
const [searchQueryLanguage, setSearchQueryLanguage] = useState(initQueryLanguage);
const [searchQueryLanguage, setSearchQueryLanguage] = useState<SearchQueryLanguage>(
initQueryLanguage
);
const [samplerShardSize, setSamplerShardSize] = useState(defaults.samplerShardSize);
// TODO - type overallStats and stats
@ -252,7 +254,7 @@ export const Page: FC = () => {
}
const { query } = getQueryFromSavedSearch(savedSearch);
const queryLanguage = query.language as SEARCH_QUERY_LANGUAGE;
const queryLanguage = query.language as SearchQueryLanguage;
const qryString = query.query;
let qry;
if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) {

View file

@ -5,8 +5,6 @@
*/
import React, { FC } from 'react';
import { parse } from 'query-string';
import { decode } from 'rison-node';
import { i18n } from '@kbn/i18n';
@ -19,6 +17,7 @@ import { Page } from '../../../data_frame_analytics/pages/analytics_exploration'
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics';
import { useUrlState } from '../../../util/url_state';
export const analyticsJobExplorationRouteFactory = (
navigateToPath: NavigateToPath,
@ -40,7 +39,8 @@ export const analyticsJobExplorationRouteFactory = (
const PageWrapper: FC<PageProps> = ({ location, deps }) => {
const { context } = useResolver('', undefined, deps.config, basicResolvers(deps));
const { _g }: Record<string, any> = parse(location.search, { sort: false });
const [globalState] = useUrlState('_g');
const urlGenerator = useMlUrlGenerator();
const {
@ -54,24 +54,16 @@ const PageWrapper: FC<PageProps> = ({ location, deps }) => {
await navigateToUrl(url);
};
let globalState: any = null;
try {
globalState = decode(_g);
} catch (error) {
// eslint-disable-next-line no-console
console.error(
'Could not parse global state. Redirecting to Data Frame Analytics Management Page.'
);
redirectToAnalyticsManagementPage();
return <></>;
}
const jobId: string = globalState.ml.jobId;
const analysisType: DataFrameAnalysisConfigType = globalState.ml.analysisType;
const defaultIsTraining: boolean | undefined = globalState.ml.defaultIsTraining;
if (!analysisType) {
redirectToAnalyticsManagementPage();
}
return (
<PageLoader context={context}>
<Page {...{ jobId, analysisType, defaultIsTraining }} />
<Page {...{ jobId, analysisType }} />
</PageLoader>
);
};

View file

@ -7,10 +7,12 @@
/**
* Creates URL to the DataFrameAnalytics page
*/
import { isEmpty } from 'lodash';
import {
DataFrameAnalyticsExplorationQueryState,
DataFrameAnalyticsExplorationUrlState,
DataFrameAnalyticsUrlState,
ExplorationPageUrlState,
MlCommonGlobalState,
} from '../../common/types/ml_url_generator';
import { ML_PAGES } from '../../common/constants/ml_url_generator';
@ -72,17 +74,31 @@ export function createDataFrameAnalyticsExplorationUrl(
let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION}`;
if (mlUrlGeneratorState) {
const { jobId, analysisType, defaultIsTraining, globalState } = mlUrlGeneratorState;
const { jobId, analysisType, queryText, globalState } = mlUrlGeneratorState;
const queryState: DataFrameAnalyticsExplorationQueryState = {
ml: {
jobId,
analysisType,
defaultIsTraining,
},
...globalState,
};
const appState = {
[ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION]: {
...(queryText ? { queryText } : {}),
},
};
if (!isEmpty(appState[ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION])) {
url = setStateToKbnUrl<AppPageState<ExplorationPageUrlState>>(
'_a',
appState,
{ useHash: false, storeInHashQuery: false },
url
);
}
url = setStateToKbnUrl<DataFrameAnalyticsExplorationQueryState>(
'_g',
queryState,
@ -104,18 +120,32 @@ export function createDataFrameAnalyticsMapUrl(
let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_MAP}`;
if (mlUrlGeneratorState) {
const { jobId, modelId, analysisType, defaultIsTraining, globalState } = mlUrlGeneratorState;
const { jobId, modelId, analysisType, globalState, queryText } = mlUrlGeneratorState;
const queryState: DataFrameAnalyticsExplorationQueryState = {
ml: {
jobId,
modelId,
analysisType,
defaultIsTraining,
},
...globalState,
};
const appState = {
[ML_PAGES.DATA_FRAME_ANALYTICS_MAP]: {
...(queryText ? { queryText } : {}),
},
};
if (!isEmpty(appState[ML_PAGES.DATA_FRAME_ANALYTICS_MAP])) {
url = setStateToKbnUrl<AppPageState<ExplorationPageUrlState>>(
'_a',
appState,
{ useHash: false, storeInHashQuery: false },
url
);
}
url = setStateToKbnUrl<DataFrameAnalyticsExplorationQueryState>(
'_g',
queryState,