Skip to content

Commit

Permalink
[ML] Data Frames - search bar on list page (#41415)
Browse files Browse the repository at this point in the history
* add search and filter to df list table

* add mode filter to list table

* adds id + description search

* type fix

* ensure search syntax is valid

* ensure types are correct

* retain filter on refresh

* fix progress bar jump
  • Loading branch information
alvarezmelissa87 committed Jul 22, 2019
1 parent b2f8eac commit f41290b
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 19 deletions.
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

0 comments on commit f41290b

Please sign in to comment.