chore(ui): migrate more components to typescript

This commit is contained in:
Łukasz Mierzwa
2020-07-14 20:18:40 +01:00
committed by Łukasz Mierzwa
parent c5d399e3eb
commit 4a16661558
43 changed files with 491 additions and 556 deletions

View File

@@ -1,5 +1,13 @@
const NewLabelName = (v: string) => `New label: ${v}`;
export const NewLabelName = (v: string) => `New label: ${v}`;
const NewLabelValue = (v: string) => `New value: ${v}`;
export const NewLabelValue = (v: string) => `New value: ${v}`;
export { NewLabelName, NewLabelValue };
export interface OptionT {
label: string;
value: string;
}
export const StringToOption = (value: string): OptionT => ({
label: value,
value: value,
});

View File

@@ -1,5 +1,4 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import React, { FC, useEffect, useState } from "react";
import { toJS } from "mobx";
import { useObserver } from "mobx-react-lite";
@@ -12,19 +11,29 @@ import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
import { APIGroup } from "Models/API";
import { APIAlertGroupT, AlertmanagerSilencePayloadT } from "Models/APITypes";
import { AlertStore } from "Stores/AlertStore";
import {
SilenceFormStore,
MatchersFromGroup,
GenerateAlertmanagerSilenceData,
} from "Stores/SilenceFormStore";
import { useFetchAny } from "Hooks/useFetchAny";
import { useFetchAny, UpstreamT } from "Hooks/useFetchAny";
import { TooltipWrapper } from "Components/TooltipWrapper";
const AlertAck = ({ alertStore, silenceFormStore, group }) => {
const [clusters, setClusters] = useState([]);
const [upstreams, setUpstreams] = useState([]);
interface ClusterT {
payload: AlertmanagerSilencePayloadT;
clusterName: string;
members: string[];
}
const AlertAck: FC<{
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
group: APIAlertGroupT;
}> = ({ alertStore, silenceFormStore, group }) => {
const [clusters, setClusters] = useState([] as ClusterT[]);
const [upstreams, setUpstreams] = useState([] as UpstreamT[]);
const [currentCluster, setCurrentCluster] = useState(0);
const [isAcking, setIsAcking] = useState(false);
@@ -46,15 +55,15 @@ const AlertAck = ({ alertStore, silenceFormStore, group }) => {
}
const alertmanagers = Object.entries(group.alertmanagerCount)
.filter(([amName, alertCount]) => alertCount > 0)
.filter(([_, alertCount]) => alertCount > 0)
.map(([amName, _]) => amName);
const clusters = Object.entries(
alertStore.data.clustersWithoutReadOnly
).filter(([clusterName, clusterMembers]) =>
).filter(([_, clusterMembers]) =>
alertmanagers.some((m) => clusterMembers.includes(m))
);
let c = [];
let c: ClusterT[] = [];
for (const [clusterName, clusterMembers] of clusters) {
const durationSeconds = toJS(
alertStore.settings.values.alertAcknowledgement.durationSeconds
@@ -94,7 +103,7 @@ const AlertAck = ({ alertStore, silenceFormStore, group }) => {
if (clusters.length) {
reset();
const cluster = clusters[currentCluster];
let u = [];
let u: UpstreamT[] = [];
cluster.members.forEach((amName) => {
const am = alertStore.data.getAlertmanagerByName(amName);
if (am !== undefined) {
@@ -119,9 +128,9 @@ const AlertAck = ({ alertStore, silenceFormStore, group }) => {
}, [alertStore.data, clusters, currentCluster, reset]);
useEffect(() => {
let timer;
let timer: number;
if (!isAcking && error) {
timer = setTimeout(() => {
timer = window.setTimeout(() => {
setUpstreams([]);
setIsAcking(false);
reset();
@@ -168,10 +177,5 @@ const AlertAck = ({ alertStore, silenceFormStore, group }) => {
)
);
};
AlertAck.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
group: APIGroup.isRequired,
};
export { AlertAck };

View File

@@ -1,11 +1,14 @@
import React from "react";
import PropTypes from "prop-types";
import React, { FC } from "react";
import { AlertStore } from "Stores/AlertStore";
import { FormatQuery, QueryOperators, StaticLabels } from "Common/Query";
import { PaginatedAlertList } from "Components/PaginatedAlertList";
const InhibitedByModalContent = ({ alertStore, fingerprints, onHide }) => {
const InhibitedByModalContent: FC<{
alertStore: AlertStore;
fingerprints: string[];
onHide: () => void;
}> = ({ alertStore, fingerprints, onHide }) => {
return (
<React.Fragment>
<div className="modal-header">
@@ -29,10 +32,5 @@ const InhibitedByModalContent = ({ alertStore, fingerprints, onHide }) => {
</React.Fragment>
);
};
InhibitedByModalContent.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
fingerprints: PropTypes.arrayOf(PropTypes.string).isRequired,
onHide: PropTypes.func.isRequired,
};
export { InhibitedByModalContent };

View File

@@ -1,5 +1,4 @@
import React, { useState, useCallback } from "react";
import PropTypes from "prop-types";
import React, { FC, useState, useCallback } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner";
@@ -16,7 +15,10 @@ const InhibitedByModalContent = React.lazy(() =>
}))
);
const InhibitedByModal = ({ alertStore, fingerprints }) => {
const InhibitedByModal: FC<{
alertStore: AlertStore;
fingerprints: string[];
}> = ({ alertStore, fingerprints }) => {
const [isVisible, setIsVisible] = useState(false);
const toggle = useCallback(() => setIsVisible(!isVisible), [isVisible]);
@@ -42,7 +44,6 @@ const InhibitedByModal = ({ alertStore, fingerprints }) => {
<InhibitedByModalContent
alertStore={alertStore}
onHide={() => setIsVisible(false)}
isVisible={isVisible}
fingerprints={fingerprints}
/>
</React.Suspense>
@@ -50,9 +51,5 @@ const InhibitedByModal = ({ alertStore, fingerprints }) => {
</React.Fragment>
);
};
InhibitedByModal.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
fingerprints: PropTypes.arrayOf(PropTypes.string).isRequired,
};
export { InhibitedByModal };

View File

@@ -1,9 +1,22 @@
import React, { useState, useRef, useEffect } from "react";
import PropTypes from "prop-types";
import React, {
FC,
useState,
useRef,
useEffect,
KeyboardEvent,
ChangeEvent,
} from "react";
import { useOnClickOutside } from "Hooks/useOnClickOutside";
const InlineEdit = ({
const InlineEdit: FC<{
className: string;
classNameEditing: string;
value: string;
onChange: (value: string) => void;
onEnterEditing: () => void;
onExitEditing: () => void;
}> = ({
className,
classNameEditing,
value,
@@ -11,8 +24,8 @@ const InlineEdit = ({
onEnterEditing,
onExitEditing,
}) => {
const ref = useRef(null);
const [editedValue, setEditedValue] = useState(null);
const ref = useRef(null as null | HTMLInputElement);
const [editedValue, setEditedValue] = useState(null as null | string);
const [isEditing, setIsEditing] = useState(false);
const startEditing = () => {
@@ -30,11 +43,11 @@ const InlineEdit = ({
}
};
const onInput = (event) => {
const onInput = (event: ChangeEvent<HTMLInputElement>) => {
setEditedValue(event.target.value.trim());
};
const onKeyDown = (event) => {
const onKeyDown = (event: KeyboardEvent) => {
if (event.keyCode === 13) {
if (editedValue) {
onChange(editedValue);
@@ -75,13 +88,5 @@ const InlineEdit = ({
</span>
);
};
InlineEdit.propTypes = {
className: PropTypes.string,
classNameEditing: PropTypes.string,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
onEnterEditing: PropTypes.func,
onExitEditing: PropTypes.func,
};
export { InlineEdit };

View File

@@ -1,7 +1,7 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import React, { FC, useState } from "react";
import { AlertStore } from "Stores/AlertStore";
import { APIAlertGroupT } from "Models/APITypes";
import { IsMobile } from "Common/Device";
import { hashObject } from "Common/Hash";
import { StaticLabel } from "Components/Labels/StaticLabel";
@@ -9,8 +9,8 @@ import { PageSelect } from "Components/Pagination";
// take a list of groups and outputs a list of label sets, this ignores
// the receiver, so we'll end up with only unique alerts
const GroupListToUniqueLabelsList = (groups) => {
const alerts = {};
const GroupListToUniqueLabelsList = (groups: APIAlertGroupT[]) => {
const alerts: { [alertHash: string]: { [labelName: string]: string } } = {};
for (const group of groups) {
for (const alert of group.alerts) {
const alertLabels = Object.assign(
@@ -26,7 +26,11 @@ const GroupListToUniqueLabelsList = (groups) => {
return Object.values(alerts);
};
const LabelSetList = ({ alertStore, labelsList, title }) => {
const LabelSetList: FC<{
alertStore: AlertStore;
labelsList: { [labelName: string]: string }[];
title?: string;
}> = ({ alertStore, labelsList, title }) => {
const [activePage, setActivePage] = useState(1);
const maxPerPage = IsMobile() ? 5 : 10;
@@ -70,10 +74,5 @@ const LabelSetList = ({ alertStore, labelsList, title }) => {
</div>
);
};
LabelSetList.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
labelsList: PropTypes.arrayOf(PropTypes.object).isRequired,
title: PropTypes.string,
};
export { LabelSetList, GroupListToUniqueLabelsList };

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,14 +7,17 @@ import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclama
import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner";
import { faTimes } from "@fortawesome/free-solid-svg-icons/faTimes";
import { AlertStore } from "Stores/AlertStore";
import { AlertStore, FilterT } from "Stores/AlertStore";
import { QueryOperators } from "Common/Query";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { GetClassAndStyle } from "Components/Labels/Utils";
import { InlineEdit } from "Components/InlineEdit";
const FilterInputLabel = ({ alertStore, filter }) => {
const onChange = (val) => {
const FilterInputLabel: FC<{
alertStore: AlertStore;
filter: FilterT;
}> = ({ alertStore, filter }) => {
const onChange = (val: string) => {
alertStore.status.resume();
// if filter is empty string then remove it
if (val === "") {
@@ -80,17 +82,5 @@ const FilterInputLabel = ({ alertStore, filter }) => {
</button>
));
};
FilterInputLabel.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
filter: PropTypes.shape({
raw: PropTypes.string,
applied: PropTypes.bool,
isValid: PropTypes.bool,
hits: PropTypes.number,
name: PropTypes.string,
matcher: PropTypes.string,
value: PropTypes.string,
}),
};
export { FilterInputLabel };

View File

@@ -1,5 +1,4 @@
import React, { useCallback } from "react";
import PropTypes from "prop-types";
import React, { FC, useCallback, MouseEvent } from "react";
import { observer } from "mobx-react-lite";
@@ -14,7 +13,16 @@ import { useFlashTransition } from "Hooks/useFlashTransition";
// Same as FilteringLabel but for labels that are counters (usually @state)
// and only renders a pill badge with the counter, it doesn't render anything
// if the counter is 0
const FilteringCounterBadge = observer(
const FilteringCounterBadge: FC<{
alertStore: AlertStore;
name: string;
value: string;
counter: number;
themed: boolean;
alwaysVisible: boolean;
defaultColor: "light" | "primary";
isAppend: boolean;
}> = observer(
({
alertStore,
name,
@@ -22,13 +30,13 @@ const FilteringCounterBadge = observer(
counter,
themed,
alwaysVisible,
defaultColor,
isAppend,
defaultColor = "light",
isAppend = true,
}) => {
const { ref, props } = useFlashTransition(counter);
const handleClick = useCallback(
(event) => {
(event: MouseEvent) => {
// left click => apply foo=bar filter
// left click + alt => apply foo!=bar filter
const operator =
@@ -82,19 +90,5 @@ const FilteringCounterBadge = observer(
);
}
);
FilteringCounterBadge.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
counter: PropTypes.number.isRequired,
themed: PropTypes.bool.isRequired,
alwaysVisible: PropTypes.bool,
defaultColor: PropTypes.oneOf(["light", "primary"]),
isAppend: PropTypes.bool,
};
FilteringCounterBadge.defaultProps = {
defaultColor: "light",
isAppend: true,
};
export { FilteringCounterBadge };

View File

@@ -1,14 +1,19 @@
import React, { useCallback } from "react";
import React, { FC, useCallback, MouseEvent } from "react";
import { useObserver } from "mobx-react-lite";
import { AlertStore } from "Stores/AlertStore";
import { QueryOperators, FormatQuery } from "Common/Query";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { GetClassAndStyle } from "Components/Labels/Utils";
const FilteringLabel = ({ alertStore, name, value }) => {
const FilteringLabel: FC<{
alertStore: AlertStore;
name: string;
value: string;
}> = ({ alertStore, name, value }) => {
const handleClick = useCallback(
(event) => {
(event: MouseEvent) => {
// left click => apply foo=bar filter
// left click + alt => apply foo!=bar filter
const operator =

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";
@@ -7,7 +6,12 @@ import { QueryOperators } from "Common/Query";
import { AlertStore } from "Stores/AlertStore";
import { GetClassAndStyle } from "Components/Labels/Utils";
const HistoryLabel = ({ alertStore, name, matcher, value }) => {
const HistoryLabel: FC<{
alertStore: AlertStore;
name: string;
matcher: string;
value: string;
}> = ({ alertStore, name, matcher, value }) => {
const cs = GetClassAndStyle(
alertStore,
matcher === QueryOperators.Equal ? name : "",
@@ -22,11 +26,5 @@ const HistoryLabel = ({ alertStore, name, matcher, value }) => {
</span>
));
};
HistoryLabel.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
matcher: PropTypes.string.isRequired,
};
export { HistoryLabel };

View File

@@ -1,5 +1,4 @@
import React, { useCallback } from "react";
import PropTypes from "prop-types";
import React, { FC, useCallback, MouseEvent } from "react";
import { observer } from "mobx-react-lite";
@@ -10,10 +9,18 @@ import { AlertStore } from "Stores/AlertStore";
import { QueryOperators, FormatQuery } from "Common/Query";
import { GetClassAndStyle } from "Components/Labels/Utils";
const LabelWithPercent = observer(
const LabelWithPercent: FC<{
alertStore: AlertStore;
name: string;
value: string;
hits: number;
percent: number;
offset: number;
isActive: boolean;
}> = observer(
({ alertStore, name, value, hits, percent, offset, isActive }) => {
const handleClick = useCallback(
(event) => {
(event: MouseEvent) => {
// left click => apply foo=bar filter
// left click + alt => apply foo!=bar filter
const operator =
@@ -70,8 +77,8 @@ const LabelWithPercent = observer(
role="progressbar"
style={{ width: offset + "%" }}
aria-valuenow={offset}
aria-valuemin="0"
aria-valuemax="100"
aria-valuemin={0}
aria-valuemax={100}
/>
)}
<div
@@ -79,22 +86,13 @@ const LabelWithPercent = observer(
role="progressbar"
style={{ width: percent + "%" }}
aria-valuenow={percent}
aria-valuemin="0"
aria-valuemax="100"
aria-valuemin={0}
aria-valuemax={100}
/>
</div>
</div>
);
}
);
LabelWithPercent.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
hits: PropTypes.number.isRequired,
percent: PropTypes.number.isRequired,
offset: PropTypes.number.isRequired,
isActive: PropTypes.bool.isRequired,
};
export { LabelWithPercent };

View File

@@ -1,11 +1,16 @@
import React from "react";
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { AlertStore } from "Stores/AlertStore";
import { GetClassAndStyle } from "Components/Labels/Utils";
// Renders a static label element, no click actions, no hover
const StaticLabel = ({ alertStore, name, value }) => {
const StaticLabel: FC<{
alertStore: AlertStore;
name: string;
value: string;
}> = ({ alertStore, name, value }) => {
const cs = GetClassAndStyle(alertStore, name, value);
return useObserver(() => (

View File

@@ -1,32 +1,34 @@
import React from "react";
import PropTypes from "prop-types";
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import Select from "react-select";
import { Settings } from "Stores/Settings";
import { OptionT } from "Common/Select";
import { Settings, CollapseStateT } from "Stores/Settings";
import { ThemeContext } from "Components/Theme";
const AlertGroupCollapseConfiguration = ({ settingsStore }) => {
const AlertGroupCollapseConfiguration: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
if (
!Object.values(settingsStore.alertGroupConfig.options)
.map((o) => o.value)
.includes(settingsStore.alertGroupConfig.config.defaultCollapseState)
) {
settingsStore.alertGroupConfig.config.defaultCollapseState =
settingsStore.alertGroupConfig.options.collapsedOnMobile.value;
"collapsedOnMobile";
}
const valueToOption = (val) => {
const valueToOption = (val: CollapseStateT): OptionT => {
return {
label: settingsStore.alertGroupConfig.options[val].label,
value: val,
};
};
const onCollapseChange = (newValue, actionMeta) => {
settingsStore.alertGroupConfig.config.defaultCollapseState = newValue.value;
const onCollapseChange = (newValue: CollapseStateT) => {
settingsStore.alertGroupConfig.config.defaultCollapseState = newValue;
};
const context = React.useContext(ThemeContext);
@@ -41,14 +43,13 @@ const AlertGroupCollapseConfiguration = ({ settingsStore }) => {
settingsStore.alertGroupConfig.config.defaultCollapseState
)}
options={Object.values(settingsStore.alertGroupConfig.options)}
onChange={onCollapseChange}
onChange={(option) =>
onCollapseChange((option as OptionT).value as CollapseStateT)
}
hideSelectedOptions
/>
</div>
));
};
AlertGroupCollapseConfiguration.propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired,
};
export { AlertGroupCollapseConfiguration };

View File

@@ -1,35 +0,0 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import InputRange from "react-input-range";
import { Settings } from "Stores/Settings";
const AlertGroupConfiguration = ({ settingsStore }) => {
const [defaultRenderCount, setDefaultRenderCount] = useState(
settingsStore.alertGroupConfig.config.defaultRenderCount
);
const onChangeComplete = (value) => {
settingsStore.alertGroupConfig.setDefaultRenderCount(value);
};
return (
<div className="form-group mb-0 text-center">
<InputRange
minValue={1}
maxValue={10}
step={1}
value={defaultRenderCount}
id="formControlRange"
onChange={setDefaultRenderCount}
onChangeComplete={onChangeComplete}
/>
</div>
);
};
AlertGroupConfiguration.propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired,
};
export { AlertGroupConfiguration };

View File

@@ -0,0 +1,32 @@
import React, { FC, useState } from "react";
import InputRange from "react-input-range";
import { Settings } from "Stores/Settings";
const AlertGroupConfiguration: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
const [defaultRenderCount, setDefaultRenderCount] = useState(
settingsStore.alertGroupConfig.config.defaultRenderCount as number
);
const onChangeComplete = (value: number) => {
settingsStore.alertGroupConfig.setDefaultRenderCount(value as number);
};
return (
<div className="form-group mb-0 text-center">
<InputRange
minValue={1}
maxValue={10}
step={1}
value={defaultRenderCount}
onChange={(value) => setDefaultRenderCount(value as number)}
onChangeComplete={(value) => onChangeComplete(value as number)}
/>
</div>
);
};
export { AlertGroupConfiguration };

View File

@@ -1,33 +1,34 @@
import React from "react";
import PropTypes from "prop-types";
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import Select from "react-select";
import { Settings } from "Stores/Settings";
import { Settings, SortOrderT } from "Stores/Settings";
import { OptionT } from "Common/Select";
import { ThemeContext } from "Components/Theme";
import { SortLabelName } from "./SortLabelName";
const AlertGroupSortConfiguration = ({ settingsStore }) => {
const AlertGroupSortConfiguration: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
if (
!Object.values(settingsStore.gridConfig.options)
.map((o) => o.value)
.includes(settingsStore.gridConfig.config.sortOrder)
) {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.default.value;
settingsStore.gridConfig.config.sortOrder = "default";
}
const onSortOrderChange = (newValue, actionMeta) => {
settingsStore.gridConfig.config.sortOrder = newValue.value;
const onSortOrderChange = (value: SortOrderT) => {
settingsStore.gridConfig.config.sortOrder = value;
};
const onSortReverseChange = (event) => {
settingsStore.gridConfig.config.reverseSort = event.target.checked;
const onSortReverseChange = (value: boolean) => {
settingsStore.gridConfig.config.reverseSort = value;
};
const valueToOption = (val) => {
const valueToOption = (val: SortOrderT) => {
return { label: settingsStore.gridConfig.options[val].label, value: val };
};
@@ -51,7 +52,9 @@ const AlertGroupSortConfiguration = ({ settingsStore }) => {
settingsStore.gridConfig.config.sortOrder
)}
options={Object.values(settingsStore.gridConfig.options)}
onChange={onSortOrderChange}
onChange={(option) =>
onSortOrderChange((option as OptionT).value as SortOrderT)
}
hideSelectedOptions
/>
</div>
@@ -70,7 +73,7 @@ const AlertGroupSortConfiguration = ({ settingsStore }) => {
type="checkbox"
value=""
checked={settingsStore.gridConfig.config.reverseSort || false}
onChange={onSortReverseChange}
onChange={(event) => onSortReverseChange(event.target.checked)}
/>
<label
className="custom-control-label cursor-pointer mr-3"
@@ -85,8 +88,5 @@ const AlertGroupSortConfiguration = ({ settingsStore }) => {
</div>
));
};
AlertGroupSortConfiguration.propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired,
};
export { AlertGroupSortConfiguration };

View File

@@ -1,13 +1,14 @@
import React from "react";
import PropTypes from "prop-types";
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { Settings } from "Stores/Settings";
const AlertGroupTitleBarColor = ({ settingsStore }) => {
const onChange = (event) => {
settingsStore.alertGroupConfig.config.colorTitleBar = event.target.checked;
const AlertGroupTitleBarColor: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
const onChange = (value: boolean) => {
settingsStore.alertGroupConfig.config.colorTitleBar = value;
};
return useObserver(() => (
@@ -22,7 +23,7 @@ const AlertGroupTitleBarColor = ({ settingsStore }) => {
checked={
settingsStore.alertGroupConfig.config.colorTitleBar || false
}
onChange={onChange}
onChange={(event) => onChange(event.target.checked)}
/>
<label
className="custom-control-label cursor-pointer mr-3"
@@ -35,8 +36,5 @@ const AlertGroupTitleBarColor = ({ settingsStore }) => {
</div>
));
};
AlertGroupTitleBarColor.propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired,
};
export { AlertGroupTitleBarColor };

View File

@@ -1,16 +1,17 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import React, { FC, useState } from "react";
import InputRange from "react-input-range";
import { Settings } from "Stores/Settings";
const FetchConfiguration = ({ settingsStore }) => {
const FetchConfiguration: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
const [fetchInterval, setFetchInterval] = useState(
settingsStore.fetchConfig.config.interval
);
const onChangeComplete = (value) => {
const onChangeComplete = (value: number) => {
settingsStore.fetchConfig.setInterval(value);
};
@@ -21,16 +22,12 @@ const FetchConfiguration = ({ settingsStore }) => {
maxValue={120}
step={10}
value={fetchInterval}
id="formControlRange"
formatLabel={(value) => `${value}s`}
onChange={setFetchInterval}
onChangeComplete={onChangeComplete}
onChange={(value) => setFetchInterval(value as number)}
onChangeComplete={(value) => onChangeComplete(value as number)}
/>
</div>
);
};
FetchConfiguration.propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired,
};
export { FetchConfiguration };

View File

@@ -1,13 +1,14 @@
import React from "react";
import PropTypes from "prop-types";
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { Settings } from "Stores/Settings";
const FilterBarConfiguration = ({ settingsStore }) => {
const onAutohideChange = (event) => {
settingsStore.filterBarConfig.config.autohide = event.target.checked;
const FilterBarConfiguration: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
const onAutohideChange = (value: boolean) => {
settingsStore.filterBarConfig.config.autohide = value;
};
return useObserver(() => (
<div className="form-group mb-0">
@@ -19,7 +20,7 @@ const FilterBarConfiguration = ({ settingsStore }) => {
type="checkbox"
value=""
checked={settingsStore.filterBarConfig.config.autohide || false}
onChange={onAutohideChange}
onChange={(event) => onAutohideChange(event.target.checked)}
/>
<label
className="custom-control-label cursor-pointer mr-3"
@@ -32,8 +33,5 @@ const FilterBarConfiguration = ({ settingsStore }) => {
</div>
));
};
FilterBarConfiguration.propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired,
};
export { FilterBarConfiguration };

View File

@@ -1,5 +1,4 @@
import React from "react";
import PropTypes from "prop-types";
import React, { FC } from "react";
import Creatable from "react-select/creatable";
@@ -7,11 +6,14 @@ import { useFetchGet } from "Hooks/useFetchGet";
import { FormatBackendURI } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { ThemeContext } from "Components/Theme";
import { NewLabelName } from "Common/Select";
import { NewLabelName, StringToOption } from "Common/Select";
const disabledLabel = "Disable multi-grid";
const valueToOption = (v) => ({ label: v ? v : disabledLabel, value: v });
const valueToOption = (v: string) => ({
label: v ? v : disabledLabel,
value: v,
});
const staticValues = [
{ label: disabledLabel, value: "" },
@@ -20,7 +22,9 @@ const staticValues = [
{ label: "@receiver", value: "@receiver" },
];
const GridLabelName = ({ settingsStore }) => {
const GridLabelName: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
const { response } = useFetchGet(FormatBackendURI(`labelNames.json`));
const context = React.useContext(ThemeContext);
@@ -38,10 +42,7 @@ const GridLabelName = ({ settingsStore }) => {
response
? [
...staticValues,
...response.map((value) => ({
label: value,
value: value,
})),
...response.map((value: string) => StringToOption(value)),
]
: staticValues
}
@@ -51,8 +52,5 @@ const GridLabelName = ({ settingsStore }) => {
/>
);
};
GridLabelName.propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired,
};
export { GridLabelName };

View File

@@ -1,14 +1,15 @@
import React from "react";
import PropTypes from "prop-types";
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { Settings } from "Stores/Settings";
import { GridLabelName } from "./GridLabelName";
const MultiGridConfiguration = ({ settingsStore }) => {
const onSortReverseChange = (event) => {
settingsStore.multiGridConfig.config.gridSortReverse = event.target.checked;
const MultiGridConfiguration: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
const onSortReverseChange = (value: boolean) => {
settingsStore.multiGridConfig.config.gridSortReverse = value;
};
return useObserver(() => (
@@ -27,7 +28,7 @@ const MultiGridConfiguration = ({ settingsStore }) => {
checked={
settingsStore.multiGridConfig.config.gridSortReverse || false
}
onChange={onSortReverseChange}
onChange={(event) => onSortReverseChange(event.target.checked)}
/>
<label
className="custom-control-label cursor-pointer mr-3"
@@ -41,8 +42,5 @@ const MultiGridConfiguration = ({ settingsStore }) => {
</div>
));
};
MultiGridConfiguration.propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired,
};
export { MultiGridConfiguration };

View File

@@ -1,18 +1,18 @@
import React from "react";
import PropTypes from "prop-types";
import React, { FC } from "react";
import Creatable from "react-select/creatable";
import { StaticLabels } from "Common/Query";
import { OptionT } from "Common/Select";
import { FormatBackendURI } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { useFetchGet } from "Hooks/useFetchGet";
import { ThemeContext } from "Components/Theme";
import { NewLabelName } from "Common/Select";
import { NewLabelName, StringToOption } from "Common/Select";
const valueToOption = (v) => ({ label: v, value: v });
const SortLabelName = ({ settingsStore }) => {
const SortLabelName: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
const { response } = useFetchGet(FormatBackendURI(`labelNames.json`));
if (!settingsStore.gridConfig.config.sortLabel) {
@@ -27,23 +27,16 @@ const SortLabelName = ({ settingsStore }) => {
classNamePrefix="react-select"
instanceId="configuration-sort-label"
formatCreateLabel={NewLabelName}
defaultValue={valueToOption(settingsStore.gridConfig.config.sortLabel)}
defaultValue={StringToOption(settingsStore.gridConfig.config.sortLabel)}
options={
response
? response.map((value) => ({
label: value,
value: value,
}))
: []
response ? response.map((value: string) => StringToOption(value)) : []
}
onChange={({ value }) => {
settingsStore.gridConfig.config.sortLabel = value;
onChange={(option) => {
settingsStore.gridConfig.config.sortLabel = (option as OptionT)
.value as string;
}}
/>
);
};
SortLabelName.propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired,
};
export { SortLabelName };

View File

@@ -1,32 +1,33 @@
import React from "react";
import PropTypes from "prop-types";
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import Select from "react-select";
import { Settings } from "Stores/Settings";
import { OptionT } from "Common/Select";
import { Settings, ThemeT } from "Stores/Settings";
import { ThemeContext } from "Components/Theme";
const ThemeConfiguration = ({ settingsStore }) => {
const ThemeConfiguration: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
if (
!Object.values(settingsStore.themeConfig.options)
.map((o) => o.value)
.includes(settingsStore.themeConfig.config.theme)
) {
settingsStore.themeConfig.config.theme =
settingsStore.themeConfig.options.auto.value;
settingsStore.themeConfig.config.theme = "auto";
}
const valueToOption = (val) => {
const valueToOption = (val: ThemeT) => {
return {
label: settingsStore.themeConfig.options[val].label,
value: val,
};
};
const onCollapseChange = (newValue, actionMeta) => {
settingsStore.themeConfig.config.theme = newValue.value;
const onCollapseChange = (newValue: ThemeT) => {
settingsStore.themeConfig.config.theme = newValue;
};
const context = React.useContext(ThemeContext);
@@ -39,14 +40,13 @@ const ThemeConfiguration = ({ settingsStore }) => {
instanceId="configuration-theme"
defaultValue={valueToOption(settingsStore.themeConfig.config.theme)}
options={Object.values(settingsStore.themeConfig.options)}
onChange={onCollapseChange}
onChange={(option) =>
onCollapseChange((option as OptionT).value as ThemeT)
}
hideSelectedOptions
/>
</div>
));
};
ThemeConfiguration.propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired,
};
export { ThemeConfiguration };

View File

@@ -1,5 +1,4 @@
import React from "react";
import PropTypes from "prop-types";
import React, { FC } from "react";
import { Settings } from "Stores/Settings";
import { Accordion } from "Components/Accordion";
@@ -13,7 +12,10 @@ import { AlertGroupTitleBarColor } from "./AlertGroupTitleBarColor";
import { ThemeConfiguration } from "./ThemeConfiguration";
import { MultiGridConfiguration } from "./MultiGridConfiguration";
const Configuration = ({ settingsStore, defaultIsOpen }) => (
const Configuration: FC<{
settingsStore: Settings;
defaultIsOpen: boolean;
}> = ({ settingsStore, defaultIsOpen }) => (
<form className="px-3 accordion">
<Accordion
text="Refresh interval"
@@ -64,9 +66,5 @@ const Configuration = ({ settingsStore, defaultIsOpen }) => (
/>
</form>
);
Configuration.propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired,
defaultIsOpen: PropTypes.bool.isRequired,
};
export { Configuration };

View File

@@ -1,12 +1,14 @@
import React from "react";
import PropTypes from "prop-types";
import React, { FC, ReactNode } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons/faInfoCircle";
import { Accordion } from "Components/Accordion";
const FilterOperatorHelp = ({ operator, description, children }) => (
const FilterOperatorHelp: FC<{
operator: string;
description: string;
}> = ({ operator, description, children }) => (
<React.Fragment>
<dt>
<kbd>{operator}</kbd> {description}
@@ -24,12 +26,12 @@ const FilterOperatorHelp = ({ operator, description, children }) => (
</dd>
</React.Fragment>
);
FilterOperatorHelp.propTypes = {
operator: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
};
const QueryHelp = ({ title, operators, warning, children }) => (
const QueryHelp: FC<{
title: string;
operators: string[];
warning?: ReactNode;
}> = ({ title, operators, warning, children }) => (
<React.Fragment>
<dt>{title}</dt>
<dd className="mb-5">
@@ -52,13 +54,10 @@ const QueryHelp = ({ title, operators, warning, children }) => (
</dd>
</React.Fragment>
);
QueryHelp.propTypes = {
title: PropTypes.string.isRequired,
operators: PropTypes.arrayOf(PropTypes.string).isRequired,
warning: PropTypes.node,
};
const FilterExample = ({ example, children }) => (
const FilterExample: FC<{
example: string;
}> = ({ example, children }) => (
<li>
<div>
<span className="badge badge-info">{example}</span>
@@ -66,12 +65,8 @@ const FilterExample = ({ example, children }) => (
<div>{children}</div>
</li>
);
FilterExample.propTypes = {
example: PropTypes.string.isRequired,
children: PropTypes.node,
};
const Help = ({ defaultIsOpen }) => (
const Help: FC<{ defaultIsOpen: boolean }> = ({ defaultIsOpen }) => (
<div className="accordion">
<Accordion
text="Fiter operators"
@@ -319,8 +314,5 @@ const Help = ({ defaultIsOpen }) => (
/>
</div>
);
Help.propTypes = {
defaultIsOpen: PropTypes.bool.isRequired,
};
export { Help };

View File

@@ -1,5 +1,4 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import React, { FC, useState } from "react";
import { useObserver } from "mobx-react-lite";
@@ -9,16 +8,19 @@ import { Tab } from "Components/Modal/Tab";
import { Configuration } from "./Configuration";
import { Help } from "./Help";
const TabNames = Object.freeze({
Configuration: "configuration",
Help: "help",
});
export type OpenTabT = "configuration" | "help";
const MainModalContent = ({
const MainModalContent: FC<{
alertStore: AlertStore;
settingsStore: Settings;
onHide: () => void;
openTab?: OpenTabT;
expandAllOptions: boolean;
}> = ({
alertStore,
settingsStore,
onHide,
openTab,
openTab = "configuration",
expandAllOptions,
}) => {
const [tab, setTab] = useState(openTab);
@@ -29,13 +31,13 @@ const MainModalContent = ({
<nav className="nav nav-pills nav-justified w-100">
<Tab
title="Configuration"
active={tab === TabNames.Configuration}
onClick={() => setTab(TabNames.Configuration)}
active={tab === "configuration"}
onClick={() => setTab("configuration")}
/>
<Tab
title="Help"
active={tab === TabNames.Help}
onClick={() => setTab(TabNames.Help)}
active={tab === "help"}
onClick={() => setTab("help")}
/>
<button type="button" className="close" onClick={onHide}>
<span>&times;</span>
@@ -43,10 +45,8 @@ const MainModalContent = ({
</nav>
</div>
<div className="modal-body">
{tab === TabNames.Help ? (
<Help defaultIsOpen={expandAllOptions} />
) : null}
{tab === TabNames.Configuration ? (
{tab === "help" ? <Help defaultIsOpen={expandAllOptions} /> : null}
{tab === "configuration" ? (
<Configuration
settingsStore={settingsStore}
defaultIsOpen={expandAllOptions}
@@ -64,15 +64,5 @@ const MainModalContent = ({
</React.Fragment>
));
};
MainModalContent.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
settingsStore: PropTypes.instanceOf(Settings).isRequired,
onHide: PropTypes.func.isRequired,
openTab: PropTypes.oneOf(Object.values(TabNames)),
expandAllOptions: PropTypes.bool.isRequired,
};
MainModalContent.defaultProps = {
openTab: TabNames.Configuration,
};
export { MainModalContent, TabNames };
export { MainModalContent };

View File

@@ -1,5 +1,4 @@
import React, { useState, useCallback } from "react";
import PropTypes from "prop-types";
import React, { FC, useState, useCallback } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCog } from "@fortawesome/free-solid-svg-icons/faCog";
@@ -17,7 +16,10 @@ const MainModalContent = React.lazy(() =>
}))
);
const MainModal = ({ alertStore, settingsStore }) => {
const MainModal: FC<{
alertStore: AlertStore;
settingsStore: Settings;
}> = ({ alertStore, settingsStore }) => {
const [isVisible, setIsVisible] = useState(false);
const toggle = useCallback(() => setIsVisible(!isVisible), [isVisible]);
@@ -51,7 +53,6 @@ const MainModal = ({ alertStore, settingsStore }) => {
alertStore={alertStore}
settingsStore={settingsStore}
onHide={() => setIsVisible(false)}
isVisible={isVisible}
expandAllOptions={false}
/>
</React.Suspense>
@@ -59,9 +60,5 @@ const MainModal = ({ alertStore, settingsStore }) => {
</React.Fragment>
);
};
MainModal.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
settingsStore: PropTypes.instanceOf(Settings).isRequired,
};
export { MainModal };

View File

@@ -1,131 +0,0 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { observer } from "mobx-react-lite";
import { AlertStore } from "Stores/AlertStore";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { LabelWithPercent } from "Components/Labels/LabelWithPercent";
import { ToggleIcon } from "Components/ToggleIcon";
const TableRows = observer(({ alertStore, nameStats }) =>
nameStats.map((nameStats) => (
<tr key={nameStats.name}>
<td width="25%" className="text-nowrap mw-100 p-1">
<span className="badge badge-light components-label mx-0 mt-0 mb-auto pl-0 text-left">
<span className="bg-primary text-white mr-1 px-1 components-labelWithPercent-percent">
{nameStats.hits}
</span>
{nameStats.name}
</span>
</td>
<td width="75%" className="mw-100 p-1">
{nameStats.values.slice(0, 9).map((valueStats, i) => (
<LabelWithPercent
alertStore={alertStore}
key={valueStats.value}
name={nameStats.name}
value={valueStats.value}
hits={valueStats.hits}
percent={valueStats.percent}
offset={valueStats.offset}
isActive={
alertStore.filters.values.filter((f) => f.raw === valueStats.raw)
.length > 0
}
/>
))}
{nameStats.values.length > 9 ? (
<div className="components-label badge my-2 text-muted mw-100">
+{nameStats.values.length - 9} more
</div>
) : null}
</td>
</tr>
))
);
const LabelsTable = observer(
({ alertStore, showAllLabels, toggleAllLabels }) => (
<React.Fragment>
<table
className="table table-borderless top-labels"
style={{ tableLayout: "fixed" }}
>
<tbody className="mw-100">
<TableRows
alertStore={alertStore}
nameStats={alertStore.data.counters.filter(
(nameStats) => nameStats.hits >= alertStore.info.totalAlerts
)}
></TableRows>
{alertStore.data.counters.filter(
(nameStats) => nameStats.hits < alertStore.info.totalAlerts
).length > 0 ? (
<tr>
<td colSpan="2" className="px-1 py-0">
<TooltipWrapper
title="Toggle all / only common labels"
delay={[3000, 100]}
>
<ToggleIcon
isOpen={showAllLabels}
className="cursor-pointer text-muted"
onClick={toggleAllLabels}
/>
</TooltipWrapper>
</td>
</tr>
) : null}
{showAllLabels ? (
<TableRows
alertStore={alertStore}
nameStats={alertStore.data.counters.filter(
(nameStats) => nameStats.hits < alertStore.info.totalAlerts
)}
></TableRows>
) : null}
</tbody>
</table>
</React.Fragment>
)
);
const NothingToShow = () => (
<div className="jumbotron bg-transparent">
<h1 className="display-5 text-placeholder text-center">
No labels to display
</h1>
</div>
);
const OverviewModalContent = observer(({ alertStore, onHide }) => {
const [showAllLabels, setShowAllLabels] = useState(false);
return (
<React.Fragment>
<div className="modal-header">
<h5 className="modal-title">Overview</h5>
<button type="button" className="close" onClick={onHide}>
<span className="align-middle">&times;</span>
</button>
</div>
<div className="modal-body">
{alertStore.data.counters.length === 0 ? (
<NothingToShow />
) : (
<LabelsTable
alertStore={alertStore}
showAllLabels={showAllLabels}
toggleAllLabels={() => setShowAllLabels(!showAllLabels)}
/>
)}
</div>
</React.Fragment>
);
});
OverviewModalContent.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
onHide: PropTypes.func.isRequired,
};
export { OverviewModalContent };

View File

@@ -0,0 +1,135 @@
import React, { FC, useState } from "react";
import { observer } from "mobx-react-lite";
import { APILabelCounterT } from "Models/APITypes";
import { AlertStore } from "Stores/AlertStore";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { LabelWithPercent } from "Components/Labels/LabelWithPercent";
import { ToggleIcon } from "Components/ToggleIcon";
const TableRows: FC<{
alertStore: AlertStore;
nameStats: APILabelCounterT[];
}> = observer(({ alertStore, nameStats }) => (
<React.Fragment>
{nameStats.map((nameStats) => (
<tr key={nameStats.name}>
<td width="25%" className="text-nowrap mw-100 p-1">
<span className="badge badge-light components-label mx-0 mt-0 mb-auto pl-0 text-left">
<span className="bg-primary text-white mr-1 px-1 components-labelWithPercent-percent">
{nameStats.hits}
</span>
{nameStats.name}
</span>
</td>
<td width="75%" className="mw-100 p-1">
{nameStats.values.slice(0, 9).map((valueStats) => (
<LabelWithPercent
alertStore={alertStore}
key={valueStats.value}
name={nameStats.name}
value={valueStats.value}
hits={valueStats.hits}
percent={valueStats.percent}
offset={valueStats.offset}
isActive={
alertStore.filters.values.filter(
(f) => f.raw === valueStats.raw
).length > 0
}
/>
))}
{nameStats.values.length > 9 ? (
<div className="components-label badge my-2 text-muted mw-100">
+{nameStats.values.length - 9} more
</div>
) : null}
</td>
</tr>
))}
</React.Fragment>
));
const LabelsTable: FC<{
alertStore: AlertStore;
showAllLabels: boolean;
toggleAllLabels: () => void;
}> = observer(({ alertStore, showAllLabels, toggleAllLabels }) => (
<React.Fragment>
<table
className="table table-borderless top-labels"
style={{ tableLayout: "fixed" }}
>
<tbody className="mw-100">
<TableRows
alertStore={alertStore}
nameStats={alertStore.data.counters.filter(
(nameStats) => nameStats.hits >= alertStore.info.totalAlerts
)}
></TableRows>
{alertStore.data.counters.filter(
(nameStats) => nameStats.hits < alertStore.info.totalAlerts
).length > 0 ? (
<tr>
<td colSpan={2} className="px-1 py-0">
<TooltipWrapper title="Toggle all / only common labels">
<ToggleIcon
isOpen={showAllLabels}
className="cursor-pointer text-muted"
onClick={toggleAllLabels}
/>
</TooltipWrapper>
</td>
</tr>
) : null}
{showAllLabels ? (
<TableRows
alertStore={alertStore}
nameStats={alertStore.data.counters.filter(
(nameStats) => nameStats.hits < alertStore.info.totalAlerts
)}
></TableRows>
) : null}
</tbody>
</table>
</React.Fragment>
));
const NothingToShow: FC<{}> = () => (
<div className="jumbotron bg-transparent">
<h1 className="display-5 text-placeholder text-center">
No labels to display
</h1>
</div>
);
const OverviewModalContent: FC<{
alertStore: AlertStore;
onHide: () => void;
}> = observer(({ alertStore, onHide }) => {
const [showAllLabels, setShowAllLabels] = useState(false);
return (
<React.Fragment>
<div className="modal-header">
<h5 className="modal-title">Overview</h5>
<button type="button" className="close" onClick={onHide}>
<span className="align-middle">&times;</span>
</button>
</div>
<div className="modal-body">
{alertStore.data.counters.length === 0 ? (
<NothingToShow />
) : (
<LabelsTable
alertStore={alertStore}
showAllLabels={showAllLabels}
toggleAllLabels={() => setShowAllLabels(!showAllLabels)}
/>
)}
</div>
</React.Fragment>
);
});
export { OverviewModalContent };

View File

@@ -1,5 +1,4 @@
import React, { useState, useCallback } from "react";
import PropTypes from "prop-types";
import React, { FC, useState, useCallback } from "react";
import { useObserver } from "mobx-react-lite";
@@ -20,7 +19,9 @@ const OverviewModalContent = React.lazy(() =>
}))
);
const OverviewModal = ({ alertStore }) => {
const OverviewModal: FC<{
alertStore: AlertStore;
}> = ({ alertStore }) => {
const [isVisible, setIsVisible] = useState(false);
const toggle = useCallback(() => setIsVisible(!isVisible), [isVisible]);
@@ -53,15 +54,11 @@ const OverviewModal = ({ alertStore }) => {
<OverviewModalContent
alertStore={alertStore}
onHide={() => setIsVisible(false)}
isVisible={isVisible}
/>
</React.Suspense>
</Modal>
</React.Fragment>
));
};
OverviewModal.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
};
export { OverviewModal };

View File

@@ -31,7 +31,7 @@ const Placeholder = () => (
const PaginatedAlertList: FC<{
alertStore: AlertStore;
filters: string[];
title: string;
title?: string;
}> = ({ alertStore, filters, title }) => {
const { response, error, isLoading } = useFetchGet(
FormatBackendURI("alerts.json?") + FormatAlertsQ(filters)

View File

@@ -18,7 +18,7 @@ const PageSelect: FC<{
totalItemsCount: number;
totalPages: number;
maxPerPage: number;
initialPage: number;
initialPage?: number;
setPageCallback: PageCallback;
}> = ({
totalItemsCount,

View File

@@ -21,10 +21,10 @@ import {
SilenceFormStore,
SilenceFormStage,
NewEmptyMatcher,
MatcherValueToObject,
NewClusterRequest,
} from "Stores/SilenceFormStore";
import { Settings } from "Stores/Settings";
import { StringToOption } from "Common/Select";
import { QueryOperators } from "Common/Query";
import { useFlashTransition } from "Hooks/useFlashTransition";
import { TooltipWrapper } from "Components/TooltipWrapper";
@@ -118,10 +118,10 @@ const SilenceForm = ({
const matcher = NewEmptyMatcher();
matcher.name = f.name;
if (f.matcher === QueryOperators.Regex) {
matcher.values = [MatcherValueToObject(`.*${f.value}.*`)];
matcher.values = [StringToOption(`.*${f.value}.*`)];
matcher.isRegex = f.matcher === QueryOperators.Regex;
} else {
matcher.values = [MatcherValueToObject(f.value)];
matcher.values = [StringToOption(f.value)];
}
silenceFormStore.data.matchers.push(matcher);
});

View File

@@ -5,11 +5,8 @@ import { mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { MockThemeContext } from "__mocks__/Theme";
import {
SilenceFormStore,
NewEmptyMatcher,
MatcherValueToObject,
} from "Stores/SilenceFormStore";
import { SilenceFormStore, NewEmptyMatcher } from "Stores/SilenceFormStore";
import { StringToOption } from "Common/Select";
import { useFetchGet } from "Hooks/useFetchGet";
import { LabelValueInput } from "./LabelValueInput";
@@ -87,8 +84,8 @@ describe("<LabelValueInput />", () => {
options.at(0).simulate("click");
options.at(1).simulate("click");
expect(matcher.values).toHaveLength(2);
expect(matcher.values).toContainEqual(MatcherValueToObject("dev"));
expect(matcher.values).toContainEqual(MatcherValueToObject("staging"));
expect(matcher.values).toContainEqual(StringToOption("dev"));
expect(matcher.values).toContainEqual(StringToOption("staging"));
});
it("selecting one option doesn't force matcher.isRegex=true", () => {
@@ -121,10 +118,7 @@ describe("<LabelValueInput />", () => {
});
it("removing last value sets matcher.values to []", () => {
matcher.values = [
MatcherValueToObject("dev"),
MatcherValueToObject("staging"),
];
matcher.values = [StringToOption("dev"), StringToOption("staging")];
const tree = MountedLabelValueInput(true);
tree.find(".react-select__multi-value__remove").at(0).simulate("click");

View File

@@ -4,12 +4,9 @@ import { mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import {
SilenceFormStore,
NewEmptyMatcher,
MatcherValueToObject,
} from "Stores/SilenceFormStore";
import { SilenceFormStore, NewEmptyMatcher } from "Stores/SilenceFormStore";
import { useFetchGet } from "Hooks/useFetchGet";
import { StringToOption } from "Common/Select";
import { MatchCounter } from "./MatchCounter";
let matcher;
@@ -19,7 +16,7 @@ beforeEach(() => {
silenceFormStore = new SilenceFormStore();
matcher = NewEmptyMatcher();
matcher.name = "foo";
matcher.values = [MatcherValueToObject("bar")];
matcher.values = [StringToOption("bar")];
});
afterEach(() => {
@@ -107,7 +104,7 @@ describe("<MatchCounter />", () => {
});
it("sends correct query string for a 'foo=~(bar|baz)' matcher", () => {
matcher.values = [MatcherValueToObject("bar"), MatcherValueToObject("baz")];
matcher.values = [StringToOption("bar"), StringToOption("baz")];
matcher.isRegex = true;
silenceFormStore.data.alertmanagers = [];
MountedMatchCounter();
@@ -117,7 +114,7 @@ describe("<MatchCounter />", () => {
});
it("selecting one Alertmanager instance appends it to the filters", () => {
silenceFormStore.data.alertmanagers = [MatcherValueToObject("am1")];
silenceFormStore.data.alertmanagers = [StringToOption("am1")];
MountedMatchCounter();
expect(useFetchGet.mock.calls[0][0]).toBe(
"./alerts.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%29%24"
@@ -126,8 +123,8 @@ describe("<MatchCounter />", () => {
it("selecting two Alertmanager instances appends it correctly to the filters", () => {
silenceFormStore.data.alertmanagers = [
MatcherValueToObject("am1"),
MatcherValueToObject("am1"),
StringToOption("am1"),
StringToOption("am1"),
];
MountedMatchCounter();
expect(useFetchGet.mock.calls[0][0]).toBe(

View File

@@ -2,11 +2,8 @@ import React from "react";
import { shallow } from "enzyme";
import {
SilenceFormStore,
NewEmptyMatcher,
MatcherValueToObject,
} from "Stores/SilenceFormStore";
import { SilenceFormStore, NewEmptyMatcher } from "Stores/SilenceFormStore";
import { StringToOption } from "Common/Select";
import { SilenceMatch } from ".";
let silenceFormStore;
@@ -33,7 +30,7 @@ const ShallowLabelValueInput = () => {
describe("<SilenceMatch />", () => {
it("allows changing matcher.isRegex value when matcher.values contains 1 element", () => {
matcher.values = [MatcherValueToObject("foo")];
matcher.values = [StringToOption("foo")];
const tree = ShallowLabelValueInput();
expect(matcher.isRegex).toBe(false);
const regex = tree.find("input[type='checkbox']");
@@ -43,7 +40,7 @@ describe("<SilenceMatch />", () => {
it("disallows changing matcher.isRegex value when matcher.values contains 2 elements", () => {
matcher.isRegex = true;
matcher.values = [MatcherValueToObject("foo"), MatcherValueToObject("bar")];
matcher.values = [StringToOption("foo"), StringToOption("bar")];
const tree = ShallowLabelValueInput();
expect(matcher.isRegex).toBe(true);
const regex = tree.find("input[type='checkbox']");

View File

@@ -11,8 +11,8 @@ import {
SilenceFormStore,
SilenceFormStage,
NewEmptyMatcher,
MatcherValueToObject,
} from "Stores/SilenceFormStore";
import { StringToOption } from "Common/Select";
import { useFetchGet } from "Hooks/useFetchGet";
import { SilencePreview } from ".";
@@ -24,7 +24,7 @@ beforeEach(() => {
const matcher = NewEmptyMatcher();
matcher.name = "foo";
matcher.values = [MatcherValueToObject("bar")];
matcher.values = [StringToOption("bar")];
silenceFormStore = new SilenceFormStore();
silenceFormStore.data.matchers = [matcher];

View File

@@ -13,9 +13,9 @@ import { Settings } from "Stores/Settings";
import {
SilenceFormStore,
NewEmptyMatcher,
MatcherValueToObject,
SilenceTabNames,
} from "Stores/SilenceFormStore";
import { StringToOption } from "Common/Select";
import { DateTimeSelect, TabNames } from "./DateTimeSelect";
import { SilenceModalContent } from "./SilenceModalContent";
@@ -24,7 +24,7 @@ import "Styles/Percy.scss";
const MockMatcher = (name, values, isRegex) => {
const matcher = NewEmptyMatcher();
matcher.name = name;
matcher.values = values.map((v) => MatcherValueToObject(v));
matcher.values = values.map((v) => StringToOption(v));
matcher.isRegex = isRegex;
return matcher;
};

View File

@@ -4,7 +4,7 @@ import merge from "lodash.merge";
import { CommonOptions } from "Common/Fetch";
interface Upstream {
export interface UpstreamT {
uri: string;
options: RequestInit;
}
@@ -16,7 +16,7 @@ interface ResponseState {
inProgress: boolean;
}
const useFetchAny = (upstreams: Upstream[], { fetcher = null } = {}) => {
const useFetchAny = (upstreams: UpstreamT[], { fetcher = null } = {}) => {
const [index, setIndex] = useState(0);
const [response, setResponse] = useState({
response: null,

View File

@@ -143,13 +143,14 @@ export interface APILabelCounterValueT {
value: string;
raw: string;
hits: number;
precent: number;
percent: number;
offset: number;
}
export interface APILabelCounterT {
name: string;
values: APILabelCounterValueT[];
hits: number;
}
export interface APISettingsT {

View File

@@ -50,10 +50,10 @@ class FetchConfig {
}
}
type collapseStateT = "expanded" | "collapsedOnMobile" | "collapsed";
export type CollapseStateT = "expanded" | "collapsedOnMobile" | "collapsed";
interface AlertGroupConfigStorage {
defaultRenderCount: number;
collapseState: collapseStateT;
defaultCollapseState: CollapseStateT;
colorTitleBar: boolean;
}
class AlertGroupConfig {
@@ -71,7 +71,7 @@ class AlertGroupConfig {
constructor(
renderCount: number,
collapseState: collapseStateT,
collapseState: CollapseStateT,
colorTitleBar: boolean
) {
this.config = localStored(
@@ -105,9 +105,9 @@ class SilenceFormConfig {
});
}
type sortOrderT = "default" | "disabled" | "startsAt" | "label";
export type SortOrderT = "default" | "disabled" | "startsAt" | "label";
interface GridConfigStorage {
sortOrder: sortOrderT;
sortOrder: SortOrderT;
sortLabel: string | null;
reverseSort: boolean | null;
groupWidth: number;
@@ -153,9 +153,9 @@ class FilterBarConfig {
}
}
type themeT = "auto" | "light" | "dark";
export type ThemeT = "auto" | "light" | "dark";
interface ThemeConfigStorage {
theme: themeT;
theme: ThemeT;
}
class ThemeConfig {
options = Object.freeze({
@@ -167,7 +167,7 @@ class ThemeConfig {
dark: { label: "Dark theme", value: "dark" },
});
config: ThemeConfigStorage;
constructor(defaultTheme: themeT) {
constructor(defaultTheme: ThemeT) {
this.config = localStored(
"themeConfig",
{

View File

@@ -10,12 +10,12 @@ import {
MockSilence,
MockAlertmanager,
} from "__mocks__/Alerts.js";
import { StringToOption } from "Common/Select";
import {
SilenceFormStore,
SilenceFormStage,
NewEmptyMatcher,
SilenceTabNames,
MatcherValueToObject,
AlertmanagerClustersToOption,
} from "./SilenceFormStore";
@@ -418,13 +418,10 @@ describe("SilenceFormStore.data", () => {
it("dumps to base64 and back", () => {
store.data.matchers = [
MockMatcher("foo", [MatcherValueToObject("bar")]),
MockMatcher("instance", [MatcherValueToObject("server0|server1")]),
MockMatcher("cluster", [
MatcherValueToObject("prod"),
MatcherValueToObject("dev"),
]),
MockMatcher("job", [MatcherValueToObject("abc.+")]),
MockMatcher("foo", [StringToOption("bar")]),
MockMatcher("instance", [StringToOption("server0|server1")]),
MockMatcher("cluster", [StringToOption("prod"), StringToOption("dev")]),
MockMatcher("job", [StringToOption("abc.+")]),
];
store.data.startsAt = new Date();
store.data.endsAt = addMinutes(addHours(store.data.startsAt, 7), 45);
@@ -439,22 +436,22 @@ describe("SilenceFormStore.data", () => {
{
isRegex: false,
name: "foo",
values: [MatcherValueToObject("bar")],
values: [StringToOption("bar")],
},
{
isRegex: false,
name: "instance",
values: [MatcherValueToObject("server0|server1")],
values: [StringToOption("server0|server1")],
},
{
isRegex: false,
name: "cluster",
values: [MatcherValueToObject("prod"), MatcherValueToObject("dev")],
values: [StringToOption("prod"), StringToOption("dev")],
},
{
isRegex: false,
name: "job",
values: [MatcherValueToObject("abc.+")],
values: [StringToOption("abc.+")],
},
]);
expect(store.data.comment).toBe("base64");

View File

@@ -16,24 +16,20 @@ import {
APIAlertmanagerUpstreamT,
AlertmanagerSilencePayloadT,
} from "Models/APITypes";
import { StringToOption, OptionT } from "Common/Select";
interface OptionT {
label: string;
value: string;
}
interface MultiValueOptionT {
export interface MultiValueOptionT {
label: string;
value: string[];
}
interface MatcherT {
export interface MatcherT {
name: string;
values: OptionT[];
isRegex: boolean;
}
interface MatcherWithIDT extends MatcherT {
export interface MatcherWithIDT extends MatcherT {
id: string;
}
@@ -59,11 +55,6 @@ const NewEmptyMatcher = (): MatcherWithIDT => {
};
};
const MatcherValueToObject = (value: string): OptionT => ({
label: value,
value: value,
});
const AlertmanagerClustersToOption = (clusterDict: {
[key: string]: string[];
}): MultiValueOptionT[] =>
@@ -101,7 +92,7 @@ const MatchersFromGroup = (
if (!stripLabels.includes(key)) {
const matcher = NewEmptyMatcher();
matcher.name = key;
matcher.values = [MatcherValueToObject(value)];
matcher.values = [StringToOption(value)];
matchers.push(matcher);
}
}
@@ -150,7 +141,7 @@ const MatchersFromGroup = (
name: key,
values: Array.from(values)
.sort()
.map((value) => MatcherValueToObject(value)),
.map((value) => StringToOption(value)),
isRegex: values.size > 1,
});
}
@@ -173,7 +164,7 @@ const GenerateAlertmanagerSilenceData = (
matchers: MatcherT[],
author: string,
comment: string,
silenceID: string | null
silenceID: string | null = null
): AlertmanagerSilencePayloadT => {
const payload: AlertmanagerSilencePayloadT = {
matchers: matchers.map((m) => ({
@@ -202,11 +193,11 @@ const UnpackRegexMatcherValues = (isRegex: boolean, value: string) => {
return value
.slice(1, -1)
.split("|")
.map((v) => MatcherValueToObject(v));
.map((v) => StringToOption(v));
} else if (isRegex && value.match(/^(\w+\|)+\w+$/)) {
return value.split("|").map((v) => MatcherValueToObject(v));
return value.split("|").map((v) => StringToOption(v));
} else {
return [MatcherValueToObject(value)];
return [StringToOption(value)];
}
};
@@ -299,7 +290,7 @@ class SilenceFormStore {
const matcher = NewEmptyMatcher();
matcher.name = m.n;
matcher.isRegex = m.r;
matcher.values = m.v.map((v) => MatcherValueToObject(v));
matcher.values = m.v.map((v) => StringToOption(v));
matchers.push(matcher);
});
@@ -503,7 +494,6 @@ export {
SilenceFormStore,
SilenceFormStage,
NewEmptyMatcher,
MatcherValueToObject,
AlertmanagerClustersToOption,
SilenceTabNames,
MatchersFromGroup,