chore(ui): migrate more code to typescript

This commit is contained in:
Łukasz Mierzwa
2020-06-29 16:14:53 +01:00
committed by Łukasz Mierzwa
parent 55170f8812
commit 4d4dd111c1
36 changed files with 392 additions and 205 deletions

View File

@@ -1,9 +1,12 @@
import React from "react";
import React, { FC, ReactNode } from "react";
import PropTypes from "prop-types";
import { CSSTransition } from "react-transition-group";
const DropdownSlide = ({ children, duration, ...props }) => (
const DropdownSlide: FC<{
children: ReactNode;
duration: number;
}> = ({ children, duration, ...props }) => (
<CSSTransition
classNames="components-animation-slide"
timeout={150}

View File

@@ -1,9 +1,13 @@
import React from "react";
import React, { FC, ReactNode } from "react";
import PropTypes from "prop-types";
import { CSSTransition } from "react-transition-group";
const MountModal = ({ children, duration, ...props }) => (
const MountModal: FC<{
children: ReactNode;
in: boolean;
unmountOnExit?: boolean;
}> = ({ children, ...props }) => (
<CSSTransition
classNames="components-animation-modal"
timeout={200}
@@ -19,9 +23,12 @@ MountModal.propTypes = {
children: PropTypes.node.isRequired,
};
const MountModalBackdrop = ({ children, duration, ...props }) => (
const MountModalBackdrop: FC<{
children: ReactNode;
in?: boolean;
unmountOnExit?: boolean;
}> = ({ children, ...props }) => (
<CSSTransition
in={true}
classNames="components-animation-backdrop"
timeout={200}
appear={true}

View File

@@ -1,10 +1,13 @@
import React from "react";
import React, { FC, ReactNode } from "react";
import { CSSTransition } from "react-transition-group";
import { ThemeContext } from "Components/Theme";
const CenteredMessage = ({ children, className }) => {
const CenteredMessage: FC<{
children: ReactNode;
className: string;
}> = ({ children, className }) => {
const context = React.useContext(ThemeContext);
return (
<CSSTransition

View File

@@ -1,5 +1,4 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import React, { useState, useEffect, FC } from "react";
import { autorun } from "mobx";
import { useObserver } from "mobx-react-lite";
@@ -8,7 +7,9 @@ import Favico from "favico.js";
import { AlertStore } from "Stores/AlertStore";
const FaviconBadge = ({ alertStore }) => {
const FaviconBadge: FC<{
alertStore: AlertStore;
}> = ({ alertStore }) => {
const [favico] = useState(
new Favico({
animation: "none",
@@ -38,8 +39,5 @@ const FaviconBadge = ({ alertStore }) => {
/>
));
};
FaviconBadge.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
};
export { FaviconBadge };

View File

@@ -1,19 +0,0 @@
import { useEffect } from "react";
import PropTypes from "prop-types";
import { AlertStore } from "Stores/AlertStore";
const FetchPauser = ({ children, alertStore }) => {
useEffect(() => {
alertStore.status.pause();
return alertStore.status.resume;
}, [alertStore.status]);
return children;
};
FetchPauser.propTypes = {
children: PropTypes.any,
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
};
export { FetchPauser };

View File

@@ -0,0 +1,17 @@
import { FC, ReactElement, useEffect } from "react";
import { AlertStore } from "Stores/AlertStore";
const FetchPauser: FC<{
children: ReactElement;
alertStore: AlertStore;
}> = ({ children, alertStore }) => {
useEffect(() => {
alertStore.status.pause();
return alertStore.status.resume;
}, [alertStore.status]);
return children;
};
export { FetchPauser };

View File

@@ -1,5 +1,4 @@
import React, { useEffect, useRef } from "react";
import PropTypes from "prop-types";
import React, { useEffect, useRef, FC } from "react";
import { reaction } from "mobx";
@@ -8,8 +7,11 @@ import addSeconds from "date-fns/addSeconds";
import { AlertStore, AlertStoreStatuses } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
const Fetcher = ({ alertStore, settingsStore }) => {
const timer = useRef(null);
const Fetcher: FC<{
alertStore: AlertStore;
settingsStore: Settings;
}> = ({ alertStore, settingsStore }) => {
const timer = useRef(undefined as number | undefined);
const getSortSettings = () => {
let sortSettings = {
@@ -81,7 +83,7 @@ const Fetcher = ({ alertStore, settingsStore }) => {
};
useEffect(() => {
return () => clearInterval(timer.current);
return () => window.clearInterval(timer.current);
}, []);
useEffect(
@@ -89,7 +91,9 @@ const Fetcher = ({ alertStore, settingsStore }) => {
reaction(
() =>
JSON.stringify({
filters: alertStore.filters.values.map((f) => f.raw).join(" "),
filters: alertStore.filters.values
.map((f: { raw: string }) => f.raw)
.join(" "),
grid: {
sortOrder: settingsStore.gridConfig.config.sortOrder,
sortLabel: settingsStore.gridConfig.config.sortLabel,
@@ -115,10 +119,10 @@ const Fetcher = ({ alertStore, settingsStore }) => {
() => alertStore.status.paused,
(paused) => {
if (paused) {
clearInterval(timer.current);
timer.current = null;
window.clearInterval(timer.current);
timer.current = undefined;
} else {
timer.current = setInterval(
timer.current = window.setInterval(
() => window.requestAnimationFrame(fetchIfIdle),
1000
);
@@ -131,9 +135,5 @@ const Fetcher = ({ alertStore, settingsStore }) => {
return <span />;
};
Fetcher.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
settingsStore: PropTypes.instanceOf(Settings).isRequired,
};
export { Fetcher };

View File

@@ -1,20 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
const Tab = ({ title, active, onClick }) => (
<span
className={`nav-item nav-link cursor-pointer mx-1 px-2 ${
active ? "active" : "components-tab-inactive"
}`}
onClick={onClick}
>
{title}
</span>
);
Tab.propTypes = {
title: PropTypes.string.isRequired,
active: PropTypes.bool,
onClick: PropTypes.func.isRequired,
};
export { Tab };

View File

@@ -0,0 +1,18 @@
import React, { FC, MouseEvent } from "react";
const Tab: FC<{
title: string;
active?: boolean;
onClick: (event: MouseEvent<HTMLElement>) => void;
}> = ({ title, active, onClick }) => (
<span
className={`nav-item nav-link cursor-pointer mx-1 px-2 ${
active ? "active" : "components-tab-inactive"
}`}
onClick={onClick}
>
{title}
</span>
);
export { Tab };

View File

@@ -3,7 +3,16 @@ import React from "react";
import { mount } from "enzyme";
import { PressKey } from "__mocks__/PressKey";
import { Modal } from ".";
import { Modal, ModalInner } from ".";
beforeEach(() => {
jest.restoreAllMocks();
});
afterEach(() => {
jest.restoreAllMocks();
document.body.className = "";
});
const fakeToggle = jest.fn();
@@ -15,12 +24,21 @@ const MountedModal = (isOpen, isUpper) => {
);
};
afterEach(() => {
jest.resetAllMocks();
document.body.className = "";
});
describe("<ModalInner />", () => {
it("scroll isn't enabled if ref is null", () => {
const useRefSpy = jest.spyOn(React, "useRef").mockImplementation(() =>
Object.defineProperty({}, "current", {
get: () => null,
set: () => {},
})
);
const tree = mount(<ModalInner isUpper toggleOpen={fakeToggle} />);
tree.setProps({ isUpper: false });
tree.setProps({ isUpper: true });
tree.setProps({ isUpper: false });
expect(useRefSpy).toHaveBeenCalledTimes(4);
});
describe("<Modal />", () => {
it("'modal-open' class is appended to MountModal container", () => {
const tree = MountedModal(true);
expect(tree.find("div").at(0).hasClass("modal-open")).toBe(true);

View File

@@ -1,6 +1,5 @@
import React, { useRef, useEffect } from "react";
import React, { FC, useEffect } from "react";
import ReactDOM from "react-dom";
import PropTypes from "prop-types";
import { disableBodyScroll, enableBodyScroll } from "body-scroll-lock";
@@ -11,18 +10,25 @@ import {
MountModalBackdrop,
} from "Components/Animations/MountModal";
const ModalInner = ({ size, isUpper, toggleOpen, children }) => {
const ref = useRef(null);
const ModalInner: FC<{
size: "lg" | "xl";
isUpper: boolean;
toggleOpen: () => void;
}> = ({ size, isUpper, toggleOpen, children }) => {
// needed for tests to spy on useRef
const ref = React.useRef(null as HTMLDivElement | null);
useEffect(() => {
document.body.classList.add("modal-open");
disableBodyScroll(ref.current, { reserveScrollBarGap: true });
if (ref.current !== null) {
document.body.classList.add("modal-open");
disableBodyScroll(ref.current, { reserveScrollBarGap: true });
let modal = ref.current;
return () => {
if (!isUpper) document.body.classList.remove("modal-open");
enableBodyScroll(modal);
};
let modal = ref.current;
return () => {
if (!isUpper) document.body.classList.remove("modal-open");
enableBodyScroll(modal);
};
}
}, [isUpper]);
useHotkeys("esc", toggleOpen);
@@ -43,7 +49,19 @@ const ModalInner = ({ size, isUpper, toggleOpen, children }) => {
);
};
const Modal = ({ size, isOpen, isUpper, toggleOpen, children, ...props }) => {
const Modal: FC<{
size?: "lg" | "xl";
isOpen: boolean;
isUpper?: boolean;
toggleOpen: () => void;
}> = ({
size = "lg",
isOpen,
isUpper = false,
toggleOpen,
children,
...props
}) => {
return ReactDOM.createPortal(
<React.Fragment>
<MountModal in={isOpen} unmountOnExit {...props}>
@@ -58,16 +76,5 @@ const Modal = ({ size, isOpen, isUpper, toggleOpen, children, ...props }) => {
document.body
);
};
Modal.propTypes = {
size: PropTypes.oneOf(["lg", "xl"]),
isOpen: PropTypes.bool.isRequired,
isUpper: PropTypes.bool,
toggleOpen: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
};
Modal.defaultProps = {
size: "lg",
isUpper: false,
};
export { Modal };
export { Modal, ModalInner };

View File

@@ -1,5 +1,4 @@
import React from "react";
import PropTypes from "prop-types";
import React, { FC, ReactNode } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
@@ -12,7 +11,7 @@ import {
} from "Components/LabelSetList";
import { useFetchGet } from "Hooks/useFetchGet";
const FetchError = ({ message }) => (
const FetchError: FC<{ message: ReactNode }> = ({ message }) => (
<div className="text-center">
<h2 className="display-2 text-danger">
<FontAwesomeIcon icon={faExclamationCircle} />
@@ -20,9 +19,6 @@ const FetchError = ({ message }) => (
<p className="lead text-muted">{message}</p>
</div>
);
FetchError.propTypes = {
message: PropTypes.node.isRequired,
};
const Placeholder = () => (
<div className="jumbotron bg-transparent">
@@ -32,7 +28,11 @@ const Placeholder = () => (
</div>
);
const PaginatedAlertList = ({ alertStore, filters, title }) => {
const PaginatedAlertList: FC<{
alertStore: AlertStore;
filters: string[];
title: string;
}> = ({ alertStore, filters, title }) => {
const { response, error, isLoading } = useFetchGet(
FormatBackendURI("alerts.json?") + FormatAlertsQ(filters)
);
@@ -51,10 +51,5 @@ const PaginatedAlertList = ({ alertStore, filters, title }) => {
/>
);
};
PaginatedAlertList.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
filters: PropTypes.arrayOf(PropTypes.string).isRequired,
title: PropTypes.string,
};
export { PaginatedAlertList };

View File

@@ -1,5 +1,4 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import React, { useState, useEffect, FC } from "react";
import { useHotkeys } from "react-hotkeys-hook";
@@ -13,16 +12,24 @@ import { faAngleDoubleRight } from "@fortawesome/free-solid-svg-icons/faAngleDou
import { IsMobile } from "Common/Device";
const PageSelect = ({
type PageCallback = (page: number) => void;
const PageSelect: FC<{
totalItemsCount: number;
totalPages: number;
maxPerPage: number;
initialPage: number;
setPageCallback: PageCallback;
}> = ({
totalItemsCount,
totalPages,
maxPerPage,
initialPage,
initialPage = 1,
setPageCallback,
}) => {
const [activePage, setActivePage] = useState(initialPage);
const onChange = (page) => {
const onChange = (page: number) => {
setActivePage(page);
setPageCallback(page);
};
@@ -83,15 +90,5 @@ const PageSelect = ({
</div>
);
};
PageSelect.propTypes = {
totalPages: PropTypes.number.isRequired,
initialPage: PropTypes.number,
maxPerPage: PropTypes.number.isRequired,
totalItemsCount: PropTypes.number.isRequired,
setPageCallback: PropTypes.func.isRequired,
};
PageSelect.defaultProps = {
initialPage: 1,
};
export { PageSelect };

View File

@@ -1,4 +1,25 @@
const ReactSelectColors = {
import { CSSProperties } from "react";
import { Styles } from "react-select";
interface ReactSelectTheme {
color: string;
singleValueColor: string;
backgroundColor: string;
borderColor: string;
focusedBoxShadow: string;
focusedBorderColor: string;
menuBackground: string;
optionHoverBackground: string;
valueContainerBackground: string;
disabledValueContainerBackground: string;
}
interface ReactSelectThemes {
Light: ReactSelectTheme;
Dark: ReactSelectTheme;
}
const ReactSelectColors: ReactSelectThemes = {
Light: {
color: "#fff",
singleValueColor: "#000",
@@ -25,8 +46,8 @@ const ReactSelectColors = {
},
};
const ReactSelectStyles = (theme) => ({
control: (base, state) =>
const ReactSelectStyles = (theme: ReactSelectTheme): Styles => ({
control: (base: CSSProperties, state: any) =>
state.isFocused
? {
...base,
@@ -47,7 +68,7 @@ const ReactSelectStyles = (theme) => ({
borderColor: theme.borderColor,
"&:hover": { borderColor: theme.borderColor },
},
valueContainer: (base, state) =>
valueContainer: (base: CSSProperties, state: any) =>
state.isMulti
? {
...base,
@@ -72,11 +93,11 @@ const ReactSelectStyles = (theme) => ({
? theme.disabledValueContainerBackground
: theme.valueContainerBackground,
},
singleValue: (base, state) => ({
singleValue: (base: CSSProperties) => ({
...base,
color: theme.singleValueColor,
}),
multiValue: (base, state) => ({
multiValue: (base: CSSProperties) => ({
...base,
borderRadius: "4px",
backgroundColor: theme.optionHoverBackground,
@@ -84,7 +105,7 @@ const ReactSelectStyles = (theme) => ({
backgroundColor: theme.optionHoverBackground,
},
}),
multiValueLabel: (base, state) => ({
multiValueLabel: (base: CSSProperties) => ({
...base,
color: theme.color,
whiteSpace: "normal",
@@ -94,7 +115,7 @@ const ReactSelectStyles = (theme) => ({
color: theme.color,
},
}),
multiValueRemove: (base, state) => ({
multiValueRemove: (base: CSSProperties) => ({
...base,
cursor: "pointer",
color: theme.color,
@@ -107,11 +128,11 @@ const ReactSelectStyles = (theme) => ({
opacity: "0.75",
},
}),
input: (base, state) => ({
input: (base: CSSProperties) => ({
...base,
color: "inherit",
}),
indicatorsContainer: (base, state) => ({
indicatorsContainer: (base: CSSProperties, state: any) => ({
...base,
backgroundColor: state.isDisabled
? theme.disabledValueContainerBackground
@@ -119,19 +140,19 @@ const ReactSelectStyles = (theme) => ({
borderTopRightRadius: "0.25rem",
borderBottomRightRadius: "0.25rem",
}),
dropdownIndicator: (base, state) =>
dropdownIndicator: (base: CSSProperties, state: any) =>
state.isFocused
? {
...base,
"&:hover": { color: "inherit" },
}
: { ...base },
menu: (base, state) => ({
menu: (base: CSSProperties) => ({
...base,
zIndex: 1500,
backgroundColor: theme.menuBackground,
}),
option: (base, state) => ({
option: (base: CSSProperties) => ({
...base,
color: "inherit",
backgroundColor: "inherit",

View File

@@ -38,7 +38,19 @@ const Placeholder = () => {
);
};
const ThemeContext = React.createContext();
interface ThemeCtx {
isDark: boolean;
reactSelectStyles: any;
animations: {
duration: number;
};
}
const ThemeContext = React.createContext({
isDark: false,
reactSelectStyles: {},
animations: { duration: 1000 },
} as ThemeCtx);
const BodyTheme = () => {
const context = React.useContext(ThemeContext);

View File

@@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, ReactNode, FC } from "react";
import { createPortal } from "react-dom";
import PropTypes from "prop-types";
import { CSSTransition } from "react-transition-group";
@@ -8,16 +7,24 @@ import { usePopper } from "react-popper";
import { useSupportsTouch } from "Hooks/useSupportsTouch";
const TooltipWrapper = ({ title, children, className }) => {
const [referenceElement, setReferenceElement] = useState(null);
const [popperElement, setPopperElement] = useState(null);
const TooltipWrapper: FC<{
title: ReactNode;
children: ReactNode;
className?: string;
}> = ({ title, children, className }) => {
const [referenceElement, setReferenceElement] = useState(
null as HTMLElement | null
);
const [popperElement, setPopperElement] = useState(
null as HTMLElement | null
);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "top",
modifiers: [
{
name: "preventOverflow",
options: {
boundariesElement: "viewport",
rootBoundary: "viewport",
},
},
],
@@ -32,22 +39,22 @@ const TooltipWrapper = ({ title, children, className }) => {
const hideTooltip = () => setIsHovering(false);
useEffect(() => {
let timerShow;
let timerHide;
let timerShow: number | undefined;
let timerHide: number | undefined;
if (!isHovering) {
if (isVisible) {
clearTimeout(timerShow);
timerHide = setTimeout(() => setIsVisible(false), 100);
window.clearTimeout(timerShow);
timerHide = window.setTimeout(() => setIsVisible(false), 100);
}
setWasClicked(false);
} else if (wasClicked) {
clearTimeout(timerShow);
clearTimeout(timerHide);
window.clearTimeout(timerShow);
window.clearTimeout(timerHide);
setIsVisible(false);
} else if (!isVisible && isHovering) {
clearTimeout(timerHide);
timerShow = setTimeout(() => setIsVisible(true), 1000);
timerShow = window.setTimeout(() => setIsVisible(true), 1000);
}
return () => {
clearTimeout(timerShow);
@@ -59,11 +66,11 @@ const TooltipWrapper = ({ title, children, className }) => {
<React.Fragment>
<div
onClick={() => setWasClicked(true)}
onMouseOver={supportsTouch ? null : showTooltip}
onMouseLeave={supportsTouch ? null : hideTooltip}
onTouchStart={supportsTouch ? showTooltip : null}
onTouchCancel={supportsTouch ? hideTooltip : null}
onTouchEnd={supportsTouch ? hideTooltip : null}
onMouseOver={supportsTouch ? undefined : showTooltip}
onMouseLeave={supportsTouch ? undefined : hideTooltip}
onTouchStart={supportsTouch ? showTooltip : undefined}
onTouchCancel={supportsTouch ? hideTooltip : undefined}
onTouchEnd={supportsTouch ? hideTooltip : undefined}
ref={setReferenceElement}
className={`${className ? className : ""} tooltip-trigger`}
>
@@ -94,10 +101,5 @@ const TooltipWrapper = ({ title, children, className }) => {
</React.Fragment>
);
};
TooltipWrapper.propTypes = {
title: PropTypes.node.isRequired,
children: PropTypes.node.isRequired,
className: PropTypes.string,
};
export { TooltipWrapper };