Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] [ML] Data Frames - search bar on list page (#41415) #41706

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface CreateRequestBody extends PreviewRequestBody {

export interface DataFrameTransformPivotConfig extends CreateRequestBody {
id: DataFrameTransformId;
mode?: string; // added property on client side to allow filtering by this field
}

// Don't allow intervals of '0', don't allow floating intervals.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,10 @@
animation: none !important;
}
}
.mlTransformProgressBar {
margin-bottom: $euiSizeM;
}

.mlTaskStateBadge, .mlTaskModeBadge {
max-width: 100px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
DATA_FRAME_TASK_STATE,
DataFrameTransformListColumn,
DataFrameTransformListRow,
DataFrameTransformState,
} from './common';
import { getActions } from './actions';

Expand All @@ -31,6 +32,29 @@ enum TASK_STATE_COLOR {
stopped = 'hollow',
}

export const getTaskStateBadge = (
state: DataFrameTransformState['task_state'],
reason?: DataFrameTransformState['reason']
) => {
const color = TASK_STATE_COLOR[state];

if (state === DATA_FRAME_TASK_STATE.FAILED && reason !== undefined) {
return (
<EuiToolTip content={reason}>
<EuiBadge className="mlTaskStateBadge" color={color}>
{state}
</EuiBadge>
</EuiToolTip>
);
}

return (
<EuiBadge className="mlTaskStateBadge" color={color}>
{state}
</EuiBadge>
);
};

