diff --git a/ui/package.json b/ui/package.json index eba8e8484e4c2..f679dd0e9e295 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,6 +10,7 @@ "test": "jest" }, "dependencies": { + "@types/react-virtualized": "^9.21.21", "@types/superagent": "^4.1.15", "ansi-to-react": "^6.1.6", "argo-ui": "git+https://github.com/argoproj/argo-ui.git", @@ -41,6 +42,7 @@ "react-router": "^4.3.1", "react-router-dom": "^4.2.2", "react-svg-piechart": "^2.4.2", + "react-virtualized": "^9.22.3", "redoc": "^2.0.0-rc.64", "rxjs": "^6.6.6", "superagent": "^8.0.9", diff --git a/ui/src/app/applications/components/application-fullscreen-logs/application-fullscreen-logs.tsx b/ui/src/app/applications/components/application-fullscreen-logs/application-fullscreen-logs.tsx index 4314d1e1c6a47..c7e669f46dded 100644 --- a/ui/src/app/applications/components/application-fullscreen-logs/application-fullscreen-logs.tsx +++ b/ui/src/app/applications/components/application-fullscreen-logs/application-fullscreen-logs.tsx @@ -3,12 +3,10 @@ import * as React from 'react'; import Helmet from 'react-helmet'; import {RouteComponentProps} from 'react-router-dom'; import {Query} from '../../../shared/components'; -import {Context} from '../../../shared/context'; import {PodsLogsViewer} from '../pod-logs-viewer/pod-logs-viewer'; import './application-fullscreen-logs.scss'; export const ApplicationFullscreenLogs = (props: RouteComponentProps<{name: string; appnamespace: string; container: string; namespace: string}>) => { - const appContext = React.useContext(Context); return ( {q => { @@ -16,8 +14,6 @@ export const ApplicationFullscreenLogs = (props: RouteComponentProps<{name: stri const name = q.get('name'); const group = q.get('group'); const kind = q.get('kind'); - const page = q.get('page'); - const untilTimes = (q.get('untilTimes') || '').split(',') || []; const title = `${podName || `${group}/${kind}/${name}`}:${props.match.params.container}`; return (
@@ -32,9 +28,6 @@ export const ApplicationFullscreenLogs = (props: RouteComponentProps<{name: stri kind={kind} name={name} podName={podName} - fullscreen={true} - page={{number: parseInt(page, 10) || 0, untilTimes}} - setPage={pageData => appContext.navigation.goto('.', {page: pageData.number, untilTimes: pageData.untilTimes.join(',')}, {replace: true})} />
); diff --git a/ui/src/app/applications/components/pod-logs-viewer/container-selector.tsx b/ui/src/app/applications/components/pod-logs-viewer/container-selector.tsx new file mode 100644 index 0000000000000..c967f84e741de --- /dev/null +++ b/ui/src/app/applications/components/pod-logs-viewer/container-selector.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import {Tooltip} from 'argo-ui'; + +export type ContainerGroup = {offset: number; containers: string[]}; + +// ContainerSelector is a component that renders a dropdown menu of containers +export const ContainerSelector = ({ + containerGroups, + containerName, + onClickContainer +}: { + containerGroups?: ContainerGroup[]; + containerName: string; + onClickContainer: (group: ContainerGroup, index: number, logs: string) => void; +}) => { + if (!containerGroups) { + return <>; + } + const containers = containerGroups?.reduce((acc, group) => acc.concat(group.containers), []); + const containerNames = containers?.map(container => container.name); + const containerGroup = (n: string) => { + return containerGroups.find(group => group.containers.find(container => container === n)); + }; + const containerIndex = (n: string) => { + return containerGroup(n).containers.findIndex(container => container === n); + }; + if (containerNames.length <= 1) return <>; + return ( + + + + ); +}; diff --git a/ui/src/app/applications/components/pod-logs-viewer/copy-logs-button.tsx b/ui/src/app/applications/components/pod-logs-viewer/copy-logs-button.tsx new file mode 100644 index 0000000000000..dca726bec8989 --- /dev/null +++ b/ui/src/app/applications/components/pod-logs-viewer/copy-logs-button.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import {useContext} from 'react'; +import {LogLoader} from './log-loader'; +import {Button} from '../../../shared/components/button'; +import {Context} from '../../../shared/context'; +import {NotificationType} from 'argo-ui/src/components/notifications/notifications'; + +// CopyLogsButton is a button that copies the logs to the clipboard +export const CopyLogsButton = ({loader}: {loader: LogLoader}) => { + const ctx = useContext(Context); + return ( + - - - - - {props.containerGroups?.length > 0 && ( - ( - - - + {(prefs: ViewPreferences) => { + return ( + +
+ + + + + + + {!follow && ( + <> + setSinceSeconds(n)} /> + + )} - items={containerItems} - /> - )} - - - - - - - - - - -
+ +
+
-                                {prefs.appDetails.darkMode ?  : }
-                            
-                        
-                        {!props.timestamp && (
-                            
-                                
-                            
-                        )}
-                        {!props.fullscreen && (
-                            
-                                
-                            
-                        )}
-
-                        
- - - - setFilterText(e.target.value)} - style={{padding: 0}} - /> -
-
- (loader = l)} - loadingRenderer={() => ( -
- {logNavigators({}, prefs.appDetails.darkMode, null)} -
{!prefs.appDetails.followLogs && 'Loading...'}
-
- )} - input={props.containerName} - load={() => { - let logsSource = services.applications - .getContainerLogs( - props.applicationName, - props.applicationNamespace, - props.namespace, - props.podName, - {group: props.group, kind: props.kind, name: props.name}, - props.containerName, - maxLines * (page.number + 1), - prefs.appDetails.followLogs && page.number === 0, - page.untilTimes[page.untilTimes.length - 1], - filterQuery, - showPreviousLogs - ) - // show only current page lines - .pipe( - scan((lines, logEntry) => { - // first equal true means retry attempt so we should clear accumulated log entries - if (logEntry.first) { - lines = [logEntry]; - } else { - lines.push(logEntry); - } - if (lines.length > maxLines) { - lines.splice(0, lines.length - maxLines); - } - return lines; - }, new Array()) - ) - // accumulate log changes and render only once every 100ms to reduce CPU usage - .pipe(bufferTime(100)) - .pipe(rxfilter(batch => batch.length > 0)) - .pipe(map(batch => batch[batch.length - 1])); - if (prefs.appDetails.followLogs) { - logsSource = logsSource.pipe(retryWhen(errors => errors.pipe(delay(500)))); - } - return logsSource; - }}> - {logs => { - logs = logs || []; - setTimeout(() => { - if (page.number === 0 && prefs.appDetails.followLogs && bottom.current) { - bottom.current.scrollIntoView({behavior: 'smooth'}); - } - }); - const pods = Array.from(new Set(logs.map(log => log.podName))); - const podColors = pods.reduce((colors, pod, i) => colors.set(pod, BASE_COLORS[i % BASE_COLORS.length]), new Map()); - const lines = logs.map(item => item.content); - const firstLine = maxLines * page.number + 1; - const lastLine = maxLines * page.number + lines.length; - const canPageBack = lines.length === maxLines; - return ( -
- {logNavigators( - { - left: () => { - if (!canPageBack) { - return; - } - setPage({number: page.number + 1, untilTimes: page.untilTimes.concat(logs[0].timeStampStr)}); - loader.reload(); - }, - bottom: () => { - bottom.current.scrollIntoView({ - behavior: 'smooth' - }); - }, - top: () => { - top.current.scrollIntoView({ - behavior: 'smooth' - }); - }, - right: () => { - if (page.number > 0) { - setPage({number: page.number - 1, untilTimes: page.untilTimes.slice(0, page.untilTimes.length - 1)}); - loader.reload(); - } - }, - end: () => { - setPage({number: 0, untilTimes: []}); - loader.reload(); - } - }, - prefs.appDetails.darkMode, - { - firstLine, - lastLine, - curPage: page.number, - canPageBack - } - )} - {!props.podName && ( - - { - setViewPodNames(!viewPodNames); - if (viewTimestamps) { - setViewTimestamps(false); - } - }} - /> - - )} -
-                                        
- {lines.map((l, i) => { - const lineNum = lastLine - i; + {(logs: LogEntry[]) => { + logs = logs || []; + + const renderLog = (log: LogEntry, lineNum: number) => + // show the pod name if there are multiple pods, pad with spaces to align + (viewPodNames + ? (lineNum === 0 || logs[lineNum - 1].podName !== log.podName + ? podColor(podName) + log.podName + reset + : ' '.repeat(log.podName.length)) + ' ' + : '') + + // show the timestamp if requested, pad with spaces to align + (viewTimestamps + ? (lineNum === 0 || logs[lineNum - 1].timeStamp !== log.timeStamp ? log.timeStampStr : ' '.repeat(log.timeStampStr.length)) + ' ' + : '') + + // show the log content, highlight the filter text + log.content.replace(highlight, (substring: string) => whiteOnYellow + substring + reset); + + const rowRenderer = ({index, key, style}: {index: number; key: string; style: React.CSSProperties}) => { return ( -
{ - setSelectedLine(selectedLine === i ? -1 : i); - }}> -
- } - items={[ - { - title: ( - - Copy Line - - ), - action: async () => { - await navigator.clipboard.writeText(l); - } - }, - { - title: ( - - Copy Line Number - - ), - action: async () => { - await navigator.clipboard.writeText(JSON.stringify(lineNum)); - } - } - ]} - /> -
- {!props.podName && ( -
- {(i === 0 || logs[i - 1].podName !== logs[i].podName) && ( - - - {logs[i].podName} - - - - - - )} -
- )} - {viewTimestamps && ( -
- {(i === 0 || logs[i - 1].timeStamp !== logs[i].timeStamp) && ( - - - {logs[i].timeStampStr} - - - )} -
- )} -
{lineNum}
-
- {l} -
-
+
+                                                    {renderLog(logs[index], index)}
+                                                
); - })} -
-
-
- ); - }} -
-
- )} - - ); -}; + }; -interface NavActions { - left?: () => void; - right?: () => void; - begin?: () => void; - end?: () => void; - bottom?: () => void; - top?: () => void; -} - -interface PageInfo { - firstLine: number; - lastLine: number; - curPage: number; - canPageBack: boolean; -} - -const NavButton = (props: {children: React.ReactNode; disabled?: boolean; onClick: () => void}) => { - return ( -
- {props.children} -
- ); -}; - -const logNavigators = (actions: NavActions, darkMode: boolean, info?: PageInfo) => { - return ( -
- {actions.begin && ( - null)}> - Begin - - )} + if (tail) { + // @ts-ignore + setTimeout(() => list.current?.scrollToRow(logs.length - 1)); + } - null)}> - Prev - - null)}> - Bottom - - null)}> - Top - -
- {info && ( - - Page {info.curPage + 1} (Lines {info.firstLine} to {info.lastLine}) + return ( + <> + + {({height}: {width: number; height: number}) => ( + + )} + + + ); + }} + + +
- )} -
- 0 && actions.right) || null} disabled={!(info && info.curPage > 0)}> - Next - - 1 && actions.end) || null} disabled={!(info && info.curPage > 1)}> - End - - + ); + }} + ); }; diff --git a/ui/src/app/applications/components/pod-logs-viewer/pod-names-toggle-button.tsx b/ui/src/app/applications/components/pod-logs-viewer/pod-names-toggle-button.tsx new file mode 100644 index 0000000000000..8304818453e8a --- /dev/null +++ b/ui/src/app/applications/components/pod-logs-viewer/pod-names-toggle-button.tsx @@ -0,0 +1,7 @@ +import * as React from 'react'; +import {ToggleButton} from '../../../shared/components/toggle-button'; + +// PodNamesToggleButton is a component that renders a toggle button that toggles pod names. +export const PodNamesToggleButton = ({viewPodNames, setViewPodNames}: {viewPodNames: boolean; setViewPodNames: (value: boolean) => void}) => ( + setViewPodNames(!viewPodNames)} toggled={viewPodNames} icon='box' /> +); diff --git a/ui/src/app/applications/components/pod-logs-viewer/show-previous-logs-toggle-button.tsx b/ui/src/app/applications/components/pod-logs-viewer/show-previous-logs-toggle-button.tsx new file mode 100644 index 0000000000000..da1493a4119bf --- /dev/null +++ b/ui/src/app/applications/components/pod-logs-viewer/show-previous-logs-toggle-button.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import {LogLoader} from './log-loader'; +import {ToggleButton} from '../../../shared/components/toggle-button'; + +// ShowPreviousLogsToggleButton is a component that renders a toggle button that toggles previous logs. +export const ShowPreviousLogsToggleButton = ({ + setPreviousLogs, + showPreviousLogs, + loader +}: { + setPreviousLogs: (value: boolean) => void; + showPreviousLogs: boolean; + loader: LogLoader; +}) => ( + { + setPreviousLogs(!showPreviousLogs); + loader.reload(); + }} + icon='angle-left' + toggled={showPreviousLogs} + /> +); diff --git a/ui/src/app/applications/components/pod-logs-viewer/since-seconds-selector.tsx b/ui/src/app/applications/components/pod-logs-viewer/since-seconds-selector.tsx new file mode 100644 index 0000000000000..e5c02ee031f80 --- /dev/null +++ b/ui/src/app/applications/components/pod-logs-viewer/since-seconds-selector.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import {Tooltip} from 'argo-ui'; + +// SinceSelector is a component that renders a dropdown menu of time ranges +export const SinceSecondsSelector = ({sinceSeconds, setSinceSeconds}: {sinceSeconds: number; setSinceSeconds: (value: number) => void}) => ( + + + +); diff --git a/ui/src/app/applications/components/pod-logs-viewer/tail-selector.tsx b/ui/src/app/applications/components/pod-logs-viewer/tail-selector.tsx new file mode 100644 index 0000000000000..8d9ebd8d6ac9a --- /dev/null +++ b/ui/src/app/applications/components/pod-logs-viewer/tail-selector.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import {Tooltip} from 'argo-ui'; + +// TailSelector is a component that renders a dropdown menu of tail options +export const TailSelector = ({tail, setTail}: {tail: number; setTail: (value: number) => void}) => ( + + + +); diff --git a/ui/src/app/applications/components/pod-logs-viewer/timestamps-toggle-button.tsx b/ui/src/app/applications/components/pod-logs-viewer/timestamps-toggle-button.tsx new file mode 100644 index 0000000000000..5d1d8ce2bda9c --- /dev/null +++ b/ui/src/app/applications/components/pod-logs-viewer/timestamps-toggle-button.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import {ToggleButton} from '../../../shared/components/toggle-button'; + +// TimestampsToggleButton is a component that renders a toggle button that toggles timestamps. +export const TimestampsToggleButton = ({ + timestamp, + viewTimestamps, + setViewTimestamps +}: { + timestamp?: string; + viewTimestamps: boolean; + setViewTimestamps: (value: boolean) => void; +}) => + !timestamp && ( + { + setViewTimestamps(!viewTimestamps); + }} + toggled={viewTimestamps} + icon='clock' + /> + ); diff --git a/ui/src/app/applications/components/resource-details/resource-details.tsx b/ui/src/app/applications/components/resource-details/resource-details.tsx index 4d336b919c70c..6477509370905 100644 --- a/ui/src/app/applications/components/resource-details/resource-details.tsx +++ b/ui/src/app/applications/components/resource-details/resource-details.tsx @@ -41,9 +41,6 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { const selectedNodeInfo = NodeInfo(new URLSearchParams(appContext.history.location.search).get('node')); const selectedNodeKey = selectedNodeInfo.key; - const page = parseInt(new URLSearchParams(appContext.history.location.search).get('page'), 10) || 0; - const untilTimes = (new URLSearchParams(appContext.history.location.search).get('untilTimes') || '').split(',') || []; - const getResourceTabs = ( node: ResourceNode, state: State, @@ -110,8 +107,6 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { applicationName={application.metadata.name} applicationNamespace={application.metadata.namespace} containerName={AppUtils.getContainerName(podState, activeContainer)} - page={{number: page, untilTimes}} - setPage={pageData => appContext.navigation.goto('.', {page: pageData.number, untilTimes: pageData.untilTimes.join(',')})} containerGroups={containerGroups} onClickContainer={onClickContainer} /> diff --git a/ui/src/app/shared/components/button.tsx b/ui/src/app/shared/components/button.tsx new file mode 100644 index 0000000000000..9acf2beeab524 --- /dev/null +++ b/ui/src/app/shared/components/button.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import {CSSProperties, MouseEventHandler, ReactNode} from 'react'; +import {Icon} from './icon'; +import {Tooltip} from 'argo-ui'; + +export const Button = ({ + onClick, + children, + title, + outline, + icon, + className, + style, + disabled +}: { + onClick?: MouseEventHandler; + children?: ReactNode; + title?: string; + outline?: boolean; + icon?: Icon; + className?: string; + style?: CSSProperties; + disabled?: boolean; +}) => ( + + + +); diff --git a/ui/src/app/shared/components/icon.ts b/ui/src/app/shared/components/icon.ts new file mode 100644 index 0000000000000..3a2afc6250298 --- /dev/null +++ b/ui/src/app/shared/components/icon.ts @@ -0,0 +1 @@ +export type Icon = string; diff --git a/ui/src/app/shared/components/spacer.tsx b/ui/src/app/shared/components/spacer.tsx new file mode 100644 index 0000000000000..6a0f96cad5da8 --- /dev/null +++ b/ui/src/app/shared/components/spacer.tsx @@ -0,0 +1,3 @@ +import * as React from 'react'; + +export const Spacer = () => ; diff --git a/ui/src/app/shared/components/toggle-button.tsx b/ui/src/app/shared/components/toggle-button.tsx new file mode 100644 index 0000000000000..4a7177cdb1ac5 --- /dev/null +++ b/ui/src/app/shared/components/toggle-button.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import {ReactNode} from 'react'; +import {Button} from './button'; +import {Icon} from './icon'; +import {ARGO_WARNING_COLOR} from './colors'; + +export const ToggleButton = ({ + title, + children, + onToggle, + toggled, + disabled, + icon +}: { + toggled: boolean; + onToggle: () => void; + children?: ReactNode; + title: string; + disabled?: boolean; + icon: Icon; +}) => ( + +); diff --git a/ui/src/app/shared/services/applications-service.ts b/ui/src/app/shared/services/applications-service.ts index 5301267b30b29..cca392bd5a717 100644 --- a/ui/src/app/shared/services/applications-service.ts +++ b/ui/src/app/shared/services/applications-service.ts @@ -227,25 +227,27 @@ export class ApplicationsService { resource: {group: string; kind: string; name: string}, containerName: string ): string { - const search = this.getLogsQuery(namespace, appNamespace, podName, resource, containerName, null, false); + const search = this.getLogsQuery({namespace, appNamespace, podName, resource, containerName, follow: false}); search.set('download', 'true'); return `api/v1/applications/${applicationName}/logs?${search.toString()}`; } - public getContainerLogs( - applicationName: string, - appNamespace: string, - namespace: string, - podName: string, - resource: {group: string; kind: string; name: string}, - containerName: string, - tail?: number, - follow?: boolean, - untilTime?: string, - filter?: string, - previous?: boolean - ): Observable { - const search = this.getLogsQuery(namespace, appNamespace, podName, resource, containerName, tail, follow, untilTime, filter, previous); + public getContainerLogs(query: { + applicationName: string; + appNamespace: string; + namespace: string; + podName: string; + resource: {group: string; kind: string; name: string}; + containerName: string; + tail?: number; + follow?: boolean; + sinceSeconds?: number; + untilTime?: string; + filter?: string; + previous?: boolean; + }): Observable { + const {applicationName} = query; + const search = this.getLogsQuery(query); const entries = requests.loadEventSource(`/applications/${applicationName}/logs?${search.toString()}`).pipe(map(data => JSON.parse(data).result as models.LogEntry)); let first = true; return new Observable(observer => { @@ -427,18 +429,21 @@ export class ApplicationsService { }); } - private getLogsQuery( - namespace: string, - appNamespace: string, - podName: string, - resource: {group: string; kind: string; name: string}, - containerName: string, - tail?: number, - follow?: boolean, - untilTime?: string, - filter?: string, - previous?: boolean - ): URLSearchParams { + private getLogsQuery(query: { + namespace: string; + appNamespace: string; + podName: string; + resource: {group: string; kind: string; name: string}; + containerName: string; + tail?: number; + follow?: boolean; + sinceSeconds?: number; + untilTime?: string; + filter?: string; + previous?: boolean; + }): URLSearchParams { + const {appNamespace, containerName, namespace, podName, resource, tail, sinceSeconds, untilTime, filter, previous} = query; + let {follow} = query; if (follow === undefined || follow === null) { follow = true; } @@ -457,6 +462,9 @@ export class ApplicationsService { if (tail) { search.set('tailLines', tail.toString()); } + if (sinceSeconds) { + search.set('sinceSeconds', sinceSeconds.toString()); + } if (untilTime) { search.set('untilTime', untilTime); } diff --git a/ui/yarn.lock b/ui/yarn.lock index 9f8aec681c6f3..a16515bf9a76c 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1010,6 +1010,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7": + version "7.20.13" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b" + integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/template@^7.14.5", "@babel/template@^7.7.0": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4" @@ -1872,7 +1879,15 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16", "@types/react@^16.8.5", "@types/react@^16.9.3": +"@types/react-virtualized@^9.21.21": + version "9.21.21" + resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.21.tgz#65c96f25314f0fb3d40536929dc78112753b49e1" + integrity sha512-Exx6I7p4Qn+BBA1SRyj/UwQlZ0I0Pq7g7uhAp0QQ4JWzZunqEqNBGTmCmMmS/3N9wFgAGWuBD16ap7k8Y14VPA== + dependencies: + "@types/prop-types" "*" + "@types/react" "^17" + +"@types/react@*", "@types/react@^16", "@types/react@^16.8.5", "@types/react@^16.9.3", "@types/react@^17": version "16.14.15" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.15.tgz#95d8fa3148050e94bcdc5751447921adbe19f9e6" integrity sha512-jOxlBV9RGZhphdeqJTCv35VZOkjY+XIEY2owwSk84BNDdDv2xS6Csj6fhi+B/q30SR9Tz8lDNt/F2Z5RF3TrRg== @@ -2998,16 +3013,16 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +clsx@^1.0.4, clsx@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + clsx@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== -clsx@^1.1.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" - integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== - co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -3609,6 +3624,14 @@ dom-converter@^0.2.0: dependencies: utila "~0.4" +dom-helpers@^5.1.3: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + dom-scroll-into-view@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/dom-scroll-into-view/-/dom-scroll-into-view-1.0.1.tgz#32abb92f0d8feca6215162aef43e4b449ab8d99c" @@ -7756,7 +7779,7 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-lifecycles-compat@^3.0.0: +react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== @@ -7860,6 +7883,18 @@ react-toastify@^9.0.8: dependencies: clsx "^1.1.1" +react-virtualized@^9.22.3: + version "9.22.3" + resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.3.tgz#f430f16beb0a42db420dbd4d340403c0de334421" + integrity sha512-MKovKMxWTcwPSxE1kK1HcheQTWfuCxAuBoSTf2gwyMM21NdX/PXUhnoP8Uc5dRKd+nKm8v41R36OellhdCpkrw== + dependencies: + "@babel/runtime" "^7.7.2" + clsx "^1.0.4" + dom-helpers "^5.1.3" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-lifecycles-compat "^3.0.4" + react@^16.9.3: version "16.14.0" resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" @@ -7988,6 +8023,11 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + regenerator-runtime@^0.13.4: version "0.13.7" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"