diff --git a/ui/src/Components/Fetcher/index.js b/ui/src/Components/Fetcher/index.js index ce0d87c5c..3b61906a0 100644 --- a/ui/src/Components/Fetcher/index.js +++ b/ui/src/Components/Fetcher/index.js @@ -25,14 +25,59 @@ const Fetcher = observer( }, markCompleted() { this.completedAt = moment(); + }, + forceUpdate() { + this.time = moment(0); } }, { update: action, - markCompleted: action + markCompleted: action, + forceUpdate: action } ); + getSortSettings = () => { + const { alertStore, settingsStore } = this.props; + + let sortSettings = { + useDefaults: false, + sortOrder: "", + sortLabel: "", + sortReverse: "" + }; + + sortSettings.useDefaults = + settingsStore.gridConfig.config.sortOrder === + settingsStore.gridConfig.options.default.value; + + if (sortSettings.useDefaults === true) { + return sortSettings; + } + + sortSettings.sortOrder = alertStore.settings.values.sorting.grid.order; + + // don't sort if sorting is disabled + if ( + sortSettings.sortOrder === + settingsStore.gridConfig.options.disabled.value + ) + return sortSettings; + + sortSettings.sortReverse = + settingsStore.gridConfig.config.reverseSort !== null + ? settingsStore.gridConfig.config.reverseSort === true + ? "1" + : "0" + : ""; + + if (settingsStore.gridConfig.config.sortLabel === null) { + sortSettings.sortLabel = settingsStore.gridConfig.config.sortLabel; + } + + return sortSettings; + }; + fetchIfIdle = () => { const { alertStore, settingsStore } = this.props; @@ -59,8 +104,7 @@ const Fetcher = observer( !alertStore.status.paused ) { this.lastTick.update(); - alertStore.fetchWithThrottle(); - this.lastTick.markCompleted(); + this.callFetch(); } }; @@ -68,6 +112,18 @@ const Fetcher = observer( window.requestAnimationFrame(this.fetchIfIdle); }; + callFetch = () => { + const { alertStore } = this.props; + + const sortSettings = this.getSortSettings(); + alertStore.fetchWithThrottle( + sortSettings.sortOrder, + sortSettings.sortLabel, + sortSettings.sortReverse + ); + this.lastTick.markCompleted(); + }; + componentDidMount() { // start first fetch once the browser is done doing busy loading window.requestAnimationFrame(this.fetchIfIdle); @@ -79,7 +135,7 @@ const Fetcher = observer( if (!alertStore.status.paused) { this.lastTick.update(); - alertStore.fetchWithThrottle(); + this.callFetch(); } } @@ -96,6 +152,9 @@ const Fetcher = observer( f.raw).join(" ")} data-interval={settingsStore.fetchConfig.config.interval} + data-grid-sort-order={settingsStore.gridConfig.config.sortOrder} + data-grid-sort-label={settingsStore.gridConfig.config.sortLabel} + data-grid-sort-reverse={settingsStore.gridConfig.config.reverseSort} /> ); } diff --git a/ui/src/Components/Fetcher/index.test.js b/ui/src/Components/Fetcher/index.test.js index 9b3507558..26245a425 100644 --- a/ui/src/Components/Fetcher/index.test.js +++ b/ui/src/Components/Fetcher/index.test.js @@ -53,20 +53,20 @@ const MountedFetcher = () => { ); }; -const FetcherSpan = (label, interval) => - ``; +const FetcherSpan = (label, interval, sortOrder) => + ``; describe("", () => { it("renders correctly with 'label=value' filter", () => { const tree = MountedFetcher(); - expect(tree.html()).toBe(FetcherSpan("label=value", 30)); + expect(tree.html()).toBe(FetcherSpan("label=value", 30, "default")); }); it("re-renders on fetch interval change", () => { const tree = MountedFetcher(); - expect(tree.html()).toBe(FetcherSpan("label=value", 30)); + expect(tree.html()).toBe(FetcherSpan("label=value", 30, "default")); settingsStore.fetchConfig.config.interval = 60; - expect(tree.html()).toBe(FetcherSpan("label=value", 60)); + expect(tree.html()).toBe(FetcherSpan("label=value", 60, "default")); }); it("changing interval changes how often fetch is called", () => { @@ -96,9 +96,9 @@ describe("", () => { it("re-renders on filters change", () => { MockEmptyAPIResponseWithoutFilters(); const tree = MountedFetcher(); - expect(tree.html()).toBe(FetcherSpan("label=value", 30)); + expect(tree.html()).toBe(FetcherSpan("label=value", 30, "default")); alertStore.filters.values = []; - expect(tree.html()).toBe(FetcherSpan("", 30)); + expect(tree.html()).toBe(FetcherSpan("", 30, "default")); }); it("calls alertStore.fetchWithThrottle on mount", () => { diff --git a/ui/src/Components/Grid/AlertGrid/index.js b/ui/src/Components/Grid/AlertGrid/index.js index a4143718a..669df534c 100644 --- a/ui/src/Components/Grid/AlertGrid/index.js +++ b/ui/src/Components/Grid/AlertGrid/index.js @@ -6,8 +6,6 @@ import { observer } from "mobx-react"; import FontFaceObserver from "fontfaceobserver"; -import moment from "moment"; - import { debounce } from "lodash"; import ReactResizeDetector from "react-resize-detector"; @@ -25,46 +23,6 @@ import { GridSizesConfig, GetGridElementWidth } from "./GridSize"; import "./index.css"; -const getGroupStartsAt = g => - moment.max(g.alerts.map(a => moment(a.startsAt))).valueOf(); - -const getLabelValue = (alertStore, settingsStore, sortOrder, sortLabel, g) => { - // if timestamp sort is enabled use latest alert for sorting - if (sortOrder === settingsStore.gridConfig.options.startsAt.value) { - return getGroupStartsAt(g); - } - - const labelValue = - 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[sortLabel] !== undefined - ) { - mappedValue = - alertStore.settings.values.sorting.valueMapping[sortLabel][labelValue]; - } - - // if we have a mapped value then return it, if not return original value - return mappedValue !== undefined ? mappedValue : labelValue; -}; - -const compareByTimestamp = (a, b) => { - const ast = getGroupStartsAt(a); - const bst = getGroupStartsAt(b); - if (ast > bst) { - return -1; - } else if (ast < bst) { - return 1; - } else { - return 0; - } -}; - const AlertGrid = observer( class AlertGrid extends Component { static propTypes = { @@ -151,68 +109,6 @@ const AlertGrid = observer( ); }); - compare = (a, b) => { - const { alertStore, settingsStore } = this.props; - - const useDefaults = - settingsStore.gridConfig.config.sortOrder === - 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 val = sortReverse ? -1 : 1; - - const av = getLabelValue( - alertStore, - settingsStore, - sortOrder, - sortLabel, - a - ); - const bv = getLabelValue( - alertStore, - settingsStore, - sortOrder, - sortLabel, - b - ); - - if (av === undefined && av === undefined) { - // if both alerts lack the label they are equal, fallback to timestamps - return compareByTimestamp(a, b); - } else if (av === undefined || av > bv) { - // if first one lacks it it's should be rendered after alerts with that label - return val; - } else if (bv === undefined || av < bv) { - // if the first one has label but the second doesn't then the second should be rendered after the first - return val * -1; - } else if ( - sortOrder !== settingsStore.gridConfig.options.startsAt.value - ) { - // if values are equal use timestamps as secondary sort, but only - // if we didn't already sort by timestamps - return compareByTimestamp(a, b); - } else { - return 0; - } - }; - componentDidMount() { // We have font-display:swap set for font assets, this means that on initial // render a fallback font might be used and later swapped for the final one diff --git a/ui/src/Components/Grid/AlertGrid/index.test.js b/ui/src/Components/Grid/AlertGrid/index.test.js index 04c269998..5c7707a5c 100644 --- a/ui/src/Components/Grid/AlertGrid/index.test.js +++ b/ui/src/Components/Grid/AlertGrid/index.test.js @@ -58,14 +58,14 @@ const MockGroup = (groupName, alertCount) => { }; const MockGroupList = (count, alertPerGroup) => { - let groups = {}; + let groups = []; for (let i = 1; i <= count; i++) { let id = `id${i}`; let hash = `hash${i}`; let group = MockGroup(`group${i}`, alertPerGroup); group.id = id; group.hash = hash; - groups[id] = group; + groups.push(group); } alertStore.data.upstreams = { counters: { total: 0, healthy: 1, failed: 0 }, @@ -157,213 +157,6 @@ describe("", () => { ]); }); - it("doesn't sort groups when sorting is set to 'disabled' and 'reverse' is on", () => { - settingsStore.gridConfig.config.sortOrder = - settingsStore.gridConfig.options.disabled.value; - settingsStore.gridConfig.config.reverseSort = true; - MockGroupList(3, 1); - const tree = ShallowAlertGrid(); - const alertGroups = tree.find("AlertGroup"); - expect(alertGroups.map(g => g.props().group.id)).toEqual([ - "id1", - "id2", - "id3" - ]); - }); - - it("groups are sorted by timestamp when sorting is set to 'startsAt'", () => { - settingsStore.gridConfig.config.sortOrder = - settingsStore.gridConfig.options.startsAt.value; - settingsStore.gridConfig.config.reverseSort = false; - - MockGroupList(3, 1); - alertStore.data.groups.id1.alerts[0].startsAt = "2001-01-01T00:00:00Z"; - alertStore.data.groups.id2.alerts[0].startsAt = "2002-01-01T00:00:00Z"; - alertStore.data.groups.id3.alerts[0].startsAt = "2000-01-01T00:00:00Z"; - - const tree = ShallowAlertGrid(); - const alertGroups = tree.find("AlertGroup"); - expect(alertGroups.map(g => g.props().group.id)).toEqual([ - "id3", - "id1", - "id2" - ]); - }); - - it("groups are sorted by reversed timestamp when sorting is set to 'startsAt' and 'reverse' is on", () => { - settingsStore.gridConfig.config.sortOrder = - settingsStore.gridConfig.options.startsAt.value; - settingsStore.gridConfig.config.reverseSort = true; - - MockGroupList(3, 1); - alertStore.data.groups.id1.alerts[0].startsAt = "2001-01-01T00:00:00Z"; - alertStore.data.groups.id2.alerts[0].startsAt = "2002-01-01T00:00:00Z"; - alertStore.data.groups.id3.alerts[0].startsAt = "2000-01-01T00:00:00Z"; - - const tree = ShallowAlertGrid(); - const alertGroups = tree.find("AlertGroup"); - expect(alertGroups.map(g => g.props().group.id)).toEqual([ - "id2", - "id1", - "id3" - ]); - }); - - it("groups are sorted by label when sorting is set to 'label'", () => { - settingsStore.gridConfig.config.sortOrder = - settingsStore.gridConfig.options.label.value; - settingsStore.gridConfig.config.sortLabel = "instance"; - settingsStore.gridConfig.config.reverseSort = false; - - MockGroupList(3, 1); - alertStore.data.groups.id1.alerts[0].labels.instance = "abc1"; - alertStore.data.groups.id2.alerts[0].labels.instance = "abc3"; - alertStore.data.groups.id3.alerts[0].labels.instance = "abc2"; - - const tree = ShallowAlertGrid(); - const alertGroups = tree.find("AlertGroup"); - expect(alertGroups.map(g => g.props().group.id)).toEqual([ - "id1", - "id3", - "id2" - ]); - }); - - it("groups are sorted by reverse label when sorting is set to 'label' and 'reverse' is on", () => { - settingsStore.gridConfig.config.sortOrder = - settingsStore.gridConfig.options.label.value; - settingsStore.gridConfig.config.sortLabel = "instance"; - settingsStore.gridConfig.config.reverseSort = true; - - MockGroupList(3, 1); - alertStore.data.groups.id1.alerts[0].labels.instance = "abc1"; - alertStore.data.groups.id2.alerts[0].labels.instance = "abc3"; - alertStore.data.groups.id3.alerts[0].labels.instance = "abc2"; - - const tree = ShallowAlertGrid(); - const alertGroups = tree.find("AlertGroup"); - expect(alertGroups.map(g => g.props().group.id)).toEqual([ - "id2", - "id3", - "id1" - ]); - }); - - it("sorting is no-op when when sorting is set to 'label' and alerts lack that label", () => { - settingsStore.gridConfig.config.sortOrder = - settingsStore.gridConfig.options.label.value; - settingsStore.gridConfig.config.sortLabel = "foo"; - settingsStore.gridConfig.config.reverseSort = false; - MockGroupList(3, 1); - const tree = ShallowAlertGrid(); - const alertGroups = tree.find("AlertGroup"); - expect(alertGroups.map(g => g.props().group.id)).toEqual([ - "id1", - "id2", - "id3" - ]); - }); - - it("label value mappings from settings are used to order alerts", () => { - alertStore.settings.values.sorting.valueMapping = { - cluster: { - prod: 1, - staging: 2, - dev: 3 - } - }; - - settingsStore.gridConfig.config.sortOrder = - settingsStore.gridConfig.options.label.value; - settingsStore.gridConfig.config.sortLabel = "cluster"; - settingsStore.gridConfig.config.reverseSort = false; - - MockGroupList(3, 1); - alertStore.data.groups.id1.alerts[0].labels.cluster = "dev"; - alertStore.data.groups.id2.alerts[0].labels.cluster = "staging"; - alertStore.data.groups.id3.alerts[0].labels.cluster = "prod"; - - const tree = ShallowAlertGrid(); - const alertGroups = tree.find("AlertGroup"); - expect(alertGroups.map(g => g.props().group.id)).toEqual([ - "id3", - "id2", - "id1" - ]); - }); - - it("label value mappings from settings are used to order alerts and reverse flag is respected", () => { - alertStore.settings.values.sorting.valueMapping = { - cluster: { - prod: 1, - staging: 2, - dev: 3 - } - }; - - settingsStore.gridConfig.config.sortOrder = - settingsStore.gridConfig.options.label.value; - settingsStore.gridConfig.config.sortLabel = "cluster"; - settingsStore.gridConfig.config.reverseSort = true; - - MockGroupList(3, 1); - alertStore.data.groups.id1.alerts[0].labels.cluster = "dev"; - alertStore.data.groups.id2.alerts[0].labels.cluster = "prod"; - alertStore.data.groups.id3.alerts[0].labels.cluster = "staging"; - - const tree = ShallowAlertGrid(); - const alertGroups = tree.find("AlertGroup"); - expect(alertGroups.map(g => g.props().group.id)).toEqual([ - "id1", - "id3", - "id2" - ]); - }); - - it("startsAt is used as secondary sort key if all labels have equal value", () => { - settingsStore.gridConfig.config.sortOrder = - settingsStore.gridConfig.options.label.value; - settingsStore.gridConfig.config.sortLabel = "cluster"; - - MockGroupList(3, 1); - alertStore.data.groups.id1.alerts[0].labels.cluster = "dev"; - alertStore.data.groups.id1.alerts[0].startsAt = "2000-01-01T00:00:02.000Z"; - alertStore.data.groups.id2.alerts[0].labels.cluster = "dev"; - alertStore.data.groups.id2.alerts[0].startsAt = "2000-01-01T00:00:03.000Z"; - alertStore.data.groups.id3.alerts[0].labels.cluster = "dev"; - alertStore.data.groups.id3.alerts[0].startsAt = "2000-01-01T00:00:01.000Z"; - - const tree = ShallowAlertGrid(); - const alertGroups = tree.find("AlertGroup"); - expect(alertGroups.map(g => g.props().group.id)).toEqual([ - "id2", - "id1", - "id3" - ]); - }); - - it("original order is preserved when startsAt is used as fallback and all alerts have the same timestamp", () => { - settingsStore.gridConfig.config.sortOrder = - settingsStore.gridConfig.options.label.value; - settingsStore.gridConfig.config.sortLabel = "cluster"; - - MockGroupList(3, 1); - alertStore.data.groups.id1.alerts[0].labels.cluster = "dev"; - alertStore.data.groups.id1.alerts[0].startsAt = "2000-01-01T00:00:01.000Z"; - alertStore.data.groups.id2.alerts[0].labels.cluster = "dev"; - alertStore.data.groups.id2.alerts[0].startsAt = "2000-01-01T00:00:01.000Z"; - alertStore.data.groups.id3.alerts[0].labels.cluster = "dev"; - alertStore.data.groups.id3.alerts[0].startsAt = "2000-01-01T00:00:01.000Z"; - - const tree = ShallowAlertGrid(); - const alertGroups = tree.find("AlertGroup"); - expect(alertGroups.map(g => g.props().group.id)).toEqual([ - "id1", - "id2", - "id3" - ]); - }); - it("doesn't throw errors after FontFaceObserver timeout", () => { MockGroupList(60, 5); ShallowAlertGrid();