Skip to content

Commit

Permalink
Improvements in Event Definitions page (#6091)
Browse files Browse the repository at this point in the history
* Remove unused execute method in EventDefinitionsStore

This resource was meant for testing the execution before the scheduler
was done.

* Add pagination to EventDefinitionsStore

* Add pagination to Event Definition list

* Add filter controls to Event Definition list

* Add grand total to PaginatedResponse

Before this change, using PaginatedResponse with a query returned a
total of elements matching the query. The response had no indication of
how many items of that entity were there, something problematic for
the frontend, since we want to render a different messages for both
situations.

This commit adds a `grandTotal` field to `PaginatedList` and adds it
into the response if the field is set, making it easier for the frontend
to detect when there are no available items from certain entity.

* Update check of empty list of event definitions

* Remove test page size

* Use single element for event definition actions

* Get event definition name from plugin

* Add more information to Event Definition list

The change includes scheduling information and notifications
information.
  • Loading branch information
Edmundo Alvarez authored and bernd committed Jul 6, 2019
1 parent e79b93d commit fc84262
Show file tree
Hide file tree
Showing 15 changed files with 244 additions and 73 deletions.
Expand Up @@ -120,7 +120,8 @@ protected PaginatedList<DTO> findPaginatedWithQueryAndSort(DBQuery.Query query,
.sort(sort)
.limit(perPage)
.skip(perPage * Math.max(0, page - 1))) {
return new PaginatedList<>(asImmutableList(cursor), cursor.count(), page, perPage);
final long grandTotal = db.count();
return new PaginatedList<>(asImmutableList(cursor), cursor.count(), page, perPage, grandTotal);
}
}

Expand Down Expand Up @@ -167,7 +168,9 @@ protected PaginatedList<DTO> findPaginatedWithQueryFilterAndSort(DBQuery.Query q
.limit(perPage)
.collect(Collectors.toList());

return new PaginatedList<>(list, total.get(), page, perPage);
final long grandTotal = db.count();

return new PaginatedList<>(list, total.get(), page, perPage, grandTotal);
}
}

Expand Down
Expand Up @@ -27,16 +27,24 @@
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

public class PaginatedList<E> extends ForwardingList<E> {

private final List<E> delegate;

private final PaginationInfo paginationInfo;

private final Long grandTotal;

public PaginatedList(@Nonnull List<E> delegate, int total, int page, int perPage) {
this(delegate, total, page, perPage, null);
}

public PaginatedList(@Nonnull List<E> delegate, int total, int page, int perPage, Long grandTotal) {
this.delegate = delegate;
this.paginationInfo = PaginationInfo.create(total, delegate.size(), page, perPage);
this.grandTotal = grandTotal;
}

@Override
Expand All @@ -48,34 +56,40 @@ public PaginationInfo pagination() {
return paginationInfo;
}

public Optional<Long> grandTotal() {
return Optional.ofNullable(grandTotal);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PaginatedList)) return false;
PaginatedList<?> that = (PaginatedList<?>) o;
return Objects.equals(pagination(), that.pagination()) &&
Objects.equals(delegate(), that.delegate());
Objects.equals(delegate(), that.delegate()) &&
Objects.equals(grandTotal(), that.grandTotal());
}

@Override
public int hashCode() {
return Objects.hash(delegate(), pagination());
return Objects.hash(delegate(), pagination(), grandTotal());
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("content", delegate)
.add("pagination_info", pagination())
.add("grand_total", grandTotal())
.toString();
}

public static <T> PaginatedList<T> emptyList(int page, int perPage) {
return new PaginatedList<>(Collections.emptyList(), 0, page, perPage);
return new PaginatedList<>(Collections.emptyList(), 0, page, perPage, 0L);
}

public static <T> PaginatedList<T> singleton(T entry, int page, int perPage) {
return new PaginatedList<T>(Collections.singletonList(entry), 1, page, perPage);
return new PaginatedList<>(Collections.singletonList(entry), 1, page, perPage, 1L);
}

@JsonAutoDetect
Expand Down
Expand Up @@ -47,6 +47,10 @@ public Map<String, Object> jsonValue() {
builder.put("query", query);
}

if (paginatedList.grandTotal().isPresent()) {
builder.put("grand_total", paginatedList.grandTotal().get());
}

return builder.build();
}

Expand Down
@@ -1,12 +1,12 @@
import Reflux from 'reflux';

const EventDefinitionsActions = Reflux.createActions({
list: { asyncResult: true },
listAll: { asyncResult: true },
listPaginated: { asyncResult: true },
get: { asyncResult: true },
create: { asyncResult: true },
update: { asyncResult: true },
delete: { asyncResult: true },
execute: { asyncResult: true },
});

