mirror of
https://github.com/prymitive/karma
synced 2026-05-19 04:26:41 +00:00
Merge pull request #458 from prymitive/grid-sort-config
feat(backend): allow configuring default grid sort options
This commit is contained in:
@@ -17,6 +17,11 @@ custom:
|
||||
filters:
|
||||
default:
|
||||
- "@receiver=by-cluster-service"
|
||||
grid:
|
||||
sorting:
|
||||
order: label
|
||||
reverse: false
|
||||
label: severity
|
||||
labels:
|
||||
color:
|
||||
static:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -82,6 +82,11 @@ filters:
|
||||
default:
|
||||
- '@state=active'
|
||||
- foo=bar
|
||||
grid:
|
||||
sorting:
|
||||
order: startsAt
|
||||
reverse: true
|
||||
label: alertname
|
||||
labels:
|
||||
keep:
|
||||
- foo
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: []
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -171,6 +171,11 @@ class AlertStore {
|
||||
annotationsHidden: [],
|
||||
annotationsVisible: [],
|
||||
sorting: {
|
||||
grid: {
|
||||
order: "startsAt",
|
||||
reverse: false,
|
||||
label: "alertname"
|
||||
},
|
||||
valueMapping: {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
|
||||
@@ -24,6 +24,11 @@ const EmptyAPIResponse = () => ({
|
||||
],
|
||||
settings: {
|
||||
sorting: {
|
||||
grid: {
|
||||
order: "startsAt",
|
||||
reverse: false,
|
||||
label: "alertname"
|
||||
},
|
||||
valueMapping: {
|
||||
cluster: {
|
||||
dev: 3,
|
||||
|
||||
5
views.go
5
views.go
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user