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

[ML] Data Frames - search bar on list page #41415

Merged
merged 9 commits into from
Jul 22, 2019
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 = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be just another React component <TaskStateBadge ... />.

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: [
alvarezmelissa87 marked this conversation as resolved.
Show resolved Hide resolved
{
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" />
alvarezmelissa87 marked this conversation as resolved.
Show resolved Hide resolved
)}
</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