diff --git a/ui/package-lock.json b/ui/package-lock.json index bf8af7460..010dff230 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -4936,6 +4936,16 @@ "@babel/types": "^7.3.0" } }, + "@types/body-scroll-lock": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@types/body-scroll-lock/-/body-scroll-lock-2.6.1.tgz", + "integrity": "sha512-PPFm/2A6LfKmSpvMg58gHtSqwwMChbcKKGhSCRIhY4MyFzhY8moAN6HrTCpOeZQUqkFdTFfMqr7njeqGLKt72Q==" + }, + "@types/bricks.js": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/bricks.js/-/bricks.js-1.8.1.tgz", + "integrity": "sha512-cIg3aZFILk8FsR8q1j76ThSo7JaLRHfk6vwiTDeqfUaAikqgu7j+bkAdiJknIbw8CNSSKo6Lrp7wiMuShVCWxg==" + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -4994,6 +5004,43 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==" }, + "@types/lodash": { + "version": "4.14.157", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.157.tgz", + "integrity": "sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ==" + }, + "@types/lodash.debounce": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz", + "integrity": "sha512-4WTmnnhCfDvvuLMaF3KV4Qfki93KebocUF45msxhYyjMttZDQYzHkO639ohhk8+oco2cluAFL3t5+Jn4mleylQ==", + "requires": { + "@types/lodash": "*" + } + }, + "@types/lodash.merge": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.6.tgz", + "integrity": "sha512-IB90krzMf7YpfgP3u/EvZEdXVvm4e3gJbUvh5ieuI+o+XqiNEt6fCzqNRaiLlPVScLI59RxIGZMQ3+Ko/DJ8vQ==", + "requires": { + "@types/lodash": "*" + } + }, + "@types/lodash.throttle": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.6.tgz", + "integrity": "sha512-/UIH96i/sIRYGC60NoY72jGkCJtFN5KVPhEMMMTjol65effe1gPn0tycJqV5tlSwMTzX8FqzB5yAj0rfGHTPNg==", + "requires": { + "@types/lodash": "*" + } + }, + "@types/lodash.uniqueid": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/lodash.uniqueid/-/lodash.uniqueid-4.0.6.tgz", + "integrity": "sha512-WXXsDm7Q1SiAeCG9ubCiDxOuLEDi5x+Crx8SgwTFgBtofATwK1jAeSbGz2bHlfqWezi7mcjynenlBFCeDLhHlw==", + "requires": { + "@types/lodash": "*" + } + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -5027,6 +5074,14 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, + "@types/promise-retry": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/promise-retry/-/promise-retry-1.1.3.tgz", + "integrity": "sha512-LxIlEpEX6frE3co3vCO2EUJfHIta1IOmhDlcAsR4GMMv9hev1iTI9VwberVGkePJAuLZs5rMucrV8CziCfuJMw==", + "requires": { + "@types/retry": "*" + } + }, "@types/prop-types": { "version": "15.7.3", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", @@ -5064,6 +5119,24 @@ "@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", + "integrity": "sha512-tXeN/mPSnB2tt7Mn2DL2I85bGUUwA9q04BK8AbdDt6usVW/cl3osocl5P9KXkzSLeCMwQ+Arusn8ww/asMRWUA==", + "requires": { + "@types/react": "*" + } + }, + "@types/react-select": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-3.0.13.tgz", + "integrity": "sha512-JxmSArGgzAOtb37+Jz2+3av8rVmp/3s3DGwlcP+g59/a3owkiuuU4/Jajd+qA32beDPHy4gJR2kkxagPY3j9kg==", + "requires": { + "@types/react": "*", + "@types/react-dom": "*", + "@types/react-transition-group": "*" + } + }, "@types/react-syntax-highlighter": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz", @@ -5091,6 +5164,19 @@ "@types/react": "*" } }, + "@types/react-transition-group": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz", + "integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==", + "requires": { + "@types/react": "*" + } + }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", diff --git a/ui/package.json b/ui/package.json index c5954680e..e8ff924f2 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,8 +11,17 @@ "@juggle/resize-observer": "3.2.0", "@popperjs/core": "2.4.4", "@sentry/browser": "5.19.2", + "@types/body-scroll-lock": "2.6.1", + "@types/bricks.js": "1.8.1", + "@types/lodash.debounce": "4.0.6", + "@types/lodash.merge": "4.6.6", + "@types/lodash.throttle": "4.1.6", + "@types/lodash.uniqueid": "4.0.6", + "@types/promise-retry": "1.1.3", "@types/react": "16.9.43", "@types/react-dom": "16.9.8", + "@types/react-js-pagination": "3.0.3", + "@types/react-select": "3.0.13", "body-scroll-lock": "3.0.3", "bootstrap": "4.5.0", "bootswatch": "4.5.0", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index b57891722..718f1b693 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -18,6 +18,7 @@ import { ReactSelectStyles, } from "Components/Theme/ReactSelect"; import { BodyTheme, ThemeContext } from "Components/Theme"; +import { UIDefaults } from "./AppBoot"; import { ErrorBoundary } from "./ErrorBoundary"; import "Styles/ResetCSS.scss"; @@ -46,19 +47,9 @@ const FaviconBadge = React.lazy(() => })) ); -interface UIDefaults { - Refresh: number; - HideFiltersWhenIdle: boolean; - ColorTitlebar: boolean; - Theme: "light" | "dark" | "auto"; - MinimalGroupWidth: number; - AlertsPerGroup: number; - CollapseGroups: "expanded" | "collapsed" | "collapsedOnMobile"; -} - interface AppProps { defaultFilters: Array; - uiDefaults: UIDefaults; + uiDefaults: UIDefaults | null; } const App: FunctionComponent = ({ defaultFilters, uiDefaults }) => { diff --git a/ui/src/AppBoot.js b/ui/src/AppBoot.ts similarity index 75% rename from ui/src/AppBoot.js rename to ui/src/AppBoot.ts index 6d867a5a7..18c77883a 100644 --- a/ui/src/AppBoot.js +++ b/ui/src/AppBoot.ts @@ -2,9 +2,19 @@ import { init } from "@sentry/browser"; +export interface UIDefaults { + Refresh: number; + HideFiltersWhenIdle: boolean; + ColorTitlebar: boolean; + Theme: "light" | "dark" | "auto"; + MinimalGroupWidth: number; + AlertsPerGroup: number; + CollapseGroups: "expanded" | "collapsed" | "collapsedOnMobile"; +} + const SettingsElement = () => document.getElementById("settings"); -const SetupSentry = (settingsElement) => { +const SetupSentry = (settingsElement: HTMLElement | null) => { if ( settingsElement !== null && settingsElement.dataset.sentryDsn && @@ -29,7 +39,7 @@ const SetupSentry = (settingsElement) => { } }; -const ParseDefaultFilters = (settingsElement) => { +const ParseDefaultFilters = (settingsElement: HTMLElement | null): string[] => { let defaultFilters = []; if ( settingsElement !== null && @@ -48,7 +58,9 @@ const ParseDefaultFilters = (settingsElement) => { return defaultFilters; }; -const ParseUIDefaults = (defaultsElement) => { +const ParseUIDefaults = ( + defaultsElement: HTMLElement | null +): UIDefaults | null => { if (defaultsElement === null) { return null; } diff --git a/ui/src/Common/Colors.js b/ui/src/Common/Colors.ts similarity index 100% rename from ui/src/Common/Colors.js rename to ui/src/Common/Colors.ts diff --git a/ui/src/Common/Device.js b/ui/src/Common/Device.ts similarity index 65% rename from ui/src/Common/Device.js rename to ui/src/Common/Device.ts index dba858c8a..98ab3e3a3 100644 --- a/ui/src/Common/Device.js +++ b/ui/src/Common/Device.ts @@ -1,4 +1,4 @@ -function IsMobile() { +function IsMobile(): boolean { return window.innerWidth < 768; } diff --git a/ui/src/Common/Fetch.js b/ui/src/Common/Fetch.ts similarity index 83% rename from ui/src/Common/Fetch.js rename to ui/src/Common/Fetch.ts index 615fbd7f4..c5feeeae1 100644 --- a/ui/src/Common/Fetch.js +++ b/ui/src/Common/Fetch.ts @@ -14,7 +14,13 @@ const FetchRetryConfig = { maxTimeout: 5000, }; -const FetchGet = async (uri, options, beforeRetry) => +type PreRetryCallback = (number: number) => void; + +const FetchGet = async ( + uri: string, + options: RequestInit, + beforeRetry: PreRetryCallback +) => await promiseRetry( (retry, number) => fetch( diff --git a/ui/src/Common/Query.js b/ui/src/Common/Query.ts similarity index 85% rename from ui/src/Common/Query.js rename to ui/src/Common/Query.ts index afb527a05..2d1902856 100644 --- a/ui/src/Common/Query.js +++ b/ui/src/Common/Query.ts @@ -14,7 +14,7 @@ const StaticLabels = Object.freeze({ SilenceID: "@silence_id", }); -function FormatQuery(name, operator, value) { +function FormatQuery(name: string, operator: string, value: string) { return `${name}${operator}${value}`; } diff --git a/ui/src/Common/Select.js b/ui/src/Common/Select.js deleted file mode 100644 index 8b9f2555f..000000000 --- a/ui/src/Common/Select.js +++ /dev/null @@ -1,5 +0,0 @@ -const NewLabelName = (v) => `New label: ${v}`; - -const NewLabelValue = (v) => `New value: ${v}`; - -export { NewLabelName, NewLabelValue }; diff --git a/ui/src/Common/Select.ts b/ui/src/Common/Select.ts new file mode 100644 index 000000000..4063e4e74 --- /dev/null +++ b/ui/src/Common/Select.ts @@ -0,0 +1,5 @@ +const NewLabelName = (v: string) => `New label: ${v}`; + +const NewLabelValue = (v: string) => `New value: ${v}`; + +export { NewLabelName, NewLabelValue }; diff --git a/ui/src/Components/Animations/DropdownSlide/index.js b/ui/src/Components/Animations/DropdownSlide/index.tsx similarity index 69% rename from ui/src/Components/Animations/DropdownSlide/index.js rename to ui/src/Components/Animations/DropdownSlide/index.tsx index 1db0d8826..f7ecb4161 100644 --- a/ui/src/Components/Animations/DropdownSlide/index.js +++ b/ui/src/Components/Animations/DropdownSlide/index.tsx @@ -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 }) => ( ( +const MountModal: FC<{ + children: ReactNode; + in: boolean; + unmountOnExit?: boolean; +}> = ({ children, ...props }) => ( ( +const MountModalBackdrop: FC<{ + children: ReactNode; + in?: boolean; + unmountOnExit?: boolean; +}> = ({ children, ...props }) => ( { +const CenteredMessage: FC<{ + children: ReactNode; + className: string; +}> = ({ children, className }) => { const context = React.useContext(ThemeContext); return ( { +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 }; diff --git a/ui/src/Components/FetchPauser/index.js b/ui/src/Components/FetchPauser/index.js deleted file mode 100644 index 464648d57..000000000 --- a/ui/src/Components/FetchPauser/index.js +++ /dev/null @@ -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 }; diff --git a/ui/src/Components/FetchPauser/index.tsx b/ui/src/Components/FetchPauser/index.tsx new file mode 100644 index 000000000..8955fb54a --- /dev/null +++ b/ui/src/Components/FetchPauser/index.tsx @@ -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 }; diff --git a/ui/src/Components/Fetcher/index.js b/ui/src/Components/Fetcher/index.tsx similarity index 85% rename from ui/src/Components/Fetcher/index.js rename to ui/src/Components/Fetcher/index.tsx index 4c297a6cd..120f6912c 100644 --- a/ui/src/Components/Fetcher/index.js +++ b/ui/src/Components/Fetcher/index.tsx @@ -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 ; }; -Fetcher.propTypes = { - alertStore: PropTypes.instanceOf(AlertStore).isRequired, - settingsStore: PropTypes.instanceOf(Settings).isRequired, -}; export { Fetcher }; diff --git a/ui/src/Components/Modal/Tab.js b/ui/src/Components/Modal/Tab.js deleted file mode 100644 index 7da32241a..000000000 --- a/ui/src/Components/Modal/Tab.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; - -const Tab = ({ title, active, onClick }) => ( - - {title} - -); -Tab.propTypes = { - title: PropTypes.string.isRequired, - active: PropTypes.bool, - onClick: PropTypes.func.isRequired, -}; - -export { Tab }; diff --git a/ui/src/Components/Modal/Tab.tsx b/ui/src/Components/Modal/Tab.tsx new file mode 100644 index 000000000..19b073bf8 --- /dev/null +++ b/ui/src/Components/Modal/Tab.tsx @@ -0,0 +1,18 @@ +import React, { FC, MouseEvent } from "react"; + +const Tab: FC<{ + title: string; + active?: boolean; + onClick: (event: MouseEvent) => void; +}> = ({ title, active, onClick }) => ( + + {title} + +); + +export { Tab }; diff --git a/ui/src/Components/Modal/index.test.js b/ui/src/Components/Modal/index.test.js index a11b2d091..ef8abe866 100644 --- a/ui/src/Components/Modal/index.test.js +++ b/ui/src/Components/Modal/index.test.js @@ -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("", () => { + 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(); + tree.setProps({ isUpper: false }); + tree.setProps({ isUpper: true }); + tree.setProps({ isUpper: false }); + expect(useRefSpy).toHaveBeenCalledTimes(4); + }); -describe("", () => { it("'modal-open' class is appended to MountModal container", () => { const tree = MountedModal(true); expect(tree.find("div").at(0).hasClass("modal-open")).toBe(true); diff --git a/ui/src/Components/Modal/index.js b/ui/src/Components/Modal/index.tsx similarity index 57% rename from ui/src/Components/Modal/index.js rename to ui/src/Components/Modal/index.tsx index 0a1ec1c83..330fe4ea4 100644 --- a/ui/src/Components/Modal/index.js +++ b/ui/src/Components/Modal/index.tsx @@ -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( @@ -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 }; diff --git a/ui/src/Components/PaginatedAlertList/index.js b/ui/src/Components/PaginatedAlertList/index.tsx similarity index 76% rename from ui/src/Components/PaginatedAlertList/index.js rename to ui/src/Components/PaginatedAlertList/index.tsx index efe3bfe0a..dc5bdbfed 100644 --- a/ui/src/Components/PaginatedAlertList/index.js +++ b/ui/src/Components/PaginatedAlertList/index.tsx @@ -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 }) => (

@@ -20,9 +19,6 @@ const FetchError = ({ message }) => (

{message}

); -FetchError.propTypes = { - message: PropTypes.node.isRequired, -}; const Placeholder = () => (
@@ -32,7 +28,11 @@ const Placeholder = () => (
); -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 }; diff --git a/ui/src/Components/Pagination/index.js b/ui/src/Components/Pagination/index.tsx similarity index 84% rename from ui/src/Components/Pagination/index.js rename to ui/src/Components/Pagination/index.tsx index c5594d4ae..770fd98ec 100644 --- a/ui/src/Components/Pagination/index.js +++ b/ui/src/Components/Pagination/index.tsx @@ -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 = ({ ); }; -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 }; diff --git a/ui/src/Components/Theme/ReactSelect.js b/ui/src/Components/Theme/ReactSelect.ts similarity index 75% rename from ui/src/Components/Theme/ReactSelect.js rename to ui/src/Components/Theme/ReactSelect.ts index 9aec43b39..c6e000c5c 100644 --- a/ui/src/Components/Theme/ReactSelect.js +++ b/ui/src/Components/Theme/ReactSelect.ts @@ -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", diff --git a/ui/src/Components/Theme/index.js b/ui/src/Components/Theme/index.tsx similarity index 84% rename from ui/src/Components/Theme/index.js rename to ui/src/Components/Theme/index.tsx index e5d0702a7..55846991e 100644 --- a/ui/src/Components/Theme/index.js +++ b/ui/src/Components/Theme/index.tsx @@ -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); diff --git a/ui/src/Components/TooltipWrapper/index.js b/ui/src/Components/TooltipWrapper/index.tsx similarity index 64% rename from ui/src/Components/TooltipWrapper/index.js rename to ui/src/Components/TooltipWrapper/index.tsx index b3151c460..2c9764c99 100644 --- a/ui/src/Components/TooltipWrapper/index.js +++ b/ui/src/Components/TooltipWrapper/index.tsx @@ -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 }) => {
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 }) => { ); }; -TooltipWrapper.propTypes = { - title: PropTypes.node.isRequired, - children: PropTypes.node.isRequired, - className: PropTypes.string, -}; export { TooltipWrapper }; diff --git a/ui/src/Components/ValidationError/index.js b/ui/src/Components/ValidationError/index.tsx similarity index 100% rename from ui/src/Components/ValidationError/index.js rename to ui/src/Components/ValidationError/index.tsx diff --git a/ui/src/Hooks/useDebounce.js b/ui/src/Hooks/useDebounce.ts similarity index 88% rename from ui/src/Hooks/useDebounce.js rename to ui/src/Hooks/useDebounce.ts index b5df57154..2b52d3a24 100644 --- a/ui/src/Hooks/useDebounce.js +++ b/ui/src/Hooks/useDebounce.ts @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; // https://usehooks.com/useDebounce/ -function useDebounce(value, delay) { +function useDebounce(value: any, delay: number) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { diff --git a/ui/src/Hooks/useFetchAny.js b/ui/src/Hooks/useFetchAny.ts similarity index 86% rename from ui/src/Hooks/useFetchAny.js rename to ui/src/Hooks/useFetchAny.ts index f9725b65f..c0db43762 100644 --- a/ui/src/Hooks/useFetchAny.js +++ b/ui/src/Hooks/useFetchAny.ts @@ -4,14 +4,26 @@ import merge from "lodash.merge"; import { CommonOptions } from "Common/Fetch"; -const useFetchAny = (upstreams, { fetcher = null } = {}) => { +interface Upstream { + uri: string; + options: RequestInit; +} + +interface ResponseState { + response: string | null; + error: string | null; + responseURI: string | null; + inProgress: boolean; +} + +const useFetchAny = (upstreams: Upstream[], { fetcher = null } = {}) => { const [index, setIndex] = useState(0); const [response, setResponse] = useState({ response: null, error: null, responseURI: null, inProgress: false, - }); + } as ResponseState); const reset = useCallback(() => { setIndex(0); @@ -39,7 +51,7 @@ const useFetchAny = (upstreams, { fetcher = null } = {}) => { try { const res = await (fetcher || fetch)( uri, - merge({}, { method: "GET" }, CommonOptions, options) + merge({}, { method: "GET" }, CommonOptions, options) as RequestInit ); if (!isCancelled) { diff --git a/ui/src/Hooks/useFetchDelete.js b/ui/src/Hooks/useFetchDelete.ts similarity index 84% rename from ui/src/Hooks/useFetchDelete.js rename to ui/src/Hooks/useFetchDelete.ts index 42db77e67..9b084b645 100644 --- a/ui/src/Hooks/useFetchDelete.js +++ b/ui/src/Hooks/useFetchDelete.ts @@ -4,9 +4,9 @@ import merge from "lodash.merge"; import { CommonOptions } from "Common/Fetch"; -const useFetchDelete = (uri, options, deps = []) => { - const [response, setResponse] = useState(null); - const [error, setError] = useState(null); +const useFetchDelete = (uri: string, options: RequestInit, deps = []) => { + const [response, setResponse] = useState(null as string | null); + const [error, setError] = useState(null as string | null); const [isDeleting, setIsDeleting] = useState(true); useEffect(() => { diff --git a/ui/src/Hooks/useFetchGet.js b/ui/src/Hooks/useFetchGet.ts similarity index 91% rename from ui/src/Hooks/useFetchGet.js rename to ui/src/Hooks/useFetchGet.ts index 17c1c446d..fa3cb7ac6 100644 --- a/ui/src/Hooks/useFetchGet.js +++ b/ui/src/Hooks/useFetchGet.ts @@ -7,11 +7,11 @@ import promiseRetry from "promise-retry"; import { CommonOptions, FetchRetryConfig } from "Common/Fetch"; const useFetchGet = ( - uri, + uri: string, { autorun = true, deps = [], fetcher = null } = {} ) => { - const [response, setResponse] = useState(null); - const [error, setError] = useState(null); + const [response, setResponse] = useState(null as any); + const [error, setError] = useState(null as string | null); const [isLoading, setIsLoading] = useState(true); const [isRetrying, setIsRetrying] = useState(false); const [retryCount, setRetryCount] = useState(0); @@ -29,7 +29,7 @@ const useFetchGet = ( setRetryCount(0); setError(null); const res = await promiseRetry( - (retry, number) => + (retry: Function, number: number) => (fetcher || fetch)( uri, merge( @@ -41,7 +41,7 @@ const useFetchGet = ( { mode: number <= FetchRetryConfig.retries ? "cors" : "no-cors", } - ) + ) as RequestInit ).catch((err) => { if (!isCanceled.current) { setIsRetrying(true); diff --git a/ui/src/Hooks/useFlashTransition.js b/ui/src/Hooks/useFlashTransition.ts similarity index 84% rename from ui/src/Hooks/useFlashTransition.js rename to ui/src/Hooks/useFlashTransition.ts index 11e7c2cc3..874359e9c 100644 --- a/ui/src/Hooks/useFlashTransition.js +++ b/ui/src/Hooks/useFlashTransition.ts @@ -1,8 +1,10 @@ import { useState, useEffect, useRef } from "react"; +import { TransitionProps } from "react-transition-group/Transition"; + import { useInView } from "react-intersection-observer"; -const defaultProps = { +const defaultProps: TransitionProps = { in: false, classNames: "components-animation-flash", timeout: 800, @@ -11,7 +13,7 @@ const defaultProps = { exit: false, }; -const useFlashTransition = (flashOn) => { +const useFlashTransition = (flashOn: any) => { const mountRef = useRef(false); const [ref, inView] = useInView(); const [isPending, setIsPending] = useState(false); diff --git a/ui/src/Hooks/useGrid.js b/ui/src/Hooks/useGrid.ts similarity index 75% rename from ui/src/Hooks/useGrid.js rename to ui/src/Hooks/useGrid.ts index 380441ac9..1554209d3 100644 --- a/ui/src/Hooks/useGrid.js +++ b/ui/src/Hooks/useGrid.ts @@ -1,15 +1,15 @@ import { useEffect, useRef, useState } from "react"; -import Bricks from "bricks.js"; +import Bricks, { SizeDetail, Instance } from "bricks.js"; -const useGrid = (sizes) => { - const ref = useRef(null); - const grid = useRef(null); +const useGrid = (sizes: SizeDetail[]) => { + const ref = useRef(null as Node | null); + const grid = useRef(null as Instance | null); const [repack, setRepack] = useState(() => () => {}); useEffect(() => { if (!grid.current && ref.current) { - grid.current = new Bricks({ + grid.current = Bricks({ container: ref.current, sizes: sizes, packed: "packed", diff --git a/ui/src/Hooks/useOnClickOutside.js b/ui/src/Hooks/useOnClickOutside.ts similarity index 54% rename from ui/src/Hooks/useOnClickOutside.js rename to ui/src/Hooks/useOnClickOutside.ts index 409eff399..0e28b8e14 100644 --- a/ui/src/Hooks/useOnClickOutside.js +++ b/ui/src/Hooks/useOnClickOutside.ts @@ -1,10 +1,18 @@ -import { useEffect } from "react"; +import { useEffect, RefObject } from "react"; + +type Handler = (event: MouseEvent | TouchEvent) => void; // https://usehooks.com/useOnClickOutside/ -function useOnClickOutside(ref, handler, enabled) { +function useOnClickOutside( + ref: RefObject, + handler: Handler, + enabled: boolean +) { useEffect(() => { - const listener = (event) => { - if (!ref.current || ref.current.contains(event.target)) { + const listener: { (event: MouseEvent | TouchEvent): void } = ( + event: MouseEvent | TouchEvent + ) => { + if (!ref.current || ref.current.contains(event.target as Node)) { return; } handler(event); diff --git a/ui/src/Hooks/useSupportsTouch.js b/ui/src/Hooks/useSupportsTouch.ts similarity index 100% rename from ui/src/Hooks/useSupportsTouch.js rename to ui/src/Hooks/useSupportsTouch.ts diff --git a/ui/src/react-app-env.d.ts b/ui/src/react-app-env.d.ts index d359ab433..bc1a38d83 100644 --- a/ui/src/react-app-env.d.ts +++ b/ui/src/react-app-env.d.ts @@ -3,3 +3,5 @@ declare module "react-media-hook" { function useMediaPredicate(query: string): boolean; } + +declare module "favico.js";