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

Implement QuickValue improvements #4205

Merged
merged 12 commits into from
Oct 5, 2017
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,24 @@
*/
package org.graylog2.dashboards.widgets.strategies;

import com.google.common.base.Splitter;
import com.google.common.collect.Maps;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import org.graylog2.dashboards.widgets.InvalidWidgetConfigurationException;
import org.graylog2.indexer.results.TermsResult;
import org.graylog2.indexer.searches.Searches;
import org.graylog2.indexer.searches.Sorting;
import org.graylog2.plugin.dashboards.widgets.ComputationResult;
import org.graylog2.plugin.dashboards.widgets.WidgetStrategy;
import org.graylog2.plugin.indexer.searches.timeranges.TimeRange;

import javax.annotation.Nullable;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Strings.isNullOrEmpty;

public class QuickvaluesWidgetStrategy implements WidgetStrategy {
Expand All @@ -45,6 +50,9 @@ public interface Factory extends WidgetStrategy.Factory<QuickvaluesWidgetStrateg
private final String field;
private final Searches searches;
private final TimeRange timeRange;
private final String sortOrder;
private final int dataTableLimit;
private final List<String> stackedFields;

@AssistedInject
public QuickvaluesWidgetStrategy(Searches searches, @Assisted Map<String, Object> config, @Assisted TimeRange timeRange, @Assisted String widgetId) throws InvalidWidgetConfigurationException {
Expand All @@ -59,6 +67,27 @@ public QuickvaluesWidgetStrategy(Searches searches, @Assisted Map<String, Object

this.field = (String) config.get("field");
this.streamId = (String) config.get("stream_id");

this.sortOrder = (String) firstNonNull(config.get("sort_order"), "desc");
this.dataTableLimit = (int) firstNonNull(config.get("data_table_limit"), 50);
this.stackedFields = getStackedFields(config.get("stacked_fields"));
}

private static List<String> getStackedFields(@Nullable Object value) {
final String stackedFieldsString = (String) firstNonNull(value, "");
return Splitter.on(',').trimResults().omitEmptyStrings().splitToList(stackedFieldsString);
}

private static Sorting.Direction getSortingDirection(String sort) {
if (isNullOrEmpty(sort)) {
return Sorting.Direction.DESC;
}

try {
return Sorting.Direction.valueOf(sort.toUpperCase(Locale.ENGLISH));
} catch (Exception e) {
return Sorting.Direction.DESC;
}
}

@Override
Expand All @@ -68,7 +97,8 @@ public ComputationResult compute() {
filter = "streams:" + streamId;
}

final TermsResult terms = searches.terms(field, 50, query, filter, this.timeRange);
final Sorting.Direction sortDirection = getSortingDirection(sortOrder);
final TermsResult terms = searches.terms(field, stackedFields, dataTableLimit, query, filter, this.timeRange, sortDirection);

Map<String, Object> result = Maps.newHashMap();
result.put("terms", terms.getTerms());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramBuilder;
Expand Down Expand Up @@ -76,6 +78,7 @@
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -274,15 +277,37 @@ public SearchResult search(SearchesConfig config) {
return new SearchResult(hits, searchResult.getTotal(), indexRanges, config.query(), requestBuilder.toString(), tookMsFromSearchResult(searchResult));
}

public TermsResult terms(String field, int size, String query, String filter, TimeRange range, Sorting.Direction sorting) {
public TermsResult terms(String field, List<String> stackedFields, int size, String query, String filter, TimeRange range, Sorting.Direction sorting) {
final Terms.Order termsOrder = sorting == Sorting.Direction.DESC ? Terms.Order.count(false) : Terms.Order.count(true);

final SearchSourceBuilder searchSourceBuilder = filteredSearchRequest(query, filter, range)
.aggregation(AggregationBuilders.terms(AGG_TERMS)
.field(field)
.size(size > 0 ? size : 50)
.order(termsOrder))
.aggregation(AggregationBuilders.missing("missing")
final SearchSourceBuilder searchSourceBuilder = filteredSearchRequest(query, filter, range);

if (stackedFields.isEmpty()) {
searchSourceBuilder.aggregation(AggregationBuilders.terms(AGG_TERMS)
.field(field)
.size(size > 0 ? size : 50)
.order(termsOrder));
} else {
// If the methods gets stacked fields, we have to use scripting to concatenate the fields.
// There is currently no other way to do this. (as of ES 5.6)
final StringBuilder scriptStringBuilder = new StringBuilder();

// Add the main field
scriptStringBuilder.append("doc['").append(field).append("'].value");

// Add all other fields
stackedFields.forEach(f -> {
scriptStringBuilder.append(" + ' - ' + ");
scriptStringBuilder.append("doc['").append(f).append("'].value");
});

searchSourceBuilder.aggregation(AggregationBuilders.terms(AGG_TERMS)
.script(new Script(scriptStringBuilder.toString(), ScriptService.ScriptType.INLINE, "painless", null))
.size(size > 0 ? size : 50)
.order(termsOrder));
}

searchSourceBuilder.aggregation(AggregationBuilders.missing("missing")
.field(field));

final Set<String> affectedIndices = determineAffectedIndices(range, filter);
Expand Down Expand Up @@ -311,6 +336,10 @@ public TermsResult terms(String field, int size, String query, String filter, Ti
);
}

public TermsResult terms(String field, int size, String query, String filter, TimeRange range, Sorting.Direction sorting) {
return terms(field, Collections.emptyList(), size, query, filter, range, sorting);
}

public TermsResult terms(String field, int size, String query, String filter, TimeRange range) {
return terms(field, size, query, filter, range, Sorting.Direction.DESC);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,17 @@ public TermsResult termsAbsolute(
@QueryParam("field") @NotEmpty String field,
@ApiParam(name = "query", value = "Query (Lucene syntax)", required = true)
@QueryParam("query") @NotEmpty String query,
@ApiParam(name = "stacked_fields", value = "Fields to stack", required = false) @QueryParam("stacked_fields") String stackedFieldsParam,
@ApiParam(name = "size", value = "Maximum number of terms to return", required = false) @QueryParam("size") int size,
@ApiParam(name = "from", value = "Timerange start. See search method description for date format", required = true) @QueryParam("from") String from,
@ApiParam(name = "to", value = "Timerange end. See search method description for date format", required = true) @QueryParam("to") String to,
@ApiParam(name = "filter", value = "Filter", required = false) @QueryParam("filter") String filter) {
@ApiParam(name = "filter", value = "Filter", required = false) @QueryParam("filter") String filter,
@ApiParam(name = "order", value = "Sorting (field:asc / field:desc)", required = false) @QueryParam("order") String order) {
checkSearchPermission(filter, RestPermissions.SEARCHES_ABSOLUTE);

return buildTermsResult(searches.terms(field, size, query, filter, buildAbsoluteTimeRange(from, to)));
final List<String> stackedFields = splitStackedFields(stackedFieldsParam);
final Sorting sortOrder = buildSorting(order);
return buildTermsResult(searches.terms(field, stackedFields, size, query, filter, buildAbsoluteTimeRange(from, to), sortOrder.getDirection()));
}

@GET
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,16 @@ public TermsResult termsKeyword(
@QueryParam("field") @NotEmpty String field,
@ApiParam(name = "query", value = "Query (Lucene syntax)", required = true)
@QueryParam("query") @NotEmpty String query,
@ApiParam(name = "stacked_fields", value = "Fields to stack", required = false) @QueryParam("stacked_fields") String stackedFieldsParam,
@ApiParam(name = "size", value = "Maximum number of terms to return", required = false) @QueryParam("size") int size,
@ApiParam(name = "keyword", value = "Range keyword", required = true) @QueryParam("keyword") String keyword,
@ApiParam(name = "filter", value = "Filter", required = false) @QueryParam("filter") String filter) {
@ApiParam(name = "filter", value = "Filter", required = false) @QueryParam("filter") String filter,
@ApiParam(name = "order", value = "Sorting (field:asc / field:desc)", required = false) @QueryParam("order") String order) {
checkSearchPermission(filter, RestPermissions.SEARCHES_KEYWORD);

return buildTermsResult(searches.terms(field, size, query, filter, buildKeywordTimeRange(keyword)));
final List<String> stackedFields = splitStackedFields(stackedFieldsParam);
final Sorting sortOrder = buildSorting(order);
return buildTermsResult(searches.terms(field, stackedFields, size, query, filter, buildKeywordTimeRange(keyword), sortOrder.getDirection()));
}

@GET
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,16 @@ public TermsResult termsRelative(
@QueryParam("field") @NotEmpty String field,
@ApiParam(name = "query", value = "Query (Lucene syntax)", required = true)
@QueryParam("query") @NotEmpty String query,
@ApiParam(name = "stacked_fields", value = "Fields to stack", required = false) @QueryParam("stacked_fields") String stackedFieldsParam,
@ApiParam(name = "size", value = "Maximum number of terms to return", required = false) @QueryParam("size") int size,
@ApiParam(name = "range", value = "Relative timeframe to search in. See search method description.", required = true) @QueryParam("range") int range,
@ApiParam(name = "filter", value = "Filter", required = false) @QueryParam("filter") String filter,
@ApiParam(name = "order", value = "Sorting (field:asc / field:desc)", required = false) @QueryParam("order") String order) {
checkSearchPermission(filter, RestPermissions.SEARCHES_RELATIVE);

final List<String> stackedFields = splitStackedFields(stackedFieldsParam);
final Sorting sortOrder = buildSorting(order);
return buildTermsResult(searches.terms(field, size, query, filter, buildRelativeTimeRange(range), sortOrder.getDirection()));
return buildTermsResult(searches.terms(field, stackedFields, size, query, filter, buildRelativeTimeRange(range), sortOrder.getDirection()));
}

@GET
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import javax.ws.rs.ForbiddenException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
Expand Down Expand Up @@ -331,4 +332,10 @@ protected org.graylog2.plugin.indexer.searches.timeranges.TimeRange restrictTime
return AbsoluteRange.create(from, to);
}

protected List<String> splitStackedFields(String stackedFieldsParam) {
if (stackedFieldsParam == null || stackedFieldsParam.isEmpty()) {
return Collections.emptyList();
}
return Splitter.on(',').trimResults().omitEmptyStrings().splitToList(stackedFieldsParam);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Reflux from 'reflux';

const FieldQuickValuesActions = Reflux.createActions({
get: { asyncResult: true },
});

export default FieldQuickValuesActions;
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';
import { Button } from 'react-bootstrap';
import { DropdownButton, MenuItem } from 'react-bootstrap';
import Reflux from 'reflux';

import QuickValuesVisualization from 'components/visualizations/QuickValuesVisualization';
import AddToDashboardMenu from 'components/dashboard/AddToDashboardMenu';
import Spinner from 'components/common/Spinner';
import UIUtils from 'util/UIUtils';
import QuickValuesOptionsForm from './QuickValuesOptionsForm';

import StoreProvider from 'injection/StoreProvider';
const FieldQuickValuesStore = StoreProvider.getStore('FieldQuickValues');
const RefreshStore = StoreProvider.getStore('Refresh');
import CombinedProvider from 'injection/CombinedProvider';

const { FieldQuickValuesStore, FieldQuickValuesActions } = CombinedProvider.get('FieldQuickValues');
const { RefreshStore } = CombinedProvider.get('Refresh');

const FieldQuickValues = React.createClass({
propTypes: {
Expand All @@ -21,12 +23,27 @@ const FieldQuickValues = React.createClass({
rangeParams: PropTypes.object.isRequired,
stream: PropTypes.object,
forceFetch: PropTypes.bool,
fields: PropTypes.arrayOf(PropTypes.object),
},
mixins: [Reflux.listenTo(RefreshStore, '_setupTimer', '_setupTimer'), Reflux.connect(FieldQuickValuesStore)],

getDefaultProps() {
return {
fields: [],
};
},
mixins: [Reflux.listenTo(RefreshStore, '_setupTimer', '_setupTimer')],

getInitialState() {
return {
field: undefined,
data: [],
showVizOptions: false,
options: {
order: 'desc',
limit: 5,
tableSize: 50,
stackedFields: '',
},
};
},

Expand Down Expand Up @@ -71,28 +88,75 @@ const FieldQuickValues = React.createClass({
},
_loadQuickValuesData() {
if (this.state.field !== undefined) {
this.setState({ loadPending: true });
const promise = FieldQuickValuesStore.getQuickValues(this.state.field);
promise.then(data => this.setState({ data: data, loadPending: false }));
FieldQuickValuesActions.get(this.state.field, this.state.options);
}
},
_resetStatus() {
this.setState(this.getInitialState());
},

_onVizOptionsChange(newOptions) {
this.setState({ options: newOptions, showVizOptions: false }, () => this._loadQuickValuesData());
},

_onVizOptionsCancel() {
this.setState({ showVizOptions: false });
},

_showVizOptions() {
this.setState({ showVizOptions: true });
},

_buildDashboardConfig() {
// Map internal state fields to widget config fields. (snake case vs. camel case)
return {
field: this.state.field,
limit: this.state.options.limit,
sort_order: this.state.options.order,
data_table_limit: this.state.options.tableSize,
stacked_fields: this.state.options.stackedFields,
};
},

render() {
let content;

let inner;
if (this.state.data.length === 0) {
inner = <Spinner />;
if (this.state.showVizOptions) {
inner = (
<div style={{ marginTop: 10 }}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add a CSS file to handle all these inline styles, please? :)

<QuickValuesOptionsForm limit={this.state.options.limit}
tableSize={this.state.options.tableSize}
order={this.state.options.order}
stackedFields={this.state.options.stackedFields}
stackedFieldsOptions={this.props.fields}
onSave={this._onVizOptionsChange}
onCancel={this._onVizOptionsCancel} />
</div>
);
} else if (this.state.data.length === 0) {
inner = (
<div style={{ maxHeight: 400, marginTop: 10 }}>
<Spinner />;
</div>
);
} else {
const config = {
show_pie_chart: true,
show_data_table: true,
data_table_limit: this.state.options.tableSize,
sort_order: this.state.options.order,
limit: this.state.options.limit,
};
inner = (
<QuickValuesVisualization id={this.state.field}
config={{ show_pie_chart: true, show_data_table: true }}
data={this.state.data}
horizontal
displayAddToSearchButton
displayAnalysisInformation />
<div style={{ maxHeight: 400, overflowY: 'auto', marginTop: 10 }}>
<QuickValuesVisualization id={this.state.field}
config={config}
data={this.state.data}
horizontal
displayAddToSearchButton
displayAnalysisInformation />
</div>
);
}

Expand All @@ -102,17 +166,24 @@ const FieldQuickValues = React.createClass({
<div className="pull-right">
<AddToDashboardMenu title="Add to dashboard"
widgetType={this.WIDGET_TYPE}
configuration={{ field: this.state.field }}
configuration={this._buildDashboardConfig()}
bsStyle="default"
pullRight
permissions={this.props.permissions}>
<Button bsSize="small" onClick={() => this._resetStatus()}>Dismiss</Button>
<DropdownButton bsSize="small"
className="graph-settings"
title="Customize"
id="customize-field-graph-dropdown">
<MenuItem onSelect={this._showVizOptions}>Configuration</MenuItem>
<MenuItem divider />
<MenuItem onSelect={() => this._resetStatus()}>Dismiss</MenuItem>
</DropdownButton>
</AddToDashboardMenu>
</div>
<h1>Quick Values for {this.state.field} {this.state.loadPending && <i
<h1>Quick Values for <em>{this.state.field}</em> {this.state.loadPending && <i
className="fa fa-spin fa-spinner" />}</h1>

<div style={{ maxHeight: 400, overflow: 'auto', marginTop: 10 }}>{inner}</div>
{inner}
</div>
);
}
Expand Down
Loading