Merge pull request #458 from prymitive/grid-sort-config

feat(backend): allow configuring default grid sort options
This commit is contained in:
Łukasz Mierzwa
2019-02-20 22:58:10 +00:00
committed by GitHub
16 changed files with 212 additions and 78 deletions

View File

@@ -17,6 +17,11 @@ custom:
filters:
default:
- "@receiver=by-cluster-service"
grid:
sorting:
order: label
reverse: false
label: severity
labels:
color:
static:

View File

@@ -245,6 +245,61 @@ filters:
default: []
```
### Grid
`grid` section allows customizing how alert grid is rendered in the UI.
Sorting configuration can be overridden by each user via UI settings.
Syntax:
```YAML
grid:
sorting:
order: string
reverse: bool
label: string
```
- `sorting:order` - default sort order for alert grid, valid values are:
- `disabled` - no sorting, alert groups are rendered in the order they are
returned by the API
- `startsAt` - sort by alert timestamps, most recent alert in each group will
be used when comparing each group
- `label` - sort by labels, if the label used for sorting is not shared by
all alerts in a group then the first alert in the group will be queried for
it
- `sorting:reverse` - default value for reversed sort order
- `sorting:label` - label name for sorting when `grid:sorting:order` is set
to `label`. Labels can be assigned custom values used only by sorting via
`labels:sorting:valueMapping`, see [Labels](#Labels) section for details.
Defaults:
```YAML
grid:
sorting:
order: startsAt
reverse: true
label: alertname
```
Example with sorting using `severity` label, with extra option to map severity
labels to numeric values for sorting:
```YAML
grid:
sorting:
order: label
reverse: false
label: severity
labels:
sorting:
valueMapping:
severity:
critical: 1
warning: 2
info: 3
```
### Labels
`labels` section allows configuring how alert labels will be rendered in the
@@ -293,6 +348,8 @@ labels:
To allow for more natural sorting `sorting:valueMapping` can be used to
map label values to integer values which will be used for sorting instead
of original string values.
Note: this option is not available via environment variables, you can only set
it via the config file.
Example with static color for the `job` label (every `job` label will have the
same color regardless of the value) and unique color for the `@receiver` label

View File

@@ -7,6 +7,7 @@ import (
"strings"
"time"
"github.com/prymitive/karma/internal/slices"
"github.com/prymitive/karma/internal/uri"
"github.com/spf13/pflag"
"github.com/spf13/viper"
@@ -60,6 +61,10 @@ func init() {
"List of labels to keep, all other labels will be stripped")
pflag.StringSlice("labels.strip", []string{}, "List of labels to ignore")
pflag.String("grid.sorting.order", "startsAt", "Default sort order for alert grid")
pflag.Bool("grid.sorting.reverse", true, "Reverse sort order")
pflag.String("grid.sorting.label", "alertname", "Label name to use when sorting alert grid by label")
pflag.Bool("log.config", true, "Log used configuration to log on startup")
pflag.String("log.level", "info",
"Log level, one of: debug, info, warning, error, fatal and panic")
@@ -137,6 +142,9 @@ func (config *configSchema) Read() {
config.Custom.JS = v.GetString("custom.js")
config.Debug = v.GetBool("debug")
config.Filters.Default = v.GetStringSlice("filters.default")
config.Grid.Sorting.Order = v.GetString("grid.sorting.order")
config.Grid.Sorting.Reverse = v.GetBool("grid.sorting.reverse")
config.Grid.Sorting.Label = v.GetString("grid.sorting.label")
config.Labels.Color.Custom = map[string]map[string]string{}
config.Labels.Color.Static = v.GetStringSlice("labels.color.static")
config.Labels.Color.Unique = v.GetStringSlice("labels.color.unique")
@@ -172,6 +180,10 @@ func (config *configSchema) Read() {
log.Fatal(err)
}
if !slices.StringInSlice([]string{"disabled", "startsAt", "label"}, config.Grid.Sorting.Order) {
log.Fatalf("Invalid grid.sorting.order value '%s', allowed options: disabled, startsAt, label", config.Grid.Sorting.Order)
}
// accept single Alertmanager server from flag/env if nothing is set yet
if len(config.Alertmanager.Servers) == 0 && v.GetString("alertmanager.uri") != "" {
log.Info("Using simple config with a single Alertmanager server")

View File

@@ -82,6 +82,11 @@ filters:
default:
- '@state=active'
- foo=bar
grid:
sorting:
order: startsAt
reverse: true
label: alertname
labels:
keep:
- foo

View File

@@ -43,6 +43,13 @@ type configSchema struct {
Filters struct {
Default []string
}
Grid struct {
Sorting struct {
Order string
Reverse bool
Label string
}
}
Labels struct {
Keep []string
Strip []string

View File

@@ -155,8 +155,16 @@ func (ag *APIAlertGroup) DedupSharedMaps() {
}
}
// GridSettings exposes all grid settings from the config file
type GridSettings struct {
Order string `json:"order"`
Reverse bool `json:"reverse"`
Label string `json:"label"`
}
// SortSettings nests all settings specific to sorting
type SortSettings struct {
Grid GridSettings `json:"grid"`
ValueMapping map[string]map[string]int `json:"valueMapping"`
}

View File

@@ -72,37 +72,48 @@ const AlertGrid = observer(
compare = (a, b) => {
const { alertStore, settingsStore } = this.props;
// don't sort if sorting is disabled
if (
const useDefaults =
settingsStore.gridConfig.config.sortOrder ===
settingsStore.gridConfig.options.disabled.value
)
settingsStore.gridConfig.options.default.value;
const sortOrder = useDefaults
? alertStore.settings.values.sorting.grid.order
: settingsStore.gridConfig.config.sortOrder;
// don't sort if sorting is disabled
if (sortOrder === settingsStore.gridConfig.options.disabled.value)
return 0;
const sortReverse =
useDefaults || settingsStore.gridConfig.config.reverseSort === undefined
? alertStore.settings.values.sorting.grid.reverse
: settingsStore.gridConfig.config.reverseSort;
const sortLabel =
useDefaults || settingsStore.gridConfig.config.sortLabel === undefined
? alertStore.settings.values.sorting.grid.label
: settingsStore.gridConfig.config.sortLabel;
const getLabelValue = g => {
// if timestamp sort is enabled use latest alert for sorting
if (
settingsStore.gridConfig.config.sortOrder ===
settingsStore.gridConfig.options.startsAt.value
) {
if (sortOrder === settingsStore.gridConfig.options.startsAt.value) {
return moment.max(g.alerts.map(a => moment(a.startsAt)));
}
const labelName = settingsStore.gridConfig.config.sortLabel;
const labelValue =
g.labels[labelName] ||
g.shared.labels[labelName] ||
g.alerts[0].labels[labelName];
g.labels[sortLabel] ||
g.shared.labels[sortLabel] ||
g.alerts[0].labels[sortLabel];
let mappedValue;
// check if we have a mapping for label value
if (
labelValue !== undefined &&
alertStore.settings.values.sorting.valueMapping[labelName] !==
alertStore.settings.values.sorting.valueMapping[sortLabel] !==
undefined
) {
mappedValue =
alertStore.settings.values.sorting.valueMapping[labelName][
alertStore.settings.values.sorting.valueMapping[sortLabel][
labelValue
];
}
@@ -111,7 +122,7 @@ const AlertGrid = observer(
return mappedValue !== undefined ? mappedValue : labelValue;
};
const val = settingsStore.gridConfig.config.reverseSort ? -1 : 1;
const val = sortReverse ? -1 : 1;
const av = getLabelValue(a);
const bv = getLabelValue(b);

View File

@@ -49,13 +49,19 @@ const AlertGroupSortConfiguration = observer(
.includes(settingsStore.gridConfig.config.sortOrder)
) {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.defaults.sortOrder;
settingsStore.gridConfig.options.default.value;
}
});
render() {
const { settingsStore } = this.props;
const hideReverse =
settingsStore.gridConfig.config.sortOrder ===
settingsStore.gridConfig.options.default.value ||
settingsStore.gridConfig.config.sortOrder ===
settingsStore.gridConfig.options.disabled.value;
return (
<div className="form-group">
<div className="text-center">
@@ -81,24 +87,28 @@ const AlertGroupSortConfiguration = observer(
<SortLabelName settingsStore={settingsStore} />
</div>
) : null}
<div className="flex-shrink-1 flex-grow-0 form-check form-check-inline flex-basis-auto mt-1 mt-lg-0 ml-0 ml-lg-1 mr-0">
<span className="custom-control custom-switch">
<input
id="configuration-sort-reverse"
className="custom-control-input"
type="checkbox"
value=""
checked={settingsStore.gridConfig.config.reverseSort}
onChange={this.onSortReverseChange}
/>
<label
className="custom-control-label cursor-pointer mr-3"
htmlFor="configuration-sort-reverse"
>
Reverse
</label>
</span>
</div>
{hideReverse ? null : (
<div className="flex-shrink-1 flex-grow-0 form-check form-check-inline flex-basis-auto mt-1 mt-lg-0 ml-0 ml-lg-1 mr-0">
<span className="custom-control custom-switch">
<input
id="configuration-sort-reverse"
className="custom-control-input"
type="checkbox"
value=""
checked={
settingsStore.gridConfig.config.reverseSort || false
}
onChange={this.onSortReverseChange}
/>
<label
className="custom-control-label cursor-pointer mr-3"
htmlFor="configuration-sort-reverse"
>
Reverse
</label>
</span>
</div>
)}
</div>
</div>
);

View File

@@ -49,7 +49,7 @@ describe("<AlertGroupSortConfiguration />", () => {
FakeConfiguration();
setTimeout(() => {
expect(settingsStore.gridConfig.config.sortOrder).toBe(
settingsStore.gridConfig.defaults.sortOrder
settingsStore.gridConfig.options.default.value
);
done();
}, 200);
@@ -74,6 +74,38 @@ describe("<AlertGroupSortConfiguration />", () => {
}, 200);
});
it("reverse checkbox is not rendered when sort order is == 'default'", () => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.default.value;
const tree = FakeConfiguration();
const labelSelect = tree.find("#configuration-sort-reverse");
expect(labelSelect).toHaveLength(0);
});
it("reverse checkbox is not rendered when sort order is == 'disabled'", () => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.disabled.value;
const tree = FakeConfiguration();
const labelSelect = tree.find("#configuration-sort-reverse");
expect(labelSelect).toHaveLength(0);
});
it("reverse checkbox is rendered when sort order is = 'startsAt'", () => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.startsAt.value;
const tree = FakeConfiguration();
const labelSelect = tree.find("#configuration-sort-reverse");
expect(labelSelect).toHaveLength(1);
});
it("reverse checkbox is rendered when sort order is = 'label'", () => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.label.value;
const tree = FakeConfiguration();
const labelSelect = tree.find("#configuration-sort-reverse");
expect(labelSelect).toHaveLength(1);
});
it("label select is not rendered when sort order is != 'label'", () => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.disabled.value;

View File

@@ -6,6 +6,7 @@ import { observer } from "mobx-react";
import CreatableSelect from "react-select/lib/Creatable";
import { StaticLabels } from "Common/Query";
import { FormatBackendURI } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { ReactSelectStyles } from "Components/MultiSelect";
@@ -18,6 +19,15 @@ const SortLabelName = observer(
settingsStore: PropTypes.instanceOf(Settings).isRequired
};
constructor(props) {
super(props);
if (!props.settingsStore.gridConfig.config.sortLabel) {
props.settingsStore.gridConfig.config.sortLabel =
StaticLabels.AlertName;
}
}
suggestions = observable({
names: []
});

View File

@@ -14,7 +14,7 @@ exports[`<AlertGroupSortConfiguration /> matches snapshot with default values 1`
<div class=\\"css-7jxtyj react-select__control\\">
<div class=\\"css-pb81dw react-select__value-container react-select__value-container--has-value\\">
<div class=\\"css-xp4uvy react-select__single-value\\">
Sort by alert timestamp
Use defaults from karma config file
</div>
<div class=\\"css-1g6gooi\\">
<div class=\\"react-select__input\\"
@@ -57,21 +57,6 @@ exports[`<AlertGroupSortConfiguration /> matches snapshot with default values 1`
</div>
</div>
</div>
<div class=\\"flex-shrink-1 flex-grow-0 form-check form-check-inline flex-basis-auto mt-1 mt-lg-0 ml-0 ml-lg-1 mr-0\\">
<span class=\\"custom-control custom-switch\\">
<input id=\\"configuration-sort-reverse\\"
class=\\"custom-control-input\\"
type=\\"checkbox\\"
value
checked
>
<label class=\\"custom-control-label cursor-pointer mr-3\\"
for=\\"configuration-sort-reverse\\"
>
Reverse
</label>
</span>
</div>
</div>
</div>
"

View File

@@ -124,7 +124,7 @@ exports[`<MainModalContent /> matches snapshot 1`] = `
<div class=\\"css-7jxtyj react-select__control\\">
<div class=\\"css-pb81dw react-select__value-container react-select__value-container--has-value\\">
<div class=\\"css-xp4uvy react-select__single-value\\">
Sort by alert timestamp
Use defaults from karma config file
</div>
<div class=\\"css-1g6gooi\\">
<div class=\\"react-select__input\\"
@@ -167,21 +167,6 @@ exports[`<MainModalContent /> matches snapshot 1`] = `
</div>
</div>
</div>
<div class=\\"flex-shrink-1 flex-grow-0 form-check form-check-inline flex-basis-auto mt-1 mt-lg-0 ml-0 ml-lg-1 mr-0\\">
<span class=\\"custom-control custom-switch\\">
<input id=\\"configuration-sort-reverse\\"
class=\\"custom-control-input\\"
type=\\"checkbox\\"
value
checked
>
<label class=\\"custom-control-label cursor-pointer mr-3\\"
for=\\"configuration-sort-reverse\\"
>
Reverse
</label>
</span>
</div>
</div>
</div>
</form>

View File

@@ -171,6 +171,11 @@ class AlertStore {
annotationsHidden: [],
annotationsVisible: [],
sorting: {
grid: {
order: "startsAt",
reverse: false,
label: "alertname"
},
valueMapping: {}
}
}

View File

@@ -1,8 +1,6 @@
import { action } from "mobx";
import { localStored } from "mobx-stored";
import { StaticLabels } from "Common/Query";
class SavedFilters {
config = localStored(
"savedFilters",
@@ -58,21 +56,15 @@ class SilenceFormConfig {
class GridConfig {
options = Object.freeze({
default: { label: "Use defaults from karma config file", value: "default" },
disabled: { label: "No sorting", value: "disabled" },
startsAt: { label: "Sort by alert timestamp", value: "startsAt" },
label: { label: "Sort by alert label", value: "label" }
});
defaults = {
sortOrder: this.options.startsAt.value,
reverseSort: true,
sortLabel: StaticLabels.AlertName
};
config = localStored(
"gridConfig",
"alertGridConfig",
{
sortOrder: this.defaults.sortOrder,
reverseSort: this.defaults.reverseSort,
sortLabel: this.defaults.sortLabel
sortOrder: this.options.default.value
},
{ delay: 100 }
);

View File

@@ -24,6 +24,11 @@ const EmptyAPIResponse = () => ({
],
settings: {
sorting: {
grid: {
order: "startsAt",
reverse: false,
label: "alertname"
},
valueMapping: {
cluster: {
dev: 3,

View File

@@ -84,6 +84,11 @@ func alerts(c *gin.Context) {
resp.Upstreams = getUpstreams()
resp.Settings = models.Settings{
Sorting: models.SortSettings{
Grid: models.GridSettings{
Order: config.Config.Grid.Sorting.Order,
Reverse: config.Config.Grid.Sorting.Reverse,
Label: config.Config.Grid.Sorting.Label,
},
ValueMapping: map[string]map[string]int{},
},
StaticColorLabels: config.Config.Labels.Color.Static,