export default EventDefinitionsActions;
Expand Up @@ -13,7 +13,7 @@ import { Spinner } from 'components/common';
import EventDefinitionForm from './EventDefinitionForm';

// Import built-in plugins
import {} from './event-definition-types';
import {} from 'components/event-definitions/event-definition-types';
import {} from 'components/event-notifications/event-notification-types';

const { EventDefinitionsActions } = CombinedProvider.get('EventDefinitions');
Expand Down
Expand Up @@ -11,7 +11,7 @@ import { Input } from 'components/bootstrap';
import FormsUtils from 'util/FormsUtils';
import AggregationExpressionParser from 'logic/alerts/AggregationExpressionParser';

import commonStyles from '../../common/commonStyles.css';
import commonStyles from '../common/commonStyles.css';

class AggregationForm extends React.Component {
static propTypes = {
Expand Down
Expand Up @@ -7,7 +7,7 @@ import FormsUtils from 'util/FormsUtils';
import FilterForm from './FilterForm';
import AggregationForm from './AggregationForm';

import commonStyles from '../../common/commonStyles.css';
import commonStyles from '../common/commonStyles.css';

const conditionTypes = {
AGGREGATION: 0,
Expand Down
Expand Up @@ -11,7 +11,7 @@ import { Input } from 'components/bootstrap';
import { naturalSortIgnoreCase } from 'util/SortUtils';
import FormsUtils from 'util/FormsUtils';

import commonStyles from '../../common/commonStyles.css';
import commonStyles from '../common/commonStyles.css';

export const TIME_UNITS = ['HOURS', 'MINUTES', 'SECONDS'];

Expand Down
@@ -0,0 +1,7 @@
:local(.definitionList) {
margin-top: 10px;
}

:local(.inline) {
display: inline-block;
}
Expand Up @@ -2,18 +2,35 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Button, Col, DropdownButton, MenuItem, Row } from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';
import lodash from 'lodash';
import { PluginStore } from 'graylog-web-plugin/plugin';
import moment from 'moment';
import {} from 'moment-duration-format';

import { NOTIFICATION_TYPE } from 'components/event-notifications/event-notification-types';

import Routes from 'routing/Routes';

import { EmptyEntity, EntityList, EntityListItem } from 'components/common';
import { EmptyEntity, EntityList, EntityListItem, PaginatedList, Pluralize, SearchForm } from 'components/common';

import styles from './EventDefinitions.css';

class EventDefinitions extends React.Component {
static propTypes = {
eventDefinitions: PropTypes.object.isRequired,
eventDefinitions: PropTypes.array.isRequired,
pagination: PropTypes.object.isRequired,
query: PropTypes.string.isRequired,
onPageChange: PropTypes.func.isRequired,
onQueryChange: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};

getConditionPlugin = (type) => {
if (type === undefined) {
return {};
}
return PluginStore.exports('eventDefinitionTypes').find(edt => edt.type === type);
};

renderEmptyContent = () => {
return (
<Row>
Expand All @@ -32,50 +49,101 @@ class EventDefinitions extends React.Component {
);
};

renderDescription = (definition) => {
let schedulingInformation = 'Not scheduled.';
if (definition.config.search_within_ms && definition.config.execute_every_ms) {
const executeEveryFormatted = moment.duration(definition.config.execute_every_ms)
.format('d [days] h [hours] m [minutes] s [seconds]', { trim: 'all', usePlural: false });
const searchWithinFormatted = moment.duration(definition.config.search_within_ms)
.format('d [days] h [hours] m [minutes] s [seconds]', { trim: 'all' });
schedulingInformation = `Runs every ${executeEveryFormatted}, searching within the last ${searchWithinFormatted}.`;
}

const notificationActions = definition.actions.filter(action => action.type === NOTIFICATION_TYPE);
let notificationsInformation = <span>Does <b>not</b> trigger any Notifications.</span>;
if (notificationActions.length > 0) {
notificationsInformation = (
<span>
Triggers {notificationActions.length}{' '}
<Pluralize singular="Notification" plural="Notifications" value={notificationActions.length} />.
</span>
);
}

return (
<React.Fragment>
<p>{definition.description}</p>
<p>{schedulingInformation} {notificationsInformation}</p>
</React.Fragment>
);
};

render() {
const { eventDefinitions, onDelete } = this.props;
const definitions = eventDefinitions.list;
const { eventDefinitions, pagination, query, onPageChange, onQueryChange, onDelete } = this.props;

if (eventDefinitions.list.length === 0) {
if (pagination.grandTotal === 0) {
return this.renderEmptyContent();
}

const items = definitions.map((definition) => {
const actions = [
<LinkContainer key={`edit-button-${definition.id}`}
to={Routes.NEXT_ALERTS.DEFINITIONS.edit(definition.id)}>
<Button bsStyle="info">Edit</Button>
</LinkContainer>,
<DropdownButton key={`actions-${definition.id}`} id="more-dropdown" title="More" pullRight>
<MenuItem onClick={onDelete(definition)}>Delete</MenuItem>
</DropdownButton>,
];

// TODO: Show something more useful ;)
const titleSuffix = lodash.get(definition, 'config.type', 'Not available')
.replace(/-v\d+/, '');
const items = eventDefinitions.map((definition) => {
const actions = (
<React.Fragment key={`actions-${definition.id}`}>
<LinkContainer to={Routes.NEXT_ALERTS.DEFINITIONS.edit(definition.id)}>
<Button bsStyle="info">Edit</Button>
</LinkContainer>
<DropdownButton id="more-dropdown" title="More" pullRight>
<MenuItem onClick={onDelete(definition)}>Delete</MenuItem>
</DropdownButton>
</React.Fragment>
);

const plugin = this.getConditionPlugin(definition.config.type);
const titleSuffix = plugin.displayName || definition.config.type;
return (
<EntityListItem key={`event-definition-${definition.id}`}
title={definition.title}
titleSuffix={`${titleSuffix}`}
description={definition.description}
titleSuffix={titleSuffix}
description={this.renderDescription(definition)}
noItemsText="Could not find any items with the given filter."
actions={actions} />
);
});

return (
<Row>
<Col md={12}>
<div className="pull-right">
<LinkContainer to={Routes.NEXT_ALERTS.DEFINITIONS.CREATE}>
<Button bsStyle="success">Create Event Definition</Button>
</LinkContainer>
</div>
</Col>
<Col md={12}>
<EntityList items={items} />
</Col>
</Row>
<React.Fragment>
<Row>
<Col md={12}>
<div className="pull-right">
<LinkContainer to={Routes.NEXT_ALERTS.DEFINITIONS.CREATE}>
<Button bsStyle="success">Create Event Definition</Button>
</LinkContainer>
</div>
</Col>
</Row>
<Row>
<Col md={12}>
<SearchForm query={query}
onSearch={onQueryChange}
onReset={onQueryChange}
searchButtonLabel="Find"
placeholder="Find Event Definitions"
wrapperClass={styles.inline}
queryWidth={200}
topMargin={0}
useLoadingState />

<PaginatedList activePage={pagination.page}
pageSize={pagination.pageSize}
pageSizes={[10, 25, 50]}
totalItems={pagination.total}
onChange={onPageChange}>
<div className={styles.definitionList}>
<EntityList items={items} />
</div>
</PaginatedList>
</Col>
</Row>
</React.Fragment>
);
}
}
Expand Down
@@ -1,8 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';

import { Spinner } from 'components/common';

import connect from 'stores/connect';
import CombinedProvider from 'injection/CombinedProvider';

import {} from 'components/event-definitions/event-definition-types';

import EventDefinitions from './EventDefinitions';

const { EventDefinitionsStore, EventDefinitionsActions } = CombinedProvider.get('EventDefinitions');
Expand All @@ -13,9 +18,28 @@ class EventDefinitionsContainer extends React.Component {
};

componentDidMount() {
EventDefinitionsActions.list();
this.fetchData({});
}

fetchData = ({ page, pageSize, query }) => {
return EventDefinitionsActions.listPaginated({
query: query,
page: page,
pageSize: pageSize,
});
};

handlePageChange = (nextPage, nextPageSize) => {
const { eventDefinitions } = this.props;
this.fetchData({ page: nextPage, pageSize: nextPageSize, query: eventDefinitions.query });
};

handleQueryChange = (nextQuery, callback = () => {}) => {
const { eventDefinitions } = this.props;
const promise = this.fetchData({ query: nextQuery, pageSize: eventDefinitions.pagination.pageSize });
promise.finally(callback);
};

handleDelete = (definition) => {
return () => {
if (window.confirm(`Are you sure you want to delete "${definition.title}"?`)) {
Expand All @@ -26,8 +50,18 @@ class EventDefinitionsContainer extends React.Component {

render() {
const { eventDefinitions } = this.props;

if (!eventDefinitions.eventDefinitions) {
return <Spinner text="Loading Event Definitions information..." />;
}

return (
<EventDefinitions eventDefinitions={eventDefinitions} onDelete={this.handleDelete} />
<EventDefinitions eventDefinitions={eventDefinitions.eventDefinitions}
pagination={eventDefinitions.pagination}
query={eventDefinitions.query}
onPageChange={this.handlePageChange}
onQueryChange={this.handleQueryChange}
onDelete={this.handleDelete} />
);
}
}
Expand Down

0 comments on commit fc84262

Please sign in to comment.