export const getColumns = (
expandedRowItemIds: DataFrameTransformId[],
setExpandedRowItemIds: React.Dispatch<React.SetStateAction<DataFrameTransformId[]>>
Expand Down Expand Up @@ -104,27 +128,16 @@ export const getColumns = (
sortable: (item: DataFrameTransformListRow) => item.state.task_state,
truncateText: true,
render(item: DataFrameTransformListRow) {
const color = TASK_STATE_COLOR[item.state.task_state];

if (item.state.task_state === DATA_FRAME_TASK_STATE.FAILED) {
return (
<EuiToolTip content={item.state.reason}>
<EuiBadge color={color}>{item.state.task_state}</EuiBadge>
</EuiToolTip>
);
}

return <EuiBadge color={color}>{item.state.task_state}</EuiBadge>;
return getTaskStateBadge(item.state.task_state, item.state.reason);
},
width: '100px',
},
{
name: i18n.translate('xpack.ml.dataframe.mode', { defaultMessage: 'Mode' }),
sortable: (item: DataFrameTransformListRow) =>
typeof item.config.sync !== 'undefined' ? 'continuous' : 'batch',
sortable: (item: DataFrameTransformListRow) => item.config.mode,
truncateText: true,
render(item: DataFrameTransformListRow) {
const mode = typeof item.config.sync !== 'undefined' ? 'continuous' : 'batch';
const mode = item.config.mode;
const color = 'hollow';
return <EuiBadge color={color}>{mode}</EuiBadge>;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,25 @@ export enum DATA_FRAME_TASK_STATE {
STOPPED = 'stopped',
}

export enum DATA_FRAME_MODE {
BATCH = 'batch',
CONTINUOUS = 'continuous',
}

export interface Clause {
type: string;
value: string;
match: string;
}

export interface Query {
ast: {
clauses: Clause[];
};
text: string;
syntax: any;
}

export interface DataFrameTransformState {
checkpoint: number;
current_position: Dictionary<any>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@ import React, { Fragment, SFC, useState } from 'react';

import { i18n } from '@kbn/i18n';

import { EuiButtonEmpty, EuiCallOut, EuiEmptyPrompt, SortDirection } from '@elastic/eui';
import { EuiBadge, EuiButtonEmpty, EuiCallOut, EuiEmptyPrompt, SortDirection } from '@elastic/eui';

import {
DataFrameTransformId,
moveToDataFrameWizard,
useRefreshTransformList,
} from '../../../../common';
import { checkPermission } from '../../../../../privilege/check_privilege';
import { getTaskStateBadge } from './columns';

import {
DataFrameTransformListColumn,
DataFrameTransformListRow,
ItemIdToExpandedRowMap,
DATA_FRAME_TASK_STATE,
DATA_FRAME_MODE,
Query,
Clause,
} from './common';
import { getTransformsFactory } from '../../services/transform_service';
import { getColumns } from './columns';
Expand All @@ -44,15 +49,26 @@ function getItemIdToExpandedRowMap(
);
}

function stringMatch(str: string | undefined, substr: string) {
return (
typeof str === 'string' &&
typeof substr === 'string' &&
(str.toLowerCase().match(substr.toLowerCase()) === null) === false
);
}

export const DataFrameTransformList: SFC = () => {
const [isInitialized, setIsInitialized] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [blockRefresh, setBlockRefresh] = useState(false);
const [filterActive, setFilterActive] = useState(false);

const [transforms, setTransforms] = useState<DataFrameTransformListRow[]>([]);
const [filteredTransforms, setFilteredTransforms] = useState<DataFrameTransformListRow[]>([]);
const [expandedRowItemIds, setExpandedRowItemIds] = useState<DataFrameTransformId[]>([]);

const [errorMessage, setErrorMessage] = useState<any>(undefined);
const [searchError, setSearchError] = useState<any>(undefined);

const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
Expand All @@ -72,10 +88,88 @@ export const DataFrameTransformList: SFC = () => {
blockRefresh
);
// Subscribe to the refresh observable to trigger reloading the transform list.
useRefreshTransformList({ isLoading: setIsLoading, onRefresh: () => getTransforms(true) });
useRefreshTransformList({
isLoading: setIsLoading,
onRefresh: () => getTransforms(true),
});
// Call useRefreshInterval() after the subscription above is set up.
useRefreshInterval(setBlockRefresh);

const onQueryChange = ({ query, error }: { query: Query; error: any }) => {
if (error) {
setSearchError(error.message);
} else {
let clauses: Clause[] = [];
if (query && query.ast !== undefined && query.ast.clauses !== undefined) {
clauses = query.ast.clauses;
}
if (clauses.length > 0) {
setFilterActive(true);
filterTransforms(clauses);
} else {
setFilterActive(false);
}
setSearchError(undefined);
}
};

const filterTransforms = (clauses: Clause[]) => {
setIsLoading(true);
// keep count of the number of matches we make as we're looping over the clauses
// we only want to return transforms which match all clauses, i.e. each search term is ANDed
// { transform-one: { transform: { id: transform-one, config: {}, state: {}, ... }, count: 0 }, transform-two: {...} }
const matches: Record<string, any> = transforms.reduce((p: Record<string, any>, c) => {
p[c.id] = {
transform: c,
count: 0,
};
return p;
}, {});

clauses.forEach(c => {
// the search term could be negated with a minus, e.g. -bananas
const bool = c.match === 'must';
let ts = [];

if (c.type === 'term') {
// filter term based clauses, e.g. bananas
// match on id and description
// if the term has been negated, AND the matches
if (bool === true) {
ts = transforms.filter(
transform =>
stringMatch(transform.id, c.value) === bool ||
stringMatch(transform.config.description, c.value) === bool
);
} else {
ts = transforms.filter(
transform =>
stringMatch(transform.id, c.value) === bool &&
stringMatch(transform.config.description, c.value) === bool
);
}
} else {
// filter other clauses, i.e. the mode and status filters
if (Array.isArray(c.value)) {
// the status value is an array of string(s) e.g. ['failed', 'stopped']
ts = transforms.filter(transform => c.value.includes(transform.state.task_state));
} else {
ts = transforms.filter(transform => transform.config.mode === c.value);
}
}

ts.forEach(t => matches[t.id].count++);
});

// loop through the matches and return only transforms which have match all the clauses
const filtered = Object.values(matches)
.filter(m => (m && m.count) >= clauses.length)
.map(m => m.transform);

setFilteredTransforms(filtered);
setIsLoading(false);
};

// Before the transforms have been loaded for the first time, display the loading indicator only.
// Otherwise a user would see 'No data frame transforms found' during the initial loading.
if (!isInitialized) {
Expand Down Expand Up @@ -143,6 +237,41 @@ export const DataFrameTransformList: SFC = () => {
hidePerPageOptions: false,
};

const search = {
onChange: onQueryChange,
box: {
incremental: true,
},
filters: [
{
type: 'field_value_selection',
field: 'state.task_state',
name: i18n.translate('xpack.ml.dataframe.statusFilter', { defaultMessage: 'Status' }),
multiSelect: 'or',
options: Object.values(DATA_FRAME_TASK_STATE).map(val => ({
value: val,
name: val,
view: getTaskStateBadge(val),
})),
},
{
type: 'field_value_selection',
field: 'config.mode',
name: i18n.translate('xpack.ml.dataframe.modeFilter', { defaultMessage: 'Mode' }),
multiSelect: false,
options: Object.values(DATA_FRAME_MODE).map(val => ({
value: val,
name: val,
view: (
<EuiBadge className="mlTaskModeBadge" color="hollow">
{val}
</EuiBadge>
),
})),
},
],
};

const onTableChange = ({
page = { index: 0, size: 10 },
sort = { field: DataFrameTransformListColumn.id, direction: SortDirection.ASC },
Expand All @@ -165,15 +294,17 @@ export const DataFrameTransformList: SFC = () => {
<TransformTable
className="mlTransformTable"
columns={columns}
error={searchError}
hasActions={false}
isExpandable={true}
isSelectable={false}
items={transforms}
items={filterActive ? filteredTransforms : transforms}
itemId={DataFrameTransformListColumn.id}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
onChange={onTableChange}
pagination={pagination}
sorting={sorting}
search={search}
data-test-subj="mlDataFramesTableTransforms"
/>
</Fragment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ import { ItemIdToExpandedRowMap } from './common';
export const ProgressBar = ({ isLoading = false }) => {
return (
<Fragment>
{isLoading && <EuiProgress size="xs" color="primary" />}
{!isLoading && <EuiProgress value={0} max={100} size="xs" />}
{isLoading && <EuiProgress className="mlTransformProgressBar" size="xs" color="primary" />}
{!isLoading && (
<EuiProgress className="mlTransformProgressBar" value={0} max={100} size="xs" />
)}
</Fragment>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
DataFrameTransformListRow,
DataFrameTransformState,
DataFrameTransformStats,
DATA_FRAME_MODE,
} from '../../components/transform_list/common';

interface DataFrameTransformStateStats {
Expand Down Expand Up @@ -92,6 +93,12 @@ export const getTransformsFactory = (
if (stats === undefined) {
return reducedtableRows;
}

config.mode =
typeof config.sync !== 'undefined'
? DATA_FRAME_MODE.CONTINUOUS
: DATA_FRAME_MODE.BATCH;

// Table with expandable rows requires `id` on the outer most level
reducedtableRows.push({
config,
Expand Down