mirror of
https://github.com/prymitive/karma
synced 2026-05-15 04:06:41 +00:00
278 lines
9.0 KiB
TypeScript
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);
|