[BeatsCM] Update table controls for better discoverability (#27043)

* working new control layout

# Conflicts:
#	x-pack/plugins/beats_management/public/components/table/table.tsx

* Finish new table controls

* check for tags

* update type to match reality

* when all beats are removed, no more error

* update label for uniformity
This commit is contained in:
Matt Apperson 2018-12-17 12:29:03 -05:00 committed by GitHub
parent 5421473a1c
commit d7a5e631d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 235 additions and 495 deletions

View file

@ -1,65 +0,0 @@
/*
* 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 { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { ActionDefinition } from './table_type_configs';
interface ActionButtonProps {
itemName: 'Beats' | 'Tags';
actions: ActionDefinition[];
intl: InjectedIntl;
isPopoverVisible: boolean;
actionHandler(action: string, payload?: any): void;
hidePopover(): void;
showPopover(): void;
}
export const ActionButton = injectI18n((props: ActionButtonProps) => {
const { actions, actionHandler, hidePopover, isPopoverVisible, showPopover, intl } = props;
if (actions.length === 0) {
return null;
}
return (
<EuiPopover
anchorPosition="downLeft"
button={
<EuiButton iconSide="right" iconType="arrowDown" onClick={showPopover}>
<FormattedMessage
id="xpack.beatsManagement.table.bulkActionButtonLabel"
defaultMessage="Bulk Action"
/>
</EuiButton>
}
closePopover={hidePopover}
id="contextMenu"
isOpen={isPopoverVisible}
panelPaddingSize="none"
withTitle
>
<EuiContextMenu
initialPanelId={0}
panels={[
{
id: 0,
title: intl.formatMessage(
{
id: 'xpack.beatsManagement.table.bulkActionMenuLabel',
defaultMessage: 'Manage {itemName}',
},
{ itemName: props.itemName }
),
items: actions.map(action => ({
...action,
onClick: () => actionHandler(action.action),
})),
},
]}
/>
</EuiPopover>
);
});

View file

@ -7,26 +7,34 @@
import { i18n } from '@kbn/i18n';
import { AssignmentActionType } from './table';
export interface AssignmentControlSchema {
export enum ActionComponentType {
Action,
Popover,
SelectionCount,
TagBadgeList,
}
export interface ControlSchema {
id?: number;
name: string;
danger?: boolean;
type: ActionComponentType;
action?: AssignmentActionType;
actionDataKey?: string;
showWarning?: boolean;
warningHeading?: string;
warningMessage?: string;
lazyLoad?: boolean;
panel?: AssignmentControlSchema;
grow?: boolean;
}
export const beatsListAssignmentOptions: AssignmentControlSchema[] = [
export const beatsListActions: ControlSchema[] = [
{
grow: false,
name: i18n.translate('xpack.beatsManagement.beatsListAssignmentOptions.unenrollButtonLabel', {
defaultMessage: 'Unenroll selected',
}),
showWarning: true,
type: ActionComponentType.Action,
warningHeading: i18n.translate(
'xpack.beatsManagement.beatsListAssignmentOptions.unenrollBeatsWarninigTitle',
{ defaultMessage: 'Unenroll selected beats?' }
@ -43,23 +51,20 @@ export const beatsListAssignmentOptions: AssignmentControlSchema[] = [
defaultMessage: 'Set tags',
}),
grow: false,
type: ActionComponentType.TagBadgeList,
actionDataKey: 'tags',
lazyLoad: true,
panel: {
id: 1,
name: i18n.translate('xpack.beatsManagement.beatsListAssignmentOptions.assignTagsName', {
defaultMessage: 'Assign tags',
}),
},
},
];
export const tagListAssignmentOptions: AssignmentControlSchema[] = [
export const tagListActions: ControlSchema[] = [
{
danger: true,
grow: false,
name: i18n.translate('xpack.beatsManagement.tagListAssignmentOptions.removeTagsButtonLabel', {
defaultMessage: 'Remove tag(s)',
defaultMessage: 'Remove selected',
}),
type: ActionComponentType.Action,
showWarning: true,
warningHeading: i18n.translate(
'xpack.beatsManagement.tagListAssignmentOptions.removeTagsWarninigTitle',
@ -73,13 +78,14 @@ export const tagListAssignmentOptions: AssignmentControlSchema[] = [
},
];
export const tagConfigAssignmentOptions: AssignmentControlSchema[] = [
export const tagConfigActions: ControlSchema[] = [
{
danger: true,
grow: false,
name: i18n.translate('xpack.beatsManagement.tagConfigAssignmentOptions.removeTagsButtonLabel', {
defaultMessage: 'Remove tag(s)',
}),
type: ActionComponentType.Action,
showWarning: true,
warningHeading: i18n.translate(
'xpack.beatsManagement.tagConfigAssignmentOptions.removeTagsWarninigTitle',

View file

@ -1,60 +0,0 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { AutocompleteField } from '../autocomplete_field/index';
import { OptionControl } from './controls/index';
import { AssignmentOptions as AssignmentOptionsType, KueryBarProps } from './table';
interface ControlBarProps {
itemType: string;
assignmentOptions: AssignmentOptionsType;
kueryBarProps?: KueryBarProps;
selectionCount: number;
intl: InjectedIntl;
}
function ControlBarUi(props: ControlBarProps) {
const {
assignmentOptions: { actionHandler, items, schema, type },
kueryBarProps,
selectionCount,
intl,
} = props;
if (type === 'none') {
return null;
}
return (
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<OptionControl
itemType={props.itemType}
schema={schema}
selectionCount={selectionCount}
actionHandler={actionHandler}
items={items}
/>
</EuiFlexItem>
{kueryBarProps && (
<EuiFlexItem>
<AutocompleteField
{...kueryBarProps}
placeholder={intl.formatMessage({
id: 'xpack.beatsManagement.table.filterResultsPlaceholder',
defaultMessage: 'Filter results',
})}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
}
export const ControlBar = injectI18n(ControlBarUi);

View file

@ -4,18 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
// @ts-ignore EuiConfirmModal typings not included in current EUI
EuiConfirmModal,
EuiOverlayMask,
EuiTextColor,
} from '@elastic/eui';
import { EuiButton, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { AssignmentActionType } from '../table';
interface ActionControlProps {
action: AssignmentActionType;
disabled: boolean;
danger?: boolean;
name: string;
showWarning?: boolean;
@ -47,16 +43,19 @@ export class ActionControl extends React.PureComponent<ActionControlProps, Actio
warningHeading,
warningMessage,
} = this.props;
return (
<div>
<EuiTextColor
color={danger ? 'danger' : 'default'}
<EuiButton
size="s"
color={danger ? 'danger' : 'primary'}
disabled={this.props.disabled}
onClick={
showWarning ? () => this.setState({ showModal: true }) : () => actionHandler(action)
}
>
{name}
</EuiTextColor>
</EuiButton>
{this.state.showModal && (
<EuiOverlayMask>
<EuiConfirmModal

View file

@ -4,193 +4,54 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiButton,
// @ts-ignore
EuiCard,
EuiContextMenu,
EuiPanel,
EuiPopover,
EuiTextColor,
EuiToolTip,
EuiToolTipProps,
} from '@elastic/eui';
import { EuiIcon } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { isArray } from 'lodash';
import React from 'react';
import { AssignmentControlSchema } from '../index';
import { ActionComponentType, ControlSchema } from '../action_schema';
import { AssignmentActionType } from '../table';
import { ActionControl } from './action_control';
import { TagBadgeList } from './tag_badge_list';
interface ComponentProps {
intl: InjectedIntl;
itemType: string;
items?: any[];
schema: AssignmentControlSchema[];
selectionCount: number;
interface ComponentProps extends ControlSchema {
actionData?: {
[key: string]: any;
};
disabled: boolean;
actionHandler(action: AssignmentActionType, payload?: any): void;
}
interface ComponentState {
showPopover: boolean;
}
interface FixedEuiToolTipProps extends EuiToolTipProps {
delay: 'regular' | 'long';
}
const FixedEuiToolTip = (EuiToolTip as any) as React.SFC<FixedEuiToolTipProps>;
class OptionControlUi extends React.PureComponent<ComponentProps, ComponentState> {
constructor(props: ComponentProps) {
super(props);
this.state = {
showPopover: false,
};
}
public schemaToPanelTree(
schemaOrArray: AssignmentControlSchema | AssignmentControlSchema[],
panels: any = []
) {
const { items, actionHandler, intl } = this.props;
let schema: AssignmentControlSchema | null = null;
let schemaArray: AssignmentControlSchema[] | null = null;
if (isArray(schemaOrArray)) {
schemaArray = schemaOrArray as AssignmentControlSchema[];
} else {
schema = schemaOrArray as AssignmentControlSchema;
}
const panel: any = {
title: schema ? schema.name : undefined,
id: panels.length,
};
if (schemaArray) {
panel.items = schemaArray.map(def => {
return {
onClick: def.lazyLoad ? () => actionHandler(AssignmentActionType.Reload) : undefined,
panel: def.panel ? def.panel.id : undefined,
name: def.action ? (
<ActionControl
actionHandler={actionHandler}
action={def.action}
danger={def.danger}
name={def.name}
showWarning={def.showWarning}
warningHeading={def.warningHeading}
warningMessage={def.warningMessage}
/>
) : (
<EuiTextColor color={def.danger ? 'danger' : 'default'}>{def.name}</EuiTextColor>
),
};
});
} else {
if (items === undefined) {
panel.content = intl.formatMessage({
id: 'xpack.beatsManagement.tableControls.unknownErrorMessage',
defaultMessage: 'Unknown Error.',
});
} else if (items.length === 0) {
panel.content = (
<EuiPanel>
<EuiCard
icon={<EuiIcon size="l" type="bolt" />}
title={intl.formatMessage({
id: 'xpack.beatsManagement.tableControls.noTagsFoundTitle',
defaultMessage: 'No tags found.',
})}
description={intl.formatMessage({
id: 'xpack.beatsManagement.tableControls.noTagsFoundDescription',
defaultMessage: 'Please create a new configuration tag.',
})}
/>
</EuiPanel>
);
} else {
panel.content = <TagBadgeList items={items} actionHandler={actionHandler} />;
export const OptionControl: React.SFC<ComponentProps> = (props: ComponentProps) => {
switch (props.type) {
case ActionComponentType.Action:
if (!props.action) {
throw Error('Action cannot be undefined');
}
}
panels.push(panel);
if (schemaArray !== null) {
schemaArray.forEach((item: AssignmentControlSchema) => {
if (item.panel) {
this.schemaToPanelTree(item.panel, panels);
}
});
}
return panels;
return (
<ActionControl
actionHandler={props.actionHandler}
action={props.action}
danger={props.danger}
name={props.name}
showWarning={props.showWarning}
warningHeading={props.warningHeading}
warningMessage={props.warningMessage}
disabled={props.disabled}
/>
);
case ActionComponentType.TagBadgeList:
if (!props.actionDataKey) {
throw Error('actionDataKey cannot be undefined');
}
if (!props.actionData) {
throw Error('actionData cannot be undefined');
}
return (
<TagBadgeList
actionHandler={props.actionHandler}
action={props.action}
name={props.name}
items={props.actionData[props.actionDataKey]}
disabled={props.disabled}
/>
);
}
public render() {
const { itemType, selectionCount, schema, intl } = this.props;
return (
<EuiPopover
button={
<FixedEuiToolTip
position="top"
delay="long"
content={
selectionCount === 0
? intl.formatMessage(
{
id: 'xpack.beatsManagement.tableControls.selectItemDescription',
defaultMessage:
'Select {itemType} to perform operations such as setting tags and unenrolling Beats.',
},
{ itemType }
)
: intl.formatMessage(
{
id: 'xpack.beatsManagement.tableControls.manageSelectedItemDescription',
defaultMessage: 'Manage your selected {itemType}',
},
{ itemType }
)
}
>
<EuiButton
color="primary"
iconSide="right"
disabled={selectionCount === 0}
iconType="arrowDown"
onClick={() => {
this.setState({
showPopover: true,
});
}}
>
<FormattedMessage
id="xpack.beatsManagement.tableControls.manageItemButtonLabel"
defaultMessage="Manage {itemType}"
values={{ itemType }}
/>
</EuiButton>
</FixedEuiToolTip>
}
closePopover={() => {
this.setState({ showPopover: false });
}}
id="assignmentList"
isOpen={this.state.showPopover}
panelPaddingSize="none"
anchorPosition="downLeft"
withTitle
>
<EuiContextMenu initialPanelId={0} panels={this.schemaToPanelTree(schema)} />
</EuiPopover>
);
}
}
export const OptionControl = injectI18n(OptionControlUi);
return <div>Invalid config</div>;
};

View file

@ -1,55 +0,0 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import React from 'react';
import { TABLE_CONFIG } from '../../../../common/constants';
import { TagBadge } from '../../tag/tag_badge';
interface TagAssignmentProps {
tag: any;
assignTag(id: string): void;
}
interface TagAssignmentState {
isFetchingTags: boolean;
}
export class TagAssignment extends React.PureComponent<TagAssignmentProps, TagAssignmentState> {
constructor(props: TagAssignmentProps) {
super(props);
this.state = {
isFetchingTags: false,
};
}
public render() {
const {
assignTag,
tag,
tag: { id },
} = this.props;
return (
<EuiFlexGroup gutterSize="xs" key={id}>
{this.state.isFetchingTags && (
<EuiFlexItem>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
)}
<EuiFlexItem>
<TagBadge
maxIdRenderSize={TABLE_CONFIG.TRUNCATE_TAG_LENGTH_SMALL}
onClick={() => assignTag(id)}
onClickAriaLabel={id}
tag={tag}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}
}

View file

@ -4,26 +4,91 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import {
EuiButton,
EuiContextMenuPanel,
EuiFlexGroup,
EuiFlexItem,
EuiPopover,
} from '@elastic/eui';
import React from 'react';
import { TABLE_CONFIG } from '../../../../common/constants/table';
import { TagBadge } from '../../tag/tag_badge';
import { AssignmentActionType } from '../index';
import { TagAssignment } from './tag_assignment';
interface TagBadgeListProps {
items: any[];
disabled: boolean;
name: string;
action?: AssignmentActionType;
actionHandler(action: AssignmentActionType, payload?: any): void;
}
export const TagBadgeList = (props: TagBadgeListProps) => (
// @ts-ignore direction prop type "column" not defined in current EUI version
<EuiFlexGroup direction="column" gutterSize="xs" style={{ margin: 10 }}>
{props.items.map((item: any) => (
<EuiFlexItem key={`${item.id}`}>
<TagAssignment
tag={item}
assignTag={(id: string) => props.actionHandler(AssignmentActionType.Assign, id)}
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
);
interface ComponentState {
isPopoverOpen: boolean;
}
export class TagBadgeList extends React.Component<TagBadgeListProps, ComponentState> {
constructor(props: TagBadgeListProps) {
super(props);
this.state = {
isPopoverOpen: false,
};
}
public render() {
const button = (
<EuiButton
size="s"
iconType="arrowDown"
iconSide="right"
onClick={this.onButtonClick}
disabled={this.props.disabled}
>
{this.props.name}
</EuiButton>
);
return (
<EuiPopover
id="contentPanel"
button={button}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenuPanel>
<EuiFlexGroup direction="column" gutterSize="xs" style={{ margin: 10 }}>
{this.props.items.map((tag: any) => (
<EuiFlexItem key={`${tag.id}`}>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
<TagBadge
maxIdRenderSize={TABLE_CONFIG.TRUNCATE_TAG_LENGTH_SMALL}
onClick={() => this.props.actionHandler(AssignmentActionType.Assign, tag.id)}
onClickAriaLabel={tag.id}
tag={tag}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiContextMenuPanel>
</EuiPopover>
);
}
private onButtonClick = () => {
this.setState(prevState => ({
isPopoverOpen: !prevState.isPopoverOpen,
}));
};
private closePopover = () => {
this.setState({
isPopoverOpen: false,
});
};
}

View file

@ -4,13 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { AssignmentActionType, AssignmentOptions, KueryBarProps, Table } from './table';
export {
AssignmentControlSchema,
beatsListAssignmentOptions,
tagConfigAssignmentOptions,
} from './assignment_schema';
export { ControlBar } from './controls';
export { beatsListActions, tagConfigActions } from './action_schema';
export { AssignmentActionType, KueryBarProps, Table } from './table';
export {
ActionDefinition,
BeatDetailTagsTable,

View file

@ -5,7 +5,9 @@
*/
import {
// @ts-ignore no typings for EuiInMemoryTable in EUI
EuiFlexGroup,
EuiFlexItem,
// @ts-ignore
EuiInMemoryTable,
EuiSpacer,
} from '@elastic/eui';
@ -14,8 +16,9 @@ import React from 'react';
import styled from 'styled-components';
import { AutocompleteSuggestion } from 'ui/autocomplete_providers';
import { TABLE_CONFIG } from '../../../common/constants';
import { AssignmentControlSchema } from './assignment_schema';
import { ControlBar } from './controls';
import { AutocompleteField } from '../autocomplete_field/index';
import { ControlSchema } from './action_schema';
import { OptionControl } from './controls/option_control';
import { TableType } from './table_type_configs';
export enum AssignmentActionType {
@ -27,13 +30,6 @@ export enum AssignmentActionType {
Search,
}
export interface AssignmentOptions {
schema: AssignmentControlSchema[];
items: any[];
type?: 'none' | 'primary' | 'assignment';
actionHandler(action: AssignmentActionType, payload?: any): void;
}
export interface KueryBarProps {
filterQueryDraft: string;
isLoadingSuggestions: boolean;
@ -46,11 +42,15 @@ export interface KueryBarProps {
}
interface TableProps {
assignmentOptions?: AssignmentOptions;
actions?: ControlSchema[];
actionData?: {
[key: string]: any;
};
hideTableControls?: boolean;
kueryBarProps?: KueryBarProps;
items: any[];
type: TableType;
actionHandler?(action: AssignmentActionType, payload?: any): void;
}
interface TableState {
@ -80,8 +80,14 @@ export class Table extends React.Component<TableProps, TableState> {
});
};
public actionHandler = (action: AssignmentActionType, payload?: any): void => {
if (this.props.actionHandler) {
this.props.actionHandler(action, payload);
}
};
public render() {
const { assignmentOptions, hideTableControls, items, kueryBarProps, type } = this.props;
const { actionData, actions, hideTableControls, items, kueryBarProps, type } = this.props;
const pagination = {
initialPageSize: TABLE_CONFIG.INITIAL_ROW_SIZE,
@ -102,14 +108,33 @@ export class Table extends React.Component<TableProps, TableState> {
return (
<TableContainer>
{!hideTableControls && assignmentOptions && (
<ControlBar
itemType={type.itemType}
assignmentOptions={assignmentOptions}
kueryBarProps={kueryBarProps}
selectionCount={this.state.selection.length}
/>
)}
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
{actions &&
actions.map(action => (
<EuiFlexItem grow={false} key={action.name}>
<OptionControl
{...action}
actionData={actionData}
actionHandler={this.actionHandler}
disabled={this.state.selection.length === 0}
/>
</EuiFlexItem>
))}
{kueryBarProps && (
<EuiFlexItem>
<AutocompleteField
{...kueryBarProps}
placeholder={i18n.translate(
'xpack.beatsManagement.table.filterResultsPlaceholder',
{
defaultMessage: 'Filter results',
}
)}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiInMemoryTable
columns={type.columnDefinitions}

View file

@ -1,29 +0,0 @@
/*
* 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 {
// @ts-ignore typings for EuiSearchar not included in EUI
EuiSearchBar,
} from '@elastic/eui';
import React from 'react';
import { FilterDefinition } from '../table';
import { AssignmentActionType } from './table';
interface TableSearchControlProps {
filters?: FilterDefinition[];
actionHandler(action: AssignmentActionType, payload?: any): void;
}
export const TableSearchControl = (props: TableSearchControlProps) => {
const { actionHandler, filters } = props;
return (
<EuiSearchBar
box={{ incremental: true }}
filters={filters}
onChange={(query: any) => actionHandler(AssignmentActionType.Search, query)}
/>
);
};

View file

@ -164,8 +164,8 @@ export const BeatsTableType: TableType = {
name: i18n.translate('xpack.beatsManagement.beatsTable.lastConfigUpdateTitle', {
defaultMessage: 'Last config update',
}),
render: (tags: BeatTag[]) =>
tags.length ? (
render: (tags?: BeatTag[]) =>
tags && tags.length ? (
<span>
{moment(first(sortByOrder(tags, ['last_updated'], ['desc'])).last_updated).fromNow()}
</span>

View file

@ -24,7 +24,6 @@ export const TagBadge = (props: TagBadgeProps) => {
onClickAriaLabel,
tag: { color, disabled, id },
} = props;
const maxIdRenderSize = props.maxIdRenderSize || TABLE_CONFIG.TRUNCATE_TAG_LENGTH;
const idToRender = id.length > maxIdRenderSize ? `${id.substring(0, maxIdRenderSize)}...` : id;
return disabled ? (

View file

@ -27,7 +27,7 @@ import { isEqual } from 'lodash';
import React from 'react';
import { BeatTag, CMBeat, ConfigurationBlock } from '../../../common/domain_types';
import { ConfigList } from '../config_list';
import { AssignmentActionType, BeatsTableType, Table, tagConfigAssignmentOptions } from '../table';
import { AssignmentActionType, BeatsTableType, Table, tagConfigActions } from '../table';
import { ConfigView } from './config_view';
import { TagBadge } from './tag_badge';
@ -190,12 +190,8 @@ export class TagEdit extends React.PureComponent<TagEditProps, TagEditState> {
</h3>
</EuiTitle>
<Table
assignmentOptions={{
schema: tagConfigAssignmentOptions,
items: [],
type: 'primary',
actionHandler: this.handleAssignmentActions,
}}
actions={tagConfigActions}
actionHandler={this.handleAssignmentActions}
items={attachedBeats}
ref={this.state.tableRef}
type={BeatsTableType}

View file

@ -73,7 +73,10 @@ export class BeatsContainer extends Container<ContainerState> {
}
const assignments = createBeatTagAssignments(beats, tagId);
await this.libs.beats.removeTagsFromBeats(assignments);
await this.reload(this.query);
// ES responds incorrectly when we call too soon
setTimeout(async () => {
await this.reload(this.query);
}, 150);
};
public assignTagsToBeats = async (beats: CMPopulatedBeat[] | string[], tagId: string) => {
@ -82,7 +85,10 @@ export class BeatsContainer extends Container<ContainerState> {
}
const assignments = createBeatTagAssignments(beats, tagId);
await this.libs.beats.assignTagsToBeats(assignments);
await this.reload(this.query);
// ES responds incorrectly when we call too soon
setTimeout(async () => {
await this.reload(this.query);
}, 150);
};
}

View file

@ -11,7 +11,7 @@ import React from 'react';
import { BeatTag } from '../../../common/domain_types';
import { Breadcrumb } from '../../components/navigation/breadcrumb';
import { AssignmentActionType, Table, TagsTableType } from '../../components/table';
import { tagListAssignmentOptions } from '../../components/table/assignment_schema';
import { tagListActions } from '../../components/table/action_schema';
import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion';
import { AppPageProps } from '../../frontend_types';
@ -79,12 +79,8 @@ class TagsPageComponent extends React.PureComponent<PageProps, PageState> {
onSubmit: () => null, // todo
value: this.props.urlState.tagsKBar || '',
}}
assignmentOptions={{
schema: tagListAssignmentOptions,
type: 'primary',
items: [],
actionHandler: this.handleTagsAction,
}}
actions={tagListActions}
actionHandler={this.handleTagsAction}
ref={this.state.tableRef}
items={this.props.containers.tags.state.list}
type={TagsTableType}

View file

@ -24,7 +24,7 @@ import { BeatTag, CMPopulatedBeat, ConfigurationBlock } from '../../../common/do
import { EnrollBeat } from '../../components/enroll_beats';
import { Breadcrumb } from '../../components/navigation/breadcrumb';
import { BeatsTableType, Table } from '../../components/table';
import { beatsListAssignmentOptions } from '../../components/table/assignment_schema';
import { beatsListActions } from '../../components/table/action_schema';
import { AssignmentActionType } from '../../components/table/table';
import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion';
import { AppPageProps } from '../../frontend_types';
@ -167,28 +167,27 @@ class BeatsPageComponent extends React.PureComponent<PageProps, PageState> {
onSubmit: () => null, // todo
value: this.props.urlState.beatsKBar || '',
}}
assignmentOptions={{
items: this.filterTags(this.props.containers.tags.state.list),
schema: beatsListAssignmentOptions,
type: 'assignment',
actionHandler: async (action: AssignmentActionType, payload: any) => {
switch (action) {
case AssignmentActionType.Assign:
const status = await this.props.containers.beats.toggleTagAssignment(
payload,
this.getSelectedBeats()
);
this.notifyUpdatedTagAssociation(status, this.getSelectedBeats(), payload);
break;
case AssignmentActionType.Delete:
this.props.containers.beats.deactivate(this.getSelectedBeats());
this.notifyBeatDisenrolled(this.getSelectedBeats());
break;
case AssignmentActionType.Reload:
this.props.containers.tags.reload();
break;
}
},
actions={beatsListActions}
actionData={{
tags: this.filterTags(this.props.containers.tags.state.list),
}}
actionHandler={async (action: AssignmentActionType, payload: any) => {
switch (action) {
case AssignmentActionType.Assign:
const status = await this.props.containers.beats.toggleTagAssignment(
payload,
this.getSelectedBeats()
);
this.notifyUpdatedTagAssociation(status, this.getSelectedBeats(), payload);
break;
case AssignmentActionType.Delete:
this.props.containers.beats.deactivate(this.getSelectedBeats());
this.notifyBeatDisenrolled(this.getSelectedBeats());
break;
case AssignmentActionType.Reload:
this.props.containers.tags.reload();
break;
}
}}
items={sortBy(this.props.containers.beats.state.list, 'id') || []}
ref={this.tableRef}
@ -349,7 +348,9 @@ class BeatsPageComponent extends React.PureComponent<PageProps, PageState> {
// union beat tags
flatten(this.getSelectedBeats().map(({ full_tags }) => full_tags))
// map tag list to bool
.map(({ configuration_blocks }) => this.configBlocksRequireUniqueness(configuration_blocks))
.map(tag => {
return this.configBlocksRequireUniqueness(tag ? tag.configuration_blocks : []);
})
// reduce to result
.reduce((acc, cur) => acc || cur, false);
}