Files
karma/ui/src/Components/Grid/AlertGrid/index.js
2019-06-20 18:52:23 +01:00

287 lines
8.9 KiB
JavaScript

import React, { Component } from "react";
import PropTypes from "prop-types";
import { observable, action, computed } from "mobx";
import { observer } from "mobx-react";
import FontFaceObserver from "fontfaceobserver";
import moment from "moment";
import { debounce } from "lodash";
import ReactResizeDetector from "react-resize-detector";
import MasonryInfiniteScroller from "react-masonry-infinite";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { AlertGroup } from "./AlertGroup";
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 = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
settingsStore: PropTypes.instanceOf(Settings).isRequired,
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired
};
constructor(props) {
super(props);
// this is used to track viewport width, when browser window is resized
// we need to recreate the entire grid object to apply new column count
// and group size
this.viewport = observable(
{
width: document.body.clientWidth,
update(width, height) {
this.width = width;
},
get gridSizesConfig() {
return GridSizesConfig(
this.width,
props.settingsStore.gridConfig.config.groupWidth
);
},
get groupWidth() {
return GetGridElementWidth(
this.width,
props.settingsStore.gridConfig.config.groupWidth
);
}
},
{
update: action.bound,
gridSizesConfig: computed,
groupWidth: computed
}
);
}
// store reference to generated masonry component so we can call it
// to repack the grid after any component was re-rendered, which could
// alter its size breaking grid layout
masonryComponentReference = observable(
{ ref: false },
{},
{ name: "Masonry reference" }
);
// store it for later
storeMasonryRef = action(ref => {
this.masonryComponentReference.ref = ref;
});
// used to call forcePack() which will repack all grid elements
// (alert groups), this needs to be called if any group size changes
masonryRepack = debounce(
action(() => {
if (this.masonryComponentReference.ref) {
this.masonryComponentReference.ref.forcePack();
}
}),
10
);
// how many alert groups to render
// FIXME reset on filter change
initial = 50;
groupsToRender = observable(
{
value: this.initial
},
{},
{ name: "Groups to render" }
);
// how many groups add to render count when user scrolls to the bottom
loadMoreStep = 30;
//
loadMore = action(() => {
const { alertStore } = this.props;
this.groupsToRender.value = Math.min(
this.groupsToRender.value + this.loadMoreStep,
Object.keys(alertStore.data.groups).length
);
});
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
// (once the final font is loaded). This means that fallback font might
// render to a different size and the swap can result in component resize.
// For our grid this resize might leave gaps since everything uses fixed
// position, so we use font observer and trigger repack when fonts are loaded
for (const fontWeight of [300, 400, 600]) {
const font = new FontFaceObserver("Open Sans", {
weight: fontWeight
});
// wait up to 30s, run no-op function on timeout
font.load(null, 30000).then(this.masonryRepack, () => {});
}
}
render() {
const { alertStore, settingsStore, silenceFormStore } = this.props;
return (
<React.Fragment>
<ReactResizeDetector
handleWidth
handleHeight
onResize={debounce(this.viewport.update, 100)}
/>
<MasonryInfiniteScroller
key={settingsStore.gridConfig.config.groupWidth}
ref={this.storeMasonryRef}
position={false}
pack={true}
sizes={this.viewport.gridSizesConfig}
loadMore={this.loadMore}
hasMore={
this.groupsToRender.value <
Object.keys(alertStore.data.groups).length
}
threshold={50}
loader={
<div key="loader" className="text-center text-muted py-3">
<FontAwesomeIcon icon={faCircleNotch} size="lg" spin />
</div>
}
>
{Object.values(alertStore.data.groups)
.sort(this.compare)
.slice(0, this.groupsToRender.value)
.map(group => (
<AlertGroup
key={group.id}
group={group}
showAlertmanagers={
Object.keys(alertStore.data.upstreams.clusters).length > 1
}
afterUpdate={this.masonryRepack}
alertStore={alertStore}
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
style={{
width: this.viewport.groupWidth
}}
/>
))}
</MasonryInfiniteScroller>
</React.Fragment>
);
}
}
);
export { AlertGrid };