From aa657ea21675a1f51eeb63a0fe0a228867c5bb50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Wed, 20 Feb 2019 20:59:54 +0000 Subject: [PATCH 1/3] feat(backend): allow configuring default grid sort options This will allow UI to get the defaults for grid sorting via the API --- docs/CONFIGURATION.md | 57 ++++++++++++++++++++++++++++++++++ internal/config/config.go | 12 +++++++ internal/config/config_test.go | 5 +++ internal/config/models.go | 7 +++++ internal/models/api.go | 8 +++++ views.go | 5 +++ 6 files changed, 94 insertions(+) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 868700096..7685293bd 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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 diff --git a/internal/config/config.go b/internal/config/config.go index 38afd7a5c..ec42b1b71 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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") diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 034dfaf94..7900d7045 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -82,6 +82,11 @@ filters: default: - '@state=active' - foo=bar +grid: + sorting: + order: startsAt + reverse: true + label: alertname labels: keep: - foo diff --git a/internal/config/models.go b/internal/config/models.go index 2b3302555..f810796f6 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -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 diff --git a/internal/models/api.go b/internal/models/api.go index 60ce14de8..fad016380 100644 --- a/internal/models/api.go +++ b/internal/models/api.go @@ -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"` } diff --git a/views.go b/views.go index c54b5fdbd..d0838f8db 100644 --- a/views.go +++ b/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, From 3bd6ebbf3a79787723225d8ab770666478b530a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Wed, 20 Feb 2019 21:00:16 +0000 Subject: [PATCH 2/3] feat(ui): use grid sorting settings from the API as defaults --- ui/src/Components/Grid/AlertGrid/index.js | 41 ++++++++++------ .../AlertGroupSortConfiguration.js | 48 +++++++++++-------- .../AlertGroupSortConfiguration.test.js | 34 ++++++++++++- .../MainModal/Configuration/SortLabelName.js | 10 ++++ .../AlertGroupSortConfiguration.test.js.snap | 17 +------ .../MainModalContent.test.js.snap | 17 +------ ui/src/Stores/AlertStore.js | 5 ++ ui/src/Stores/Settings.js | 14 ++---- ui/src/__mocks__/Fetch.js | 5 ++ 9 files changed, 113 insertions(+), 78 deletions(-) diff --git a/ui/src/Components/Grid/AlertGrid/index.js b/ui/src/Components/Grid/AlertGrid/index.js index 85ec39c6f..7e11d0db0 100644 --- a/ui/src/Components/Grid/AlertGrid/index.js +++ b/ui/src/Components/Grid/AlertGrid/index.js @@ -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); diff --git a/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.js b/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.js index 9938a744c..2dd593cc4 100644 --- a/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.js +++ b/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.js @@ -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 (
@@ -81,24 +87,28 @@ const AlertGroupSortConfiguration = observer(
) : null} -
- - - - -
+ {hideReverse ? null : ( +
+ + + + +
+ )}
); diff --git a/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.test.js b/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.test.js index 94542c916..867d6bc08 100644 --- a/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.test.js +++ b/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.test.js @@ -49,7 +49,7 @@ describe("", () => { FakeConfiguration(); setTimeout(() => { expect(settingsStore.gridConfig.config.sortOrder).toBe( - settingsStore.gridConfig.defaults.sortOrder + settingsStore.gridConfig.options.default.value ); done(); }, 200); @@ -74,6 +74,38 @@ describe("", () => { }, 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; diff --git a/ui/src/Components/MainModal/Configuration/SortLabelName.js b/ui/src/Components/MainModal/Configuration/SortLabelName.js index cc49f0f0f..dca03f1eb 100644 --- a/ui/src/Components/MainModal/Configuration/SortLabelName.js +++ b/ui/src/Components/MainModal/Configuration/SortLabelName.js @@ -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: [] }); diff --git a/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupSortConfiguration.test.js.snap b/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupSortConfiguration.test.js.snap index 19d7b8744..305552b77 100644 --- a/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupSortConfiguration.test.js.snap +++ b/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupSortConfiguration.test.js.snap @@ -14,7 +14,7 @@ exports[` matches snapshot with default values 1`
- Sort by alert timestamp + Use defaults from karma config file
matches snapshot with default values 1`
-
- - - - -
" diff --git a/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap b/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap index 5cc2ac6b7..9fc52d13f 100644 --- a/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap +++ b/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap @@ -124,7 +124,7 @@ exports[` matches snapshot 1`] = `
- Sort by alert timestamp + Use defaults from karma config file
matches snapshot 1`] = `
-
- - - - -
diff --git a/ui/src/Stores/AlertStore.js b/ui/src/Stores/AlertStore.js index c9bcabfa5..dfd75948b 100644 --- a/ui/src/Stores/AlertStore.js +++ b/ui/src/Stores/AlertStore.js @@ -171,6 +171,11 @@ class AlertStore { annotationsHidden: [], annotationsVisible: [], sorting: { + grid: { + order: "startsAt", + reverse: false, + label: "alertname" + }, valueMapping: {} } } diff --git a/ui/src/Stores/Settings.js b/ui/src/Stores/Settings.js index 7af1f2795..9dc47b0ef 100644 --- a/ui/src/Stores/Settings.js +++ b/ui/src/Stores/Settings.js @@ -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 } ); diff --git a/ui/src/__mocks__/Fetch.js b/ui/src/__mocks__/Fetch.js index 1555242ad..1c6430373 100644 --- a/ui/src/__mocks__/Fetch.js +++ b/ui/src/__mocks__/Fetch.js @@ -24,6 +24,11 @@ const EmptyAPIResponse = () => ({ ], settings: { sorting: { + grid: { + order: "startsAt", + reverse: false, + label: "alertname" + }, valueMapping: { cluster: { dev: 3, From 6922690d3d31d03f20dea04a7f3183fcfa74903f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Wed, 20 Feb 2019 22:00:41 +0000 Subject: [PATCH 3/3] chore(demo): set custom default grid sorting using severity label --- demo/karma.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/demo/karma.yaml b/demo/karma.yaml index 9fd62d364..ce48a6c6d 100644 --- a/demo/karma.yaml +++ b/demo/karma.yaml @@ -17,6 +17,11 @@ custom: filters: default: - "@receiver=by-cluster-service" +grid: + sorting: + order: label + reverse: false + label: severity labels: color: static: