chore(ui): migrate more components to typescript

This commit is contained in:
Łukasz Mierzwa
2020-07-16 11:43:19 +01:00
committed by Łukasz Mierzwa
parent 3b68860f38
commit e55988588c
70 changed files with 907 additions and 1069 deletions

21
ui/package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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,

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 (

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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:

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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) {

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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)
);

View File

@@ -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 };

View File

@@ -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}

View File

@@ -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 };

View File

@@ -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}

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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,
}) => {

View File

@@ -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)) {

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -54,6 +54,7 @@ const Modal: FC<{
isOpen: boolean;
isUpper?: boolean;
toggleOpen: () => void;
onExited?: () => void;
}> = ({
size = "lg",
isOpen,

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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,

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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,

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -56,7 +56,6 @@ const MountedSilenceModalContent = () => {
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
onHide={MockOnHide}
onDeleteModalClose={jest.fn()}
/>
);
};

View File

@@ -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>&times;</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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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>
);

View File

@@ -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");
});
});

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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(() => () => {});

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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;

View File

@@ -5,3 +5,7 @@ declare module "react-media-hook" {
}
declare module "favico.js";
declare module "react-json-pretty/dist/monikai";
declare module "react-linkify";