diff --git a/.eslintrc.js b/.eslintrc.js index 37ad9f7c..3bc282d1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,8 +6,9 @@ module.exports = { 'prettier', 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. + 'plugin:react/recommended', ], - plugins: ['@typescript-eslint', 'no-only-tests'], + plugins: ['@typescript-eslint', 'no-only-tests', 'react'], parserOptions: { ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features sourceType: 'module', // Allows for the use of imports diff --git a/package.json b/package.json index 0665d227..21ac4bb1 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,10 @@ "@types/express": "^4.17.2", "@types/jest": "^24.9.0", "@types/node": "^13.1.8", + "@types/pretty-bytes": "^5.2.0", + "@types/react": "^16.9.18", + "@types/react-dom": "^16.9.5", + "@types/react-highlight": "^0.12.2", "@typescript-eslint/eslint-plugin": "^2.17.0", "@typescript-eslint/parser": "^2.17.0", "@typescript-eslint/typescript-estree": "^2.17.0", @@ -63,8 +67,10 @@ "prettier": "^1.19.1", "prettier-eslint": "^9.0.1", "react-dev-utils": "^8.0.0", + "source-map-loader": "^0.2.4", "style-loader": "^0.23.1", "ts-jest": "^24.3.0", + "ts-loader": "^6.2.1", "ts-node": "^8.6.2", "typescript": "^3.7.5", "webpack": "^4.41.5", diff --git a/src/routes/queues.ts b/src/routes/queues.ts index 39efbf37..fb8d68e4 100644 --- a/src/routes/queues.ts +++ b/src/routes/queues.ts @@ -1,10 +1,7 @@ import { parse as parseRedisInfo } from 'redis-info' import { RequestHandler, Request } from 'express' import { Job } from 'bull' -import { - Job as JobMq, - // Queue as QueueMq -} from 'bullmq' +import { Job as JobMq } from 'bullmq' import { BullBoardQueues, BullBoardQueue } from '../@types' @@ -86,7 +83,7 @@ const getDataForQueues = async ( } } - const counts = await Promise.all( + const queues = await Promise.all( pairs.map(async ([name, { queue }]) => { const counts = await queue.getJobCounts(...statuses) @@ -105,7 +102,7 @@ const getDataForQueues = async ( return { stats, - queues: counts, + queues, } } diff --git a/src/ui/components/App.js b/src/ui/components/App.tsx similarity index 55% rename from src/ui/components/App.js rename to src/ui/components/App.tsx index eec1f693..c1922656 100644 --- a/src/ui/components/App.js +++ b/src/ui/components/App.tsx @@ -1,10 +1,19 @@ import React from 'react' -import Queue from './Queue' -import RedisStats from './RedisStats' -import Header from './Header' -import useStore from './hooks/useStore' -export default function App({ basePath }) { +import { Queue as QueueElement } from './Queue' +import { RedisStats } from './RedisStats' +import { Header } from './Header' +import { useStore } from './hooks/useStore' +import { Job, JobCounts } from 'bull' +import { Job as JobMq } from 'bullmq' + +interface Queueue { + name: string + counts: JobCounts + jobs: (Job | JobMq)[] +} + +export const App = ({ basePath }: { basePath: string }) => { const { state, selectedStatuses, @@ -23,9 +32,14 @@ export default function App({ basePath }) { 'Loading...' ) : ( <> - - {state.data.queues.map(queue => ( - + ) : ( + <>No stats to display + )} + + {state.data.queues.map((queue: Queueue) => ( + { const scrolled = useScrolled() return ( diff --git a/src/ui/components/Queue.js b/src/ui/components/Queue.js deleted file mode 100644 index 144d4cd8..00000000 --- a/src/ui/components/Queue.js +++ /dev/null @@ -1,341 +0,0 @@ -import React, { useState } from 'react' -import { - getYear, - format, - isToday, - formatDistance, - formatDistanceStrict, -} from 'date-fns' -import Highlight from 'react-highlight/lib/optimized' - -const today = new Date() - -function formatDate(ts) { - if (isToday(ts)) { - return format(ts, 'HH:mm:ss') - } - - return getYear(ts) === getYear(today) - ? format(ts, 'MM/dd HH:mm:ss') - : format(ts, 'MM/dd/yyyy HH:mm:ss') -} - -function TS({ ts, prev }) { - const date = formatDate(ts) - - return ( - <> - {date}{' '} - {ts && prev && ( - <> - ({formatDistance(ts, prev, { includeSeconds: true })}) - - )} - - ) -} - -function MenuItem({ status, count, onClick, selected }) { - return ( -
- {status !== 'latest' && {count}} {status} -
- ) -} - -const statuses = [ - 'latest', - 'active', - 'waiting', - 'completed', - 'failed', - 'delayed', - 'paused', -] - -const fields = { - latest: ['id', 'timestamps', 'name', 'progress', 'attempts', 'data', 'opts'], - completed: [ - 'id', - 'timestamps', - 'name', - 'progress', - 'attempts', - 'data', - 'opts', - ], - delayed: ['id', 'timestamps', 'name', 'attempts', 'delay', 'data', 'opts'], - paused: ['id', 'timestamps', 'name', 'attempts', 'data', 'opts'], - active: ['id', 'timestamps', 'name', 'progress', 'attempts', 'data', 'opts'], - waiting: ['id', 'timestamps', 'name', 'data', 'opts'], - failed: [ - 'id', - 'failedReason', - 'name', - 'timestamps', - 'progress', - 'attempts', - 'retry', - ], -} - -function PlusIcon() { - return ( - - - - - - - ) -} - -function PlayIcon() { - return ( - - - - - - - ) -} - -function CheckIcon() { - return ( - - - - - - ) -} - -const fieldComponents = { - id: ({ job }) => { - return #{job.id} - }, - timestamps: ({ job }) => { - return ( -
-
- -
- {job.processedOn && ( -
- -
- )} - {job.finishedOn && ( -
- -
- )} -
- ) - }, - name: ({ job }) => { - if (job.name === '__default__') { - return '--' - } - return job.name - }, - finish: ({ job }) => { - return - }, - progress: ({ job }) => { - switch (typeof job.progress) { - case 'object': - return ( - - {JSON.stringify(job.progress, null, 2)} - - ) - case 'number': - if (job.progress > 100) { - return
{job.progress}
- } - - return ( -
-
- {job.progress} - %  -
-
- ) - default: - return '--' - } - }, - attempts: ({ job }) => { - return job.attempts - }, - delay: ({ job }) => { - return formatDistanceStrict(job.timestamp + job.delay, Date.now()) - }, - failedReason: ({ job }) => { - return ( - <> - {job.failedReason || 'NA'} - {job.stacktrace} - - ) - }, - data: ({ job }) => { - const [showData, toggleData] = useState(false) - - return ( - <> - - - {showData && JSON.stringify(job.data, null, 2)} - - - ) - }, - opts: ({ job }) => { - return ( - - {JSON.stringify(job.opts, null, 2)} - - ) - }, - retry: ({ retryJob }) => { - return - }, -} - -function Jobs({ retryJob, queue: { jobs, name }, status }) { - if (!jobs.length) { - return `No jobs with status ${status}` - } - - return ( - - - - {fields[status].map(field => ( - - ))} - - - - {jobs.map(job => { - return ( - - {fields[status].map(field => { - const Field = fieldComponents[field] - return ( - - ) - })} - - ) - })} - -
{field}
- -
- ) -} - -const actions = { - failed: ({ retryAll, cleanAllFailed }) => { - return ( -
- - -
- ) - }, - delayed: ({ cleanAllDelayed }) => { - return - }, -} - -function QueueActions(props) { - const Actions = - actions[props.status] || - (() => { - return null - }) - return ( -
- -
- ) -} - -export default function Queue({ - retryAll, - retryJob, - cleanAllDelayed, - cleanAllFailed, - queue, - selectStatus, - selectedStatus, -}) { - return ( -
-

- {queue.name} - {queue.version === 4 && bullmq} -

-
- {statuses.map(status => ( - selectStatus({ [queue.name]: status })} - selected={selectedStatus === status} - /> - ))} -
- {selectedStatus && ( - <> - - - - )} -
- ) -} diff --git a/src/ui/components/Queue.tsx b/src/ui/components/Queue.tsx new file mode 100644 index 00000000..a9cfac4d --- /dev/null +++ b/src/ui/components/Queue.tsx @@ -0,0 +1,297 @@ +import React, { useState } from 'react' +import { + getYear, + format, + isToday, + formatDistance, + formatDistanceStrict, +} from 'date-fns' +import Highlight from 'react-highlight' +import { Job } from 'bull' +import { Job as JobMq } from 'bullmq' + +import { FIELDS, STATUSES } from './constants' + +const today = new Date() + +// FIXME: typings +const formatDate = (ts: any) => { + if (isToday(ts)) { + return format(ts, 'HH:mm:ss') + } + + return getYear(ts) === getYear(today) + ? format(ts, 'MM/dd HH:mm:ss') + : format(ts, 'MM/dd/yyyy HH:mm:ss') +} + +const Timestamp = ({ ts, prev }: { ts: any; prev?: any }) => { + const date = formatDate(ts) + + return ( + <> + {date}{' '} + {ts && prev && ( + <> + ({formatDistance(ts, prev, { includeSeconds: true })}) + + )} + + ) +} + +// FIXME: typings +const MenuItem = ({ status, count, onClick, selected }: any) => ( +
+ {status !== 'latest' && {count}} {status} +
+) + +const PlusIcon = () => ( + + + + + + +) + +const PlayIcon = () => ( + + + + + + +) + +const CheckIcon = () => ( + + + + + +) + +const fieldComponents = { + id: ({ job }: { job: Job | JobMq }) => #{job.id}, + + timestamps: ({ job }: { job: Job | JobMq }) => ( +
+
+ +
+ {job.processedOn && ( +
+ +
+ )} + {job.finishedOn && ( +
+ +
+ )} +
+ ), + + name: ({ job }: { job: Job | JobMq }) => + job.name === '__default__' ? '--' : job.name, + + finish: ({ job }: { job: Job | JobMq }) => ( + + ), + + progress: ({ job }: { job: Job | JobMq }) => { + switch (typeof job.progress) { + case 'object': + return ( + + {JSON.stringify(job.progress, null, 2)} + + ) + case 'number': + if (job.progress > 100) { + return
{job.progress}
+ } + + return ( +
+
+ {job.progress} + %  +
+
+ ) + default: + return '--' + } + }, + + // FIXME: typings + attempts: ({ job }: { job: any }) => job.attempts, + + // FIXME: typings + delay: ({ job }: { job: any }) => + formatDistanceStrict(job.timestamp + job.delay, Date.now()), + + // FIXME: typings + failedReason: ({ job }: any) => { + return ( + <> + {job.failedReason || 'NA'} + {job.stacktrace} + + ) + }, + + data: ({ job }: { job: Job | JobMq }) => { + const [showData, toggleData] = useState(false) + + return ( + <> + + + {showData && JSON.stringify(job.data, null, 2)} + + + ) + }, + + opts: ({ job }: { job: Job | JobMq }) => ( + {JSON.stringify(job.opts, null, 2)} + ), + + // FIXME: typings + retry: ({ retryJob }: { retryJob: () => void }) => ( + + ), +} + +const Jobs = ({ + retryJob, + queue: { jobs, name }, + status, +}: { + // FIXME: typings + retryJob: any + queue: { + jobs: any + name: any + } + status: string +}) => { + if (!jobs.length) { + return <>No jobs with status {status} + } + + return ( + + + + {FIELDS[status].map(field => ( + + ))} + + + + {jobs.map((job: Job | JobMq) => ( + + {FIELDS[status].map(field => { + // FIXME: typings + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + const Field = fieldComponents[field] + + return ( + + ) + })} + + ))} + +
{field}
+ +
+ ) +} + +const actions = { + failed: ({ retryAll, cleanAllFailed }: any) => ( +
+ + +
+ ), + delayed: ({ cleanAllDelayed }: any) => ( + + ), +} + +// FIXME: typings +const QueueActions = (props: { + [key: string]: any + status: 'failed' | 'delayed' +}) => { + const Actions = + actions[props.status] || + (() => { + return null + }) + + return ( +
+ +
+ ) +} + +// FIXME: typings +export const Queue = ({ + cleanAllDelayed, + cleanAllFailed, + queue, + retryAll, + retryJob, + selectedStatus, + selectStatus, +}: any) => ( +
+

+ {queue.name} + {queue.version === 4 && bullmq} +

+
+ {Object.keys(STATUSES).map(status => ( + selectStatus({ [queue.name]: status })} + selected={selectedStatus === status} + /> + ))} +
+ {selectedStatus && ( + <> + + + + )} +
+) diff --git a/src/ui/components/RedisStats.js b/src/ui/components/RedisStats.js deleted file mode 100644 index f0a648c8..00000000 --- a/src/ui/components/RedisStats.js +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react' -import formatBytes from 'pretty-bytes' - -function RedisLogo() { - return ( - - - - - - ) -} - -function getMemoryUsage(used_memory, total_system_memory) { - if (!total_system_memory) { - return formatBytes(parseInt(used_memory, 10)) - } - - return `${((used_memory / total_system_memory) * 100).toFixed(2)}%` -} - -export default function RedisStats({ stats }) { - if (stats == null || Object.entries(stats).length == 0) { - return 'No stats to display' - } - - const { - redis_version, - used_memory, - total_system_memory, - mem_fragmentation_ratio, - connected_clients, - blocked_clients, - } = stats - - return ( -
-
- -
- -
- Version -

{redis_version}

-
- -
- Memory usage -

{getMemoryUsage(used_memory, total_system_memory)}

- {total_system_memory ? ( - - {formatBytes(parseInt(used_memory))} of{' '} - {formatBytes(parseInt(total_system_memory))} - - ) : ( - - Could not retrieve total_system_memory - - )} -
- -
- Fragmentation ratio -

{mem_fragmentation_ratio}

-
- -
- Connected clients -

{connected_clients}

-
- -
- Blocked clients -

{blocked_clients}

-
-
- ) -} diff --git a/src/ui/components/RedisStats.tsx b/src/ui/components/RedisStats.tsx new file mode 100644 index 00000000..d5c3418b --- /dev/null +++ b/src/ui/components/RedisStats.tsx @@ -0,0 +1,95 @@ +/* eslint-disable @typescript-eslint/camelcase */ + +import React from 'react' +import formatBytes from 'pretty-bytes' + +interface ValidMetrics { + total_system_memory: string + redis_version?: string + used_memory: string + mem_fragmentation_ratio?: string + connected_clients?: string + blocked_clients?: string +} + +const RedisLogo = () => ( + + + + + +) + +const getMemoryUsage = ( + used_memory: ValidMetrics['used_memory'], + total_system_memory: ValidMetrics['total_system_memory'], +) => + total_system_memory + ? `${( + (parseInt(used_memory, 10) / parseInt(total_system_memory, 10)) * + 100 + ).toFixed(2)}%` + : formatBytes(parseInt(used_memory, 10)) + +export const RedisStats = ({ stats }: { stats: ValidMetrics }) => { + const { + redis_version, + used_memory, + total_system_memory, + mem_fragmentation_ratio, + connected_clients, + blocked_clients, + } = stats + + return ( +
+
+ +
+ +
+ Version +

{redis_version}

+
+ +
+ Memory usage +

{getMemoryUsage(used_memory, total_system_memory)}

+ {total_system_memory ? ( + + {formatBytes(parseInt(used_memory))} of{' '} + {formatBytes(parseInt(total_system_memory))} + + ) : ( + + Could not retrieve total_system_memory + + )} +
+ +
+ Fragmentation ratio +

{mem_fragmentation_ratio}

+
+ +
+ Connected clients +

{connected_clients}

+
+ +
+ Blocked clients +

{blocked_clients}

+
+
+ ) +} diff --git a/src/ui/components/constants.ts b/src/ui/components/constants.ts new file mode 100644 index 00000000..d0db5ca3 --- /dev/null +++ b/src/ui/components/constants.ts @@ -0,0 +1,59 @@ +export const STATUSES = { + active: 'active', + completed: 'completed', + delayed: 'delayed', + failed: 'failed', + latest: 'latest', + paused: 'paused', + waiting: 'waiting', +} + +export const FIELDS = { + [STATUSES.active]: [ + 'attempts', + 'data', + 'id', + 'name', + 'opts', + 'progress', + 'timestamps', + ], + [STATUSES.completed]: [ + 'attempts', + 'data', + 'id', + 'name', + 'opts', + 'progress', + 'timestamps', + ], + [STATUSES.delayed]: [ + 'attempts', + 'data', + 'delay', + 'id', + 'name', + 'opts', + 'timestamps', + ], + [STATUSES.failed]: [ + 'attempts', + 'failedReason', + 'id', + 'name', + 'progress', + 'retry', + 'timestamps', + ], + [STATUSES.latest]: [ + 'attempts', + 'data', + 'id', + 'name', + 'opts', + 'progress', + 'timestamps', + ], + [STATUSES.paused]: ['attempts', 'data', 'id', 'name', 'opts', 'timestamps'], + [STATUSES.waiting]: ['data', 'id', 'name', 'opts', 'timestamps'], +} diff --git a/src/ui/components/hooks/useScrolled.js b/src/ui/components/hooks/useScrolled.ts similarity index 75% rename from src/ui/components/hooks/useScrolled.js rename to src/ui/components/hooks/useScrolled.ts index 97e75cf6..da9855cb 100644 --- a/src/ui/components/hooks/useScrolled.js +++ b/src/ui/components/hooks/useScrolled.ts @@ -1,13 +1,11 @@ import { useState, useEffect } from 'react' -export default function useScrolled() { +export const useScrolled = () => { const [scrolled, setScrolled] = useState( typeof window === 'undefined' ? false : window.scrollY > 20, ) - function handleScroll() { - setScrolled(window.scrollY > 20) - } + const handleScroll = () => setScrolled(window.scrollY > 20) useEffect(() => { window.addEventListener('scroll', handleScroll) diff --git a/src/ui/components/hooks/useStore.js b/src/ui/components/hooks/useStore.ts similarity index 59% rename from src/ui/components/hooks/useStore.js rename to src/ui/components/hooks/useStore.ts index d3b586a7..e6d6f98b 100644 --- a/src/ui/components/hooks/useStore.js +++ b/src/ui/components/hooks/useStore.ts @@ -3,63 +3,58 @@ import qs from 'querystring' const interval = 5000 -export default function useStore(basePath) { +export const useStore = (basePath: string) => { const [state, setState] = useState({ data: null, loading: true, - }) - const [selectedStatuses, setSelectedStatuses] = useState({}) + } as any) + const [selectedStatuses, setSelectedStatuses] = useState({} as any) const poll = useRef() + const stopPolling = () => { + if (poll.current) { + clearTimeout(poll.current) + poll.current = undefined + } + } useEffect(() => { stopPolling() runPolling() + return stopPolling }, [selectedStatuses]) - const stopPolling = () => { - if (poll.current) { - clearTimeout(poll.current) - poll.current = null - } - } - const runPolling = () => { update() - .catch(error => { - console.error('Failed to poll', error) - }) + .catch(error => console.error('Failed to poll', error)) .then(() => { - const timeoutId = setTimeout(() => { - runPolling() - }, interval) - poll.current = timeoutId + const timeoutId = setTimeout(runPolling, interval) + poll.current = timeoutId as any }) } - const update = () => { - return fetch(`${basePath}/queues/?${qs.encode(selectedStatuses)}`) + const update = () => + fetch(`${basePath}/queues/?${qs.encode(selectedStatuses)}`) .then(res => (res.ok ? res.json() : Promise.reject(res))) .then(data => setState({ data, loading: false })) - } - const retryJob = queueName => job => () => + const retryJob = (queueName: string) => (job: { id: string }) => () => fetch(`${basePath}/queues/${queueName}/${job.id}/retry`, { method: 'put', }).then(update) - const retryAll = queueName => () => - fetch(`${basePath}/queues/${queueName}/retry`, { method: 'put' }).then( - update, - ) + const retryAll = (queueName: string) => () => + fetch(`${basePath}/queues/${queueName}/retry`, { + method: 'put', + }).then(update) - const cleanAllDelayed = queueName => () => + const cleanAllDelayed = (queueName: string) => () => fetch(`${basePath}/queues/${queueName}/clean/delayed`, { method: 'put', }).then(update) - const cleanAllFailed = queueName => () => + const cleanAllFailed = (queueName: string) => () => fetch(`${basePath}/queues/${queueName}/clean/failed`, { method: 'put', }).then(update) diff --git a/src/ui/index.js b/src/ui/index.js deleted file mode 100644 index 144fb47b..00000000 --- a/src/ui/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' -import { render } from 'react-dom' -import './index.css' -import './xcode.css' -import App from './components/App' - -render(, document.getElementById('root')) diff --git a/src/ui/index.tsx b/src/ui/index.tsx new file mode 100644 index 00000000..1d000d72 --- /dev/null +++ b/src/ui/index.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { render } from 'react-dom' + +import './index.css' +import './xcode.css' +import { App } from './components/App' + +// eslint-disable-next-line @typescript-eslint/ban-ts-ignore +// @ts-ignore +const basePath = window.basePath + +render(, document.getElementById('root')) diff --git a/tsconfig.json b/tsconfig.json index 44e3b43c..beb7705c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,18 +2,19 @@ "compilerOptions": { "outDir": "./dist", "esModuleInterop": true, - "lib": ["es2019"], + "lib": ["es2019", "dom"], "module": "commonjs", "moduleResolution": "node", "noImplicitAny": true, "sourceMap": true, "strict": true, + "jsx": "react", "target": "es2019", "noUnusedParameters": true, "noUnusedLocals": true, "resolveJsonModule": true, "declaration": true }, - "include": ["./src"], - "exclude": ["./src/ui"] + "include": ["./src"] + // "exclude": ["./src/ui"] } diff --git a/webpack.config.js b/webpack.config.js index fc032947..479352df 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -7,11 +7,14 @@ module.exports = { mode: 'production', bail: true, devtool: false, - entry: ['./src/ui/index.js'], + entry: ['./src/ui/index.tsx'], output: { path: path.resolve(__dirname, './static'), filename: 'bundle.js', }, + resolve: { + extensions: ['.ts', '.tsx', '.js', '.jsx'], + }, module: { rules: [ { @@ -19,7 +22,21 @@ module.exports = { loader: 'babel-loader', options: { presets: ['react-app'] }, }, - { test: /\.css$/, use: ['style-loader', 'css-loader'] }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + { + test: /\.ts(x?)$/, + exclude: /node_modules/, + use: [{ loader: 'ts-loader' }], + }, + { + // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. + enforce: 'pre', + test: /\.js$/, + loader: 'source-map-loader', + }, ], }, plugins: [ diff --git a/yarn.lock b/yarn.lock index e10e6659..779b0ca6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1232,11 +1232,45 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.8.tgz#1d590429fe8187a02707720ecf38a6fe46ce294b" integrity sha512-6XzyyNM9EKQW4HKuzbo/CkOIjn/evtCmsU+MUM1xDfJ+3/rNjBttM1NgN7AOQvN6tP1Sl1D1PIKMreTArnxM9A== +"@types/pretty-bytes@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@types/pretty-bytes/-/pretty-bytes-5.2.0.tgz#857dcf4a21839e5bfb1c188dda62f986fdfa2348" + integrity sha512-dJhMFphDp6CE+OAZVyqzha9KsmgeqRMbZN4dIbMSrfObiuzfjucwKdn6zu+ttrjMwmz+Vz71/xXgHx5pO0axhA== + dependencies: + pretty-bytes "*" + +"@types/prop-types@*": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + "@types/range-parser@*": version "1.2.3" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== +"@types/react-dom@^16.9.5": + version "16.9.5" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.5.tgz#5de610b04a35d07ffd8f44edad93a71032d9aaa7" + integrity sha512-BX6RQ8s9D+2/gDhxrj8OW+YD4R+8hj7FEM/OJHGNR0KipE1h1mSsf39YeyC81qafkq+N3rU3h3RFbLSwE5VqUg== + dependencies: + "@types/react" "*" + +"@types/react-highlight@^0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/react-highlight/-/react-highlight-0.12.2.tgz#205b3437bf3611c185884a564d20548a4780480b" + integrity sha512-Pd0jkT7OB0eVccQeytwf1e+RoszedNHnsmJWbyr4F6407xdRz1Si7zmF9imO/Qr88DidnNv63Vyy9UfGHSMDrg== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^16.9.18": + version "16.9.18" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.18.tgz#7dfb420a780e3740d43563506009834a86ae2af0" + integrity sha512-MvjiKX/kUE8o49ipppg49RDZ97p4XfW1WWksp/UlTUSJpisyhzd62pZAMXxAscFLoxfYOflkGANAnGkSeHTFQg== + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + "@types/serve-static@*": version "1.13.3" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" @@ -1808,6 +1842,13 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async@^2.5.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + dependencies: + lodash "^4.17.14" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -2045,7 +2086,7 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@~3.0.2: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -2317,7 +2358,7 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -2750,6 +2791,11 @@ cssstyle@^1.0.0: dependencies: cssom "0.3.x" +csstype@^2.2.0: + version "2.6.8" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.8.tgz#0fb6fc2417ffd2816a418c9336da74d7f07db431" + integrity sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA== + cyclist@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" @@ -3069,6 +3115,15 @@ enhanced-resolve@4.1.0, enhanced-resolve@^4.1.0: memory-fs "^0.4.0" tapable "^1.0.0" +enhanced-resolve@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz#2937e2b8066cd0fe7ce0990a98f0d71a35189f66" + integrity sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.5.0" + tapable "^1.0.0" + errno@^0.1.3, errno@~0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" @@ -5559,6 +5614,14 @@ memory-fs@^0.4.0, memory-fs@^0.4.1: errno "^0.1.3" readable-stream "^2.0.1" +memory-fs@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" + integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -5598,6 +5661,14 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" +micromatch@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -6377,7 +6448,7 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.4, picomatch@^2.0.7: +picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7: version "2.2.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== @@ -6519,7 +6590,7 @@ prettier@^1.19.1, prettier@^1.7.0: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== -pretty-bytes@5.3.0: +pretty-bytes@*, pretty-bytes@5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== @@ -7433,6 +7504,14 @@ source-list-map@^2.0.0: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== +source-map-loader@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.4.tgz#c18b0dc6e23bf66f6792437557c569a11e072271" + integrity sha512-OU6UJUty+i2JDpTItnizPrlpOIBLmQbWMuBg9q5bVtnHACqw1tn9nNwqJLbv0/00JjnJb/Ee5g5WS5vrRv7zIQ== + dependencies: + async "^2.5.0" + loader-utils "^1.1.0" + source-map-resolve@^0.5.0: version "0.5.2" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" @@ -7985,6 +8064,17 @@ ts-jest@^24.3.0: semver "^5.5" yargs-parser "10.x" +ts-loader@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-6.2.1.tgz#67939d5772e8a8c6bdaf6277ca023a4812da02ef" + integrity sha512-Dd9FekWuABGgjE1g0TlQJ+4dFUfYGbYcs52/HQObE0ZmUNjQlmLAS7xXsSzy23AMaMwipsx5sNHvoEpT2CZq1g== + dependencies: + chalk "^2.3.0" + enhanced-resolve "^4.0.0" + loader-utils "^1.0.2" + micromatch "^4.0.0" + semver "^6.0.0" + ts-node@^8.6.2: version "8.6.2" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.6.2.tgz#7419a01391a818fbafa6f826a33c1a13e9464e35"