mirror of
https://github.com/prymitive/karma
synced 2026-05-17 04:16:42 +00:00
chore(ui): migrate more components to typescript
This commit is contained in:
committed by
Łukasz Mierzwa
parent
3b68860f38
commit
e55988588c
21
ui/package-lock.json
generated
21
ui/package-lock.json
generated
@@ -4956,6 +4956,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
|
||||
"integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag=="
|
||||
},
|
||||
"@types/fontfaceobserver": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/fontfaceobserver/-/fontfaceobserver-0.0.6.tgz",
|
||||
"integrity": "sha512-QJ1znjr9CDax2L17rgBnDOfNHsC1XtVAMswu+lRWvWb+kANhHA0slUNSSBsG8FVNvM4I4yXlN9doJRot3A2hkQ=="
|
||||
},
|
||||
"@types/glob": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
|
||||
@@ -5116,6 +5121,14 @@
|
||||
"csstype": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"@types/react-autosuggest": {
|
||||
"version": "9.3.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-autosuggest/-/react-autosuggest-9.3.14.tgz",
|
||||
"integrity": "sha512-cvGpKaQaNsFbDxTwP56VKVj2FO6SpJ9PsrQtlVzN7aVa/SsMZoQrBLEUx5HQKfIS4Zupb6K4tHmIyTjF7AEcow==",
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"version": "16.9.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.8.tgz",
|
||||
@@ -5124,6 +5137,14 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-highlighter": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-highlighter/-/react-highlighter-0.3.4.tgz",
|
||||
"integrity": "sha512-wM8l3QxU1P6rAeCIJUNug407Z8igm90xX9jDpjem1+pVzEI3VQpKiQj7oEinQVq7425Dmgyon8XfqbxD0mTknA==",
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-js-pagination": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-js-pagination/-/react-js-pagination-3.0.3.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@sentry/browser": "5.19.2",
|
||||
"@types/body-scroll-lock": "2.6.1",
|
||||
"@types/bricks.js": "1.8.1",
|
||||
"@types/fontfaceobserver": "0.0.6",
|
||||
"@types/lodash.debounce": "4.0.6",
|
||||
"@types/lodash.merge": "4.6.6",
|
||||
"@types/lodash.throttle": "4.1.6",
|
||||
@@ -20,7 +21,9 @@
|
||||
"@types/promise-retry": "1.1.3",
|
||||
"@types/qs": "6.9.3",
|
||||
"@types/react": "16.9.43",
|
||||
"@types/react-autosuggest": "9.3.14",
|
||||
"@types/react-dom": "16.9.8",
|
||||
"@types/react-highlighter": "0.3.4",
|
||||
"@types/react-js-pagination": "3.0.3",
|
||||
"@types/react-select": "3.0.13",
|
||||
"body-scroll-lock": "3.0.3",
|
||||
@@ -42,7 +45,6 @@
|
||||
"mobx-stored": "1.1.0",
|
||||
"node-sass": "4.14.1",
|
||||
"promise-retry": "2.0.1",
|
||||
"prop-types": "15.7.2",
|
||||
"qs": "6.9.4",
|
||||
"react": "16.13.1",
|
||||
"react-app-polyfill": "1.0.6",
|
||||
|
||||
@@ -7,6 +7,11 @@ export interface OptionT {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface MultiValueOptionT {
|
||||
label: string;
|
||||
value: string[];
|
||||
}
|
||||
|
||||
export const StringToOption = (value: string): OptionT => ({
|
||||
label: value,
|
||||
value: value,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { CSSTransition } from "react-transition-group";
|
||||
|
||||
const DropdownSlide: FC<{
|
||||
children: ReactNode;
|
||||
duration: number;
|
||||
}> = ({ children, duration, ...props }) => (
|
||||
in?: boolean;
|
||||
unmountOnExit?: boolean;
|
||||
}> = ({ children, ...props }) => (
|
||||
<CSSTransition
|
||||
classNames="components-animation-slide"
|
||||
timeout={150}
|
||||
@@ -17,8 +17,5 @@ const DropdownSlide: FC<{
|
||||
{children}
|
||||
</CSSTransition>
|
||||
);
|
||||
DropdownSlide.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export { DropdownSlide };
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { CSSTransition } from "react-transition-group";
|
||||
|
||||
@@ -19,9 +18,6 @@ const MountModal: FC<{
|
||||
{children}
|
||||
</CSSTransition>
|
||||
);
|
||||
MountModal.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const MountModalBackdrop: FC<{
|
||||
children: ReactNode;
|
||||
@@ -39,8 +35,5 @@ const MountModalBackdrop: FC<{
|
||||
{children}
|
||||
</CSSTransition>
|
||||
);
|
||||
MountModalBackdrop.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export { MountModal, MountModalBackdrop };
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ThemeContext } from "Components/Theme";
|
||||
|
||||
const CenteredMessage: FC<{
|
||||
children: ReactNode;
|
||||
className: string;
|
||||
className?: string;
|
||||
}> = ({ children, className }) => {
|
||||
const context = React.useContext(ThemeContext);
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import React, { useRef, useState, useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, {
|
||||
FC,
|
||||
Ref,
|
||||
CSSProperties,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
} from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
@@ -10,11 +16,10 @@ import { faCaretDown } from "@fortawesome/free-solid-svg-icons/faCaretDown";
|
||||
import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash";
|
||||
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons/faExternalLinkAlt";
|
||||
|
||||
import { APIAlert, APIGroup } from "Models/API";
|
||||
import { APIAlertT, APIAlertGroupT } from "Models/APITypes";
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import {
|
||||
SilenceFormStore,
|
||||
SilenceTabNames,
|
||||
AlertmanagerClustersToOption,
|
||||
} from "Stores/SilenceFormStore";
|
||||
import { CommonPopperModifiers } from "Common/Popper";
|
||||
@@ -28,8 +33,13 @@ const PopperModifiers = [
|
||||
{ name: "offset", options: { offset: "-5px, 0px" } },
|
||||
];
|
||||
|
||||
const onSilenceClick = (alertStore, silenceFormStore, group, alert) => {
|
||||
let clusters = {};
|
||||
const onSilenceClick = (
|
||||
alertStore: AlertStore,
|
||||
silenceFormStore: SilenceFormStore,
|
||||
group: APIAlertGroupT,
|
||||
alert: APIAlertT
|
||||
) => {
|
||||
let clusters: { [cluster: string]: string[] } = {};
|
||||
Object.entries(alertStore.data.clustersWithoutReadOnly).forEach(
|
||||
([cluster, members]) => {
|
||||
if (alert.alertmanager.map((am) => am.cluster).includes(cluster)) {
|
||||
@@ -45,11 +55,20 @@ const onSilenceClick = (alertStore, silenceFormStore, group, alert) => {
|
||||
AlertmanagerClustersToOption(clusters),
|
||||
[alert]
|
||||
);
|
||||
silenceFormStore.tab.setTab(SilenceTabNames.Editor);
|
||||
silenceFormStore.tab.setTab("editor");
|
||||
silenceFormStore.toggle.show();
|
||||
};
|
||||
|
||||
const MenuContent = ({
|
||||
const MenuContent: FC<{
|
||||
popperPlacement?: string;
|
||||
popperRef?: Ref<any>;
|
||||
popperStyle?: CSSProperties;
|
||||
group: APIAlertGroupT;
|
||||
alert: APIAlertT;
|
||||
afterClick: () => void;
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
}> = ({
|
||||
popperPlacement,
|
||||
popperRef,
|
||||
popperStyle,
|
||||
@@ -102,22 +121,14 @@ const MenuContent = ({
|
||||
</FetchPauser>
|
||||
);
|
||||
};
|
||||
MenuContent.propTypes = {
|
||||
popperPlacement: PropTypes.string,
|
||||
popperRef: PropTypes.func,
|
||||
popperStyle: PropTypes.object,
|
||||
group: APIGroup.isRequired,
|
||||
alert: APIAlert.isRequired,
|
||||
afterClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const AlertMenu = ({
|
||||
group,
|
||||
alert,
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
setIsMenuOpen,
|
||||
}) => {
|
||||
const AlertMenu: FC<{
|
||||
group: APIAlertGroupT;
|
||||
alert: APIAlertT;
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
setIsMenuOpen: (isOpen: boolean) => void;
|
||||
}> = ({ group, alert, alertStore, silenceFormStore, setIsMenuOpen }) => {
|
||||
const [isHidden, setIsHidden] = useState(true);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
@@ -173,12 +184,5 @@ const AlertMenu = ({
|
||||
</span>
|
||||
));
|
||||
};
|
||||
AlertMenu.propTypes = {
|
||||
group: APIGroup.isRequired,
|
||||
alert: APIAlert.isRequired,
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
setIsMenuOpen: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { AlertMenu, MenuContent };
|
||||
@@ -1,9 +1,12 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
import { APIAlert, APIGroup } from "Models/API";
|
||||
import {
|
||||
APIAlertT,
|
||||
APIAlertGroupT,
|
||||
APIAlertmanagerStateT,
|
||||
} from "Models/APITypes";
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { BorderClassMap } from "Common/Colors";
|
||||
@@ -14,7 +17,16 @@ import { RenderNonLinkAnnotation, RenderLinkAnnotation } from "../Annotation";
|
||||
import { AlertMenu } from "./AlertMenu";
|
||||
import { RenderSilence } from "../Silences";
|
||||
|
||||
const Alert = ({
|
||||
const Alert: FC<{
|
||||
group: APIAlertGroupT;
|
||||
alert: APIAlertT;
|
||||
showAlertmanagers: boolean;
|
||||
showReceiver: boolean;
|
||||
afterUpdate: () => void;
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
setIsMenuOpen: (isOpen: boolean) => void;
|
||||
}> = ({
|
||||
group,
|
||||
alert,
|
||||
showAlertmanagers,
|
||||
@@ -34,9 +46,14 @@ const Alert = ({
|
||||
BorderClassMap[alert.state] || "border-default",
|
||||
];
|
||||
|
||||
const silences = {};
|
||||
let clusters = [];
|
||||
let inhibitedBy = [];
|
||||
const silences: {
|
||||
[cluster: string]: {
|
||||
alertmanager: APIAlertmanagerStateT;
|
||||
silences: string[];
|
||||
};
|
||||
} = {};
|
||||
let clusters: string[] = [];
|
||||
let inhibitedBy: string[] = [];
|
||||
for (const am of alert.alertmanager) {
|
||||
if (!clusters.includes(am.cluster)) {
|
||||
clusters.push(am.cluster);
|
||||
@@ -49,8 +66,8 @@ const Alert = ({
|
||||
if (!silences[am.cluster]) {
|
||||
silences[am.cluster] = {
|
||||
alertmanager: am,
|
||||
silences: [
|
||||
...new Set(
|
||||
silences: Array.from(
|
||||
new Set(
|
||||
am.silencedBy.filter(
|
||||
(silenceID) =>
|
||||
!(
|
||||
@@ -58,8 +75,8 @@ const Alert = ({
|
||||
group.shared.silences[am.cluster].includes(silenceID)
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -120,28 +137,19 @@ const Alert = ({
|
||||
<RenderLinkAnnotation key={a.name} name={a.name} value={a.value} />
|
||||
))}
|
||||
{Object.entries(silences).map(([cluster, clusterSilences]) =>
|
||||
clusterSilences.silences.map((silenceID) =>
|
||||
RenderSilence(
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
afterUpdate,
|
||||
cluster,
|
||||
silenceID
|
||||
)
|
||||
)
|
||||
clusterSilences.silences.map((silenceID) => (
|
||||
<RenderSilence
|
||||
key={silenceID}
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
afterUpdate={afterUpdate}
|
||||
cluster={cluster}
|
||||
silenceID={silenceID}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</li>
|
||||
));
|
||||
};
|
||||
Alert.propTypes = {
|
||||
group: APIGroup.isRequired,
|
||||
alert: APIAlert.isRequired,
|
||||
showAlertmanagers: PropTypes.bool.isRequired,
|
||||
showReceiver: PropTypes.bool.isRequired,
|
||||
afterUpdate: PropTypes.func.isRequired,
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
setIsMenuOpen: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { Alert };
|
||||
@@ -1,97 +0,0 @@
|
||||
import React, { useEffect, useRef, useState, memo } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import Linkify from "react-linkify";
|
||||
|
||||
import { CSSTransition } from "react-transition-group";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons/faExternalLinkAlt";
|
||||
import { faSearchPlus } from "@fortawesome/free-solid-svg-icons/faSearchPlus";
|
||||
import { faSearchMinus } from "@fortawesome/free-solid-svg-icons/faSearchMinus";
|
||||
|
||||
import { TooltipWrapper } from "Components/TooltipWrapper";
|
||||
import { useFlashTransition } from "Hooks/useFlashTransition";
|
||||
|
||||
const RenderNonLinkAnnotation = memo(
|
||||
({ name, value, visible, afterUpdate }) => {
|
||||
const mountRef = useRef(false);
|
||||
|
||||
const [isVisible, setIsVisible] = useState(visible);
|
||||
|
||||
useEffect(() => {
|
||||
if (mountRef.current) {
|
||||
afterUpdate();
|
||||
} else {
|
||||
mountRef.current = true;
|
||||
}
|
||||
});
|
||||
|
||||
const { ref, props } = useFlashTransition(value);
|
||||
|
||||
const className =
|
||||
"mb-1 p-1 bg-light d-inline-block rounded components-grid-annotation text-break mw-100";
|
||||
|
||||
return (
|
||||
<TooltipWrapper title="Toggle annotation value">
|
||||
<div
|
||||
className={`${className}${isVisible ? "" : " cursor-pointer"}`}
|
||||
onClick={isVisible ? undefined : () => setIsVisible(!isVisible)}
|
||||
>
|
||||
{isVisible ? (
|
||||
<React.Fragment>
|
||||
<span
|
||||
onClick={() => setIsVisible(false)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<FontAwesomeIcon icon={faSearchMinus} className="mr-1" />
|
||||
<span className="text-muted">{name}: </span>
|
||||
</span>
|
||||
<Linkify
|
||||
properties={{
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
}}
|
||||
>
|
||||
<CSSTransition {...props}>
|
||||
<span ref={ref}>{value}</span>
|
||||
</CSSTransition>
|
||||
</Linkify>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<FontAwesomeIcon icon={faSearchPlus} className="mr-1" />
|
||||
{name}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
RenderNonLinkAnnotation.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
afterUpdate: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const RenderLinkAnnotation = ({ name, value }) => {
|
||||
return (
|
||||
<a
|
||||
key={name}
|
||||
href={value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="components-label components-label-with-hover badge components-grid-annotation-link"
|
||||
>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} /> {name}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
RenderLinkAnnotation.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export { RenderNonLinkAnnotation, RenderLinkAnnotation };
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { FC, useEffect, useRef, useState, memo } from "react";
|
||||
|
||||
import Linkify from "react-linkify";
|
||||
|
||||
import { CSSTransition } from "react-transition-group";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons/faExternalLinkAlt";
|
||||
import { faSearchPlus } from "@fortawesome/free-solid-svg-icons/faSearchPlus";
|
||||
import { faSearchMinus } from "@fortawesome/free-solid-svg-icons/faSearchMinus";
|
||||
|
||||
import { TooltipWrapper } from "Components/TooltipWrapper";
|
||||
import { useFlashTransition } from "Hooks/useFlashTransition";
|
||||
|
||||
const RenderNonLinkAnnotation: FC<{
|
||||
name: string;
|
||||
value: string;
|
||||
visible: boolean;
|
||||
afterUpdate: () => void;
|
||||
}> = memo(({ name, value, visible, afterUpdate }) => {
|
||||
const mountRef = useRef(false);
|
||||
|
||||
const [isVisible, setIsVisible] = useState(visible);
|
||||
|
||||
useEffect(() => {
|
||||
if (mountRef.current) {
|
||||
afterUpdate();
|
||||
} else {
|
||||
mountRef.current = true;
|
||||
}
|
||||
});
|
||||
|
||||
const { ref, props } = useFlashTransition(value);
|
||||
|
||||
const className =
|
||||
"mb-1 p-1 bg-light d-inline-block rounded components-grid-annotation text-break mw-100";
|
||||
|
||||
return (
|
||||
<TooltipWrapper title="Toggle annotation value">
|
||||
<div
|
||||
className={`${className}${isVisible ? "" : " cursor-pointer"}`}
|
||||
onClick={isVisible ? undefined : () => setIsVisible(!isVisible)}
|
||||
>
|
||||
{isVisible ? (
|
||||
<React.Fragment>
|
||||
<span
|
||||
onClick={() => setIsVisible(false)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<FontAwesomeIcon icon={faSearchMinus} className="mr-1" />
|
||||
<span className="text-muted">{name}: </span>
|
||||
</span>
|
||||
<Linkify
|
||||
properties={{
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
}}
|
||||
>
|
||||
<CSSTransition {...props}>
|
||||
<span ref={ref}>{value}</span>
|
||||
</CSSTransition>
|
||||
</Linkify>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<FontAwesomeIcon icon={faSearchPlus} className="mr-1" />
|
||||
{name}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
const RenderLinkAnnotation: FC<{
|
||||
name: string;
|
||||
value: string;
|
||||
}> = ({ name, value }) => {
|
||||
return (
|
||||
<a
|
||||
key={name}
|
||||
href={value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="components-label components-label-with-hover badge components-grid-annotation-link"
|
||||
>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} /> {name}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export { RenderNonLinkAnnotation, RenderLinkAnnotation };
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IsMobile } from "Common/Device";
|
||||
import { Settings } from "Stores/Settings";
|
||||
|
||||
const DefaultDetailsCollapseValue = (settingsStore) => {
|
||||
const DefaultDetailsCollapseValue = (settingsStore: Settings): boolean => {
|
||||
let defaultCollapseState;
|
||||
switch (settingsStore.alertGroupConfig.config.defaultCollapseState) {
|
||||
case settingsStore.alertGroupConfig.options.collapsed.value:
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
import { APIGroup } from "Models/API";
|
||||
import { APIAlertGroupT } from "Models/APITypes";
|
||||
import { StaticLabels } from "Common/Query";
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
@@ -11,13 +10,13 @@ import { FilteringLabel } from "Components/Labels/FilteringLabel";
|
||||
import { RenderNonLinkAnnotation, RenderLinkAnnotation } from "../Annotation";
|
||||
import { RenderSilence } from "../Silences";
|
||||
|
||||
const GroupFooter = ({
|
||||
group,
|
||||
alertmanagers,
|
||||
afterUpdate,
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
}) => {
|
||||
const GroupFooter: FC<{
|
||||
group: APIAlertGroupT;
|
||||
alertmanagers: string[];
|
||||
afterUpdate: () => void;
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
}> = ({ group, alertmanagers, afterUpdate, alertStore, silenceFormStore }) => {
|
||||
return useObserver(() => (
|
||||
<div className="card-footer components-grid-alertgrid-alertgroup-footer px-2 py-1">
|
||||
<div className="mb-1">
|
||||
@@ -64,27 +63,21 @@ const GroupFooter = ({
|
||||
{Object.keys(group.shared.silences).length === 0 ? null : (
|
||||
<div className="components-grid-alertgrid-alertgroup-shared-silence rounded-0 border-0">
|
||||
{Object.entries(group.shared.silences).map(([cluster, silences]) =>
|
||||
silences.map((silenceID) =>
|
||||
RenderSilence(
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
afterUpdate,
|
||||
cluster,
|
||||
silenceID
|
||||
)
|
||||
)
|
||||
silences.map((silenceID) => (
|
||||
<RenderSilence
|
||||
key={silenceID}
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
afterUpdate={afterUpdate}
|
||||
cluster={cluster}
|
||||
silenceID={silenceID}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
GroupFooter.propTypes = {
|
||||
group: APIGroup.isRequired,
|
||||
alertmanagers: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
afterUpdate: PropTypes.func.isRequired,
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
};
|
||||
|
||||
export { GroupFooter };
|
||||
@@ -1,5 +1,11 @@
|
||||
import React, { useRef, useState, useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, {
|
||||
FC,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
Ref,
|
||||
CSSProperties,
|
||||
} from "react";
|
||||
|
||||
import copy from "copy-to-clipboard";
|
||||
|
||||
@@ -10,12 +16,11 @@ import { faEllipsisV } from "@fortawesome/free-solid-svg-icons/faEllipsisV";
|
||||
import { faShareSquare } from "@fortawesome/free-solid-svg-icons/faShareSquare";
|
||||
import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash";
|
||||
|
||||
import { APIGroup } from "Models/API";
|
||||
import { APIAlertGroupT } from "Models/APITypes";
|
||||
import { FormatAlertsQ } from "Stores/AlertStore";
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import {
|
||||
SilenceFormStore,
|
||||
SilenceTabNames,
|
||||
AlertmanagerClustersToOption,
|
||||
} from "Stores/SilenceFormStore";
|
||||
import { QueryOperators, StaticLabels, FormatQuery } from "Common/Query";
|
||||
@@ -29,8 +34,12 @@ const PopperModifiers = [
|
||||
{ name: "offset", options: { offset: "-5px, 0px" } },
|
||||
];
|
||||
|
||||
const onSilenceClick = (alertStore, silenceFormStore, group) => {
|
||||
let clusters = {};
|
||||
const onSilenceClick = (
|
||||
alertStore: AlertStore,
|
||||
silenceFormStore: SilenceFormStore,
|
||||
group: APIAlertGroupT
|
||||
) => {
|
||||
let clusters: { [cluster: string]: string[] } = {};
|
||||
Object.entries(alertStore.data.clustersWithoutReadOnly).forEach(
|
||||
([cluster, members]) => {
|
||||
members.forEach((member) => {
|
||||
@@ -47,11 +56,19 @@ const onSilenceClick = (alertStore, silenceFormStore, group) => {
|
||||
alertStore.settings.values.silenceForm.strip.labels,
|
||||
AlertmanagerClustersToOption(clusters)
|
||||
);
|
||||
silenceFormStore.tab.setTab(SilenceTabNames.Editor);
|
||||
silenceFormStore.tab.setTab("editor");
|
||||
silenceFormStore.toggle.show();
|
||||
};
|
||||
|
||||
const MenuContent = ({
|
||||
const MenuContent: FC<{
|
||||
popperPlacement?: string;
|
||||
popperRef?: Ref<any>;
|
||||
popperStyle?: CSSProperties;
|
||||
group: APIAlertGroupT;
|
||||
afterClick: () => void;
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
}> = ({
|
||||
popperPlacement,
|
||||
popperRef,
|
||||
popperStyle,
|
||||
@@ -110,21 +127,14 @@ const MenuContent = ({
|
||||
</FetchPauser>
|
||||
);
|
||||
};
|
||||
MenuContent.propTypes = {
|
||||
popperPlacement: PropTypes.string,
|
||||
popperRef: PropTypes.func,
|
||||
popperStyle: PropTypes.object,
|
||||
group: APIGroup.isRequired,
|
||||
afterClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const GroupMenu = ({
|
||||
group,
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
themed,
|
||||
setIsMenuOpen,
|
||||
}) => {
|
||||
const GroupMenu: FC<{
|
||||
group: APIAlertGroupT;
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
themed: boolean;
|
||||
setIsMenuOpen: (isOpen: boolean) => void;
|
||||
}> = ({ group, alertStore, silenceFormStore, themed, setIsMenuOpen }) => {
|
||||
const [isHidden, setIsHidden] = useState(true);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
@@ -176,12 +186,5 @@ const GroupMenu = ({
|
||||
</span>
|
||||
);
|
||||
};
|
||||
GroupMenu.propTypes = {
|
||||
group: APIGroup.isRequired,
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
themed: PropTypes.bool.isRequired,
|
||||
setIsMenuOpen: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { GroupMenu, MenuContent };
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, MouseEvent } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
import { APIGroup } from "Models/API";
|
||||
import { APIAlertGroupT } from "Models/APITypes";
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { FilteringLabel } from "Components/Labels/FilteringLabel";
|
||||
@@ -13,7 +12,16 @@ import { AlertAck } from "Components/AlertAck";
|
||||
import { ToggleIcon } from "Components/ToggleIcon";
|
||||
import { GroupMenu } from "./GroupMenu";
|
||||
|
||||
const GroupHeader = ({
|
||||
const GroupHeader: FC<{
|
||||
isCollapsed: boolean;
|
||||
setIsCollapsed: (isCollapsed: boolean) => void;
|
||||
group: APIAlertGroupT;
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
themedCounters: boolean;
|
||||
setIsMenuOpen: (isOpen: boolean) => void;
|
||||
gridLabelValue: string;
|
||||
}> = ({
|
||||
isCollapsed,
|
||||
setIsCollapsed,
|
||||
group,
|
||||
@@ -23,7 +31,7 @@ const GroupHeader = ({
|
||||
setIsMenuOpen,
|
||||
gridLabelValue,
|
||||
}) => {
|
||||
const onCollapseClick = (event) => {
|
||||
const onCollapseClick = (event: MouseEvent) => {
|
||||
// left click => toggle current group
|
||||
// left click + alt => toggle all groups
|
||||
setIsCollapsed(!isCollapsed);
|
||||
@@ -107,15 +115,5 @@ const GroupHeader = ({
|
||||
</h5>
|
||||
));
|
||||
};
|
||||
GroupHeader.propTypes = {
|
||||
isCollapsed: PropTypes.bool.isRequired,
|
||||
setIsCollapsed: PropTypes.func.isRequired,
|
||||
group: APIGroup.isRequired,
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
themedCounters: PropTypes.bool.isRequired,
|
||||
setIsMenuOpen: PropTypes.func.isRequired,
|
||||
gridLabelValue: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export { GroupHeader };
|
||||
@@ -1,19 +1,25 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { APISilenceT } from "Models/APITypes";
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { ManagedSilence } from "Components/ManagedSilence";
|
||||
const FallbackSilenceDesciption = ({ silenceID }) => {
|
||||
|
||||
const FallbackSilenceDesciption: FC<{
|
||||
silenceID: string;
|
||||
}> = ({ silenceID }) => {
|
||||
return (
|
||||
<div className="m-1">
|
||||
<small className="text-muted">Silenced by {silenceID}</small>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
FallbackSilenceDesciption.propTypes = {
|
||||
silenceID: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const GetSilenceFromStore = (alertStore, cluster, silenceID) => {
|
||||
const GetSilenceFromStore = (
|
||||
alertStore: AlertStore,
|
||||
cluster: string,
|
||||
silenceID: string
|
||||
): APISilenceT | null => {
|
||||
const amSilences = alertStore.data.silences[cluster];
|
||||
if (!amSilences) return null;
|
||||
|
||||
@@ -24,13 +30,13 @@ const GetSilenceFromStore = (alertStore, cluster, silenceID) => {
|
||||
return silence;
|
||||
};
|
||||
|
||||
const RenderSilence = (
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
afterUpdate,
|
||||
cluster,
|
||||
silenceID
|
||||
) => {
|
||||
const RenderSilence: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
afterUpdate: () => void;
|
||||
cluster: string;
|
||||
silenceID: string;
|
||||
}> = ({ alertStore, silenceFormStore, afterUpdate, cluster, silenceID }) => {
|
||||
const silence = GetSilenceFromStore(alertStore, cluster, silenceID);
|
||||
|
||||
if (silence === null) {
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useCallback, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useEffect, useCallback, useState, ReactNode } from "react";
|
||||
|
||||
import { useObserver } 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 { APIGroup } from "Models/API";
|
||||
import { APIAlertT, APIAlertGroupT, AlertStateT } from "Models/APITypes";
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
@@ -18,7 +18,11 @@ import { Alert } from "./Alert";
|
||||
import { GroupFooter } from "./GroupFooter";
|
||||
import { DefaultDetailsCollapseValue } from "./DetailsToggle";
|
||||
|
||||
const LoadButton = ({ icon, action, tooltip }) => {
|
||||
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" onClick={action}>
|
||||
@@ -27,13 +31,8 @@ const LoadButton = ({ icon, action, tooltip }) => {
|
||||
</TooltipWrapper>
|
||||
);
|
||||
};
|
||||
LoadButton.propTypes = {
|
||||
icon: FontAwesomeIcon.propTypes.icon.isRequired,
|
||||
action: PropTypes.func.isRequired,
|
||||
tooltip: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const AllAlertsAreUsingSameAlertmanagers = (alerts) => {
|
||||
const AllAlertsAreUsingSameAlertmanagers = (alerts: APIAlertT[]): boolean => {
|
||||
const usedAMs = alerts.map((alert) =>
|
||||
alert.alertmanager.map((am) => am.name).sort()
|
||||
);
|
||||
@@ -42,7 +41,17 @@ const AllAlertsAreUsingSameAlertmanagers = (alerts) => {
|
||||
);
|
||||
};
|
||||
|
||||
const AlertGroup = ({
|
||||
const AlertGroup: FC<{
|
||||
group: APIAlertGroupT;
|
||||
showAlertmanagers: boolean;
|
||||
afterUpdate: () => void;
|
||||
alertStore: AlertStore;
|
||||
settingsStore: Settings;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
groupWidth: number;
|
||||
gridLabelValue: string;
|
||||
initialAlertsToRender?: number;
|
||||
}> = ({
|
||||
group,
|
||||
showAlertmanagers,
|
||||
afterUpdate,
|
||||
@@ -78,7 +87,7 @@ const AlertGroup = ({
|
||||
// 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) => {
|
||||
const getStepSize = (totalSize: number) => {
|
||||
const val = Math.min(
|
||||
Math.max(Math.round((totalSize - defaultRenderCount) / 5), 5),
|
||||
totalSize - defaultRenderCount
|
||||
@@ -121,7 +130,7 @@ const AlertGroup = ({
|
||||
afterUpdate();
|
||||
});
|
||||
|
||||
let footerAlertmanagers = [];
|
||||
let footerAlertmanagers: string[] = [];
|
||||
let showAlertmanagersInFooter = false;
|
||||
|
||||
// There's no need to render @alertmanager labels if there's only 1
|
||||
@@ -146,11 +155,11 @@ const AlertGroup = ({
|
||||
let cardBackgroundClass = "bg-light";
|
||||
if (settingsStore.alertGroupConfig.config.colorTitleBar) {
|
||||
const stateList = Object.entries(group.stateCount)
|
||||
.filter(([k, v]) => v !== 0)
|
||||
.filter(([_, v]) => v !== 0)
|
||||
.map(([k, _]) => k);
|
||||
if (stateList.length === 1) {
|
||||
const state = stateList.pop();
|
||||
cardBackgroundClass = BackgroundClassMap[state];
|
||||
cardBackgroundClass = BackgroundClassMap[state as AlertStateT];
|
||||
themedCounters = false;
|
||||
}
|
||||
}
|
||||
@@ -160,7 +169,7 @@ const AlertGroup = ({
|
||||
className="components-grid-alertgrid-alertgroup"
|
||||
style={{
|
||||
width: groupWidth,
|
||||
zIndex: isMenuOpen ? 100 : null,
|
||||
zIndex: isMenuOpen ? 100 : undefined,
|
||||
}}
|
||||
data-defaultrendercount={
|
||||
settingsStore.alertGroupConfig.config.defaultRenderCount
|
||||
@@ -236,16 +245,5 @@ const AlertGroup = ({
|
||||
</div>
|
||||
));
|
||||
};
|
||||
AlertGroup.propTypes = {
|
||||
afterUpdate: PropTypes.func.isRequired,
|
||||
group: APIGroup.isRequired,
|
||||
showAlertmanagers: PropTypes.bool.isRequired,
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
groupWidth: PropTypes.number.isRequired,
|
||||
gridLabelValue: PropTypes.string.isRequired,
|
||||
initialAlertsToRender: PropTypes.number,
|
||||
};
|
||||
|
||||
export { AlertGroup };
|
||||
@@ -1,10 +1,18 @@
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, {
|
||||
FC,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
MouseEvent,
|
||||
Ref,
|
||||
} from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
import debounce from "lodash.debounce";
|
||||
|
||||
import { SizeDetail } from "bricks.js";
|
||||
|
||||
import TransitionGroup from "react-transition-group/TransitionGroup";
|
||||
import { CSSTransition } from "react-transition-group";
|
||||
|
||||
@@ -16,14 +24,22 @@ import { faAngleDoubleDown } from "@fortawesome/free-solid-svg-icons/faAngleDoub
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { APIGrid } from "Models/API";
|
||||
import { APIGridT } from "Models/APITypes";
|
||||
import { useGrid } from "Hooks/useGrid";
|
||||
import { ThemeContext } from "Components/Theme";
|
||||
import { DefaultDetailsCollapseValue } from "./AlertGroup/DetailsToggle";
|
||||
import { AlertGroup } from "./AlertGroup";
|
||||
import { Swimlane } from "./Swimlane";
|
||||
|
||||
const Grid = ({
|
||||
const Grid: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
settingsStore: Settings;
|
||||
gridSizesConfig: SizeDetail[];
|
||||
groupWidth: number;
|
||||
grid: APIGridT;
|
||||
outerPadding: number;
|
||||
}> = ({
|
||||
alertStore,
|
||||
settingsStore,
|
||||
silenceFormStore,
|
||||
@@ -46,7 +62,7 @@ const Grid = ({
|
||||
debouncedRepack();
|
||||
}, [debouncedRepack, isExpanded]);
|
||||
|
||||
const onCollapseClick = (event) => {
|
||||
const onCollapseClick = (event: MouseEvent) => {
|
||||
// left click => toggle current grid
|
||||
// left click + alt => toggle all grids
|
||||
|
||||
@@ -118,7 +134,7 @@ const Grid = ({
|
||||
</CSSTransition>
|
||||
<div
|
||||
className="components-grid"
|
||||
ref={ref}
|
||||
ref={ref as Ref<HTMLDivElement>}
|
||||
key={settingsStore.gridConfig.config.groupWidth}
|
||||
style={{
|
||||
paddingLeft: outerPadding + "px",
|
||||
@@ -182,14 +198,5 @@ const Grid = ({
|
||||
</React.Fragment>
|
||||
));
|
||||
};
|
||||
Grid.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
gridSizesConfig: PropTypes.array.isRequired,
|
||||
groupWidth: PropTypes.number.isRequired,
|
||||
grid: APIGrid.isRequired,
|
||||
outerPadding: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export { Grid };
|
||||
@@ -1,6 +1,8 @@
|
||||
import { SizeDetail } from "bricks.js";
|
||||
|
||||
// grid sizes, defines how many columns are used depending on the screen width
|
||||
// this is config as expected by https://github.com/callmecavs/bricks.js#sizes
|
||||
const GridSizesConfig = (canvasWidth, baseWidth) => {
|
||||
const GridSizesConfig = (baseWidth: number): SizeDetail[] => {
|
||||
const generatedSizes = [];
|
||||
for (let i = 2; i < 20; i++) {
|
||||
generatedSizes.push({
|
||||
@@ -12,14 +14,19 @@ const GridSizesConfig = (canvasWidth, baseWidth) => {
|
||||
return [...[{ columns: 1, gutter: 0 }], ...generatedSizes];
|
||||
};
|
||||
|
||||
const GetColumnsCount = (canvasWidth, baseWidth) =>
|
||||
[{ mq: "0px", columns: 1 }, ...GridSizesConfig(canvasWidth, baseWidth)]
|
||||
const GetColumnsCount = (canvasWidth: number, baseWidth: number): number =>
|
||||
[{ mq: "0px", columns: 1 }, ...GridSizesConfig(baseWidth)]
|
||||
.filter((gs) => gs.mq !== undefined)
|
||||
.filter((gs) => canvasWidth >= Number.parseInt(gs.mq))
|
||||
.filter((gs) => canvasWidth >= Number.parseInt(gs.mq as string))
|
||||
.map((gs) => gs.columns)
|
||||
.pop();
|
||||
.pop() as number;
|
||||
|
||||
const GetGridElementWidth = (innerWidth, outerWidth, outerPadding, baseWidth) =>
|
||||
const GetGridElementWidth = (
|
||||
innerWidth: number,
|
||||
outerWidth: number,
|
||||
outerPadding: number,
|
||||
baseWidth: number
|
||||
): number =>
|
||||
Math.floor(
|
||||
(innerWidth - outerPadding) / GetColumnsCount(outerWidth, baseWidth)
|
||||
);
|
||||
@@ -1,17 +1,21 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, MouseEvent } from "react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faTh } from "@fortawesome/free-solid-svg-icons/faTh";
|
||||
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { APIGrid } from "Models/API";
|
||||
import { APIGridT } from "Models/APITypes";
|
||||
import { FilteringLabel } from "Components/Labels/FilteringLabel";
|
||||
import { FilteringCounterBadge } from "Components/Labels/FilteringCounterBadge";
|
||||
import { TooltipWrapper } from "Components/TooltipWrapper";
|
||||
import { ToggleIcon } from "Components/ToggleIcon";
|
||||
|
||||
const Swimlane = ({ alertStore, grid, isExpanded, onToggle }) => {
|
||||
const Swimlane: FC<{
|
||||
alertStore: AlertStore;
|
||||
grid: APIGridT;
|
||||
isExpanded: boolean;
|
||||
onToggle: (event: MouseEvent) => void;
|
||||
}> = ({ alertStore, grid, isExpanded, onToggle }) => {
|
||||
return (
|
||||
<h5 className="components-grid-swimlane d-flex flex-row justify-content-between rounded px-2 py-1 mt-2 mb-0 border border-dark">
|
||||
<span className="flex-shrink-0 flex-grow-0">
|
||||
@@ -63,11 +67,5 @@ const Swimlane = ({ alertStore, grid, isExpanded, onToggle }) => {
|
||||
</h5>
|
||||
);
|
||||
};
|
||||
Swimlane.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
grid: APIGrid.isRequired,
|
||||
isExpanded: PropTypes.bool.isRequired,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { Swimlane };
|
||||
@@ -97,7 +97,7 @@ const ShallowGrid = () => {
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
settingsStore={settingsStore}
|
||||
gridSizesConfig={GridSizesConfig(1024, 420)}
|
||||
gridSizesConfig={GridSizesConfig(420)}
|
||||
groupWidth={420}
|
||||
grid={MockGrid()}
|
||||
outerPadding={0}
|
||||
@@ -111,7 +111,7 @@ const MountedGrid = () => {
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
settingsStore={settingsStore}
|
||||
gridSizesConfig={GridSizesConfig(1024, 420)}
|
||||
gridSizesConfig={GridSizesConfig(420)}
|
||||
groupWidth={420}
|
||||
grid={MockGrid()}
|
||||
outerPadding={0}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, Ref, useEffect, useState } from "react";
|
||||
|
||||
import { autorun } from "mobx";
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
@@ -13,12 +12,16 @@ import { useWindowSize } from "Hooks/useWindowSize";
|
||||
import { Grid } from "./Grid";
|
||||
import { GridSizesConfig, GetGridElementWidth } from "./GridSize";
|
||||
|
||||
const AlertGrid = ({ alertStore, settingsStore, silenceFormStore }) => {
|
||||
const AlertGrid: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
settingsStore: Settings;
|
||||
}> = ({ alertStore, settingsStore, silenceFormStore }) => {
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
const { ref, width: bodyWidth } = useDimensions();
|
||||
|
||||
const [gridSizesConfig, setGridSizesConfig] = useState(
|
||||
GridSizesConfig(windowWidth, settingsStore.gridConfig.config.groupWidth)
|
||||
GridSizesConfig(settingsStore.gridConfig.config.groupWidth)
|
||||
);
|
||||
const [groupWidth, setGroupWidth] = useState(
|
||||
GetGridElementWidth(
|
||||
@@ -33,10 +36,7 @@ const AlertGrid = ({ alertStore, settingsStore, silenceFormStore }) => {
|
||||
() =>
|
||||
autorun(() => {
|
||||
setGridSizesConfig(
|
||||
GridSizesConfig(
|
||||
windowWidth,
|
||||
settingsStore.gridConfig.config.groupWidth
|
||||
)
|
||||
GridSizesConfig(settingsStore.gridConfig.config.groupWidth)
|
||||
);
|
||||
setGroupWidth(
|
||||
GetGridElementWidth(
|
||||
@@ -52,7 +52,7 @@ const AlertGrid = ({ alertStore, settingsStore, silenceFormStore }) => {
|
||||
|
||||
return useObserver(() => (
|
||||
<React.Fragment>
|
||||
<div ref={ref} />
|
||||
<div ref={ref as Ref<HTMLDivElement>} />
|
||||
{alertStore.data.grids.map((grid) => (
|
||||
<Grid
|
||||
key={`${grid.labelName}/${grid.labelValue}`}
|
||||
@@ -68,10 +68,5 @@ const AlertGrid = ({ alertStore, settingsStore, silenceFormStore }) => {
|
||||
</React.Fragment>
|
||||
));
|
||||
};
|
||||
AlertGrid.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
};
|
||||
|
||||
export { AlertGrid };
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from "react";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faMugHot } from "@fortawesome/free-solid-svg-icons/faMugHot";
|
||||
|
||||
import { CenteredMessage } from "Components/CenteredMessage";
|
||||
|
||||
const EmptyGrid = () => (
|
||||
const EmptyGrid: FC = () => (
|
||||
<CenteredMessage>
|
||||
<FontAwesomeIcon
|
||||
icon={faMugHot}
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
|
||||
|
||||
import { CenteredMessage } from "Components/CenteredMessage";
|
||||
|
||||
const FatalError = ({ message }) => (
|
||||
const FatalError: FC<{ message: string }> = ({ message }) => (
|
||||
<CenteredMessage>
|
||||
<div className="container-fluid text-center">
|
||||
<FontAwesomeIcon
|
||||
@@ -19,8 +18,5 @@ const FatalError = ({ message }) => (
|
||||
</div>
|
||||
</CenteredMessage>
|
||||
);
|
||||
FatalError.propTypes = {
|
||||
message: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export { FatalError };
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useEffect } from "react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
|
||||
@@ -7,7 +6,9 @@ import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner";
|
||||
|
||||
import { CenteredMessage } from "Components/CenteredMessage";
|
||||
|
||||
const ReloadNeeded = ({ reloadAfter }) => {
|
||||
const ReloadNeeded: FC<{
|
||||
reloadAfter: number;
|
||||
}> = ({ reloadAfter }) => {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => window.location.reload(), reloadAfter);
|
||||
return () => clearTimeout(timer);
|
||||
@@ -29,8 +30,5 @@ const ReloadNeeded = ({ reloadAfter }) => {
|
||||
</CenteredMessage>
|
||||
);
|
||||
};
|
||||
ReloadNeeded.propTypes = {
|
||||
reloadAfter: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export { ReloadNeeded };
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useEffect } from "react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faRocket } from "@fortawesome/free-solid-svg-icons/faRocket";
|
||||
@@ -9,7 +8,10 @@ import { CenteredMessage } from "Components/CenteredMessage";
|
||||
|
||||
import "csshake/scss/csshake-slow.scss";
|
||||
|
||||
const UpgradeNeeded = ({ newVersion, reloadAfter }) => {
|
||||
const UpgradeNeeded: FC<{
|
||||
newVersion: string;
|
||||
reloadAfter: number;
|
||||
}> = ({ newVersion, reloadAfter }) => {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => window.location.reload(), reloadAfter);
|
||||
return () => clearTimeout(timer);
|
||||
@@ -32,9 +34,5 @@ const UpgradeNeeded = ({ newVersion, reloadAfter }) => {
|
||||
</CenteredMessage>
|
||||
);
|
||||
};
|
||||
UpgradeNeeded.propTypes = {
|
||||
newVersion: PropTypes.string.isRequired,
|
||||
reloadAfter: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export { UpgradeNeeded };
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
@@ -13,7 +12,11 @@ import { UpgradeNeeded } from "./UpgradeNeeded";
|
||||
import { ReloadNeeded } from "./ReloadNeeded";
|
||||
import { EmptyGrid } from "./EmptyGrid";
|
||||
|
||||
const Grid = ({ alertStore, settingsStore, silenceFormStore }) => {
|
||||
const Grid: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
settingsStore: Settings;
|
||||
}> = ({ alertStore, settingsStore, silenceFormStore }) => {
|
||||
return useObserver(() =>
|
||||
alertStore.info.upgradeNeeded ? (
|
||||
<UpgradeNeeded newVersion={alertStore.info.version} reloadAfter={3000} />
|
||||
@@ -50,10 +53,5 @@ const Grid = ({ alertStore, settingsStore, silenceFormStore }) => {
|
||||
)
|
||||
);
|
||||
};
|
||||
Grid.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
};
|
||||
|
||||
export { Grid };
|
||||
@@ -19,9 +19,9 @@ const FilteringCounterBadge: FC<{
|
||||
value: string;
|
||||
counter: number;
|
||||
themed: boolean;
|
||||
alwaysVisible: boolean;
|
||||
defaultColor: "light" | "primary";
|
||||
isAppend: boolean;
|
||||
alwaysVisible?: boolean;
|
||||
defaultColor?: "light" | "primary";
|
||||
isAppend?: boolean;
|
||||
}> = observer(
|
||||
({
|
||||
alertStore,
|
||||
@@ -29,7 +29,7 @@ const FilteringCounterBadge: FC<{
|
||||
value,
|
||||
counter,
|
||||
themed,
|
||||
alwaysVisible,
|
||||
alwaysVisible = false,
|
||||
defaultColor = "light",
|
||||
isAppend = true,
|
||||
}) => {
|
||||
|
||||
@@ -5,13 +5,28 @@ import {
|
||||
StateLabelClassMap,
|
||||
} from "Common/Colors";
|
||||
import { StaticLabels } from "Common/Query";
|
||||
import { AlertStateT } from "Models/APITypes";
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
|
||||
const isBackgroundDark = (brightness) => brightness <= 125;
|
||||
const isBackgroundDark = (brightness: number) => brightness <= 125;
|
||||
|
||||
const GetClassAndStyle = (alertStore, name, value, extraClass, baseClass) => {
|
||||
export interface ClassAndStyleT {
|
||||
style: { [key: string]: string | number };
|
||||
className: string;
|
||||
baseClassNames: string[];
|
||||
colorClassNames: string[];
|
||||
}
|
||||
|
||||
const GetClassAndStyle = (
|
||||
alertStore: AlertStore,
|
||||
name: string,
|
||||
value: string,
|
||||
extraClass?: string,
|
||||
baseClass?: "badge" | "btn"
|
||||
): ClassAndStyleT => {
|
||||
const elementType = baseClass || "badge";
|
||||
|
||||
const data = {
|
||||
const data: ClassAndStyleT = {
|
||||
style: {},
|
||||
className: "",
|
||||
baseClassNames: ["components-label", elementType],
|
||||
@@ -22,8 +37,8 @@ const GetClassAndStyle = (alertStore, name, value, extraClass, baseClass) => {
|
||||
data.colorClassNames.push(AlertNameLabelClassMap[elementType]);
|
||||
} else if (name === StaticLabels.State) {
|
||||
data.colorClassNames.push(
|
||||
StateLabelClassMap[value]
|
||||
? `${elementType}-${StateLabelClassMap[value]}`
|
||||
StateLabelClassMap[value as AlertStateT]
|
||||
? `${elementType}-${StateLabelClassMap[value as AlertStateT]}`
|
||||
: DefaultLabelClassMap[elementType]
|
||||
);
|
||||
} else if (alertStore.settings.values.staticColorLabels.includes(name)) {
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useEffect, useState, ReactNode } from "react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
|
||||
@@ -7,7 +6,7 @@ import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclama
|
||||
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
|
||||
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
|
||||
|
||||
import { APISilence } from "Models/API";
|
||||
import { APISilenceT } from "Models/APITypes";
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { FormatQuery, QueryOperators, StaticLabels } from "Common/Query";
|
||||
@@ -15,7 +14,7 @@ import { useFetchDelete } from "Hooks/useFetchDelete";
|
||||
import { Modal } from "Components/Modal";
|
||||
import { PaginatedAlertList } from "Components/PaginatedAlertList";
|
||||
|
||||
const ProgressMessage = () => (
|
||||
const ProgressMessage: FC = () => (
|
||||
<div className="text-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faCircleNotch}
|
||||
@@ -25,7 +24,9 @@ const ProgressMessage = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const ErrorMessage = ({ message }) => (
|
||||
const ErrorMessage: FC<{
|
||||
message: ReactNode;
|
||||
}> = ({ message }) => (
|
||||
<div className="text-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faExclamationCircle}
|
||||
@@ -34,11 +35,8 @@ const ErrorMessage = ({ message }) => (
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
);
|
||||
ErrorMessage.propTypes = {
|
||||
message: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const SuccessMessage = () => (
|
||||
const SuccessMessage: FC = () => (
|
||||
<div className="text-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
@@ -51,7 +49,11 @@ const SuccessMessage = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const DeleteResult = ({ alertStore, cluster, silence }) => {
|
||||
const DeleteResult: FC<{
|
||||
alertStore: AlertStore;
|
||||
cluster: string;
|
||||
silence: APISilenceT;
|
||||
}> = ({ alertStore, cluster, silence }) => {
|
||||
const [currentTime, setCurrentTime] = useState(Math.floor(Date.now()));
|
||||
|
||||
const am = alertStore.data.readWriteAlertmanagers
|
||||
@@ -97,19 +99,14 @@ const DeleteResult = ({ alertStore, cluster, silence }) => {
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
DeleteResult.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
cluster: PropTypes.string.isRequired,
|
||||
silence: APISilence.isRequired,
|
||||
};
|
||||
|
||||
const DeleteSilenceModalContent = ({
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
cluster,
|
||||
silence,
|
||||
onHide,
|
||||
}) => {
|
||||
const DeleteSilenceModalContent: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
cluster: string;
|
||||
silence: APISilenceT;
|
||||
onHide: () => void;
|
||||
}> = ({ alertStore, silenceFormStore, cluster, silence, onHide }) => {
|
||||
const [confirm, setConfirm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -149,7 +146,7 @@ const DeleteSilenceModalContent = ({
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger mr-2"
|
||||
onClick={setConfirm}
|
||||
onClick={() => setConfirm(true)}
|
||||
disabled={confirm}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="mr-1" />
|
||||
@@ -162,21 +159,14 @@ const DeleteSilenceModalContent = ({
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
DeleteSilenceModalContent.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
cluster: PropTypes.string.isRequired,
|
||||
silence: APISilence.isRequired,
|
||||
onHide: PropTypes.func,
|
||||
};
|
||||
|
||||
const DeleteSilence = ({
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
cluster,
|
||||
silence,
|
||||
isUpper,
|
||||
}) => {
|
||||
const DeleteSilence: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
cluster: string;
|
||||
silence: APISilenceT;
|
||||
isUpper?: boolean;
|
||||
}> = ({ alertStore, silenceFormStore, cluster, silence, isUpper = false }) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const members = alertStore.data.getClusterAlertmanagersWithoutReadOnly(
|
||||
@@ -214,15 +204,5 @@ const DeleteSilence = ({
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
DeleteSilence.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
cluster: PropTypes.string.isRequired,
|
||||
silence: APISilence.isRequired,
|
||||
isUpper: PropTypes.bool,
|
||||
};
|
||||
DeleteSilence.defaultProps = {
|
||||
isUpper: false,
|
||||
};
|
||||
|
||||
export { DeleteSilence, DeleteSilenceModalContent, DeleteResult };
|
||||
@@ -1,24 +1,30 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons/faExternalLinkAlt";
|
||||
import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash";
|
||||
|
||||
import { APISilence } from "Models/API";
|
||||
import { APISilenceT } from "Models/APITypes";
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { FilteringCounterBadge } from "Components/Labels/FilteringCounterBadge";
|
||||
import { ToggleIcon } from "Components/ToggleIcon";
|
||||
import { SilenceProgress } from "./SilenceProgress";
|
||||
|
||||
const SilenceComment = ({
|
||||
const SilenceComment: FC<{
|
||||
cluster: string;
|
||||
silence: APISilenceT;
|
||||
alertCount: number;
|
||||
collapsed: boolean;
|
||||
collapseToggle: () => void;
|
||||
alertStore: AlertStore;
|
||||
alertCountAlwaysVisible: boolean;
|
||||
}> = ({
|
||||
cluster,
|
||||
silence,
|
||||
alertCount,
|
||||
alertCountAlwaysVisible,
|
||||
collapsed,
|
||||
collapseToggle,
|
||||
afterUpdate,
|
||||
alertStore,
|
||||
}) => {
|
||||
const comment = silence.ticketURL ? (
|
||||
@@ -94,13 +100,5 @@ const SilenceComment = ({
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
SilenceComment.propTypes = {
|
||||
cluster: PropTypes.string.isRequired,
|
||||
silence: APISilence.isRequired,
|
||||
alertCount: PropTypes.number.isRequired,
|
||||
collapsed: PropTypes.bool.isRequired,
|
||||
collapseToggle: PropTypes.func.isRequired,
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
};
|
||||
|
||||
export { SilenceComment };
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useState } from "react";
|
||||
|
||||
import parseISO from "date-fns/parseISO";
|
||||
|
||||
@@ -16,7 +15,8 @@ import { faHome } from "@fortawesome/free-solid-svg-icons/faHome";
|
||||
import { faFingerprint } from "@fortawesome/free-solid-svg-icons/faFingerprint";
|
||||
import { faCopy } from "@fortawesome/free-solid-svg-icons/faCopy";
|
||||
|
||||
import { APISilence } from "Models/API";
|
||||
import { APISilenceT } from "Models/APITypes";
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { QueryOperators } from "Common/Query";
|
||||
import { TooltipWrapper } from "Components/TooltipWrapper";
|
||||
@@ -25,7 +25,9 @@ import { DateFromNow } from "Components/DateFromNow";
|
||||
import { useFlashTransition } from "Hooks/useFlashTransition";
|
||||
import { DeleteSilence } from "./DeleteSilence";
|
||||
|
||||
const SilenceIDCopyButton = ({ id }) => {
|
||||
const SilenceIDCopyButton: FC<{
|
||||
id: string;
|
||||
}> = ({ id }) => {
|
||||
const [clickCount, setClickCount] = useState(0);
|
||||
const { ref, props } = useFlashTransition(clickCount);
|
||||
|
||||
@@ -47,13 +49,20 @@ const SilenceIDCopyButton = ({ id }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const SilenceDetails = ({
|
||||
const SilenceDetails: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
silence: APISilenceT;
|
||||
cluster: string;
|
||||
onEditSilence: () => void;
|
||||
isUpper?: boolean;
|
||||
}> = ({
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
silence,
|
||||
cluster,
|
||||
onEditSilence,
|
||||
isUpper,
|
||||
isUpper = false,
|
||||
}) => {
|
||||
const isExpired = parseISO(silence.endsAt) < new Date();
|
||||
let expiresClass = "";
|
||||
@@ -186,15 +195,5 @@ const SilenceDetails = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
SilenceDetails.propTypes = {
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
cluster: PropTypes.string.isRequired,
|
||||
silence: APISilence.isRequired,
|
||||
onEditSilence: PropTypes.func.isRequired,
|
||||
isUpper: PropTypes.bool,
|
||||
};
|
||||
SilenceDetails.defaultProps = {
|
||||
isUpper: false,
|
||||
};
|
||||
|
||||
export { SilenceDetails };
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
import parseISO from "date-fns/parseISO";
|
||||
import getUnixTime from "date-fns/getUnixTime";
|
||||
|
||||
import { APISilence } from "Models/API";
|
||||
import { APISilenceT } from "Models/APITypes";
|
||||
import { DateFromNow } from "Components/DateFromNow";
|
||||
|
||||
import "./SilenceProgress.scss";
|
||||
|
||||
const calculatePercent = (startsAt, endsAt) => {
|
||||
const calculatePercent = (startsAt: string, endsAt: string) => {
|
||||
const durationDone =
|
||||
getUnixTime(new Date()) - getUnixTime(parseISO(startsAt));
|
||||
const durationTotal =
|
||||
@@ -18,7 +18,9 @@ const calculatePercent = (startsAt, endsAt) => {
|
||||
return Math.floor((durationDone / durationTotal) * 100);
|
||||
};
|
||||
|
||||
const SilenceProgress = ({ silence }) => {
|
||||
const SilenceProgress: FC<{
|
||||
silence: APISilenceT;
|
||||
}> = ({ silence }) => {
|
||||
const [progress, setProgress] = useState(
|
||||
calculatePercent(silence.startsAt, silence.endsAt)
|
||||
);
|
||||
@@ -50,16 +52,13 @@ const SilenceProgress = ({ silence }) => {
|
||||
role="progressbar"
|
||||
style={{ width: progress + "%" }}
|
||||
aria-valuenow={progress}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
)
|
||||
);
|
||||
};
|
||||
SilenceProgress.propTypes = {
|
||||
silence: APISilence.isRequired,
|
||||
};
|
||||
|
||||
export { SilenceProgress };
|
||||
@@ -1,27 +1,39 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
|
||||
import { APISilence } from "Models/API";
|
||||
import { APISilenceT, APIAlertmanagerUpstreamT } from "Models/APITypes";
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { SilenceFormStore, SilenceTabNames } from "Stores/SilenceFormStore";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { SilenceComment } from "./SilenceComment";
|
||||
import { SilenceDetails } from "./SilenceDetails";
|
||||
|
||||
const GetAlertmanager = (alertStore, cluster) =>
|
||||
const GetAlertmanager = (
|
||||
alertStore: AlertStore,
|
||||
cluster: string
|
||||
): APIAlertmanagerUpstreamT =>
|
||||
alertStore.data.readWriteAlertmanagers
|
||||
.filter((u) => u.cluster === cluster)
|
||||
.slice(0, 1)[0];
|
||||
|
||||
const ManagedSilence = ({
|
||||
const ManagedSilence: FC<{
|
||||
cluster: string;
|
||||
alertCount: number;
|
||||
alertCountAlwaysVisible: boolean;
|
||||
silence: APISilenceT;
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
isOpen?: boolean;
|
||||
onDidUpdate?: () => void;
|
||||
isNested?: boolean;
|
||||
}> = ({
|
||||
cluster,
|
||||
alertCount,
|
||||
alertCountAlwaysVisible,
|
||||
silence,
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
isOpen,
|
||||
isOpen = false,
|
||||
onDidUpdate,
|
||||
isNested,
|
||||
isNested = false,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (onDidUpdate) onDidUpdate();
|
||||
@@ -34,7 +46,7 @@ const ManagedSilence = ({
|
||||
|
||||
silenceFormStore.data.fillFormFromSilence(alertmanager, silence);
|
||||
silenceFormStore.data.resetProgress();
|
||||
silenceFormStore.tab.setTab(SilenceTabNames.Editor);
|
||||
silenceFormStore.tab.setTab("editor");
|
||||
silenceFormStore.toggle.show();
|
||||
};
|
||||
|
||||
@@ -66,21 +78,5 @@ const ManagedSilence = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
ManagedSilence.propTypes = {
|
||||
cluster: PropTypes.string.isRequired,
|
||||
alertCount: PropTypes.number.isRequired,
|
||||
alertCountAlwaysVisible: PropTypes.bool.isRequired,
|
||||
silence: APISilence.isRequired,
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
onDidUpdate: PropTypes.func,
|
||||
onDeleteModalClose: PropTypes.func,
|
||||
isOpen: PropTypes.bool,
|
||||
isNested: PropTypes.bool,
|
||||
};
|
||||
ManagedSilence.defaultProps = {
|
||||
isOpen: false,
|
||||
isNested: false,
|
||||
};
|
||||
|
||||
export { ManagedSilence, GetAlertmanager };
|
||||
@@ -54,6 +54,7 @@ const Modal: FC<{
|
||||
isOpen: boolean;
|
||||
isUpper?: boolean;
|
||||
toggleOpen: () => void;
|
||||
onExited?: () => void;
|
||||
}> = ({
|
||||
size = "lg",
|
||||
isOpen,
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
|
||||
import { faPauseCircle } from "@fortawesome/free-regular-svg-icons/faPauseCircle";
|
||||
|
||||
import { AlertStore, AlertStoreStatuses } from "Stores/AlertStore";
|
||||
|
||||
const FetchIcon = ({ icon, color, visible, spin }) => (
|
||||
const FetchIcon: FC<{
|
||||
icon: IconDefinition;
|
||||
color?: string;
|
||||
visible?: boolean;
|
||||
spin?: boolean;
|
||||
}> = ({ icon, color = "muted", visible = true, spin = false }) => (
|
||||
<FontAwesomeIcon
|
||||
style={{ opacity: visible ? 1 : 0 }}
|
||||
className={`mx-1 text-${color}`}
|
||||
@@ -18,19 +23,10 @@ const FetchIcon = ({ icon, color, visible, spin }) => (
|
||||
spin={spin}
|
||||
/>
|
||||
);
|
||||
FetchIcon.propTypes = {
|
||||
icon: FontAwesomeIcon.propTypes.icon.isRequired,
|
||||
color: PropTypes.string,
|
||||
visible: PropTypes.bool,
|
||||
spin: PropTypes.bool,
|
||||
};
|
||||
FetchIcon.defaultProps = {
|
||||
color: "muted",
|
||||
visible: true,
|
||||
spin: false,
|
||||
};
|
||||
|
||||
const FetchIndicator = ({ alertStore }) => {
|
||||
const FetchIndicator: FC<{
|
||||
alertStore: AlertStore;
|
||||
}> = ({ alertStore }) => {
|
||||
return useObserver(() =>
|
||||
alertStore.status.paused ? (
|
||||
<FetchIcon icon={faPauseCircle} />
|
||||
@@ -49,8 +45,5 @@ const FetchIndicator = ({ alertStore }) => {
|
||||
)
|
||||
);
|
||||
};
|
||||
FetchIndicator.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
};
|
||||
|
||||
export { FetchIndicator };
|
||||
@@ -1,11 +1,20 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, {
|
||||
FC,
|
||||
Ref,
|
||||
CSSProperties,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
import { localStored } from "mobx-stored";
|
||||
|
||||
import { Manager, Reference, Popper } from "react-popper";
|
||||
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faHistory } from "@fortawesome/free-solid-svg-icons/faHistory";
|
||||
import { faCaretDown } from "@fortawesome/free-solid-svg-icons/faCaretDown";
|
||||
@@ -13,7 +22,7 @@ import { faSave } from "@fortawesome/free-regular-svg-icons/faSave";
|
||||
import { faUndoAlt } from "@fortawesome/free-solid-svg-icons/faUndoAlt";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
|
||||
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { AlertStore, FilterT } from "Stores/AlertStore";
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { IsMobile } from "Common/Device";
|
||||
import { CommonPopperModifiers } from "Common/Popper";
|
||||
@@ -21,9 +30,16 @@ import { DropdownSlide } from "Components/Animations/DropdownSlide";
|
||||
import { HistoryLabel } from "Components/Labels/HistoryLabel";
|
||||
import { useOnClickOutside } from "Hooks/useOnClickOutside";
|
||||
|
||||
interface ReduceFilterT {
|
||||
raw: string;
|
||||
name: string;
|
||||
matcher: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
// takes a filter object out of alertStore.history.values and creates a new
|
||||
// object with only those keys that will be stored in history
|
||||
function ReduceFilter(filter) {
|
||||
function ReduceFilter(filter: FilterT): ReduceFilterT {
|
||||
return {
|
||||
raw: filter.raw,
|
||||
name: filter.name,
|
||||
@@ -32,7 +48,13 @@ function ReduceFilter(filter) {
|
||||
};
|
||||
}
|
||||
|
||||
const ActionButton = ({ color, icon, title, action, afterClick }) => (
|
||||
const ActionButton: FC<{
|
||||
color: string;
|
||||
icon: IconDefinition;
|
||||
title: ReactNode;
|
||||
action: () => void;
|
||||
afterClick: () => void;
|
||||
}> = ({ color, icon, title, action, afterClick }) => (
|
||||
<button
|
||||
className={`component-history-button btn btn-sm btn-${color}`}
|
||||
onClick={() => {
|
||||
@@ -44,15 +66,17 @@ const ActionButton = ({ color, icon, title, action, afterClick }) => (
|
||||
{title}
|
||||
</button>
|
||||
);
|
||||
ActionButton.propTypes = {
|
||||
color: PropTypes.string.isRequired,
|
||||
title: PropTypes.node.isRequired,
|
||||
icon: FontAwesomeIcon.propTypes.icon.isRequired,
|
||||
action: PropTypes.func.isRequired,
|
||||
afterClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const HistoryMenu = ({
|
||||
const HistoryMenu: FC<{
|
||||
popperPlacement?: string;
|
||||
popperRef?: Ref<any>;
|
||||
popperStyle?: CSSProperties;
|
||||
filters: ReduceFilterT[][];
|
||||
alertStore: AlertStore;
|
||||
settingsStore: Settings;
|
||||
afterClick: () => void;
|
||||
onClear: () => void;
|
||||
}> = ({
|
||||
popperPlacement,
|
||||
popperRef,
|
||||
popperStyle,
|
||||
@@ -132,20 +156,17 @@ const HistoryMenu = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
HistoryMenu.propTypes = {
|
||||
popperPlacement: PropTypes.string,
|
||||
popperRef: PropTypes.func,
|
||||
popperStyle: PropTypes.object,
|
||||
filters: PropTypes.array.isRequired,
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
afterClick: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const History = ({ alertStore, settingsStore }) => {
|
||||
interface historyStorageT {
|
||||
filters: ReduceFilterT[][];
|
||||
}
|
||||
|
||||
const History: FC<{
|
||||
alertStore: AlertStore;
|
||||
settingsStore: Settings;
|
||||
}> = ({ alertStore, settingsStore }) => {
|
||||
// this will be dumped to local storage via mobx-stored
|
||||
const history = localStored(
|
||||
const history: historyStorageT = localStored(
|
||||
"history.filters",
|
||||
{
|
||||
filters: [],
|
||||
@@ -190,7 +211,7 @@ const History = ({ alertStore, settingsStore }) => {
|
||||
}
|
||||
});
|
||||
|
||||
const ref = useRef(null);
|
||||
const ref = useRef(null as null | HTMLElement);
|
||||
useOnClickOutside(ref, hide, isVisible);
|
||||
|
||||
return useObserver(() => (
|
||||
@@ -245,9 +266,5 @@ const History = ({ alertStore, settingsStore }) => {
|
||||
</span>
|
||||
));
|
||||
};
|
||||
History.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
};
|
||||
|
||||
export { History, HistoryMenu, ReduceFilter };
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useState, useRef, useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useEffect, useState, useRef, useCallback } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
@@ -19,9 +18,12 @@ import { FilterInputLabel } from "Components/Labels/FilterInputLabel";
|
||||
import { AutosuggestTheme } from "./Constants";
|
||||
import { History } from "./History";
|
||||
|
||||
const FilterInput = ({ alertStore, settingsStore }) => {
|
||||
const autosuggestRef = useRef();
|
||||
const inputRef = useRef();
|
||||
const FilterInput: FC<{
|
||||
alertStore: AlertStore;
|
||||
settingsStore: Settings;
|
||||
}> = ({ alertStore, settingsStore }) => {
|
||||
const autosuggestRef = useRef(null as null | Autosuggest<any, any>);
|
||||
const inputRef = useRef(null as null | HTMLElement);
|
||||
const formRef = useRef(null);
|
||||
|
||||
const [suggestions, setSuggestions] = useState([]);
|
||||
@@ -31,14 +33,14 @@ const FilterInput = ({ alertStore, settingsStore }) => {
|
||||
const onSuggestionsClearRequested = useCallback(() => setSuggestions([]), []);
|
||||
|
||||
const onSuggestionSelected = useCallback(
|
||||
(event, { suggestion }) => {
|
||||
(_, { suggestion }) => {
|
||||
setValue("");
|
||||
alertStore.filters.addFilter(suggestion);
|
||||
},
|
||||
[alertStore.filters]
|
||||
);
|
||||
|
||||
const onChange = useCallback((event, { newValue }) => setValue(newValue), []);
|
||||
const onChange = useCallback((_, { newValue }) => setValue(newValue), []);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(event) => {
|
||||
@@ -52,9 +54,11 @@ const FilterInput = ({ alertStore, settingsStore }) => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current = autosuggestRef.current.input.parentElement;
|
||||
inputRef.current = ((autosuggestRef.current as Autosuggest)
|
||||
.input as HTMLInputElement).parentElement;
|
||||
if (!IsMobile()) {
|
||||
autosuggestRef.current.input.focus();
|
||||
((autosuggestRef.current as Autosuggest)
|
||||
.input as HTMLInputElement).focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -90,16 +94,20 @@ const FilterInput = ({ alertStore, settingsStore }) => {
|
||||
}
|
||||
}, [response, error, isLoading, onSuggestionsClearRequested]);
|
||||
|
||||
const onInputClick = (event) => {
|
||||
const onInputClick = (className: string) => {
|
||||
if (
|
||||
typeof event.target.className === "string" &&
|
||||
event.target.className.split(" ").includes("form-control")
|
||||
typeof className === "string" &&
|
||||
className.split(" ").includes("form-control")
|
||||
) {
|
||||
autosuggestRef.current.input.focus();
|
||||
((autosuggestRef.current as Autosuggest)
|
||||
.input as HTMLInputElement).focus();
|
||||
}
|
||||
};
|
||||
|
||||
const renderSuggestion = (suggestion, { query, isHighlighted }) => {
|
||||
const renderSuggestion = (
|
||||
suggestion: string,
|
||||
{ query }: { query: string }
|
||||
) => {
|
||||
return (
|
||||
<Highlight
|
||||
matchElement="span"
|
||||
@@ -111,13 +119,16 @@ const FilterInput = ({ alertStore, settingsStore }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderInputComponent = (inputProps) => {
|
||||
const { value } = inputProps;
|
||||
const renderInputComponent: FC<{ value: string }> = ({
|
||||
value,
|
||||
...inputProps
|
||||
}) => {
|
||||
return (
|
||||
<input
|
||||
className="components-filterinput-wrapper text-white mw-100"
|
||||
placeholder=""
|
||||
size={value.length + 1}
|
||||
value={value}
|
||||
{...inputProps}
|
||||
/>
|
||||
);
|
||||
@@ -140,7 +151,9 @@ const FilterInput = ({ alertStore, settingsStore }) => {
|
||||
</div>
|
||||
<div
|
||||
className="form-control components-filterinput border-0 rounded-0 bg-transparent"
|
||||
onClick={onInputClick}
|
||||
onClick={(event) =>
|
||||
onInputClick((event.target as HTMLDivElement).className)
|
||||
}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
@@ -158,7 +171,7 @@ const FilterInput = ({ alertStore, settingsStore }) => {
|
||||
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
||||
onSuggestionSelected={onSuggestionSelected}
|
||||
shouldRenderSuggestions={(value) =>
|
||||
value && value.trim().length > 1
|
||||
value ? value.trim().length > 1 : false
|
||||
}
|
||||
getSuggestionValue={(suggestion) => suggestion}
|
||||
renderSuggestion={renderSuggestion}
|
||||
@@ -181,9 +194,5 @@ const FilterInput = ({ alertStore, settingsStore }) => {
|
||||
</form>
|
||||
));
|
||||
};
|
||||
FilterInput.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
};
|
||||
|
||||
export { FilterInput };
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useRef, useEffect, useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useState, useRef, useEffect, useCallback } from "react";
|
||||
|
||||
import { reaction } from "mobx";
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
@@ -24,8 +23,13 @@ import { FilterInput } from "./FilterInput";
|
||||
const DesktopIdleTimeout = 1000 * 60 * 3;
|
||||
const MobileIdleTimeout = 1000 * 12;
|
||||
|
||||
const NavBar = ({ alertStore, settingsStore, silenceFormStore, fixedTop }) => {
|
||||
const idleTimer = useRef(null);
|
||||
const NavBar: FC<{
|
||||
alertStore: AlertStore;
|
||||
settingsStore: Settings;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
fixedTop?: boolean;
|
||||
}> = ({ alertStore, settingsStore, silenceFormStore, fixedTop = true }) => {
|
||||
const idleTimer = useRef(null as null | IdleTimer);
|
||||
const [isIdle, setIsIdle] = useState(false);
|
||||
const [containerClass, setContainerClass] = useState("visible");
|
||||
|
||||
@@ -51,16 +55,16 @@ const NavBar = ({ alertStore, settingsStore, silenceFormStore, fixedTop }) => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
let timer: number;
|
||||
if (isIdle) {
|
||||
timer = setTimeout(
|
||||
timer = window.setTimeout(
|
||||
() => updateBodyPaddingTop(true),
|
||||
context.animations.duration
|
||||
);
|
||||
} else {
|
||||
updateBodyPaddingTop(false);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [height, updateBodyPaddingTop, isIdle, context.animations.duration]);
|
||||
|
||||
useEffect(
|
||||
@@ -134,14 +138,5 @@ const NavBar = ({ alertStore, settingsStore, silenceFormStore, fixedTop }) => {
|
||||
</IdleTimer>
|
||||
));
|
||||
};
|
||||
NavBar.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
fixedTop: PropTypes.bool,
|
||||
};
|
||||
NavBar.defaultProps = {
|
||||
fixedTop: true,
|
||||
};
|
||||
|
||||
export { NavBar, MobileIdleTimeout, DesktopIdleTimeout };
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useEffect } from "react";
|
||||
|
||||
import { autorun } from "mobx";
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
@@ -11,10 +10,14 @@ import {
|
||||
SilenceFormStore,
|
||||
AlertmanagerClustersToOption,
|
||||
} from "Stores/SilenceFormStore";
|
||||
import { MultiValueOptionT } from "Common/Select";
|
||||
import { ThemeContext } from "Components/Theme";
|
||||
import { ValidationError } from "Components/ValidationError";
|
||||
|
||||
const AlertManagerInput = ({ alertStore, silenceFormStore }) => {
|
||||
const AlertManagerInput: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
}> = ({ alertStore, silenceFormStore }) => {
|
||||
useEffect(() => {
|
||||
if (silenceFormStore.data.alertmanagers.length === 0) {
|
||||
silenceFormStore.data.setAlertmanagers(
|
||||
@@ -68,15 +71,13 @@ const AlertManagerInput = ({ alertStore, silenceFormStore }) => {
|
||||
}
|
||||
isMulti
|
||||
onChange={(newValue) => {
|
||||
silenceFormStore.data.setAlertmanagers(newValue || []);
|
||||
silenceFormStore.data.setAlertmanagers(
|
||||
(newValue as MultiValueOptionT[]) || ([] as MultiValueOptionT[])
|
||||
);
|
||||
}}
|
||||
isDisabled={silenceFormStore.data.silenceID !== null}
|
||||
/>
|
||||
));
|
||||
};
|
||||
AlertManagerInput.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
};
|
||||
|
||||
export { AlertManagerInput };
|
||||
@@ -1,20 +1,20 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, ChangeEvent } from "react";
|
||||
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faUser } from "@fortawesome/free-solid-svg-icons/faUser";
|
||||
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
|
||||
const IconInput = ({
|
||||
type,
|
||||
autoComplete,
|
||||
icon,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
...extra
|
||||
}) => (
|
||||
const IconInput: FC<{
|
||||
type: string;
|
||||
autoComplete: string;
|
||||
icon: IconDefinition;
|
||||
placeholder: string;
|
||||
value: string;
|
||||
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
readOnly?: boolean;
|
||||
}> = ({ type, autoComplete, icon, placeholder, value, onChange, ...extra }) => (
|
||||
<div className="input-group mb-3">
|
||||
<div className="input-group-prepend">
|
||||
<span className="input-group-text">
|
||||
@@ -33,16 +33,10 @@ const IconInput = ({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
IconInput.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
autoComplete: PropTypes.string.isRequired,
|
||||
icon: FontAwesomeIcon.propTypes.icon.isRequired,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
const AuthenticatedAuthorInput = ({ alertStore }) => (
|
||||
const AuthenticatedAuthorInput: FC<{
|
||||
alertStore: AlertStore;
|
||||
}> = ({ alertStore }) => (
|
||||
<IconInput
|
||||
type="text"
|
||||
autoComplete="email"
|
||||
@@ -52,8 +46,5 @@ const AuthenticatedAuthorInput = ({ alertStore }) => (
|
||||
readOnly={true}
|
||||
/>
|
||||
);
|
||||
AuthenticatedAuthorInput.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
};
|
||||
|
||||
export { IconInput, AuthenticatedAuthorInput };
|
||||
@@ -64,8 +64,6 @@ afterEach(() => {
|
||||
global.window.innerWidth = 1024;
|
||||
});
|
||||
|
||||
const MockOnDeleteModalClose = jest.fn();
|
||||
|
||||
const MockSilenceList = (count) => {
|
||||
let silences = [];
|
||||
for (var index = 1; index <= count; index++) {
|
||||
@@ -86,7 +84,6 @@ const MountedBrowser = () => {
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
settingsStore={settingsStore}
|
||||
onDeleteModalClose={MockOnDeleteModalClose}
|
||||
/>,
|
||||
{
|
||||
wrappingComponent: ThemeContext.Provider,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useState, useEffect, ReactNode } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
@@ -11,17 +10,20 @@ import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclama
|
||||
import { faSortAmountDownAlt } from "@fortawesome/free-solid-svg-icons/faSortAmountDownAlt";
|
||||
import { faSortAmountUp } from "@fortawesome/free-solid-svg-icons/faSortAmountUp";
|
||||
|
||||
import { APIManagedSilenceT } from "Models/APITypes";
|
||||
import { AlertStore, FormatBackendURI } from "Stores/AlertStore";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { useFetchGet } from "Hooks/useFetchGet";
|
||||
import { useFetchGet, FetchGetOptionsT } from "Hooks/useFetchGet";
|
||||
import { useDebounce } from "Hooks/useDebounce";
|
||||
import { IsMobile } from "Common/Device";
|
||||
import { ManagedSilence } from "Components/ManagedSilence";
|
||||
import { PageSelect } from "Components/Pagination";
|
||||
import { ThemeContext } from "Components/Theme";
|
||||
|
||||
const FetchError = ({ message }) => (
|
||||
const FetchError: FC<{
|
||||
message: ReactNode;
|
||||
}> = ({ message }) => (
|
||||
<div className="text-center">
|
||||
<h2 className="display-2 text-danger">
|
||||
<FontAwesomeIcon icon={faExclamationCircle} />
|
||||
@@ -29,11 +31,10 @@ const FetchError = ({ message }) => (
|
||||
<p className="lead text-muted">{message}</p>
|
||||
</div>
|
||||
);
|
||||
FetchError.propTypes = {
|
||||
message: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const Placeholder = ({ content }) => {
|
||||
const Placeholder: FC<{
|
||||
content: ReactNode;
|
||||
}> = ({ content }) => {
|
||||
const context = React.useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
@@ -49,16 +50,12 @@ const Placeholder = ({ content }) => {
|
||||
</CSSTransition>
|
||||
);
|
||||
};
|
||||
Placeholder.propTypes = {
|
||||
content: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const Browser = ({
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
settingsStore,
|
||||
onDeleteModalClose,
|
||||
}) => {
|
||||
const Browser: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
settingsStore: Settings;
|
||||
}> = ({ alertStore, silenceFormStore, settingsStore }) => {
|
||||
const maxPerPage = IsMobile() ? 4 : 6;
|
||||
const [sortReverse, setSortReverse] = useState(false);
|
||||
const [showExpired, setShowExpired] = useState(false);
|
||||
@@ -79,7 +76,7 @@ const Browser = ({
|
||||
showExpired ? "1" : "0"
|
||||
}&searchTerm=${debouncedSearchTerm}`
|
||||
),
|
||||
{ deps: [currentTime] }
|
||||
{ deps: [currentTime] } as FetchGetOptionsT
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -148,7 +145,7 @@ const Browser = ({
|
||||
<Placeholder content="Nothing to show" />
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{response
|
||||
{(response as APIManagedSilenceT[])
|
||||
.slice((activePage - 1) * maxPerPage, activePage * maxPerPage)
|
||||
.map((silence) => (
|
||||
<ManagedSilence
|
||||
@@ -173,11 +170,5 @@ const Browser = ({
|
||||
</React.Fragment>
|
||||
));
|
||||
};
|
||||
Browser.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
onDeleteModalClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { Browser };
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useEffect, useRef } from "react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
@@ -7,13 +6,18 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faAngleUp } from "@fortawesome/free-solid-svg-icons/faAngleUp";
|
||||
import { faAngleDown } from "@fortawesome/free-solid-svg-icons/faAngleDown";
|
||||
|
||||
const Duration = observer(({ value, label, onInc, onDec }) => {
|
||||
const rootRef = useRef(null);
|
||||
const Duration: FC<{
|
||||
value: number;
|
||||
label: string;
|
||||
onInc: () => void;
|
||||
onDec: () => void;
|
||||
}> = observer(({ value, label, onInc, onDec }) => {
|
||||
const rootRef = useRef(null as null | HTMLDivElement);
|
||||
|
||||
useEffect(() => {
|
||||
const cancelWheel = (event) => event.preventDefault();
|
||||
const cancelWheel = (event: any) => event.preventDefault();
|
||||
|
||||
const elem = rootRef.current;
|
||||
const elem = rootRef.current as HTMLDivElement;
|
||||
|
||||
elem.addEventListener("wheel", cancelWheel, { passive: false });
|
||||
|
||||
@@ -22,8 +26,8 @@ const Duration = observer(({ value, label, onInc, onDec }) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onWheel = (event) => {
|
||||
if (event.deltaY < 0) {
|
||||
const onWheel = (deltaY: number) => {
|
||||
if (deltaY < 0) {
|
||||
onInc();
|
||||
} else {
|
||||
onDec();
|
||||
@@ -31,7 +35,11 @@ const Duration = observer(({ value, label, onInc, onDec }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={rootRef} onWheel={onWheel} className="components-duration">
|
||||
<div
|
||||
ref={rootRef}
|
||||
onWheel={(event) => onWheel(event.deltaY)}
|
||||
className="components-duration"
|
||||
>
|
||||
<table className="w-100">
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -71,11 +79,5 @@ const Duration = observer(({ value, label, onInc, onDec }) => {
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Duration.propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
onInc: PropTypes.func.isRequired,
|
||||
onDec: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { Duration };
|
||||
@@ -1,13 +1,18 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useEffect, useRef, MouseEvent, WheelEvent } from "react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faAngleUp } from "@fortawesome/free-solid-svg-icons/faAngleUp";
|
||||
import { faAngleDown } from "@fortawesome/free-solid-svg-icons/faAngleDown";
|
||||
|
||||
const IconTd = ({ icon, onClick, onWheel, className }) => (
|
||||
const IconTd: FC<{
|
||||
icon: IconDefinition;
|
||||
onClick: (event: MouseEvent) => void;
|
||||
onWheel: (event: WheelEvent) => void;
|
||||
className: string;
|
||||
}> = ({ icon, onClick, onWheel, className }) => (
|
||||
<td className={className} onWheel={onWheel}>
|
||||
<span onClick={onClick}>
|
||||
<FontAwesomeIcon
|
||||
@@ -18,21 +23,21 @@ const IconTd = ({ icon, onClick, onWheel, className }) => (
|
||||
</span>
|
||||
</td>
|
||||
);
|
||||
IconTd.propTypes = {
|
||||
icon: FontAwesomeIcon.propTypes.icon.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onWheel: PropTypes.func.isRequired,
|
||||
className: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const HourMinute = observer(
|
||||
const HourMinute: FC<{
|
||||
dateValue: Date;
|
||||
onHourInc: () => void;
|
||||
onHourDec: () => void;
|
||||
onMinuteInc: () => void;
|
||||
onMinuteDec: () => void;
|
||||
}> = observer(
|
||||
({ dateValue, onHourInc, onHourDec, onMinuteInc, onMinuteDec }) => {
|
||||
const rootRef = useRef(null);
|
||||
const rootRef = useRef(null as null | HTMLDivElement);
|
||||
|
||||
useEffect(() => {
|
||||
const cancelWheel = (event) => event.preventDefault();
|
||||
const cancelWheel = (event: any) => event.preventDefault();
|
||||
|
||||
const elem = rootRef.current;
|
||||
const elem = rootRef.current as HTMLDivElement;
|
||||
|
||||
elem.addEventListener("wheel", cancelWheel, { passive: false });
|
||||
|
||||
@@ -41,7 +46,7 @@ const HourMinute = observer(
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onHourWheel = (event) => {
|
||||
const onHourWheel = (event: WheelEvent) => {
|
||||
if (event.deltaY < 0) {
|
||||
onHourInc();
|
||||
} else {
|
||||
@@ -49,7 +54,7 @@ const HourMinute = observer(
|
||||
}
|
||||
};
|
||||
|
||||
const onMinuteWheel = (event) => {
|
||||
const onMinuteWheel = (event: WheelEvent) => {
|
||||
if (event.deltaY < 0) {
|
||||
onMinuteInc();
|
||||
} else {
|
||||
@@ -114,12 +119,5 @@ const HourMinute = observer(
|
||||
);
|
||||
}
|
||||
);
|
||||
HourMinute.propTypes = {
|
||||
dateValue: PropTypes.instanceOf(Date).isRequired,
|
||||
onHourInc: PropTypes.func.isRequired,
|
||||
onHourDec: PropTypes.func.isRequired,
|
||||
onMinuteInc: PropTypes.func.isRequired,
|
||||
onMinuteDec: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { HourMinute };
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useEffect, useState, useCallback, ReactNode } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
@@ -15,13 +14,17 @@ import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { Duration } from "./Duration";
|
||||
import { HourMinute } from "./HourMinute";
|
||||
|
||||
const nowZeroSeconds = () => {
|
||||
const nowZeroSeconds = (): Date => {
|
||||
const now = new Date();
|
||||
now.setSeconds(0);
|
||||
return now;
|
||||
};
|
||||
|
||||
const OffsetBadge = ({ startDate, endDate, prefixLabel }) => {
|
||||
const OffsetBadge: FC<{
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
prefixLabel: string;
|
||||
}> = ({ startDate, endDate, prefixLabel }) => {
|
||||
const days = differenceInDays(endDate, startDate);
|
||||
const hours = differenceInHours(endDate, startDate) % 24;
|
||||
const minutes = differenceInMinutes(endDate, startDate) % 60;
|
||||
@@ -35,13 +38,12 @@ const OffsetBadge = ({ startDate, endDate, prefixLabel }) => {
|
||||
</span>
|
||||
);
|
||||
};
|
||||
OffsetBadge.propTypes = {
|
||||
startDate: PropTypes.instanceOf(Date).isRequired,
|
||||
endDate: PropTypes.instanceOf(Date).isRequired,
|
||||
prefixLabel: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const Tab = ({ title, active, onClick }) => (
|
||||
const Tab: FC<{
|
||||
title: ReactNode;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
}> = ({ title, active, onClick }) => (
|
||||
<li className="nav-item">
|
||||
<span
|
||||
className={`nav-link cursor-pointer ${active ? "active" : ""}`}
|
||||
@@ -51,19 +53,10 @@ const Tab = ({ title, active, onClick }) => (
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
Tab.propTypes = {
|
||||
title: PropTypes.node.isRequired,
|
||||
active: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const TabNames = Object.freeze({
|
||||
Start: "start",
|
||||
End: "end",
|
||||
Duration: "duration",
|
||||
});
|
||||
|
||||
const TabContentStart = ({ silenceFormStore }) => {
|
||||
const TabContentStart: FC<{
|
||||
silenceFormStore: SilenceFormStore;
|
||||
}> = ({ silenceFormStore }) => {
|
||||
return useObserver(() => (
|
||||
<div className="d-flex flex-sm-row flex-column justify-content-around mx-3 mt-2">
|
||||
<div className="d-flex justify-content-center align-items-center">
|
||||
@@ -74,7 +67,7 @@ const TabContentStart = ({ silenceFormStore }) => {
|
||||
before: nowZeroSeconds(),
|
||||
}}
|
||||
todayButton="Today"
|
||||
onDayClick={(val, ...mod) => {
|
||||
onDayClick={(val) => {
|
||||
const startsAt = new Date(val);
|
||||
startsAt.setHours(silenceFormStore.data.startsAt.getHours());
|
||||
startsAt.setMinutes(silenceFormStore.data.startsAt.getMinutes());
|
||||
@@ -103,7 +96,9 @@ const TabContentStart = ({ silenceFormStore }) => {
|
||||
));
|
||||
};
|
||||
|
||||
const TabContentEnd = ({ silenceFormStore }) => {
|
||||
const TabContentEnd: FC<{ silenceFormStore: SilenceFormStore }> = ({
|
||||
silenceFormStore,
|
||||
}) => {
|
||||
return useObserver(() => (
|
||||
<div className="d-flex flex-sm-row flex-column justify-content-around mx-3 mt-2">
|
||||
<div className="d-flex justify-content-center align-items-center">
|
||||
@@ -144,7 +139,7 @@ const TabContentEnd = ({ silenceFormStore }) => {
|
||||
};
|
||||
|
||||
// calculate value for duration increase button using a goal step
|
||||
const CalculateChangeValueUp = (currentValue, step) => {
|
||||
const CalculateChangeValueUp = (currentValue: number, step: number): number => {
|
||||
// if current value is less than step (but >0) then use 1
|
||||
if (currentValue > 0 && currentValue < step) {
|
||||
return 1;
|
||||
@@ -154,7 +149,10 @@ const CalculateChangeValueUp = (currentValue, step) => {
|
||||
};
|
||||
|
||||
// calculate value for duration decrease button using a goal step
|
||||
const CalculateChangeValueDown = (currentValue, step) => {
|
||||
const CalculateChangeValueDown = (
|
||||
currentValue: number,
|
||||
step: number
|
||||
): number => {
|
||||
// if current value is less than step (but >0) then use 1
|
||||
if (currentValue > 0 && currentValue < step) {
|
||||
return 1;
|
||||
@@ -163,7 +161,9 @@ const CalculateChangeValueDown = (currentValue, step) => {
|
||||
return currentValue % step || step;
|
||||
};
|
||||
|
||||
const TabContentDuration = ({ silenceFormStore }) => {
|
||||
const TabContentDuration: FC<{
|
||||
silenceFormStore: SilenceFormStore;
|
||||
}> = ({ silenceFormStore }) => {
|
||||
return useObserver(() => (
|
||||
<div className="d-flex flex-sm-row flex-column justify-content-around mt-2 mx-3">
|
||||
<Duration
|
||||
@@ -198,11 +198,11 @@ const TabContentDuration = ({ silenceFormStore }) => {
|
||||
</div>
|
||||
));
|
||||
};
|
||||
TabContentDuration.propTypes = {
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
};
|
||||
|
||||
const DateTimeSelect = ({ silenceFormStore, openTab }) => {
|
||||
const DateTimeSelect: FC<{
|
||||
silenceFormStore: SilenceFormStore;
|
||||
openTab?: "start" | "end" | "duration";
|
||||
}> = ({ silenceFormStore, openTab = "duration" }) => {
|
||||
const [currentTab, setCurrentTab] = useState(openTab);
|
||||
const [timeNow, setTimeNow] = useState(nowZeroSeconds());
|
||||
|
||||
@@ -231,8 +231,8 @@ const DateTimeSelect = ({ silenceFormStore, openTab }) => {
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
active={currentTab === TabNames.Start}
|
||||
onClick={() => setCurrentTab(TabNames.Start)}
|
||||
active={currentTab === "start"}
|
||||
onClick={() => setCurrentTab("start")}
|
||||
/>
|
||||
<Tab
|
||||
title={
|
||||
@@ -245,8 +245,8 @@ const DateTimeSelect = ({ silenceFormStore, openTab }) => {
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
active={currentTab === TabNames.End}
|
||||
onClick={() => setCurrentTab(TabNames.End)}
|
||||
active={currentTab === "end"}
|
||||
onClick={() => setCurrentTab("end")}
|
||||
/>
|
||||
<Tab
|
||||
title={
|
||||
@@ -259,36 +259,23 @@ const DateTimeSelect = ({ silenceFormStore, openTab }) => {
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
active={currentTab === TabNames.Duration}
|
||||
onClick={() => setCurrentTab(TabNames.Duration)}
|
||||
active={currentTab === "duration"}
|
||||
onClick={() => setCurrentTab("duration")}
|
||||
/>
|
||||
</ul>
|
||||
<div className="tab-content mb-3">
|
||||
{currentTab === TabNames.Duration ? (
|
||||
{currentTab === "duration" ? (
|
||||
<TabContentDuration silenceFormStore={silenceFormStore} />
|
||||
) : null}
|
||||
{currentTab === TabNames.Start ? (
|
||||
{currentTab === "start" ? (
|
||||
<TabContentStart silenceFormStore={silenceFormStore} />
|
||||
) : null}
|
||||
{currentTab === TabNames.End ? (
|
||||
{currentTab === "end" ? (
|
||||
<TabContentEnd silenceFormStore={silenceFormStore} />
|
||||
) : null}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
));
|
||||
};
|
||||
DateTimeSelect.propTypes = {
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
openTab: PropTypes.oneOf(Object.values(TabNames)),
|
||||
};
|
||||
DateTimeSelect.defaultProps = {
|
||||
openTab: TabNames.Duration,
|
||||
};
|
||||
|
||||
export {
|
||||
DateTimeSelect,
|
||||
TabContentStart,
|
||||
TabContentEnd,
|
||||
TabContentDuration,
|
||||
TabNames,
|
||||
};
|
||||
export { DateTimeSelect, TabContentStart, TabContentEnd, TabContentDuration };
|
||||
@@ -1,6 +1,8 @@
|
||||
import { FormatQuery, QueryOperators, StaticLabels } from "Common/Query";
|
||||
import { MultiValueOptionT } from "Common/Select";
|
||||
import { MatcherT } from "Stores/SilenceFormStore";
|
||||
|
||||
const MatcherToFilter = (matcher) => {
|
||||
const MatcherToFilter = (matcher: MatcherT): string => {
|
||||
const operator = matcher.isRegex
|
||||
? QueryOperators.Regex
|
||||
: QueryOperators.Equal;
|
||||
@@ -15,8 +17,10 @@ const MatcherToFilter = (matcher) => {
|
||||
);
|
||||
};
|
||||
|
||||
const AlertManagersToFilter = (alertmanagers) => {
|
||||
let amNames = [].concat(...alertmanagers.map((am) => am.value));
|
||||
const AlertManagersToFilter = (alertmanagers: MultiValueOptionT[]): string => {
|
||||
let amNames: string[] = ([] as string[]).concat(
|
||||
...alertmanagers.map((am) => am.value)
|
||||
);
|
||||
return FormatQuery(
|
||||
StaticLabels.AlertManager,
|
||||
QueryOperators.Regex,
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
@@ -8,7 +7,9 @@ import * as theme from "react-json-pretty/dist/monikai";
|
||||
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
|
||||
const PayloadPreview = ({ silenceFormStore }) => {
|
||||
const PayloadPreview: FC<{
|
||||
silenceFormStore: SilenceFormStore;
|
||||
}> = ({ silenceFormStore }) => {
|
||||
return useObserver(() => (
|
||||
<JSONPretty
|
||||
json={silenceFormStore.data.toAlertmanagerPayload}
|
||||
@@ -17,8 +18,5 @@ const PayloadPreview = ({ silenceFormStore }) => {
|
||||
/>
|
||||
));
|
||||
};
|
||||
PayloadPreview.propTypes = {
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
};
|
||||
|
||||
export { PayloadPreview };
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useEffect, useState, MouseEvent, FormEvent } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
SilenceFormStage,
|
||||
NewEmptyMatcher,
|
||||
NewClusterRequest,
|
||||
ClusterRequestT,
|
||||
} from "Stores/SilenceFormStore";
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { StringToOption } from "Common/Select";
|
||||
@@ -35,7 +35,9 @@ import { DateTimeSelect } from "./DateTimeSelect";
|
||||
import { PayloadPreview } from "./PayloadPreview";
|
||||
import { IconInput, AuthenticatedAuthorInput } from "./AuthorInput";
|
||||
|
||||
const ShareButton = ({ silenceFormStore }) => {
|
||||
const ShareButton: FC<{
|
||||
silenceFormStore: SilenceFormStore;
|
||||
}> = ({ silenceFormStore }) => {
|
||||
const [clickCount, setClickCount] = useState(0);
|
||||
|
||||
const baseURL = [
|
||||
@@ -81,12 +83,12 @@ const ShareButton = ({ silenceFormStore }) => {
|
||||
));
|
||||
};
|
||||
|
||||
const SilenceForm = ({
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
settingsStore,
|
||||
previewOpen,
|
||||
}) => {
|
||||
const SilenceForm: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
settingsStore: Settings;
|
||||
previewOpen: boolean;
|
||||
}> = ({ alertStore, silenceFormStore, settingsStore, previewOpen }) => {
|
||||
const [showPreview, setShowPreview] = useState(previewOpen);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -146,23 +148,23 @@ const SilenceForm = ({
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const addMore = (event) => {
|
||||
const addMore = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
silenceFormStore.data.addEmptyMatcher();
|
||||
};
|
||||
|
||||
const onAuthorChange = (event) => {
|
||||
silenceFormStore.data.author = event.target.value;
|
||||
const onAuthorChange = (author: string) => {
|
||||
silenceFormStore.data.author = author;
|
||||
};
|
||||
|
||||
const onCommentChange = (event) => {
|
||||
silenceFormStore.data.comment = event.target.value;
|
||||
const onCommentChange = (comment: string) => {
|
||||
silenceFormStore.data.comment = comment;
|
||||
};
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
const handleSubmit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
let rbc = {};
|
||||
let rbc: { [label: string]: ClusterRequestT } = {};
|
||||
silenceFormStore.data.alertmanagers.forEach((am) => {
|
||||
rbc[am.label] = NewClusterRequest(am.label, am.value);
|
||||
});
|
||||
@@ -217,7 +219,7 @@ const SilenceForm = ({
|
||||
placeholder="Author"
|
||||
icon={faUser}
|
||||
value={silenceFormStore.data.author}
|
||||
onChange={onAuthorChange}
|
||||
onChange={(event) => onAuthorChange(event.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -227,7 +229,7 @@ const SilenceForm = ({
|
||||
placeholder="Comment"
|
||||
icon={faCommentDots}
|
||||
value={silenceFormStore.data.comment}
|
||||
onChange={onCommentChange}
|
||||
onChange={(event) => onCommentChange(event.target.value)}
|
||||
/>
|
||||
<div className="d-flex flex-row justify-content-between">
|
||||
<span
|
||||
@@ -262,11 +264,5 @@ const SilenceForm = ({
|
||||
</form>
|
||||
));
|
||||
};
|
||||
SilenceForm.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
previewOpen: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export { SilenceForm };
|
||||
@@ -1,16 +1,18 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import Creatable from "react-select/creatable";
|
||||
|
||||
import { SilenceFormMatcher } from "Models/SilenceForm";
|
||||
import { FormatBackendURI } from "Stores/AlertStore";
|
||||
import { MatcherWithIDT } from "Stores/SilenceFormStore";
|
||||
import { useFetchGet } from "Hooks/useFetchGet";
|
||||
import { ValidationError } from "Components/ValidationError";
|
||||
import { ThemeContext } from "Components/Theme";
|
||||
import { NewLabelName } from "Common/Select";
|
||||
import { NewLabelName, OptionT, StringToOption } from "Common/Select";
|
||||
|
||||
const LabelNameInput = ({ matcher, isValid }) => {
|
||||
const LabelNameInput: FC<{
|
||||
matcher: MatcherWithIDT;
|
||||
isValid: boolean;
|
||||
}> = ({ matcher, isValid }) => {
|
||||
const { response } = useFetchGet(FormatBackendURI(`labelNames.json`));
|
||||
|
||||
const context = React.useContext(ThemeContext);
|
||||
@@ -21,28 +23,17 @@ const LabelNameInput = ({ matcher, isValid }) => {
|
||||
classNamePrefix="react-select"
|
||||
instanceId={`silence-input-label-name-${matcher.id}`}
|
||||
formatCreateLabel={NewLabelName}
|
||||
defaultValue={
|
||||
matcher.name ? { label: matcher.name, value: matcher.name } : null
|
||||
}
|
||||
defaultValue={matcher.name ? StringToOption(matcher.name) : null}
|
||||
options={
|
||||
response
|
||||
? response.map((value) => ({
|
||||
label: value,
|
||||
value: value,
|
||||
}))
|
||||
: []
|
||||
response ? response.map((value: string) => StringToOption(value)) : []
|
||||
}
|
||||
placeholder={isValid ? "Label name" : <ValidationError />}
|
||||
onChange={({ value }) => {
|
||||
matcher.name = value;
|
||||
onChange={(option) => {
|
||||
matcher.name = (option as OptionT).value;
|
||||
}}
|
||||
hideSelectedOptions
|
||||
/>
|
||||
);
|
||||
};
|
||||
LabelNameInput.propTypes = {
|
||||
matcher: SilenceFormMatcher.isRequired,
|
||||
isValid: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export { LabelNameInput };
|
||||
@@ -1,23 +1,28 @@
|
||||
import React, { useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useEffect, ReactNode, ComponentType } from "react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import { components } from "react-select";
|
||||
import {
|
||||
components,
|
||||
PlaceholderProps,
|
||||
ValueContainerProps,
|
||||
} from "react-select";
|
||||
|
||||
import Creatable from "react-select/creatable";
|
||||
|
||||
import { FormatBackendURI } from "Stores/AlertStore";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { SilenceFormMatcher } from "Models/SilenceForm";
|
||||
import { SilenceFormStore, MatcherWithIDT } from "Stores/SilenceFormStore";
|
||||
import { useFetchGet } from "Hooks/useFetchGet";
|
||||
import { hashObject } from "Common/Hash";
|
||||
import { NewLabelValue } from "Common/Select";
|
||||
import { NewLabelValue, OptionT, StringToOption } from "Common/Select";
|
||||
import { ValidationError } from "Components/ValidationError";
|
||||
import { ThemeContext } from "Components/Theme";
|
||||
import { MatchCounter } from "./MatchCounter";
|
||||
|
||||
const GenerateHashFromMatchers = (silenceFormStore, matcher) =>
|
||||
const GenerateHashFromMatchers = (
|
||||
silenceFormStore: SilenceFormStore,
|
||||
matcher: MatcherWithIDT
|
||||
): string =>
|
||||
hashObject({
|
||||
alertmanagers: silenceFormStore.data.alertmanagers,
|
||||
matcher: {
|
||||
@@ -27,31 +32,42 @@ const GenerateHashFromMatchers = (silenceFormStore, matcher) =>
|
||||
},
|
||||
});
|
||||
|
||||
const Placeholder = (props) => {
|
||||
const Placeholder: FC<{}> = (props) => {
|
||||
return (
|
||||
<div>
|
||||
<components.Placeholder {...props} />
|
||||
<components.Placeholder {...(props as PlaceholderProps<any>)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ValueContainer = ({ children, ...props }) => (
|
||||
<components.ValueContainer {...props}>
|
||||
{props.selectProps.matcher.values.length > 0 ? (
|
||||
const ValueContainer: FC<{
|
||||
selectProps: {
|
||||
silenceFormStore: SilenceFormStore;
|
||||
matcher: MatcherWithIDT;
|
||||
};
|
||||
props: ValueContainerProps<any>;
|
||||
children: ReactNode;
|
||||
}> = ({ children, selectProps, ...props }) => (
|
||||
<components.ValueContainer {...(props as any)}>
|
||||
{selectProps.matcher.values.length > 0 ? (
|
||||
<MatchCounter
|
||||
key={GenerateHashFromMatchers(
|
||||
props.selectProps.silenceFormStore,
|
||||
props.selectProps.matcher
|
||||
selectProps.silenceFormStore,
|
||||
selectProps.matcher
|
||||
)}
|
||||
silenceFormStore={props.selectProps.silenceFormStore}
|
||||
matcher={props.selectProps.matcher}
|
||||
silenceFormStore={selectProps.silenceFormStore}
|
||||
matcher={selectProps.matcher}
|
||||
/>
|
||||
) : null}
|
||||
{children}
|
||||
</components.ValueContainer>
|
||||
);
|
||||
|
||||
const LabelValueInput = observer(({ silenceFormStore, matcher, isValid }) => {
|
||||
const LabelValueInput: FC<{
|
||||
silenceFormStore: SilenceFormStore;
|
||||
matcher: MatcherWithIDT;
|
||||
isValid: boolean;
|
||||
}> = observer(({ silenceFormStore, matcher, isValid }) => {
|
||||
const { response, get, cancelGet } = useFetchGet(
|
||||
FormatBackendURI(`labelValues.json?name=${matcher.name}`),
|
||||
{ autorun: false }
|
||||
@@ -74,36 +90,28 @@ const LabelValueInput = observer(({ silenceFormStore, matcher, isValid }) => {
|
||||
formatCreateLabel={NewLabelValue}
|
||||
defaultValue={matcher.values}
|
||||
options={
|
||||
response
|
||||
? response.map((value) => ({
|
||||
label: value,
|
||||
value: value,
|
||||
}))
|
||||
: []
|
||||
response ? response.map((value: string) => StringToOption(value)) : []
|
||||
}
|
||||
placeholder={isValid ? "Label value" : <ValidationError />}
|
||||
onChange={(newValue) => {
|
||||
const value = newValue || [];
|
||||
matcher.values = value;
|
||||
matcher.values = (newValue || []) as OptionT[];
|
||||
// force regex if we have multiple values
|
||||
if (value.length > 1 && matcher.isRegex === false) {
|
||||
if (matcher.values.length > 1 && matcher.isRegex === false) {
|
||||
matcher.isRegex = true;
|
||||
} else if (value.length === 1 && matcher.isRegex === true) {
|
||||
} else if (matcher.values.length === 1 && matcher.isRegex === true) {
|
||||
matcher.isRegex = false;
|
||||
}
|
||||
}}
|
||||
hideSelectedOptions
|
||||
isMulti
|
||||
components={{ ValueContainer, Placeholder }}
|
||||
components={{
|
||||
ValueContainer: ValueContainer as ComponentType<any>,
|
||||
Placeholder: Placeholder,
|
||||
}}
|
||||
silenceFormStore={silenceFormStore}
|
||||
matcher={matcher}
|
||||
/>
|
||||
);
|
||||
});
|
||||
LabelValueInput.propTypes = {
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
matcher: SilenceFormMatcher.isRequired,
|
||||
isValid: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export { LabelValueInput };
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
@@ -8,13 +7,15 @@ import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclama
|
||||
import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner";
|
||||
|
||||
import { FormatBackendURI, FormatAlertsQ } from "Stores/AlertStore";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { SilenceFormMatcher } from "Models/SilenceForm";
|
||||
import { SilenceFormStore, MatcherWithIDT } from "Stores/SilenceFormStore";
|
||||
import { TooltipWrapper } from "Components/TooltipWrapper";
|
||||
import { useFetchGet } from "Hooks/useFetchGet";
|
||||
import { MatcherToFilter, AlertManagersToFilter } from "../Matchers";
|
||||
|
||||
const MatchCounter = ({ silenceFormStore, matcher }) => {
|
||||
const MatchCounter: FC<{
|
||||
silenceFormStore: SilenceFormStore;
|
||||
matcher: MatcherWithIDT;
|
||||
}> = ({ silenceFormStore, matcher }) => {
|
||||
const filters = [MatcherToFilter(matcher)];
|
||||
if (silenceFormStore.data.alertmanagers.length) {
|
||||
filters.push(AlertManagersToFilter(silenceFormStore.data.alertmanagers));
|
||||
@@ -52,9 +53,5 @@ const MatchCounter = ({ silenceFormStore, matcher }) => {
|
||||
)
|
||||
);
|
||||
};
|
||||
MatchCounter.propTypes = {
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
matcher: SilenceFormMatcher.isRequired,
|
||||
};
|
||||
|
||||
export { MatchCounter };
|
||||
@@ -1,24 +1,22 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
|
||||
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { SilenceFormStore, MatcherWithIDT } from "Stores/SilenceFormStore";
|
||||
import { TooltipWrapper } from "Components/TooltipWrapper";
|
||||
import { SilenceFormMatcher } from "Models/SilenceForm";
|
||||
import { LabelNameInput } from "./LabelNameInput";
|
||||
import { LabelValueInput } from "./LabelValueInput";
|
||||
|
||||
const SilenceMatch = ({
|
||||
silenceFormStore,
|
||||
matcher,
|
||||
showDelete,
|
||||
onDelete,
|
||||
isValid,
|
||||
}) => {
|
||||
const SilenceMatch: FC<{
|
||||
silenceFormStore: SilenceFormStore;
|
||||
matcher: MatcherWithIDT;
|
||||
showDelete: boolean;
|
||||
onDelete: () => void;
|
||||
isValid: boolean;
|
||||
}> = ({ silenceFormStore, matcher, showDelete, onDelete, isValid }) => {
|
||||
return useObserver(() => (
|
||||
<div className="d-flex flex-fill flex-lg-row flex-column mb-3">
|
||||
<div className="flex-shrink-0 flex-grow-0 flex-basis-25 pr-lg-2 pb-2 pb-lg-0">
|
||||
@@ -68,12 +66,5 @@ const SilenceMatch = ({
|
||||
</div>
|
||||
));
|
||||
};
|
||||
SilenceMatch.propTypes = {
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
matcher: SilenceFormMatcher.isRequired,
|
||||
showDelete: PropTypes.bool.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
isValid: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export { SilenceMatch };
|
||||
@@ -56,7 +56,6 @@ const MountedSilenceModalContent = () => {
|
||||
settingsStore={settingsStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
onHide={MockOnHide}
|
||||
onDeleteModalClose={jest.fn()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
@@ -20,7 +19,7 @@ import { SilencePreview } from "./SilencePreview";
|
||||
import { SilenceSubmitController } from "./SilenceSubmit/SilenceSubmitController";
|
||||
import { Browser } from "./Browser";
|
||||
|
||||
const ReadOnlyPlaceholder = () => (
|
||||
const ReadOnlyPlaceholder: FC = () => (
|
||||
<div className="jumbotron bg-transparent">
|
||||
<h1 className="display-5 text-placeholder text-center">
|
||||
<FontAwesomeIcon icon={faLock} className="mr-3" />
|
||||
@@ -29,13 +28,18 @@ const ReadOnlyPlaceholder = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const SilenceModalContent = ({
|
||||
const SilenceModalContent: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
settingsStore: Settings;
|
||||
onHide: () => void;
|
||||
previewOpen?: boolean;
|
||||
}> = ({
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
settingsStore,
|
||||
onHide,
|
||||
previewOpen,
|
||||
onDeleteModalClose,
|
||||
previewOpen = false,
|
||||
}) => {
|
||||
return useObserver(() => (
|
||||
<React.Fragment>
|
||||
@@ -53,12 +57,12 @@ const SilenceModalContent = ({
|
||||
: "Silence submitted"
|
||||
}
|
||||
active={silenceFormStore.tab.current === SilenceTabNames.Editor}
|
||||
onClick={() => silenceFormStore.tab.setTab(SilenceTabNames.Editor)}
|
||||
onClick={() => silenceFormStore.tab.setTab("editor")}
|
||||
/>
|
||||
<Tab
|
||||
title="Browse"
|
||||
active={silenceFormStore.tab.current === SilenceTabNames.Browser}
|
||||
onClick={() => silenceFormStore.tab.setTab(SilenceTabNames.Browser)}
|
||||
onClick={() => silenceFormStore.tab.setTab("browser")}
|
||||
/>
|
||||
<button type="button" className="close" onClick={onHide}>
|
||||
<span>×</span>
|
||||
@@ -107,23 +111,11 @@ const SilenceModalContent = ({
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
settingsStore={settingsStore}
|
||||
onDeleteModalClose={onDeleteModalClose}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
));
|
||||
};
|
||||
SilenceModalContent.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
onHide: PropTypes.func.isRequired,
|
||||
previewOpen: PropTypes.bool,
|
||||
onDeleteModalClose: PropTypes.func.isRequired,
|
||||
};
|
||||
SilenceModalContent.defaultProps = {
|
||||
previewOpen: false,
|
||||
};
|
||||
|
||||
export { SilenceModalContent };
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowLeft } from "@fortawesome/free-solid-svg-icons/faArrowLeft";
|
||||
@@ -10,7 +9,10 @@ import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { PaginatedAlertList } from "Components/PaginatedAlertList";
|
||||
import { MatcherToFilter, AlertManagersToFilter } from "../Matchers";
|
||||
|
||||
const SilencePreview = ({ alertStore, silenceFormStore }) => {
|
||||
const SilencePreview: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
}> = ({ alertStore, silenceFormStore }) => {
|
||||
const filters = [
|
||||
...silenceFormStore.data.matchers.map((m) => MatcherToFilter(m)),
|
||||
AlertManagersToFilter(silenceFormStore.data.alertmanagers),
|
||||
@@ -46,9 +48,5 @@ const SilencePreview = ({ alertStore, silenceFormStore }) => {
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
SilencePreview.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
};
|
||||
|
||||
export { SilencePreview };
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
@@ -12,7 +11,10 @@ import { AlertStore } from "Stores/AlertStore";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { SilenceSubmitProgress } from "./SilenceSubmitProgress";
|
||||
|
||||
const SingleClusterStatus = ({ silenceFormStore, alertStore }) => {
|
||||
const SingleClusterStatus: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
}> = ({ silenceFormStore, alertStore }) => {
|
||||
const clusterRequest = Object.values(
|
||||
silenceFormStore.data.requestsByCluster
|
||||
)[0];
|
||||
@@ -64,7 +66,10 @@ const SingleClusterStatus = ({ silenceFormStore, alertStore }) => {
|
||||
));
|
||||
};
|
||||
|
||||
const MultiClusterStatus = ({ silenceFormStore, alertStore }) => {
|
||||
const MultiClusterStatus: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
}> = ({ silenceFormStore, alertStore }) => {
|
||||
return useObserver(() => (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-borderless">
|
||||
@@ -129,7 +134,10 @@ const MultiClusterStatus = ({ silenceFormStore, alertStore }) => {
|
||||
));
|
||||
};
|
||||
|
||||
const SilenceSubmitController = ({ silenceFormStore, alertStore }) => {
|
||||
const SilenceSubmitController: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
}> = ({ silenceFormStore, alertStore }) => {
|
||||
return useObserver(() => (
|
||||
<React.Fragment>
|
||||
{Object.keys(silenceFormStore.data.requestsByCluster).length === 1 ? (
|
||||
@@ -156,9 +164,5 @@ const SilenceSubmitController = ({ silenceFormStore, alertStore }) => {
|
||||
</React.Fragment>
|
||||
));
|
||||
};
|
||||
SilenceSubmitController.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
};
|
||||
|
||||
export { SilenceSubmitController, MultiClusterStatus, SingleClusterStatus };
|
||||
@@ -1,27 +1,30 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
|
||||
|
||||
import { APISilenceMatcher } from "Models/API";
|
||||
import { AlertmanagerSilencePayloadT } from "Models/APITypes";
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { useFetchAny } from "Hooks/useFetchAny";
|
||||
import { useFetchAny, UpstreamT } from "Hooks/useFetchAny";
|
||||
|
||||
const SilenceSubmitProgress = ({
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
cluster,
|
||||
members,
|
||||
payload,
|
||||
}) => {
|
||||
const [upstreams, setUpstreams] = useState([]);
|
||||
interface PostResponseT {
|
||||
silenceID: string;
|
||||
}
|
||||
|
||||
const SilenceSubmitProgress: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
cluster: string;
|
||||
members: string[];
|
||||
payload: AlertmanagerSilencePayloadT;
|
||||
}> = ({ alertStore, silenceFormStore, cluster, members, payload }) => {
|
||||
const [upstreams, setUpstreams] = useState([] as UpstreamT[]);
|
||||
const { response, error, inProgress, responseURI } = useFetchAny(upstreams);
|
||||
const [publicURIs, setPublicURIs] = useState({});
|
||||
const [publicURIs, setPublicURIs] = useState({} as { [key: string]: string });
|
||||
|
||||
useEffect(() => {
|
||||
let uris = {};
|
||||
let uris: { [uri: string]: string } = {};
|
||||
let membersToTry = [];
|
||||
for (const member of members) {
|
||||
if (alertStore.data.isReadOnlyAlertmanager(member)) {
|
||||
@@ -60,29 +63,16 @@ const SilenceSubmitProgress = ({
|
||||
silenceFormStore.data.requestsByCluster[cluster].error = error;
|
||||
} else if (!inProgress && response !== null) {
|
||||
silenceFormStore.data.requestsByCluster[cluster].isDone = true;
|
||||
silenceFormStore.data.requestsByCluster[cluster].silenceID =
|
||||
response.silenceID;
|
||||
silenceFormStore.data.requestsByCluster[
|
||||
cluster
|
||||
].silenceLink = `${publicURIs[responseURI]}/#/silences/${response.silenceID}`;
|
||||
].silenceID = (response as PostResponseT).silenceID;
|
||||
silenceFormStore.data.requestsByCluster[cluster].silenceLink = `${
|
||||
publicURIs[responseURI as string]
|
||||
}/#/silences/${(response as PostResponseT).silenceID}`;
|
||||
}
|
||||
}, [cluster, error, inProgress, publicURIs, response, responseURI]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return <FontAwesomeIcon className="text-muted" icon={faCircleNotch} spin />;
|
||||
};
|
||||
SilenceSubmitProgress.propTypes = {
|
||||
cluster: PropTypes.string.isRequired,
|
||||
members: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
payload: PropTypes.exact({
|
||||
matchers: PropTypes.arrayOf(APISilenceMatcher).isRequired,
|
||||
startsAt: PropTypes.string.isRequired,
|
||||
endsAt: PropTypes.string.isRequired,
|
||||
createdBy: PropTypes.string.isRequired,
|
||||
comment: PropTypes.string.isRequired,
|
||||
id: PropTypes.string,
|
||||
}).isRequired,
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
};
|
||||
|
||||
export { SilenceSubmitProgress };
|
||||
@@ -176,7 +176,6 @@ storiesOf("SilenceModal", module)
|
||||
settingsStore={settingsStore}
|
||||
onHide={() => {}}
|
||||
previewOpen={true}
|
||||
onDeleteModalClose={() => {}}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal>
|
||||
@@ -186,7 +185,6 @@ storiesOf("SilenceModal", module)
|
||||
settingsStore={settingsStore}
|
||||
onHide={() => {}}
|
||||
previewOpen={true}
|
||||
onDeleteModalClose={() => {}}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal>
|
||||
@@ -267,7 +265,6 @@ storiesOf("SilenceModal", module)
|
||||
silenceFormStore={silenceFormStore}
|
||||
settingsStore={settingsStore}
|
||||
onHide={() => {}}
|
||||
onDeleteModalClose={() => {}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
@@ -316,7 +313,6 @@ storiesOf("SilenceModal", module)
|
||||
silenceFormStore={silenceFormStore}
|
||||
settingsStore={settingsStore}
|
||||
onHide={() => {}}
|
||||
onDeleteModalClose={() => {}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -175,21 +175,4 @@ describe("<SilenceModal />", () => {
|
||||
tree.unmount();
|
||||
expect(document.body.className.split(" ")).not.toContain("modal-open");
|
||||
});
|
||||
|
||||
it("'modal-open' class is preserved on body node after remountModal is called", () => {
|
||||
let callbacks = [];
|
||||
jest.spyOn(React, "useCallback").mockImplementation((fn) => {
|
||||
callbacks.push(fn);
|
||||
return fn;
|
||||
});
|
||||
|
||||
silenceFormStore.toggle.visible = true;
|
||||
MountedSilenceModal();
|
||||
|
||||
expect(callbacks.length).toBeGreaterThan(0);
|
||||
act(() => {
|
||||
callbacks.forEach((f) => f());
|
||||
});
|
||||
expect(document.body.className.split(" ")).toContain("modal-open");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
@@ -20,13 +19,11 @@ const SilenceModalContent = React.lazy(() =>
|
||||
}))
|
||||
);
|
||||
|
||||
const SilenceModal = ({ alertStore, silenceFormStore, settingsStore }) => {
|
||||
// uses React.useCallback instead of useCallback for tests
|
||||
const onDeleteModalClose = React.useCallback(() => {
|
||||
const event = new CustomEvent("remountModal");
|
||||
window.dispatchEvent(event);
|
||||
}, []);
|
||||
|
||||
const SilenceModal: FC<{
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
settingsStore: Settings;
|
||||
}> = ({ alertStore, silenceFormStore, settingsStore }) => {
|
||||
return useObserver(() => (
|
||||
<React.Fragment>
|
||||
<li
|
||||
@@ -64,17 +61,11 @@ const SilenceModal = ({ alertStore, silenceFormStore, settingsStore }) => {
|
||||
silenceFormStore={silenceFormStore}
|
||||
settingsStore={settingsStore}
|
||||
onHide={silenceFormStore.toggle.hide}
|
||||
onDeleteModalClose={onDeleteModalClose}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</Modal>
|
||||
</React.Fragment>
|
||||
));
|
||||
};
|
||||
SilenceModal.propTypes = {
|
||||
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
|
||||
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired,
|
||||
};
|
||||
|
||||
export { SilenceModal };
|
||||
@@ -10,7 +10,7 @@ export interface UpstreamT {
|
||||
}
|
||||
|
||||
interface ResponseState {
|
||||
response: string | null;
|
||||
response: string | { [key: string]: any } | null;
|
||||
error: string | null;
|
||||
responseURI: string | null;
|
||||
inProgress: boolean;
|
||||
|
||||
@@ -4,7 +4,11 @@ import merge from "lodash.merge";
|
||||
|
||||
import { CommonOptions } from "Common/Fetch";
|
||||
|
||||
const useFetchDelete = (uri: string, options: RequestInit, deps = []) => {
|
||||
const useFetchDelete = (
|
||||
uri: string,
|
||||
options: RequestInit,
|
||||
deps: any[] = []
|
||||
) => {
|
||||
const [response, setResponse] = useState(null as string | null);
|
||||
const [error, setError] = useState(null as string | null);
|
||||
const [isDeleting, setIsDeleting] = useState(true);
|
||||
|
||||
@@ -6,9 +6,17 @@ import promiseRetry from "promise-retry";
|
||||
|
||||
import { CommonOptions, FetchRetryConfig } from "Common/Fetch";
|
||||
|
||||
type FetchFunctionT = (request: RequestInfo) => Promise<Response>;
|
||||
|
||||
export interface FetchGetOptionsT {
|
||||
autorun?: boolean;
|
||||
deps?: any[];
|
||||
fetcher?: null | FetchFunctionT;
|
||||
}
|
||||
|
||||
const useFetchGet = (
|
||||
uri: string,
|
||||
{ autorun = true, deps = [], fetcher = null } = {}
|
||||
{ autorun = true, deps = [], fetcher = null }: FetchGetOptionsT = {}
|
||||
) => {
|
||||
const [response, setResponse] = useState(null as any);
|
||||
const [error, setError] = useState(null as string | null);
|
||||
@@ -28,7 +36,7 @@ const useFetchGet = (
|
||||
setIsLoading(true);
|
||||
setRetryCount(0);
|
||||
setError(null);
|
||||
const res = await promiseRetry(
|
||||
const res: Response = await promiseRetry(
|
||||
(retry: Function, number: number) =>
|
||||
(fetcher || fetch)(
|
||||
uri,
|
||||
@@ -42,7 +50,7 @@ const useFetchGet = (
|
||||
mode: number <= FetchRetryConfig.retries ? "cors" : "no-cors",
|
||||
}
|
||||
) as RequestInit
|
||||
).catch((err) => {
|
||||
).catch((err: Error) => {
|
||||
if (!isCanceled.current) {
|
||||
setIsRetrying(true);
|
||||
setRetryCount(number);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
import Bricks, { SizeDetail, Instance } from "bricks.js";
|
||||
|
||||
const useGrid = (sizes: SizeDetail[]) => {
|
||||
const ref = useRef(null as Node | null);
|
||||
const ref = useRef(null as HTMLElement | null);
|
||||
const grid = useRef(null as Instance | null);
|
||||
const [repack, setRepack] = useState(() => () => {});
|
||||
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const AlertState = PropTypes.oneOf(["unprocessed", "active", "suppressed"]);
|
||||
|
||||
const Annotation = PropTypes.exact({
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
isLink: PropTypes.bool.isRequired,
|
||||
});
|
||||
|
||||
const APIAlertAlertmanagerState = PropTypes.exact({
|
||||
fingerprint: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
cluster: PropTypes.string.isRequired,
|
||||
state: AlertState.isRequired,
|
||||
startsAt: PropTypes.string.isRequired,
|
||||
source: PropTypes.string.isRequired,
|
||||
silencedBy: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
inhibitedBy: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
});
|
||||
|
||||
const APIAlert = PropTypes.exact({
|
||||
id: PropTypes.string.isRequired,
|
||||
annotations: PropTypes.arrayOf(Annotation).isRequired,
|
||||
labels: PropTypes.object.isRequired,
|
||||
startsAt: PropTypes.string.isRequired,
|
||||
state: AlertState.isRequired,
|
||||
alertmanager: PropTypes.arrayOf(APIAlertAlertmanagerState).isRequired,
|
||||
receiver: PropTypes.string.isRequired,
|
||||
});
|
||||
|
||||
const APIStateCount = PropTypes.exact({
|
||||
active: PropTypes.number.isRequired,
|
||||
suppressed: PropTypes.number.isRequired,
|
||||
unprocessed: PropTypes.number.isRequired,
|
||||
});
|
||||
|
||||
const APIGroup = PropTypes.exact({
|
||||
receiver: PropTypes.string.isRequired,
|
||||
labels: PropTypes.object.isRequired,
|
||||
alerts: PropTypes.arrayOf(APIAlert),
|
||||
id: PropTypes.string.isRequired,
|
||||
alertmanagerCount: PropTypes.objectOf(PropTypes.number).isRequired,
|
||||
stateCount: APIStateCount.isRequired,
|
||||
shared: PropTypes.exact({
|
||||
annotations: PropTypes.arrayOf(Annotation).isRequired,
|
||||
labels: PropTypes.object.isRequired,
|
||||
silences: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string))
|
||||
.isRequired,
|
||||
}).isRequired,
|
||||
});
|
||||
|
||||
const APISilenceMatcher = PropTypes.exact({
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
isRegex: PropTypes.bool.isRequired,
|
||||
});
|
||||
|
||||
const APISilence = PropTypes.exact({
|
||||
id: PropTypes.string.isRequired,
|
||||
matchers: PropTypes.arrayOf(APISilenceMatcher).isRequired,
|
||||
startsAt: PropTypes.string.isRequired,
|
||||
endsAt: PropTypes.string.isRequired,
|
||||
createdAt: PropTypes.string.isRequired,
|
||||
createdBy: PropTypes.string.isRequired,
|
||||
comment: PropTypes.string.isRequired,
|
||||
ticketID: PropTypes.string.isRequired,
|
||||
ticketURL: PropTypes.string.isRequired,
|
||||
});
|
||||
|
||||
const APIAlertmanagerUpstream = PropTypes.exact({
|
||||
name: PropTypes.string.isRequired,
|
||||
cluster: PropTypes.string.isRequired,
|
||||
uri: PropTypes.string.isRequired,
|
||||
publicURI: PropTypes.string.isRequired,
|
||||
readonly: PropTypes.bool.isRequired,
|
||||
headers: PropTypes.object.isRequired,
|
||||
corsCredentials: PropTypes.oneOf(["omit", "same-origin", "include"])
|
||||
.isRequired,
|
||||
error: PropTypes.string.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
clusterMembers: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
});
|
||||
|
||||
const APIGrid = PropTypes.exact({
|
||||
labelName: PropTypes.string.isRequired,
|
||||
labelValue: PropTypes.string.isRequired,
|
||||
alertGroups: PropTypes.arrayOf(APIGroup).isRequired,
|
||||
stateCount: APIStateCount.isRequired,
|
||||
});
|
||||
|
||||
export {
|
||||
APIAlert,
|
||||
APIGroup,
|
||||
APISilence,
|
||||
APISilenceMatcher,
|
||||
APIAlertAlertmanagerState,
|
||||
APIAlertmanagerUpstream,
|
||||
APIGrid,
|
||||
};
|
||||
@@ -77,6 +77,13 @@ export interface APISilenceT {
|
||||
ticketURL: string;
|
||||
}
|
||||
|
||||
export interface APIManagedSilenceT {
|
||||
alertCount: number;
|
||||
cluster: string;
|
||||
isExpired: boolean;
|
||||
silence: APISilenceT;
|
||||
}
|
||||
|
||||
export interface APIGridT {
|
||||
labelName: string;
|
||||
labelValue: string;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const SilenceFormSuggestion = PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
});
|
||||
|
||||
const SilenceFormMatcher = PropTypes.exact({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
values: PropTypes.arrayOf(SilenceFormSuggestion).isRequired,
|
||||
isRegex: PropTypes.bool.isRequired,
|
||||
});
|
||||
|
||||
export { SilenceFormMatcher, SilenceFormSuggestion };
|
||||
@@ -16,12 +16,7 @@ import {
|
||||
APIAlertmanagerUpstreamT,
|
||||
AlertmanagerSilencePayloadT,
|
||||
} from "Models/APITypes";
|
||||
import { StringToOption, OptionT } from "Common/Select";
|
||||
|
||||
export interface MultiValueOptionT {
|
||||
label: string;
|
||||
value: string[];
|
||||
}
|
||||
import { StringToOption, OptionT, MultiValueOptionT } from "Common/Select";
|
||||
|
||||
export interface MatcherT {
|
||||
name: string;
|
||||
@@ -80,7 +75,7 @@ const SilenceTabNames = Object.freeze({
|
||||
const MatchersFromGroup = (
|
||||
group: APIAlertGroupT,
|
||||
stripLabels: string[],
|
||||
alerts: APIAlertT[],
|
||||
alerts?: APIAlertT[],
|
||||
onlyActive?: boolean
|
||||
): MatcherWithIDT[] => {
|
||||
let matchers: MatcherWithIDT[] = [];
|
||||
@@ -149,12 +144,24 @@ const MatchersFromGroup = (
|
||||
return matchers;
|
||||
};
|
||||
|
||||
const NewClusterRequest = (cluster: string, members: string[]) => ({
|
||||
export interface ClusterRequestT {
|
||||
cluster: string;
|
||||
members: string[];
|
||||
isDone: boolean;
|
||||
silenceID: undefined | string;
|
||||
silenceLink: undefined | string;
|
||||
error: null | string;
|
||||
}
|
||||
|
||||
const NewClusterRequest = (
|
||||
cluster: string,
|
||||
members: string[]
|
||||
): ClusterRequestT => ({
|
||||
cluster: cluster,
|
||||
members: members,
|
||||
isDone: false,
|
||||
silenceID: null,
|
||||
silenceLink: null,
|
||||
silenceID: undefined,
|
||||
silenceLink: undefined,
|
||||
error: null,
|
||||
});
|
||||
|
||||
@@ -201,10 +208,6 @@ const UnpackRegexMatcherValues = (isRegex: boolean, value: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
interface ClusterRequestT {
|
||||
foo: boolean;
|
||||
// FIXME
|
||||
}
|
||||
class SilenceFormStore {
|
||||
toggle = observable(
|
||||
{
|
||||
@@ -367,7 +370,7 @@ class SilenceFormStore {
|
||||
group: APIAlertGroupT,
|
||||
stripLabels: string[],
|
||||
alertmanagers: MultiValueOptionT[],
|
||||
alerts: APIAlertT[]
|
||||
alerts?: APIAlertT[]
|
||||
) {
|
||||
this.alertmanagers = alertmanagers;
|
||||
|
||||
|
||||
4
ui/src/react-app-env.d.ts
vendored
4
ui/src/react-app-env.d.ts
vendored
@@ -5,3 +5,7 @@ declare module "react-media-hook" {
|
||||
}
|
||||
|
||||
declare module "favico.js";
|
||||
|
||||
declare module "react-json-pretty/dist/monikai";
|
||||
|
||||
declare module "react-linkify";
|
||||
|
||||
Reference in New Issue
Block a user