mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
647b9914c9
commit
f39da3e680
25 changed files with 340 additions and 155 deletions
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -286,6 +286,7 @@ export const EvaluatePanel: FC<EvaluatePanelProps> = ({ jobConfig, jobStatus, se
|
|||
return (
|
||||
<>
|
||||
<ExpandableSection
|
||||
urlStateKey={'evaluation'}
|
||||
dataTestId="ClassificationEvaluation"
|
||||
title={
|
||||
<FormattedMessage
|
||||
|
|
|
@ -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}`}>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -153,6 +153,7 @@ export const ExpandableSectionResults: FC<ExpandableSectionResultsProps> = ({
|
|||
return (
|
||||
<>
|
||||
<ExpandableSection
|
||||
urlStateKey={'results'}
|
||||
dataTestId="results"
|
||||
content={resultsSectionContent}
|
||||
headerItems={resultsSectionHeaderItems}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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`.
|
||||
|
|
|
@ -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" />
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -222,6 +222,7 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
|
|||
return (
|
||||
<>
|
||||
<ExpandableSection
|
||||
urlStateKey={'evaluation'}
|
||||
dataTestId="RegressionEvaluation"
|
||||
title={
|
||||
<FormattedMessage
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -237,11 +237,11 @@ export const FeatureImportanceSummaryPanel: FC<FeatureImportanceSummaryPanelProp
|
|||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}, [totalFeatureImportance, jobConfig]);
|
||||
return (
|
||||
<>
|
||||
<ExpandableSection
|
||||
urlStateKey={'feature_importance'}
|
||||
isExpanded={noDataCallOut === undefined}
|
||||
dataTestId="FeatureImportanceSummary"
|
||||
title={
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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`,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue