Files
karma/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx
2020-12-01 10:18:38 +00:00

278 lines
9.0 KiB
TypeScript

import React, { FC, useEffect, useCallback, useState, ReactNode } from "react";
import { observer } from "mobx-react-lite";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus";
import { faMinus } from "@fortawesome/free-solid-svg-icons/faMinus";
import { faEllipsisH } from "@fortawesome/free-solid-svg-icons/faEllipsisH";
import { APIAlertT, APIAlertGroupT, AlertStateT } from "Models/APITypes";
import { Settings } from "Stores/Settings";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { BackgroundClassMap } from "Common/Colors";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { ThemeContext } from "Components/Theme";
import GroupHeader from "./GroupHeader";
import Alert from "./Alert";
import GroupFooter from "./GroupFooter";
import { DefaultDetailsCollapseValue } from "./DetailsToggle";
const LoadButton: FC<{
icon: IconDefinition;
action: () => void;
tooltip: ReactNode;
}> = ({ icon, action, tooltip }) => {
return (
<TooltipWrapper title={tooltip}>
<button
type="button"
className="btn btn-sm py-0 with-click"
onClick={action}
>
<FontAwesomeIcon className="text-muted" icon={icon} />
</button>
</TooltipWrapper>
);
};
const AllAlertsAreUsingSameAlertmanagers = (alerts: APIAlertT[]): boolean => {
const usedAMs = alerts.map((alert) =>
alert.alertmanager.map((am) => am.name).sort()
);
return usedAMs.every(
(listOfAMs) => JSON.stringify(listOfAMs) === JSON.stringify(usedAMs[0])
);
};
const AlertGroup: FC<{
group: APIAlertGroupT;
showAlertmanagers: boolean;
afterUpdate: () => void;
alertStore: AlertStore;
settingsStore: Settings;
silenceFormStore: SilenceFormStore;
groupWidth: number;
gridLabelValue: string;
initialAlertsToRender?: number;
}> = ({
group,
showAlertmanagers,
afterUpdate,
silenceFormStore,
alertStore,
settingsStore,
groupWidth,
gridLabelValue,
initialAlertsToRender,
}) => {
const defaultRenderCount =
settingsStore.alertGroupConfig.config.defaultRenderCount;
const [alertsToRenderInternal, setAlertsToRender] = useState<number | null>(
initialAlertsToRender || null
);
const alertsToRender = alertsToRenderInternal || defaultRenderCount;
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
const [isCollapsed, setIsCollapsed] = useState<boolean>(
DefaultDetailsCollapseValue(settingsStore)
);
// Used to calculate step size when loading more alerts.
// Step is calculated from the excesive alert count
// (what's > defaultRenderCount) by dividing it into 5 clicks.
// Don't use step lower than 5, too much clicking if we have a group of 9:
// * we'll show initially 5
// * step would be 1
// * 4 extra clicks to see the entire group
// but ensure that step wouldn't push us above totalSize
// With 9 alerts and rendering 5 initially we want to show extra 9 after one
// click, and when user clicks showLess we want to go back to 5.
const getStepSize = (totalSize: number) => {
const val = Math.min(
Math.max(Math.round((totalSize - defaultRenderCount) / 5), 5),
totalSize - defaultRenderCount
);
return val;
};
const loadMore = () => {
const step = getStepSize(group.alerts.length);
// show cur+step, but not more that total alert count
setAlertsToRender(Math.min(alertsToRender + step, group.alerts.length));
};
const loadLess = () => {
const step = getStepSize(group.alerts.length);
// show cur-step, but not less than 1
setAlertsToRender(Math.max(alertsToRender - step, 1));
};
const onAlertGroupCollapseEvent = useCallback(
(event) => {
if (event.detail.gridLabelValue === gridLabelValue) {
setIsCollapsed(event.detail.value);
}
},
[gridLabelValue]
);
useEffect(() => {
window.addEventListener("alertGroupCollapse", onAlertGroupCollapseEvent);
return () => {
window.removeEventListener(
"alertGroupCollapse",
onAlertGroupCollapseEvent
);
};
}, [onAlertGroupCollapseEvent]);
useEffect(() => {
afterUpdate();
});
const footerAlertmanagers: string[] = [];
let showAlertmanagersInFooter = false;
// There's no need to render @alertmanager labels if there's only 1
// alertmanager upstream
if (showAlertmanagers) {
// Decide if we show @alertmanager label in footer or for every alert
// we show it in the footer only if every alert has the same set of
// alertmanagers (and there's > 1 alert to show, there's no footer for 1)
showAlertmanagersInFooter =
group.alerts.length > 1 &&
AllAlertsAreUsingSameAlertmanagers(group.alerts);
if (showAlertmanagersInFooter) {
for (const am of group.alerts[0].alertmanager) {
if (!footerAlertmanagers.includes(am.cluster)) {
footerAlertmanagers.push(am.cluster);
}
}
}
}
let themedCounters = true;
let cardBackgroundClass = "bg-light";
if (settingsStore.alertGroupConfig.config.colorTitleBar) {
const stateList = Object.entries(group.stateCount)
.filter(([_, v]) => v !== 0)
.map(([k, _]) => k);
if (stateList.length === 1) {
const state = stateList.pop();
cardBackgroundClass = BackgroundClassMap[state as AlertStateT];
themedCounters = false;
}
}
const context = React.useContext(ThemeContext);
return (
<div
className={`components-grid-alertgrid-alertgroup ${
context.animations.duration ? "animate" : ""
}`}
style={{
width: groupWidth,
zIndex: isMenuOpen ? 100 : undefined,
}}
data-defaultrendercount={
settingsStore.alertGroupConfig.config.defaultRenderCount
}
>
<div
className={`card ${cardBackgroundClass}`}
data-colortitlebar={settingsStore.alertGroupConfig.config.colorTitleBar}
>
<GroupHeader
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
group={group}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
themedCounters={themedCounters}
setIsMenuOpen={setIsMenuOpen}
gridLabelValue={gridLabelValue}
/>
{isCollapsed ? null : (
<div className="card-body px-2 py-1 components-grid-alertgrid-card">
<ul className="list-group">
{group.alerts
.slice(0, alertStore.ui.isIdle ? 1 : alertsToRender)
.map((alert) => (
<Alert
key={alert.id}
group={group}
alert={alert}
showAlertmanagers={
showAlertmanagers && !showAlertmanagersInFooter
}
showReceiver={
alertStore.data.receivers.length > 1 &&
group.alerts.length === 1
}
afterUpdate={afterUpdate}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
setIsMenuOpen={setIsMenuOpen}
/>
))}
{group.alerts.length > defaultRenderCount ? (
<li
className="list-group-item border-0 p-0 text-center bg-transparent"
style={{
lineHeight: alertStore.ui.isIdle ? "1rem" : undefined,
}}
>
{alertStore.ui.isIdle ? (
<FontAwesomeIcon
icon={faEllipsisH}
className="text-muted"
/>
) : (
<React.Fragment>
<LoadButton
icon={faMinus}
action={loadLess}
tooltip="Show fewer alerts in this group"
/>
<small className="text-muted mx-2">
{Math.min(alertsToRender, group.alerts.length)}
{" of "}
{group.alerts.length}
</small>
<LoadButton
icon={faPlus}
action={loadMore}
tooltip="Show more alerts in this group"
/>
</React.Fragment>
)}
</li>
) : null}
</ul>
</div>
)}
{isCollapsed === false && group.alerts.length > 1 ? (
<GroupFooter
group={group}
alertmanagers={footerAlertmanagers}
afterUpdate={afterUpdate}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
showAnnotations={!alertStore.ui.isIdle}
showSilences={!alertStore.ui.isIdle}
/>
) : null}
</div>
</div>
);
};
export default observer(AlertGroup);